diff --git a/.docker/frankenphp/Caddyfile b/.docker/frankenphp/Caddyfile new file mode 100644 index 00000000..83839304 --- /dev/null +++ b/.docker/frankenphp/Caddyfile @@ -0,0 +1,55 @@ +{ + {$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 +} diff --git a/.docker/frankenphp/conf.d/app.dev.ini b/.docker/frankenphp/conf.d/app.dev.ini new file mode 100644 index 00000000..e50f43d0 --- /dev/null +++ b/.docker/frankenphp/conf.d/app.dev.ini @@ -0,0 +1,5 @@ +; 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 diff --git a/.docker/frankenphp/conf.d/app.ini b/.docker/frankenphp/conf.d/app.ini new file mode 100644 index 00000000..10a062f2 --- /dev/null +++ b/.docker/frankenphp/conf.d/app.ini @@ -0,0 +1,18 @@ +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 \ No newline at end of file diff --git a/.docker/frankenphp/conf.d/app.prod.ini b/.docker/frankenphp/conf.d/app.prod.ini new file mode 100644 index 00000000..3bcaa71e --- /dev/null +++ b/.docker/frankenphp/conf.d/app.prod.ini @@ -0,0 +1,2 @@ +opcache.preload_user = root +opcache.preload = /app/config/preload.php diff --git a/.docker/frankenphp/docker-entrypoint.sh b/.docker/frankenphp/docker-entrypoint.sh new file mode 100644 index 00000000..1655af5a --- /dev/null +++ b/.docker/frankenphp/docker-entrypoint.sh @@ -0,0 +1,60 @@ +#!/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 "$@" \ No newline at end of file diff --git a/.docker/frankenphp/worker.Caddyfile b/.docker/frankenphp/worker.Caddyfile new file mode 100644 index 00000000..d384ae4c --- /dev/null +++ b/.docker/frankenphp/worker.Caddyfile @@ -0,0 +1,4 @@ +worker { + file ./public/index.php + env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime +} diff --git a/.docker/partdb-entrypoint.sh b/.docker/partdb-entrypoint.sh index 3e06256a..ffd2b24a 100644 --- a/.docker/partdb-entrypoint.sh +++ b/.docker/partdb-entrypoint.sh @@ -39,8 +39,50 @@ if [ -d /var/www/html/var/db ]; then fi fi -# Start PHP-FPM -service php8.1-fpm start +# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile) +service phpPHP_VERSION-fpm start + + +# Run migrations if automigration is enabled via env variable DB_AUTOMIGRATE +if [ "$DB_AUTOMIGRATE" = "true" ]; then + echo "Waiting for database to be ready..." + ATTEMPTS_LEFT_TO_REACH_DATABASE=60 + until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(sudo -E -u www-data php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do + if [ $? -eq 255 ]; then + # If the Doctrine command exits with 255, an unrecoverable error occurred + ATTEMPTS_LEFT_TO_REACH_DATABASE=0 + break + fi + sleep 1 + ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1)) + echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left." + done + + if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then + echo "The database is not up or not reachable:" + echo "$DATABASE_ERROR" + exit 1 + else + echo "The database is now ready and reachable" + fi + + # Check if there are any available migrations to do, by executing doctrine:migrations:up-to-date + # and checking if the exit code is 0 (up to date) or 1 (not up to date) + if sudo -E -u www-data php bin/console doctrine:migrations:up-to-date --no-interaction; then + echo "Database is up to date, no migrations necessary." + else + echo "Migrations available..." + echo "Do backup of database..." + + sudo -E -u www-data mkdir -p /var/www/html/uploads/.automigration-backup/ + # Backup the database + sudo -E -u www-data php bin/console partdb:backup -n --database /var/www/html/uploads/.automigration-backup/backup-$(date +%Y-%m-%d_%H-%M-%S).zip + + # Check if there are any migration files + sudo -E -u www-data php bin/console doctrine:migrations:migrate --no-interaction + fi + +fi # first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint) if [ "${1#-}" != "$1" ]; then diff --git a/.docker/symfony.conf b/.docker/symfony.conf index 2f8e7f66..b5229bf6 100644 --- a/.docker/symfony.conf +++ b/.docker/symfony.conf @@ -25,15 +25,28 @@ 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 ENFORCE_CHANGE_COMMENTS_FOR - PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI + PassEnv APP_ENV APP_DEBUG APP_SECRET REDIRECT_TO_HTTPS DISABLE_YEAR2038_BUG_CHECK + PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN + PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR DATABASE_MYSQL_USE_SSL_CA DATABASE_MYSQL_SSL_VERIFY_CERT + PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI CHECK_FOR_UPDATES ATTACHMENT_DOWNLOAD_BY_DEFAULT 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 HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA HISTORY_SAVE_NEW_DATA PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP PassEnv DEMO_MODE NO_URL_REWRITE_AVAILABLE FIXER_API_KEY BANNER - PassEnv SAML_ENABLED SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAMLP_SP_PRIVATE_KEY + # In old version the SAML sp private key env, was wrongly named SAMLP_SP_PRIVATE_KEY, keep it for backward compatibility + PassEnv SAML_ENABLED SAML_BEHIND_PROXY SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAML_SP_PRIVATE_KEY SAMLP_SP_PRIVATE_KEY + PassEnv TABLE_DEFAULT_PAGE_SIZE TABLE_PARTS_DEFAULT_COLUMNS + PassEnv PROVIDER_DIGIKEY_CLIENT_ID PROVIDER_DIGIKEY_SECRET PROVIDER_DIGIKEY_CURRENCY PROVIDER_DIGIKEY_LANGUAGE PROVIDER_DIGIKEY_COUNTRY + PassEnv PROVIDER_ELEMENT14_KEY PROVIDER_ELEMENT14_STORE_ID + PassEnv PROVIDER_TME_KEY PROVIDER_TME_SECRET PROVIDER_TME_CURRENCY PROVIDER_TME_LANGUAGE PROVIDER_TME_COUNTRY PROVIDER_TME_GET_GROSS_PRICES + PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS + PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE + PassEnv PROVIDER_LCSC_ENABLED PROVIDER_LCSC_CURRENCY + PassEnv PROVIDER_OEMSECRETS_KEY PROVIDER_OEMSECRETS_COUNTRY_CODE PROVIDER_OEMSECRETS_CURRENCY PROVIDER_OEMSECRETS_ZERO_PRICE PROVIDER_OEMSECRETS_SET_PARAM PROVIDER_OEMSECRETS_SORT_CRITERIA + PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT + PassEnv PROVIDER_POLLIN_ENABLED + PassEnv EDA_KICAD_CATEGORY_DEPTH # For most configuration files from conf-available/, which are # enabled or disabled at a global level, it is possible to diff --git a/.dockerignore b/.dockerignore index 8929729c..472b1bb3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,8 @@ tests/ docs/ .git +/public/media/* + ###> symfony/framework-bundle ### /.env.local /.env.local.php @@ -42,3 +44,39 @@ yarn-error.log /phpunit.xml .phpunit.result.cache ###< 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 + diff --git a/.env b/.env index 7db81e46..1806e9c6 100644 --- a/.env +++ b/.env @@ -14,6 +14,19 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db" # 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 +# 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 ################################################################################### @@ -29,13 +42,15 @@ 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 +# Set this to 1, if the "download external files" checkbox should be checked by default for new attachments +ATTACHMENT_DOWNLOAD_BY_DEFAULT=0 # Use gravatars for user avatars, when user has no own avatar defined USE_GRAVATAR=0 # The maximum allowed size for attachment files in bytes (you can use M for megabytes and G for gigabytes) # Please note that the php.ini setting upload_max_filesize also limits the maximum size of uploaded files MAX_ATTACHMENT_FILE_SIZE="100M" -# The public reachable URL of this Part-DB installation. This is used for generating links to the website in emails and so on +# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates # This must end with a slash! DEFAULT_URI="https://partdb.changeme.invalid/" @@ -44,6 +59,9 @@ DEFAULT_URI="https://partdb.changeme.invalid/" # Leave this empty, to make all change reasons optional ENFORCE_CHANGE_COMMENTS_FOR="" +# Disable that if you do not want that Part-DB connects to GitHub to check for available updates, or if your server can not connect to the internet +CHECK_FOR_UPDATES=1 + ################################################################################### # Email settings ################################################################################### @@ -71,6 +89,9 @@ HISTORY_SAVE_CHANGED_FIELDS=1 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 +# Save the new data of an element that gets changed or added. This allows an easy comparison of the old and new data on the detail page +# This option only becomes active when HISTORY_SAVE_CHANGED_DATA is set to 1 +HISTORY_SAVE_NEW_DATA=1 ################################################################################### # Error pages settings @@ -81,12 +102,160 @@ 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... ERROR_PAGE_SHOW_HELP=1 +################################################################################## +# Part table settings +################################################################################## + +# The default page size for the part table (set to -1 to show all parts on one page) +TABLE_DEFAULT_PAGE_SIZE=50 +# Configure which columns will be visible by default in the parts table (and in which order). +# This is a comma separated list of column names. See documentation for available values. +TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount + +################################################################################## +# Info provider settings +################################################################################## + +# Digikey Provider: +# You can get your client id and secret from https://developer.digikey.com/ +PROVIDER_DIGIKEY_CLIENT_ID= +PROVIDER_DIGIKEY_SECRET= +# The currency to get prices in +PROVIDER_DIGIKEY_CURRENCY=EUR +# The language to get results in (en, de, fr, it, es, zh, ja, ko) +PROVIDER_DIGIKEY_LANGUAGE=en +# The country to get results for +PROVIDER_DIGIKEY_COUNTRY=DE + +# Farnell Provider: +# You can get your API key from https://partner.element14.com/ +PROVIDER_ELEMENT14_KEY= +# Configure the store domain you want to use. This decides the language and currency of results. You can get a list of available stores from https://partner.element14.com/docs/Product_Search_API_REST__Description +PROVIDER_ELEMENT14_STORE_ID=de.farnell.com + +# TME Provider: +# You can get your API key from https://developers.tme.eu/en/ +PROVIDER_TME_KEY= +PROVIDER_TME_SECRET= +# The currency to get prices in +PROVIDER_TME_CURRENCY=EUR +# The language to get results in (en, de, pl) +PROVIDER_TME_LANGUAGE=en +# The country to get results for +PROVIDER_TME_COUNTRY=DE +# [DEPRECATED] Set this to 1 to get gross prices (including VAT) instead of net prices +# With private API keys, this option cannot be used anymore is ignored by Part-DB. The VAT inclusion depends on your TME account settings. +PROVIDER_TME_GET_GROSS_PRICES=1 + +# Octopart / Nexar Provider: +# You can get your API key from https://nexar.com/api +PROVIDER_OCTOPART_CLIENT_ID= +PROVIDER_OCTOPART_SECRET= +# The currency and country to get prices for (you have to set both to get meaningful results) +# 3 letter ISO currency code (e.g. EUR, USD, GBP) +PROVIDER_OCTOPART_CURRENCY=EUR +# 2 letter ISO country code (e.g. DE, US, GB) +PROVIDER_OCTOPART_COUNTRY=DE +# The number of results to get from Octopart while searching (please note that this counts towards your API limits) +PROVIDER_OCTOPART_SEARCH_LIMIT=10 +# Set to false to include non authorized offers in the results +PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS=1 + +# Mouser Provider API V2: +# You can get your API key from https://www.mouser.it/api-hub/ +PROVIDER_MOUSER_KEY= +# Filter search results by RoHS compliance and stock availability: +# Available options: None | Rohs | InStock | RohsAndInStock +PROVIDER_MOUSER_SEARCH_OPTION='None' +# The number of results to get from Mouser while searching (please note that this value is max 50) +PROVIDER_MOUSER_SEARCH_LIMIT=50 +# It is recommended to leave this set to 'true'. The option is not really good doumented by Mouser: +# Used when searching for keywords in the language specified when you signed up for Search API. +PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true' + +# LCSC Provider: +# LCSC does not provide an offical API, so this used the API LCSC uses to render their webshop. +# LCSC did not intended the use of this API and it could break any time, so use it at your own risk. + +# We dont require an API key for LCSC, just set this to 1 to enable LCSC support +PROVIDER_LCSC_ENABLED=0 +# The currency to get prices in (e.g. EUR, USD, etc.) +PROVIDER_LCSC_CURRENCY=EUR + +# Oemsecrets Provider API 3.0.1: +# You can get your API key from https://www.oemsecrets.com/api +PROVIDER_OEMSECRETS_KEY= +# The country you want the output for +PROVIDER_OEMSECRETS_COUNTRY_CODE=DE +# Available country code are: +# AD, AE, AQ, AR, AT, AU, BE, BO, BR, BV, BY, CA, CH, CL, CN, CO, CZ, DE, DK, EC, EE, EH, +# ES, FI, FK, FO, FR, GB, GE, GF, GG, GI, GL, GR, GS, GY, HK, HM, HR, HU, IE, IM, IN, IS, +# IT, JM, JP, KP, KR, KZ, LI, LK, LT, LU, MC, MD, ME, MK, MT, NL, NO, NZ, PE, PH, PL, PT, +# PY, RO, RS, RU, SB, SD, SE, SG, SI, SJ, SK, SM, SO, SR, SY, SZ, TC, TF, TG, TH, TJ, TK, +# TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VE, VG, VI, VN, VU, WF, YE, +# ZA, ZM, ZW +# +# The currency you want the prices to be displayed in +PROVIDER_OEMSECRETS_CURRENCY=EUR +# Available currency are:AUD, CAD, CHF, CNY, DKK, EUR, GBP, HKD, ILS, INR, JPY, KRW, NOK, +# NZD, RUB, SEK, SGD, TWD, USD +# +# If PROVIDER_OEMSECRETS_ZERO_PRICE is set to 0, distributors with zero prices +# will be discarded from the creation of a new part (set to 1 otherwise) +PROVIDER_OEMSECRETS_ZERO_PRICE=0 +# +# When PROVIDER_OEMSECRETS_SET_PARAM is set to 1 the parameters for the part are generated +# from the description transforming unstructured descriptions into structured parameters; +# each parameter in description should have the form: "...;name1:value1;name2:value2" +PROVIDER_OEMSECRETS_SET_PARAM=1 +# +# This environment variable determines the sorting criteria for product results. +# The sorting process first arranges items based on the provided keyword. +# Then, if set to 'C', it further sorts by completeness (prioritizing items with the most +# detailed information). If set to 'M', it further sorts by manufacturer name. +#If unset or set to any other value, no sorting is performed. +PROVIDER_OEMSECRETS_SORT_CRITERIA=C + + +# Reichelt provider: +# Reichelt.com offers no official API, so this info provider webscrapes the website to extract info +# It could break at any time, use it at your own risk +# We dont require an API key for Reichelt, just set this to 1 to enable Reichelt support +PROVIDER_REICHELT_ENABLED=0 +# The country to get prices for +PROVIDER_REICHELT_COUNTRY=DE +# The language to get results in (en, de, fr, nl, pl, it, es) +PROVIDER_REICHELT_LANGUAGE=en +# Include VAT in prices (set to 1 to include VAT, 0 to exclude VAT) +PROVIDER_REICHELT_INCLUDE_VAT=1 +# The currency to get prices in (only for countries with countries other than EUR) +PROVIDER_REICHELT_CURRENCY=EUR + +# Pollin provider: +# Pollin.de offers no official API, so this info provider webscrapes the website to extract info +# It could break at any time, use it at your own risk +# We dont require an API key for Pollin, just set this to 1 to enable Pollin support +PROVIDER_POLLIN_ENABLED=0 + +################################################################################## +# EDA integration related settings +################################################################################## + +# This value determines the depth of the category tree, that is visible inside KiCad +# 0 means that only the top level categories are visible. Set to a value > 0 to show more levels. +# Set to -1, to show all parts of Part-DB inside a single category in KiCad +EDA_KICAD_CATEGORY_DEPTH=0 + ################################################################################### # SAML Single sign on-settings ################################################################################### # 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 @@ -113,7 +282,7 @@ 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 -SAMLP_SP_PRIVATE_KEY="MIIE..." +SAML_SP_PRIVATE_KEY="MIIE..." ###################################################################################### @@ -126,6 +295,9 @@ DEMO_MODE=0 # In that case all URL contains the index.php front controller in URL NO_URL_REWRITE_AVAILABLE=0 +# Set to 1, if Part-DB should redirect all HTTP requests to HTTPS. You dont need to configure this, if your webserver already does this. +REDIRECT_TO_HTTPS=0 + # If you want to use fixer.io for currency conversion, you have to set this to your API key FIXER_API_KEY=CHANGEME @@ -136,9 +308,11 @@ BANNER="" APP_ENV=prod APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 +# Set this to zero, if you want to disable the year 2038 bug check on 32-bit systems (it will cause errors with current 32-bit PHP versions) +DISABLE_YEAR2038_BUG_CHECK=0 # Set the trusted IPs here, when using an reverse proxy -#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 +#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)$' @@ -147,3 +321,7 @@ APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 # 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 ### diff --git a/.env.dev b/.env.dev new file mode 100644 index 00000000..e69de29b diff --git a/.env.test b/.env.test index a9b0cccf..3dbece81 100644 --- a/.env.test +++ b/.env.test @@ -5,5 +5,9 @@ SYMFONY_DEPRECATIONS_HELPER=999999 PANTHER_APP_ENV=panther 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 -DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db \ No newline at end of file +#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 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6a0a3e1f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# For sh files, always use LF line endings +*.sh text eol=lf \ No newline at end of file diff --git a/.github/assets/legacy_import/db_jbtronics.sql b/.github/assets/legacy_import/db_jbtronics.sql new file mode 100644 index 00000000..5237461f --- /dev/null +++ b/.github/assets/legacy_import/db_jbtronics.sql @@ -0,0 +1,752 @@ +-- 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 */; diff --git a/.github/assets/legacy_import/db_minimal.sql b/.github/assets/legacy_import/db_minimal.sql new file mode 100644 index 00000000..7f29d5dc --- /dev/null +++ b/.github/assets/legacy_import/db_minimal.sql @@ -0,0 +1,736 @@ +-- 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 */; diff --git a/.github/assets/legacy_import/test_legacy_import.sh b/.github/assets/legacy_import/test_legacy_import.sh new file mode 100644 index 00000000..54637c4c --- /dev/null +++ b/.github/assets/legacy_import/test_legacy_import.sh @@ -0,0 +1,54 @@ +# +# 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 . +# + +#!/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 \ No newline at end of file diff --git a/.github/workflows/assets_artifact_build.yml b/.github/workflows/assets_artifact_build.yml index 0c3c1568..0bbfe432 100644 --- a/.github/workflows/assets_artifact_build.yml +++ b/.github/workflows/assets_artifact_build.yml @@ -19,14 +19,22 @@ jobs: APP_ENV: prod steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - 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@v3 + - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -40,7 +48,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v3 + - 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 }} @@ -49,7 +57,7 @@ jobs: ${{ runner.os }}-yarn- - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '18' @@ -69,13 +77,13 @@ jobs: run: zip -r /tmp/partdb_assets.zip public/build/ vendor/ - name: Upload assets artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Only dependencies and built assets path: /tmp/partdb_assets.zip - name: Upload full artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Full Part-DB including dependencies and built assets path: /tmp/partdb_with_assets.zip diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index f00b30bf..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [master, ] - pull_request: - # The branches below must be a subset of the branches above - branches: [master] - schedule: - - cron: '0 14 * * 3' - -jobs: - analyse: - name: Analyse - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 79d01893..64287d83 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -17,11 +17,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Docker meta id: docker_meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: # list of Docker images to use as base name for tags images: | @@ -49,23 +49,23 @@ jobs: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: platforms: 'arm64,arm' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v7 diff --git a/.github/workflows/docker_frankenphp.yml b/.github/workflows/docker_frankenphp.yml new file mode 100644 index 00000000..d8cd0695 --- /dev/null +++ b/.github/workflows/docker_frankenphp.yml @@ -0,0 +1,77 @@ +name: Docker Image Build (FrankenPHP) + +on: + #schedule: + # - cron: '0 10 * * *' # everyday at 10am + push: + branches: + - '**' + - '!l10n_**' + tags: + - 'v*.*.*' + - 'v*.*.*-**' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Docker meta + id: docker_meta + uses: docker/metadata-action@v5 + with: + # list of Docker images to use as base name for tags + images: | + partdborg/part-db + # Mark the image build from master as latest (as we dont have really releases yet) + tags: | + type=edge,branch=master + type=ref,event=branch, + type=ref,event=tag, + type=schedule + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=ref,event=branch + type=ref,event=pr + labels: | + org.opencontainers.image.source=${{ github.event.repository.clone_url }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.title=Part-DB + org.opencontainers.image.description=Part-DB is a web application for managing electronic components and your inventory. + org.opencontainers.image.url=https://github.com/Part-DB/Part-DB-server + org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-server + org.opencontainers.image.authors=Jan Böhmer + org.opencontainers.licenses=AGPL-3.0-or-later + + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: 'arm64,arm' + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - + name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile-frankenphp + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 8a735f52..20150b28 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -16,14 +16,22 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - 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@v3 + - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -38,18 +46,20 @@ jobs: - name: Lint twig templates run: ./bin/console lint:twig templates --env=prod - - - name: Lint translations - run: ./bin/console lint:xliff translations + + # This causes problems with emtpy language files + #- name: Lint translations + # run: ./bin/console lint:xliff translations - name: Check dependencies for security uses: symfonycorp/security-checker-action@v5 - name: Check doctrine mapping 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 - run: ./bin/console cache:clear --env dev + run: php -d xdebug.max_nesting_level=1000 ./bin/console cache:clear --env dev - name: Run PHPstan run: composer phpstan diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6011db29..8e6ea54c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,13 +13,13 @@ on: jobs: phpunit: 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 strategy: + fail-fast: false matrix: - php-versions: [ '7.4', '8.0', '8.1', '8.2' ] - db-type: [ 'mysql', 'sqlite' ] + php-versions: [ '8.1', '8.2', '8.3', '8.4' ] + db-type: [ 'mysql', 'sqlite', 'postgres' ] env: # Note that we set DATABASE URL later based on our db-type matrix value @@ -27,30 +27,45 @@ jobs: SYMFONY_DEPRECATIONS_HELPER: disabled PHP_VERSION: ${{ matrix.php-versions }} DB_TYPE: ${{ matrix.db-type }} + CHECK_FOR_UPDATES: false # Disable update checks for tests steps: - name: Set Database env for MySQL - run: echo "DATABASE_URL=mysql://root:root@127.0.0.1:3306/test" >> $GITHUB_ENV + run: echo "DATABASE_URL=mysql://root:root@127.0.0.1:3306/partdb?serverVersion=8.0.35" >> $GITHUB_ENV if: matrix.db-type == 'mysql' - name: Set Database env for SQLite run: echo "DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"" >> $GITHUB_ENV 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 - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} coverage: pcov - extensions: mbstring, intl, gd, xsl, gmp, bcmath + ini-values: xdebug.max_nesting_level=1000 + extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr - name: Start MySQL run: sudo systemctl start mysql.service + if: matrix.db-type == 'mysql' - #- name: Setup 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 # uses: mirromutth/mysql-action@v1.1 # with: # mysql version: 5.7 @@ -63,7 +78,7 @@ jobs: id: composer-cache run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -74,7 +89,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v3 + - 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 }} @@ -86,7 +101,7 @@ jobs: run: composer install --prefer-dist --no-progress - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '18' @@ -98,26 +113,24 @@ jobs: - name: Create DB run: php bin/console --env test doctrine:database:create --if-not-exists -n - 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' + if: matrix.db-type == 'mysql' || matrix.db-type == 'postgres' - name: Do migrations 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 - run: php bin/console --env test doctrine:fixtures:load -n --purger reset_autoincrement_purger + run: php bin/console --env test partdb:fixtures:load -n - name: Run PHPunit and generate coverage run: ./bin/phpunit --coverage-clover=coverage.xml - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: env_vars: PHP_VERSION,DB_TYPE + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true - name: Test app:clean-attachments run: php bin/console partdb:attachments:clean-unused -n @@ -130,4 +143,12 @@ jobs: - name: Test check-requirements command 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 diff --git a/.gitignore b/.gitignore index 1d28a771..b726f64c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ yarn-error.log /phpunit.xml .phpunit.result.cache ###< phpunit/phpunit ### + +###> phpstan/phpstan ### +phpstan.neon +###< phpstan/phpstan ### diff --git a/Dockerfile b/Dockerfile index d222b4c8..0f909f16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,64 @@ -FROM debian:bullseye-slim +ARG BASE_IMAGE=debian:bookworm-slim +ARG PHP_VERSION=8.3 + +FROM ${BASE_IMAGE} AS base +ARG PHP_VERSION # Install needed dependencies for PHP build #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 \ # && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* -RUN apt-get update && apt-get -y install apt-transport-https lsb-release ca-certificates curl zip \ +RUN apt-get update && apt-get -y install \ + 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 \ && 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 install -y apache2 php8.1 php8.1-fpm php8.1-opcache php8.1-curl php8.1-gd php8.1-mbstring php8.1-xml php8.1-bcmath php8.1-intl php8.1-zip php8.1-xsl php8.1-sqlite3 php8.1-mysql gpg \ - && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*; - -ENV APACHE_CONFDIR /etc/apache2 -ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars - + && apt-get install -y \ + apache2 \ + php${PHP_VERSION} \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-opcache \ + 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 -RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html + && mkdir -p /var/www/html \ + && chown -R www-data:www-data /var/www/html \ +# delete the "index.html" that installing Apache drops in here + && rm -rvf /var/www/html/* + +# Install node and yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + curl -sL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get update && apt-get install -y \ + nodejs \ + yarn \ + && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* + +# Install composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +ENV APACHE_CONFDIR=/etc/apache2 +ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars # Configure apache 2 (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/Dockerfile) # generically convert lines like @@ -27,78 +69,94 @@ RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html # so that they can be overridden at runtime ("-e APACHE_RUN_USER=...") RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$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 ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \ ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \ ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \ - ln -sfT /dev/stderr /var/log/php8.1-fpm.log; \ chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR"; -# Enable php-fpm -RUN a2enmod proxy_fcgi setenvif && a2enconf php8.1-fpm +# --- + +FROM scratch AS apache-config +ARG PHP_VERSION +# Configure php-fpm to log to stdout of the container (stdout of PID 1) +# We have to use /proc/1/fd/1 because /dev/stdout or /proc/self/fd/1 does not point to the container stdout (because we use apache as entrypoint) +# We also disable the clear_env option to allow the use of environment variables in php-fpm +COPY <'; \ - echo '\tSetHandler application/x-httpd-php'; \ - echo ''; \ - echo; \ - echo 'DirectoryIndex disabled'; \ - echo 'DirectoryIndex index.php index.html'; \ - echo; \ - echo ''; \ - echo '\tOptions -Indexes'; \ - echo '\tAllowOverride All'; \ - echo ''; \ - } | tee "$APACHE_CONFDIR/conf-available/docker-php.conf" \ - && a2enconf docker-php +COPY < + SetHandler application/x-httpd-php + + +DirectoryIndex disabled +DirectoryIndex index.php index.html + + + Options -Indexes + AllowOverride All + +EOF # Enable opcache and configure it recommended for symfony (see https://symfony.com/doc/current/performance.html) -RUN \ - { \ - echo 'opcache.memory_consumption=256'; \ - echo 'opcache.max_accelerated_files=20000'; \ - echo 'opcache.validate_timestamp=0'; \ - # Configure Realpath cache for performance - echo 'realpath_cache_size=4096K'; \ - echo 'realpath_cache_ttl=600'; \ - } > /etc/php/8.1/fpm/conf.d/symfony-recommended.ini +COPY < /etc/php/8.1/fpm/conf.d/partdb.ini +COPY < ## Features -* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer -and multiple store locations and price information. Parts can be grouped using tags. You can associate various files like datasheets or pictures with the parts. -* Multi-Language support (currently German, English, Russian, Japanese and French (experimental)) + +* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer, + and multiple store locations and price information. Parts can be grouped using tags. You can associate various files + like datasheets or pictures with the parts. +* 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 -* 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. Password reset via email can be setuped. -* Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) -* Import/Export system for parts and datastructure. BOM import for projects from KiCAD is supported. -* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB -* Event log: Track what changes happens 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 and SQLite supported as database backends +* 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. + Password reset via email can be set up. +* Optional support for single sign-on (SSO) via SAML (using an intermediate service + like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) +* Import/Export system for parts and data structure. BOM import for projects from KiCAD is supported. +* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build + 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 multiple currencies and automatic update of exchange rates supported * 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 makerspaces, where many users have should have (controlled) access to the shared inventory. +With these features, Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory, +or maker spaces, where many users should have (controlled) access to the shared inventory. Part-DB is also used by small companies and universities for managing their inventory. ## Requirements - * A **web server** (like Apache2 or nginx) that is capable of running [Symfony 5](https://symfony.com/doc/current/reference/requirements.html), - this includes a minimum PHP version of **PHP 7.4** - * A **MySQL** (at least 5.7) /**MariaDB** (at least 10.2.2) database server if you do not want to use SQLite. - * Shell access to your server is highly suggested! - * For building the client side assets **yarn** and **nodejs** is needed. - + +* A **web server** (like Apache2 or nginx) that is capable of + running [Symfony 6](https://symfony.com/doc/current/reference/requirements.html), + this includes a minimum PHP version of **PHP 8.1** +* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite. +* Shell access to your server is highly recommended! +* For building the client-side assets **yarn** and **nodejs** (>= 18.0) is needed. + ## Installation -If you want to upgrade your legacy (< 1.0.0) version of Part-DB to this version, please read [this](https://docs.part-db.de/upgrade_legacy.html) first. -*Hint:* A docker image is available under [jbtronics/part-db1](https://hub.docker.com/r/jbtronics/part-db1). How to set up Part-DB via docker is described [here](https://docs.part-db.de/installation/installation_docker.html). +If you want to upgrade your legacy (< 1.0.0) version of Part-DB to this version, please +read [this](https://docs.part-db.de/upgrade_legacy.html) first. -**Below you find some very rough outline of the installation process, see [here](https://docs.part-db.de/installation/) for a detailed guide how to install Part-DB.** +*Hint:* A docker image is available under [jbtronics/part-db1](https://hub.docker.com/r/jbtronics/part-db1). How to set +up Part-DB via docker is described [here](https://docs.part-db.de/installation/installation_docker.html). + +**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. -2. Configure your webserver to serve from the `public/` folder. See [here](https://symfony.com/doc/current/setup/web_server_configuration.html) -for additional information. +2. Configure your webserver to serve from the `public/` folder. + See [here](https://symfony.com/doc/current/setup/web_server_configuration.html) + for additional information. 3. Copy the global config file `cp .env .env.local` and edit `.env.local`: * 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 (see [here](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url)) for the format. - In bigger instances with concurrent accesses, MySQL is more performant. This can not be changed easily later, so choose wisely. + * 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. + 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` -5. If you have put Part-DB into a sub-directory on your server (like `part-db/`), you have to edit the file -`webpack.config.js` and uncomment the lines (remove the `//` before the lines) `.setPublicPath('/part-db/build')` (line 43) and - `.setManifestKeyPrefix('build/')` (line 44). You have to replace `/part-db` with your own path on line 44. -6. Install client side dependencies and build it: `yarn install` and `yarn build` -7. _Optional_ (speeds up first load): Warmup cache: `php bin/console cache:warmup` -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. -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). - Run `php bin/console cache:clear` when you changed something. -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. - 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. +5. Install client side dependencies and build it: `yarn install` and `yarn build` +6. _Optional_ (speeds up first load): Warmup cache: `php bin/console cache:warmup` +7. 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**: + These 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 + you installed/upgraded Part-DB. Check if `partdb.default_currency` matches your mainly used currency (this can not be + changed after creating price information). + Run `php bin/console cache:clear` when you change something. +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 and repeat the steps 4. to 7. -Normally a random password is generated when the admin user is created during inital database creation, -however you can set the inital admin password, by setting the `INITIAL_ADMIN_PW` env var. +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. +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 -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 +will get HTTP/HTTPS mixup and wrong hostnames. +If the reverse proxy is on a different server (or it cannot access Part-DB via localhost) you have to set +the `TRUSTED_PROXIES` env variable to match your reverse proxy's IP address (or IP block). You can do this in +your `.env.local` or (when using docker) in your `docker-compose.yml` file. ## Donate for development + 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 + * [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 * [Fontawesome](https://fontawesome.com/): Used as icon set -* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend Javascript +* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend + Javascript ## Authors -* **Jan Böhmer** - *Inital work* - [Github](https://github.com/jbtronics/) -See also the list of [contributors](https://github.com/Part-DB/Part-DB-server/graphs/contributors) who participated in this project. +* **Jan Böhmer** - *Initial work* - [GitHub](https://github.com/jbtronics/) + +See also the list of [contributors](https://github.com/Part-DB/Part-DB-server/graphs/contributors) who participated in +this project. Based on the original Part-DB by Christoph Lechner and K. Jacobs ## License + 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) as long as you publish the source code for every change you make under the AGPL, too. diff --git a/VERSION b/VERSION index f0bb29e7..511a76e6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0 +1.17.1 diff --git a/assets/bootstrap.js b/assets/bootstrap.js index 58308a6b..c26293e2 100644 --- a/assets/bootstrap.js +++ b/assets/bootstrap.js @@ -4,8 +4,7 @@ import { startStimulusApp } from '@symfony/stimulus-bridge'; export const app = startStimulusApp(require.context( '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', true, - /\.(j|t)sx?$/ + /\.[jt]sx?$/ )); - // register any custom, 3rd party controllers here // app.register('some_controller_name', SomeImportedController); diff --git a/assets/ckeditor/html_label.js b/assets/ckeditor/html_label.js index b5ca5c3e..9040f3c7 100644 --- a/assets/ckeditor/html_label.js +++ b/assets/ckeditor/html_label.js @@ -181,7 +181,8 @@ Editor.defaultConfig = { 'DejaVu Serif, serif', 'Helvetica, Arial, sans-serif', 'Times New Roman, Times, serif', - 'Courier New, Courier, monospace' + 'Courier New, Courier, monospace', + 'Unifont, monospace', ], supportAllValues: true }, diff --git a/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js index 91d20ace..37e1dcbe 100644 --- a/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js +++ b/assets/ckeditor/plugins/PartDBLabel/PartDBLabelUI.js @@ -76,6 +76,7 @@ const PLACEHOLDERS = [ ['[[FOOTPRINT_FULL]]', 'Footprint (Full path)'], ['[[MASS]]', 'Mass'], ['[[MPN]]', 'Manufacturer Product Number (MPN)'], + ['[[IPN]]', 'Internal Part Number (IPN)'], ['[[TAGS]]', 'Tags'], ['[[M_STATUS]]', 'Manufacturing status'], ['[[DESCRIPTION]]', 'Description'], @@ -84,6 +85,9 @@ const PLACEHOLDERS = [ ['[[COMMENT_T]]', 'Comment (plain text)'], ['[[LAST_MODIFIED]]', 'Last modified 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'], ] }, { @@ -124,6 +128,8 @@ const PLACEHOLDERS = [ ['[[BARCODE_QR]]', 'QR code linking to this element'], ['[[BARCODE_C128]]', 'Code 128 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'], ] }, { diff --git a/assets/ckeditor/plugins/PartDBLabel/lang/de.js b/assets/ckeditor/plugins/PartDBLabel/lang/de.js index 3cc43dc5..748b1607 100644 --- a/assets/ckeditor/plugins/PartDBLabel/lang/de.js +++ b/assets/ckeditor/plugins/PartDBLabel/lang/de.js @@ -39,6 +39,7 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, { 'Footprint (Full path)': 'Footprint (Vollständiger Pfad)', 'Mass': 'Gewicht', 'Manufacturer Product Number (MPN)': 'Hersteller Produktnummer (MPN)', + 'Internal Part Number (IPN)': 'Internal Part Number (IPN)', 'Tags': 'Tags', 'Manufacturing status': 'Herstellungsstatus', 'Description': 'Beschreibung', @@ -47,6 +48,9 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, { 'Comment (plain text)': 'Kommentar (Nur-Text)', 'Last modified datetime': 'Zuletzt geändert', '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 name': 'Lot Name', @@ -65,6 +69,8 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, { '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 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', 'Name': 'Name', diff --git a/assets/ckeditor/plugins/special_characters_emoji.js b/assets/ckeditor/plugins/special_characters_emoji.js index 9877e6f6..1d4ec000 100644 --- a/assets/ckeditor/plugins/special_characters_emoji.js +++ b/assets/ckeditor/plugins/special_characters_emoji.js @@ -30,9 +30,73 @@ export default class SpecialCharactersEmoji extends Plugin { const editor = this.editor; const specialCharsPlugin = editor.plugins.get('SpecialCharacters'); + //Add greek characters to special characters + specialCharsPlugin.addItems('Greek', this.getGreek()); + + //Add Emojis to special characters specialCharsPlugin.addItems('Emoji', this.getEmojis()); } + getGreek() { + return [ + { 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: '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: 'Ϻ' }, + ]; + } + getEmojis() { //Map our emoji data to the format the plugin expects return emoji.map(emoji => { diff --git a/assets/controllers/common/darkmode_controller.js b/assets/controllers/common/darkmode_controller.js index e7c18e67..71111166 100644 --- a/assets/controllers/common/darkmode_controller.js +++ b/assets/controllers/common/darkmode_controller.js @@ -18,43 +18,118 @@ */ import {Controller} from "@hotwired/stimulus"; -import Darkmode from "darkmode-js/src"; -import "darkmode-js" export default class extends Controller { - _darkmode; - connect() { - if (typeof window.getComputedStyle(document.body).mixBlendMode == 'undefined') { - console.warn("The browser does not support mix blend mode. Darkmode will not work."); + this.setMode(this.getMode()); + document.querySelectorAll('input[name="darkmode"]').forEach((radio) => { + 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; } - try { - const darkmode = new Darkmode(); - this._darkmode = darkmode; - - //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); + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + this._enableDarkmode(); + } else { + this._disableDarkmode(); } - + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { + console.log('Prefered color scheme changed to ' + event.matches ? 'dark' : 'light'); + this._setDarkmodeAuto(); + }); } - _showWidget() { - this.element.classList.remove('hidden'); - } - - toggleDarkmode() { - this._darkmode.toggle(); + /** + * Check if darkmode is activated + * @return {boolean} + */ + isDarkmodeActivated() { + return document.documentElement.getAttribute('data-bs-theme') === 'dark'; } } \ No newline at end of file diff --git a/assets/controllers/common/hide_sidebar_controller.js b/assets/controllers/common/hide_sidebar_controller.js index d4c1b5e2..4be304ff 100644 --- a/assets/controllers/common/hide_sidebar_controller.js +++ b/assets/controllers/common/hide_sidebar_controller.js @@ -88,5 +88,8 @@ export default class extends Controller { } else { this.hideSidebar(); } + + //Hide the tootip on the button + this._toggle_button.blur(); } } \ No newline at end of file diff --git a/assets/controllers/common/markdown_controller.js b/assets/controllers/common/markdown_controller.js index 08c013ca..b6ef0034 100644 --- a/assets/controllers/common/markdown_controller.js +++ b/assets/controllers/common/markdown_controller.js @@ -20,16 +20,26 @@ 'use strict'; 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 "../../css/app/markdown.css"; -export default class extends Controller { +export default class MarkdownController extends Controller { + + static _marked = new Marked([ + { + gfm: true, + }, + gfmHeadingId(), + mangle(), + ]) + ; connect() { - this.configureMarked(); this.render(); //Dispatch an event that we are now finished @@ -43,7 +53,7 @@ export default class extends Controller { let raw = this.element.dataset['markdown']; //Apply purified parsed markdown - this.element.innerHTML = DOMPurify.sanitize(marked(this.unescapeHTML(raw))); + this.element.innerHTML = DOMPurify.sanitize(MarkdownController._marked.parse(this.unescapeHTML(raw))); for(let a of this.element.querySelectorAll('a')) { //Mark all links as external @@ -79,10 +89,23 @@ export default class extends Controller { /** * Configure the marked parser */ - configureMarked() + /*static newMarked() { + const marked = new Marked([ + { + gfm: true, + }, + gfmHeadingId(), + mangle(), + ]) + ; + + marked.use(mangle()); + marked.use(gfmHeadingId({ + })); + marked.setOptions({ gfm: true, }); - } + }*/ } \ No newline at end of file diff --git a/assets/controllers/elements/attachment_autocomplete_controller.js b/assets/controllers/elements/attachment_autocomplete_controller.js index fe44baee..f8bc301e 100644 --- a/assets/controllers/elements/attachment_autocomplete_controller.js +++ b/assets/controllers/elements/attachment_autocomplete_controller.js @@ -23,6 +23,12 @@ 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) + export default class extends Controller { _tomSelect; @@ -46,6 +52,12 @@ export default class extends Controller { } return '
' + escape(data.label) + '
'; } + }, + plugins: { + 'autoselect_typed': {}, + 'click_to_edit': {}, + 'clear_button': {}, + "restore_on_backspace": {} } }; diff --git a/assets/controllers/elements/ckeditor_controller.js b/assets/controllers/elements/ckeditor_controller.js index 4de536fe..079ee2ad 100644 --- a/assets/controllers/elements/ckeditor_controller.js +++ b/assets/controllers/elements/ckeditor_controller.js @@ -53,6 +53,7 @@ export default class extends Controller { const config = { language: language, + licenseKey: "GPL", } const watchdog = new EditorWatchdog(); @@ -70,7 +71,9 @@ export default class extends Controller { editor_div.classList.add(...new_classes.split(",")); } - console.log(editor); + //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 => { console.error(error); diff --git a/assets/controllers/elements/collection_type_controller.js b/assets/controllers/elements/collection_type_controller.js index 4f46cb0d..8b816f30 100644 --- a/assets/controllers/elements/collection_type_controller.js +++ b/assets/controllers/elements/collection_type_controller.js @@ -61,7 +61,7 @@ export default class extends Controller { if(!prototype) { 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 permsissions to create a new element. (No protoype element is set)"); + bootbox.alert("You do not have the permissions to create a new element. (No protoype element is set)"); return; } @@ -75,13 +75,49 @@ export default class extends Controller { //Insert new html after the last child element //If the table has a tbody, insert it there + //Afterwards return the newly created row if(targetTable.tBodies[0]) { targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr); + return targetTable.tBodies[0].lastElementChild; } else { //Otherwise just insert it 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 * @param event diff --git a/assets/controllers/elements/datatables/datatables_controller.js b/assets/controllers/elements/datatables/datatables_controller.js index 67e8ef6e..5a50623d 100644 --- a/assets/controllers/elements/datatables/datatables_controller.js +++ b/assets/controllers/elements/datatables/datatables_controller.js @@ -24,18 +24,25 @@ import 'datatables.net-bs5/css/dataTables.bootstrap5.css' import 'datatables.net-buttons-bs5/css/buttons.bootstrap5.css' import 'datatables.net-fixedheader-bs5/css/fixedHeader.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 import 'datatables.net-bs5'; import 'datatables.net-buttons-bs5'; import 'datatables.net-buttons/js/buttons.colVis.js'; import 'datatables.net-fixedheader-bs5'; -import 'datatables.net-select-bs5'; import 'datatables.net-colreorder-bs5'; import 'datatables.net-responsive-bs5'; 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'; export default class extends Controller { @@ -65,12 +72,19 @@ export default class extends Controller { localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) ); } - stateLoadCallback(settings) { - const data = JSON.parse( localStorage.getItem(this.getStateSaveKey()) ); + stateLoadCallback() { + const json = 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 - data.start = 0; + delete data.start; + //Reset the data length to the default value by deleting the length property + delete data.length; } return data; @@ -88,6 +102,19 @@ 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 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 = { colReorder: true, responsive: true, @@ -97,7 +124,7 @@ export default class extends Controller { }, buttons: [{ "extend": 'colvis', - 'className': 'mr-2 btn-light', + 'className': 'mr-2 btn-outline-secondary', 'columns': ':not(.no-colvis)', "text": "" }], @@ -112,7 +139,7 @@ export default class extends Controller { if(this.isSelectable()) { options.select = { style: 'multi+shift', - selector: 'td.select-checkbox' + selector: 'td.dt-select', }; } @@ -123,6 +150,28 @@ export default class extends Controller { 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 promise.then((dt) => { const event = new CustomEvent(EVENT_DT_LOADED, {bubbles: true}); @@ -144,27 +193,6 @@ export default class extends Controller { dt.fixedHeader.headerOffset($("#navbar").outerHeight()); }); - //Register event handler to selectAllRows checkbox if available - if (this.isSelectable()) { - promise.then((dt) => { - const selectAllCheckbox = this.element.querySelector('thead th.select-checkbox'); - selectAllCheckbox.addEventListener('click', () => { - if(selectAllCheckbox.parentElement.classList.contains('selected')) { - dt.rows().deselect(); - selectAllCheckbox.parentElement.classList.remove('selected'); - } else { - dt.rows().select(); - selectAllCheckbox.parentElement.classList.add('selected'); - } - }); - - //When any row is deselected, also deselect the selectAll checkbox - dt.on('deselect.dt', () => { - selectAllCheckbox.parentElement.classList.remove('selected'); - }); - }); - } - //Allow to further configure the datatable promise.then(this._afterLoaded.bind(this)); @@ -203,4 +231,16 @@ export default class extends Controller { 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(); + } + } diff --git a/assets/controllers/elements/delete_btn_controller.js b/assets/controllers/elements/delete_btn_controller.js index 21a25328..9ab15f7d 100644 --- a/assets/controllers/elements/delete_btn_controller.js +++ b/assets/controllers/elements/delete_btn_controller.js @@ -29,62 +29,47 @@ export default class extends Controller 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) { //If a user has not already confirmed the deletion, just let turbo do its work - if(this._confirmed) { + if (this._confirmed) { this._confirmed = false; return; } //Prevent turbo from doing its work event.preventDefault(); + event.stopPropagation(); const message = this.element.dataset.deleteMessage; const title = this.element.dataset.deleteTitle; - const form = this.element; + //Use event target, to find the form, where the submit button was clicked + const form = event.target; + const submitter = event.submitter; 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({ message: message, title: title, callback: function (result) { //If the dialog was confirmed, then submit the form. if (result) { + //Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form 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 { that._confirmed = false; } diff --git a/assets/controllers/elements/json_formatter_controller.js b/assets/controllers/elements/json_formatter_controller.js new file mode 100644 index 00000000..c72814e3 --- /dev/null +++ b/assets/controllers/elements/json_formatter_controller.js @@ -0,0 +1,40 @@ +/* + * 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 . + */ + + +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()); + } +} \ No newline at end of file diff --git a/assets/controllers/elements/link_confirm_controller.js b/assets/controllers/elements/link_confirm_controller.js new file mode 100644 index 00000000..3d59b492 --- /dev/null +++ b/assets/controllers/elements/link_confirm_controller.js @@ -0,0 +1,72 @@ +/* + * 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 . + */ + +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; + } + } + }); + } +} \ No newline at end of file diff --git a/assets/controllers/elements/localStorage_checkbox_controller.js b/assets/controllers/elements/localStorage_checkbox_controller.js new file mode 100644 index 00000000..70ef877d --- /dev/null +++ b/assets/controllers/elements/localStorage_checkbox_controller.js @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +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; + } +} diff --git a/assets/controllers/elements/part_search_controller.js b/assets/controllers/elements/part_search_controller.js new file mode 100644 index 00000000..c33cece0 --- /dev/null +++ b/assets/controllers/elements/part_search_controller.js @@ -0,0 +1,200 @@ +/* + * 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 . + */ + +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`${trans(STATISTICS_PARTS)} +
`; + }, + item({item, components, html}) { + const details_url = part_detail_uri_template.replace('__ID__', item.id); + + return html` + +
+
+ ${item.name} +
+
+
+ + ${components.Highlight({hit: item, attribute: 'name'})} + +
+
+ ${components.Highlight({hit: item, attribute: 'description'})} + ${item.category ? html`

${components.Highlight({hit: item, attribute: 'category'})}

` : ""} + ${item.footprint ? html`

${components.Highlight({hit: item, attribute: 'footprint'})}

` : ""} +
+
+
+
+ `; + }, + }, + }, + ]; + }, + }); + + //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); + }); + } + + } +} \ No newline at end of file diff --git a/assets/controllers/elements/part_select_controller.js b/assets/controllers/elements/part_select_controller.js index c7507636..5abd5ba3 100644 --- a/assets/controllers/elements/part_select_controller.js +++ b/assets/controllers/elements/part_select_controller.js @@ -27,7 +27,7 @@ export default class extends Controller { } let tmp = '
' + - "
" + + "
" + (data.image ? "" : "") + "
" + "
" + diff --git a/assets/controllers/elements/password_strength_estimate_controller.js b/assets/controllers/elements/password_strength_estimate_controller.js new file mode 100644 index 00000000..0fc9c578 --- /dev/null +++ b/assets/controllers/elements/password_strength_estimate_controller.js @@ -0,0 +1,123 @@ +/* + * 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 . + */ + + +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(" ")); + } +} \ No newline at end of file diff --git a/assets/controllers/elements/static_file_autocomplete_controller.js b/assets/controllers/elements/static_file_autocomplete_controller.js new file mode 100644 index 00000000..31ca0314 --- /dev/null +++ b/assets/controllers/elements/static_file_autocomplete_controller.js @@ -0,0 +1,106 @@ +/* + * 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 . + */ + +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; + +import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit' +import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed' + +TomSelect.define('click_to_edit', TomSelect_click_to_edit) +TomSelect.define('autoselect_typed', TomSelect_autoselect_typed) + +/** + * This is the frontend controller for StaticFileAutocompleteType form element. + * Basically it loads a text file from the given url (via data-url) and uses it as a source for the autocomplete. + * The file is just a list of strings, one per line, which will be used as the autocomplete options. + * Lines starting with # will be ignored. + */ +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + persistent: false, + create: true, + maxItems: 1, + maxOptions: 100, + createOnBlur: true, + selectOnTab: true, + valueField: 'text', + searchField: 'text', + orderField: 'text', + + //This a an ugly solution to disable the delimiter parsing of the TomSelect plugin + delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING', + plugins: { + 'autoselect_typed': {}, + 'click_to_edit': {}, + 'clear_button': {}, + 'restore_on_backspace': {} + } + }; + + if (this.element.dataset.url) { + const url = this.element.dataset.url; + settings.load = (query, callback) => { + const self = this; + if (self.loading > 1) { + callback(); + return; + } + + fetch(url) + .then(response => response.text()) + .then(text => { + // Convert the text file to array + let lines = text.split("\n"); + //Remove all lines beginning with # + lines = lines.filter(x => !x.startsWith("#")); + + //Convert the array to an object, where each line is in the text field + lines = lines.map(x => { + return {text: x}; + }); + + + //Unset the load function to prevent endless recursion + self._tomSelect.settings.load = null; + + callback(lines); + }).catch(() => { + callback(); + }); + }; + } + + this._tomSelect = new TomSelect(this.element, settings); + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } + +} diff --git a/assets/controllers/elements/structural_entity_select_controller.js b/assets/controllers/elements/structural_entity_select_controller.js index 38480cfa..a1114a97 100644 --- a/assets/controllers/elements/structural_entity_select_controller.js +++ b/assets/controllers/elements/structural_entity_select_controller.js @@ -22,6 +22,10 @@ import '../../css/components/tom-select_extensions.css'; import TomSelect from "tom-select"; 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 { _tomSelect; @@ -36,12 +40,20 @@ export default class extends Controller { const allowAdd = this.element.getAttribute("data-allow-add") === "true"; const addHint = this.element.getAttribute("data-add-hint") ?? ""; + + + let settings = { allowEmptyOption: true, selectOnTab: true, maxOptions: null, - create: allowAdd, - createFilter: /\D/, //Must contain a non-digit character, otherwise they would be recognized as DB ID + create: allowAdd ? this.createItem.bind(this) : false, + createFilter: this.createFilter.bind(this), + + // This three options allow us to paste element names with commas: (see issue #538) + maxItems: 1, + delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$", + splitOn: null, searchField: [ {field: "text", weight : 2}, @@ -52,15 +64,108 @@ export default class extends Controller { render: { item: this.renderItem.bind(this), option: this.renderOption.bind(this), - option_create: function(data, escape) { + option_create: (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 '
 ' + escape(data.input) + '… ' + '(' + addHint +')' + '
'; }, + }, + + //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); + //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() { @@ -78,14 +183,27 @@ export default class extends Controller { } if (data.short) { - return '
' + escape(data.short) + '
'; + let short = escape(data.short) + + //Make text italic, if the item is not yet in the DB + if (data.not_in_db_yet) { + short = '' + short + ''; + } + + return '
' + short + '
'; } let name = ""; if (data.parent) { name += escape(data.parent) + " → "; } - name += "" + escape(data.text) + ""; + + if (data.not_in_db_yet) { + //Not yet added items are shown italic and with a badge + name += "" + escape(data.text) + "" + "" + trans(ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB) + ""; + } else { + name += "" + escape(data.text) + ""; + } return '
' + (data.image ? "" : "") + name + '
'; } diff --git a/assets/controllers/elements/tagsinput_controller.js b/assets/controllers/elements/tagsinput_controller.js index acfcd0fa..1f10c457 100644 --- a/assets/controllers/elements/tagsinput_controller.js +++ b/assets/controllers/elements/tagsinput_controller.js @@ -23,14 +23,21 @@ 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) + export default class extends Controller { _tomSelect; connect() { let settings = { plugins: { - remove_button:{ - } + remove_button:{}, + 'autoselect_typed': {}, + 'click_to_edit': {}, }, persistent: false, selectOnTab: true, diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index bdb150c2..bb64839c 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -81,31 +81,71 @@ export default class extends Controller { this._tree.remove(); } + const BS53Theme = { + getOptions() { + return { + onhoverColor: 'var(--bs-secondary-bg)', + }; + } + } + this._tree = new BSTreeView(this.treeTarget, { levels: 1, showTags: this._showTags, data: data, showIcon: true, + preventUnselect: true, + allowReselect: true, onNodeSelected: (event) => { const node = event.detail.node; if (node.href) { window.Turbo.visit(node.href, {action: "advance"}); + this._registerURLWatcher(node); } }, - //onNodeContextmenu: contextmenu_handler, - }, [BS5Theme, FAIconTheme]); + }, [BS5Theme, BS53Theme, FAIconTheme]); this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => { /** @type {BSTreeView} */ const treeView = event.detail.treeView; 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 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) { //Find the node that was clicked and open link in new tab diff --git a/assets/controllers/pages/association_edit_type_select_controller.js b/assets/controllers/pages/association_edit_type_select_controller.js new file mode 100644 index 00000000..10badf9c --- /dev/null +++ b/assets/controllers/pages/association_edit_type_select_controller.js @@ -0,0 +1,44 @@ +/* + * 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 . + */ + +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'); + } + } +} \ No newline at end of file diff --git a/assets/controllers/pages/barcode_scan_controller.js b/assets/controllers/pages/barcode_scan_controller.js index 5f82a39e..368fef43 100644 --- a/assets/controllers/pages/barcode_scan_controller.js +++ b/assets/controllers/pages/barcode_scan_controller.js @@ -20,7 +20,7 @@ import {Controller} from "@hotwired/stimulus"; //import * as ZXing from "@zxing/library"; -import {Html5QrcodeScanner, Html5Qrcode} from "html5-qrcode"; +import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode"; /* stimulusFetch: 'lazy' */ export default class extends Controller { @@ -50,7 +50,7 @@ export default class extends Controller { }); this._scanner = new Html5QrcodeScanner(this.element.id, { - fps: 2, + fps: 10, qrbox: qrboxFunction, experimentalFeatures: { //This option improves reading quality on android chrome @@ -61,6 +61,11 @@ export default class extends Controller { this._scanner.render(this.onScanSuccess.bind(this)); } + disconnect() { + this._scanner.pause(); + this._scanner.clear(); + } + onScanSuccess(decodedText, decodedResult) { //Put our decoded Text into the input box document.getElementById('scan_dialog_input').value = decodedText; diff --git a/assets/controllers/pages/dont_check_quantity_checkbox_controller.js b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js new file mode 100644 index 00000000..2abd3d77 --- /dev/null +++ b/assets/controllers/pages/dont_check_quantity_checkbox_controller.js @@ -0,0 +1,65 @@ +/* + * 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 . + */ + +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'); + } + }); + } +} \ No newline at end of file diff --git a/assets/controllers/pages/latex_preview_controller.js b/assets/controllers/pages/latex_preview_controller.js index c836faa6..6113393a 100644 --- a/assets/controllers/pages/latex_preview_controller.js +++ b/assets/controllers/pages/latex_preview_controller.js @@ -25,9 +25,20 @@ import "katex/dist/katex.css"; export default class extends Controller { static targets = ["input", "preview"]; + static values = { + unit: {type: Boolean, default: false} //Render as upstanding (non-italic) text, useful for units + } + updatePreview() { - katex.render(this.inputTarget.value, this.previewTarget, { + let value = ""; + if (this.unitValue) { + value = "\\mathrm{" + this.inputTarget.value + "}"; + } else { + value = this.inputTarget.value; + } + + katex.render(value, this.previewTarget, { throwOnError: false, }); } diff --git a/assets/controllers/pages/parameters_autocomplete_controller.js b/assets/controllers/pages/parameters_autocomplete_controller.js index f6504990..cd82875a 100644 --- a/assets/controllers/pages/parameters_autocomplete_controller.js +++ b/assets/controllers/pages/parameters_autocomplete_controller.js @@ -22,6 +22,13 @@ import TomSelect from "tom-select"; import katex from "katex"; 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' */ export default class extends Controller { @@ -53,7 +60,10 @@ export default class extends Controller connect() { const settings = { plugins: { - clear_button:{} + 'autoselect_typed': {}, + 'click_to_edit': {}, + 'clear_button': {}, + 'restore_on_backspace': {} }, persistent: false, maxItems: 1, diff --git a/assets/controllers/pages/part_merge_modal_controller.js b/assets/controllers/pages/part_merge_modal_controller.js new file mode 100644 index 00000000..e9e41302 --- /dev/null +++ b/assets/controllers/pages/part_merge_modal_controller.js @@ -0,0 +1,68 @@ +/* + * 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 . + */ + +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'); + } +} \ No newline at end of file diff --git a/assets/css/app/bs-overrides.css b/assets/css/app/bs-overrides.css index 6043aafe..070f353d 100644 --- a/assets/css/app/bs-overrides.css +++ b/assets/css/app/bs-overrides.css @@ -99,10 +99,25 @@ label:not(.form-check-label, .custom-control-label) { form .col-form-label.required:after, form label.required:after { bottom: 4px; - color: var(--bs-dark); + color: var(--bs-secondary-color); content: "\2022"; filter: opacity(75%); position: relative; right: -2px; 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; } \ No newline at end of file diff --git a/assets/css/app/helpers.css b/assets/css/app/helpers.css index acc2682e..8e7b6fa3 100644 --- a/assets/css/app/helpers.css +++ b/assets/css/app/helpers.css @@ -67,7 +67,6 @@ ul.structural_link { padding-bottom: 7px; padding-left: 0; list-style: none; - background-color: inherit; } /* Display list items side by side */ @@ -79,7 +78,7 @@ ul.structural_link li { /* Add a slash symbol (/) before/behind each list item */ ul.structural_link li+li:before { padding: 2px; - color: grey; + color: var(--bs-tertiary-color); /*content: "/\00a0";*/ font-family: "Font Awesome 5 Free"; font-weight: 900; @@ -89,13 +88,13 @@ ul.structural_link li+li:before { /* Add a color to all links inside the list */ ul.structural_link li a { - color: #0275d8; + color: var(--bs-link-color); text-decoration: none; } /* Add a color on mouse-over */ ul.structural_link li a:hover { - color: #01447e; + color: var(--bs-link-hover-color); text-decoration: underline; } @@ -112,4 +111,11 @@ ul.structural_link li a:hover { .permission-checkbox:checked { background-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; } \ No newline at end of file diff --git a/assets/css/app/images.css b/assets/css/app/images.css index 9168484d..214776e7 100644 --- a/assets/css/app/images.css +++ b/assets/css/app/images.css @@ -51,7 +51,6 @@ .part-table-image { max-height: 40px; object-fit: contain; - width: 100%; } .part-info-image { @@ -61,4 +60,4 @@ .object-fit-cover { object-fit: cover; -} \ No newline at end of file +} diff --git a/assets/css/app/layout.css b/assets/css/app/layout.css index 059fc131..4be123a7 100644 --- a/assets/css/app/layout.css +++ b/assets/css/app/layout.css @@ -78,8 +78,6 @@ body { overflow: -moz-scrollbars-none; /* Use standard version for hiding the scrollbar */ scrollbar-width: none; - - background-color: var(--light); } #sidebar-container { @@ -110,8 +108,8 @@ body { .back-to-top { cursor: pointer; position: fixed; - bottom: 20px; - right: 20px; + bottom: 60px; + right: 40px; display:none; z-index: 1030; } diff --git a/assets/css/app/tables.css b/assets/css/app/tables.css index ef257a55..ae892f50 100644 --- a/assets/css/app/tables.css +++ b/assets/css/app/tables.css @@ -63,10 +63,6 @@ table.dataTable > tbody > tr.selected > td > a { margin-block-end: 0; } -.card-footer-table { - padding-top: 0; -} - table.dataTable { margin-top: 0 !important; } @@ -84,14 +80,24 @@ th.select-checkbox { * Datatables definitions/overrides ********************************************************************/ -.dataTables_length { +.dt-length { display: inline-flex; } /** Fix datatables select-checkbox position */ table.dataTable tr.selected td.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); } @@ -103,43 +109,4 @@ Classes for Datatables export #export-messageTop, .export-helper{ display: none; -} - -/****************************************************** - * Styling for the select all checkbox in the parts table - * Should match the styling of the select checkbox - ******************************************************/ -table.dataTable > thead > tr > th.select-checkbox { - position: relative; -} -table.dataTable > thead > tr > th.select-checkbox:before, -table.dataTable > thead > tr > th.select-checkbox:after { - display: block; - position: absolute; - top: 1.2em; - left: 50%; - width: 12px; - height: 12px; - box-sizing: border-box; -} -table.dataTable > thead > tr > th.select-checkbox:before { - content: " "; - margin-top: -5px; - margin-left: -6px; - border: 1px solid black; - border-radius: 3px; -} -table.dataTable > thead > tr.selected > th.select-checkbox:after { - content: "✓"; - font-size: 20px; - margin-top: -23px; - margin-left: -6px; - text-align: center; - /*text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9; */ -} -table.dataTable.compact > thead > tr > th.select-checkbox:before { - margin-top: -12px; -} -table.dataTable.compact > thead > tr.selected > th.select-checkbox:after { - margin-top: -16px; -} +} \ No newline at end of file diff --git a/assets/css/components/autocomplete_bootstrap_theme.css b/assets/css/components/autocomplete_bootstrap_theme.css new file mode 100644 index 00000000..d86232e5 --- /dev/null +++ b/assets/css/components/autocomplete_bootstrap_theme.css @@ -0,0 +1,1120 @@ +/* + * 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 . + */ + +/*** + * This file is based on the autocomplete-theme-classic from Algolia and modifies it to fit better into the bootstrap 5 + * theme of Part-DB. + */ + +/*! @algolia/autocomplete-theme-classic 1.17.0 | MIT License | © Algolia, Inc. and contributors | https://github.com/algolia/autocomplete */ +/* ----------------*/ +/* 1. CSS Variables*/ +/* 2. Dark Mode*/ +/* 3. Autocomplete*/ +/* 4. Panel*/ +/* 5. Sources*/ +/* 6. Hit Layout*/ +/* 7. Panel Header*/ +/* 8. Panel Footer*/ +/* 9. Detached Mode*/ +/* 10. Gradients*/ +/* 11. Utilities*/ +/* ----------------*/ +/* Note:*/ +/* This theme reflects the markup structure of autocomplete with SCSS indentation.*/ +/* We use the SASS `@at-root` function to keep specificity low.*/ +/* ----------------*/ +/* 1. CSS Variables*/ +/* ----------------*/ +:root { + /* Input*/ + --aa-search-input-height: 44px; + --aa-input-icon-size: 20px; + /* Size and spacing*/ + --aa-base-unit: 16; + --aa-spacing-factor: 1; + --aa-spacing: calc(var(--aa-base-unit) * var(--aa-spacing-factor) * 1px); + --aa-spacing-half: calc(var(--aa-spacing) / 2); + --aa-panel-max-height: 650px; + /* Z-index*/ + --aa-base-z-index: 9999; + /* Font*/ + --aa-font-size: calc(var(--aa-base-unit) * 1px); + --aa-font-family: inherit; + --aa-font-weight-medium: 500; + --aa-font-weight-semibold: 600; + --aa-font-weight-bold: 700; + /* Icons*/ + --aa-icon-size: 20px; + --aa-icon-stroke-width: 1.6; + --aa-icon-color-rgb: 119, 119, 163; + --aa-icon-color-alpha: 1; + --aa-action-icon-size: 20px; + /* Text colors*/ + --aa-text-color-rgb: 38, 38, 39; + --aa-text-color-alpha: 1; + --aa-primary-color-rgb: 62, 52, 211; + --aa-primary-color-alpha: 0.2; + --aa-muted-color-rgb: 128, 126, 163; + --aa-muted-color-alpha: 0.6; + /* Border colors*/ + --aa-panel-border-color-rgb: 128, 126, 163; + --aa-panel-border-color-alpha: 0.3; + --aa-input-border-color-rgb: 128, 126, 163; + --aa-input-border-color-alpha: 0.8; + /* Background colors*/ + --aa-background-color-rgb: 255, 255, 255; + --aa-background-color-alpha: 1; + --aa-input-background-color-rgb: 255, 255, 255; + --aa-input-background-color-alpha: 1; + --aa-selected-color-rgb: 179, 173, 214; + --aa-selected-color-alpha: 0.205; + --aa-description-highlight-background-color-rgb: 245, 223, 77; + --aa-description-highlight-background-color-alpha: 0.5; + /* Detached mode*/ + --aa-detached-media-query: (max-width: 680px); + --aa-detached-modal-media-query: (min-width: 680px); + --aa-detached-modal-max-width: 680px; + --aa-detached-modal-max-height: 500px; + --aa-overlay-color-rgb: 115, 114, 129; + --aa-overlay-color-alpha: 0.4; + /* Shadows*/ + --aa-panel-shadow: 0 0 0 1px rgba(35, 38, 59, .1), + 0 6px 16px -4px rgba(35, 38, 59, .15); + /* Scrollbar*/ + --aa-scrollbar-width: 13px; + --aa-scrollbar-track-background-color-rgb: 234, 234, 234; + --aa-scrollbar-track-background-color-alpha: 1; + --aa-scrollbar-thumb-background-color-rgb: var(--aa-background-color-rgb); + --aa-scrollbar-thumb-background-color-alpha: 1; + /* Touch screens*/ +} +@media (hover: none) and (pointer: coarse) { + :root { + --aa-spacing-factor: 1.2; + --aa-action-icon-size: 22px; + } +} + +/* ----------------*/ +/* 2. Dark Mode*/ +/* ----------------*/ +body { + /* stylelint-disable selector-no-qualifying-type, selector-class-pattern */ + /* stylelint-enable selector-no-qualifying-type, selector-class-pattern */ +} + +/* Reset for `@extend`*/ +.aa-Panel *, .aa-Autocomplete *, +.aa-DetachedFormContainer * { + box-sizing: border-box; +} + +/* Init for `@extend`*/ +.aa-Panel, .aa-Autocomplete, +.aa-DetachedFormContainer { + color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha)); + color: var(--bs-body-color); + font-family: inherit; + font-weight: normal; + line-height: 1em; + margin: 0; + padding: 0; + text-align: left; +} + +/* ----------------*/ +/* 3. Autocomplete*/ +/* ----------------*/ +.aa-Autocomplete, +.aa-DetachedFormContainer { + /* Search box*/ +} +.aa-Form { + align-items: center; + background-color: var(--bs-body-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + color: var(--bs-body-color); + transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; + display: flex; + line-height: 1em; + margin: 0; + position: relative; + width: 100%; +} +.aa-Form:focus-within { + background-color: var(--bs-body-bg); + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13,110,253,.25); + color: var(--bs-body-color); + outline: 0; +} +.aa-InputWrapperPrefix { + align-items: center; + display: flex; + flex-shrink: 0; + height: 44px; + height: var(--aa-search-input-height); + order: 1; + /* Container for search and loading icons*/ +} +.aa-Label, +.aa-LoadingIndicator { + cursor: auto; + cursor: initial; + flex-shrink: 0; + height: 100%; + padding: 0; + text-align: left; +} +.aa-Label svg, +.aa-LoadingIndicator svg { + color: rgba(var(--bs-primary-rgb), 1.0); + height: auto; + max-height: 20px; + max-height: var(--aa-input-icon-size); + stroke-width: 1.6; + stroke-width: var(--aa-icon-stroke-width); + width: 20px; + width: var(--aa-input-icon-size); +} + +.aa-SubmitButton, +.aa-LoadingIndicator { + height: 100%; + padding-left: calc((16 * 1 * 1px) * 0.75 - 1px); + padding-left: calc(calc(16 * 1 * 1px) * 0.75 - 1px); + padding-left: calc(var(--aa-spacing) * 0.75 - 1px); + padding-right: calc((16 * 1 * 1px) / 2); + padding-right: calc(calc(16 * 1 * 1px) / 2); + padding-right: var(--aa-spacing-half); + width: calc((16 * 1 * 1px) * 1.75 + 20px - 1px); + width: calc(calc(16 * 1 * 1px) * 1.75 + 20px - 1px); + width: calc(var(--aa-spacing) * 1.75 + var(--aa-icon-size) - 1px); +} +@media (hover: none) and (pointer: coarse) { + .aa-SubmitButton, + .aa-LoadingIndicator { + padding-left: calc(((16 * 1 * 1px) / 2) / 2 - 1px); + padding-left: calc(calc(calc(16 * 1 * 1px) / 2) / 2 - 1px); + padding-left: calc(var(--aa-spacing-half) / 2 - 1px); + width: calc(20px + (16 * 1 * 1px) * 1.25 - 1px); + width: calc(20px + calc(16 * 1 * 1px) * 1.25 - 1px); + width: calc(var(--aa-icon-size) + var(--aa-spacing) * 1.25 - 1px); + } +} + +.aa-SubmitButton { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: none; + border: 0; + margin: 0; +} + +.aa-LoadingIndicator { + align-items: center; + display: flex; + justify-content: center; +} +.aa-LoadingIndicator[hidden] { + display: none; +} + +.aa-InputWrapper { + order: 3; + position: relative; + width: 100%; + /* Search box input (with placeholder and query)*/ +} +.aa-Input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: none; + border: 0; + color: var(--bs-body-color); + font: inherit; + height: 44px; + height: var(--aa-search-input-height); + padding: 0; + width: 100%; + /* Focus is set and styled on the parent, it isn't necessary here*/ + /* Remove native appearance*/ +} +.aa-Input::-moz-placeholder { + color: var(--bs-secondary-color); + opacity: 1; +} +.aa-Input::placeholder { + color: var(--bs-secondary-color); + opacity: 1; +} +.aa-Input:focus { + box-shadow: none; + outline: none; +} +.aa-Input::-webkit-search-decoration, .aa-Input::-webkit-search-cancel-button, .aa-Input::-webkit-search-results-button, .aa-Input::-webkit-search-results-decoration { + -webkit-appearance: none; + appearance: none; +} + +.aa-InputWrapperSuffix { + align-items: center; + display: flex; + height: 44px; + height: var(--aa-search-input-height); + order: 4; + /* Accelerator to clear the query*/ +} +.aa-ClearButton { + align-items: center; + background: none; + border: 0; + color: var(--bs-secondary-color); + cursor: pointer; + display: flex; + height: 100%; + margin: 0; + padding: 0 calc((16 * 1 * 1px) * 0.8333333333 - 0.5px); + padding: 0 calc(calc(16 * 1 * 1px) * 0.8333333333 - 0.5px); + padding: 0 calc(var(--aa-spacing) * 0.8333333333 - 0.5px); +} +@media (hover: none) and (pointer: coarse) { + .aa-ClearButton { + padding: 0 calc((16 * 1 * 1px) * 0.6666666667 - 0.5px); + padding: 0 calc(calc(16 * 1 * 1px) * 0.6666666667 - 0.5px); + padding: 0 calc(var(--aa-spacing) * 0.6666666667 - 0.5px); + } +} +.aa-ClearButton:hover, .aa-ClearButton:focus { + color: var(--bs-body-color); +} +.aa-ClearButton[hidden] { + display: none; +} +.aa-ClearButton svg { + stroke-width: 1.6; + stroke-width: var(--aa-icon-stroke-width); + width: 20px; + width: var(--aa-icon-size); +} + +/* ----------------*/ +/* 4. Panel*/ +/* ----------------*/ +.aa-Panel { + --bs-dropdown-header-padding-x: 1rem; + --bs-dropdown-header-padding-y: 0.5rem; + --bs-dropdown-font-size: 1rem; + --bs-dropdown-color: var(--bs-body-color); + --bs-dropdown-bg: var(--bs-body-bg); + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-border-radius: var(--bs-border-radius); + --bs-dropdown-border-width: var(--bs-border-width); + + z-index: 1000; + + box-shadow: 0 0 0 1px rgba(35, 38, 59, 0.1); + overflow: hidden; + position: absolute; + transition: opacity 200ms ease-in, filter 200ms ease-in; + /* When a request isn't resolved yet*/ + + padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); + margin: 0; + font-size: var(--bs-dropdown-font-size); + color: var(--bs-dropdown-color); + background-color: var(--bs-dropdown-bg); + background-clip: padding-box; + border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); + border-radius: var(--bs-dropdown-border-radius); +} +@media screen and (prefers-reduced-motion) { + .aa-Panel { + transition: none; + } +} +.aa-Panel button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: none; + border: 0; + margin: 0; + padding: 0; +} +.aa-PanelLayout { + height: 100%; + margin: 0; + max-height: 650px; + max-height: var(--aa-panel-max-height); + overflow-y: auto; + padding: 0; + position: relative; + text-align: left; +} +.aa-PanelLayoutColumns--twoGolden { + display: grid; + grid-template-columns: 39.2% auto; + overflow: hidden; + padding: 0; +} + +.aa-PanelLayoutColumns--two { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + overflow: hidden; + padding: 0; +} + +.aa-PanelLayoutColumns--three { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + overflow: hidden; + padding: 0; +} + +.aa-Panel--stalled .aa-Source { + filter: grayscale(1); + opacity: 0.8; +} + +.aa-Panel--scrollable { + margin: 0; + max-height: 650px; + max-height: var(--aa-panel-max-height); + overflow-x: hidden; + overflow-y: auto; + padding: calc((16 * 1 * 1px) / 2); + padding: calc(calc(16 * 1 * 1px) / 2); + padding: var(--aa-spacing-half); + scrollbar-color: rgba(255, 255, 255, 1) rgba(234, 234, 234, 1); + scrollbar-color: rgba(var(--aa-scrollbar-thumb-background-color-rgb), var(--aa-scrollbar-thumb-background-color-alpha)) rgba(var(--aa-scrollbar-track-background-color-rgb), var(--aa-scrollbar-track-background-color-alpha)); + scrollbar-width: thin; +} +.aa-Panel--scrollable::-webkit-scrollbar { + width: 13px; + width: var(--aa-scrollbar-width); +} +.aa-Panel--scrollable::-webkit-scrollbar-track { + background-color: rgba(234, 234, 234, 1); + background-color: rgba(var(--aa-scrollbar-track-background-color-rgb), var(--aa-scrollbar-track-background-color-alpha)); +} +.aa-Panel--scrollable::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 1); + background-color: rgba(var(--aa-scrollbar-thumb-background-color-rgb), var(--aa-scrollbar-thumb-background-color-alpha)); + border-color: rgba(234, 234, 234, 1); + border-color: rgba(var(--aa-scrollbar-track-background-color-rgb), var(--aa-scrollbar-track-background-color-alpha)); + border-radius: 9999px; + border-style: solid; + border-width: 3px 2px 3px 3px; +} + +/* ----------------*/ +/* 5. Sources*/ +/* Each source can be styled independently*/ +/* ----------------*/ +.aa-Source { + margin: 0; + padding: 0; + position: relative; + width: 100%; + /* List of results inside the source*/ + /* Source title*/ + /* See all button*/ +} +.aa-Source:empty { + /* Hide empty section*/ + display: none; +} +.aa-SourceNoResults { + font-size: 1em; + margin: 0; + padding: calc(16 * 1 * 1px); + padding: var(--aa-spacing); +} + +.aa-List { + list-style: none; + margin: 0; + padding: 0; + position: relative; +} + +.aa-SourceHeader { + margin: calc((16 * 1 * 1px) / 2) 0.5em calc((16 * 1 * 1px) / 2) 0; + margin: calc(calc(16 * 1 * 1px) / 2) 0.5em calc(calc(16 * 1 * 1px) / 2) 0; + margin: var(--aa-spacing-half) 0.5em var(--aa-spacing-half) 0; + padding: 0; + position: relative; + /* Hide empty header*/ + /* Title typography*/ + /* Line separator*/ +} +.aa-SourceHeader:empty { + display: none; +} +.aa-SourceHeaderTitle { + background: var(--bs-body-bg); + color: rgba(var(--bs-primary-rgb), 1.0); + display: inline-block; + font-size: 0.8em; + font-weight: 600; + font-weight: var(--aa-font-weight-semibold); + margin: 0; + padding: 0 calc((16 * 1 * 1px) / 2) 0 0; + padding: 0 calc(calc(16 * 1 * 1px) / 2) 0 0; + padding: 0 var(--aa-spacing-half) 0 0; + position: relative; + z-index: 9999; + z-index: var(--aa-base-z-index); +} + +.aa-SourceHeaderLine { + border-bottom: solid 1px rgba(var(--bs-primary-rgb), 1.0); + display: block; + height: 2px; + left: 0; + margin: 0; + opacity: 0.3; + padding: 0; + position: absolute; + right: 0; + top: calc((16 * 1 * 1px) / 2); + top: calc(calc(16 * 1 * 1px) / 2); + top: var(--aa-spacing-half); + z-index: calc(9999 - 1); + z-index: calc(var(--aa-base-z-index) - 1); +} + +.aa-SourceFooterSeeAll { + background: linear-gradient(180deg, var(--bs-body-bg), rgba(128, 126, 163, 0.14)); + border: 1px solid var(--bs-secondary-color); + border-radius: 5px; + box-shadow: inset 0 0 2px #fff, 0 2px 2px -1px rgba(76, 69, 88, 0.15); + color: inherit; + font-size: 0.95em; + font-weight: 500; + padding: 0.475em 1em 0.6em; + -webkit-text-decoration: none; + text-decoration: none; +} +.aa-SourceFooterSeeAll:focus, .aa-SourceFooterSeeAll:hover { + border: 1px solid rgba(62, 52, 211, 1); + border: 1px solid rgba(var(--bs-primary-rgb), 1); + color: rgba(62, 52, 211, 1); + color: rgba(var(--bs-primary-rgb), 1); +} + +/* ----------------*/ +/* 6. Hit Layout*/ +/* ----------------*/ +.aa-Item { + align-items: center; + border-radius: 3px; + cursor: pointer; + display: grid; + min-height: calc((16 * 1 * 1px) * 2.5); + min-height: calc(calc(16 * 1 * 1px) * 2.5); + min-height: calc(var(--aa-spacing) * 2.5); + padding: calc(((16 * 1 * 1px) / 2) / 2); + padding: calc(calc(calc(16 * 1 * 1px) / 2) / 2); + padding: calc(var(--aa-spacing-half) / 2); + /* When the result is active*/ + /* The result type icon inlined SVG or image*/ + /* wrap hit with url but we don't need to see it*/ + /* Secondary click actions*/ +} +.aa-Item[aria-selected=true] { + background-color: var(--bs-tertiary-bg); +} +.aa-Item[aria-selected=true] .aa-ItemActionButton, +.aa-Item[aria-selected=true] .aa-ActiveOnly { + visibility: visible; +} +.aa-ItemIcon { + align-items: center; + background: var(--bs-body-bg); + border-radius: 3px; + box-shadow: inset 0 0 0 1px rgba(128, 126, 163, 0.3); + box-shadow: inset 0 0 0 1px rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha)); + color: rgba(119, 119, 163, 1); + color: rgba(var(--aa-icon-color-rgb), var(--aa-icon-color-alpha)); + display: flex; + flex-shrink: 0; + font-size: 0.7em; + height: calc(20px + ((16 * 1 * 1px) / 2)); + height: calc(20px + calc(calc(16 * 1 * 1px) / 2)); + height: calc(var(--aa-icon-size) + var(--aa-spacing-half)); + justify-content: center; + overflow: hidden; + stroke-width: 1.6; + stroke-width: var(--aa-icon-stroke-width); + text-align: center; + width: calc(20px + ((16 * 1 * 1px) / 2)); + width: calc(20px + calc(calc(16 * 1 * 1px) / 2)); + width: calc(var(--aa-icon-size) + var(--aa-spacing-half)); +} +.aa-ItemIcon img { + height: auto; + max-height: calc(20px + ((16 * 1 * 1px) / 2) - 8px); + max-height: calc(20px + calc(calc(16 * 1 * 1px) / 2) - 8px); + max-height: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px); + max-width: calc(20px + ((16 * 1 * 1px) / 2) - 8px); + max-width: calc(20px + calc(calc(16 * 1 * 1px) / 2) - 8px); + max-width: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px); + width: auto; +} +.aa-ItemIcon svg { + height: 20px; + height: var(--aa-icon-size); + width: 20px; + width: var(--aa-icon-size); +} +.aa-ItemIcon--alignTop { + align-self: flex-start; +} + +.aa-ItemIcon--noBorder { + background: none; + box-shadow: none; +} + +.aa-ItemIcon--picture { + height: 96px; + width: 96px; +} +.aa-ItemIcon--picture img { + max-height: 100%; + max-width: 100%; + padding: calc((16 * 1 * 1px) / 2); + padding: calc(calc(16 * 1 * 1px) / 2); + padding: var(--aa-spacing-half); +} + +.aa-ItemContent { + align-items: center; + cursor: pointer; + display: grid; + gap: calc((16 * 1 * 1px) / 2); + gap: calc(calc(16 * 1 * 1px) / 2); + grid-gap: calc((16 * 1 * 1px) / 2); + grid-gap: calc(calc(16 * 1 * 1px) / 2); + grid-gap: var(--aa-spacing-half); + gap: var(--aa-spacing-half); + grid-auto-flow: column; + line-height: 1.25em; + overflow: hidden; +} +.aa-ItemContent:empty { + display: none; +} +.aa-ItemContent mark { + background: var(--bs-highlight-bg); + color: var(--bs-body-color); + font-style: normal; + padding: 0; + font-weight: 700; + font-weight: var(--aa-font-weight-bold); +} +.aa-ItemContent--dual { + display: flex; + flex-direction: column; + justify-content: space-between; + text-align: left; +} +.aa-ItemContent--dual .aa-ItemContentTitle, +.aa-ItemContent--dual .aa-ItemContentSubtitle { + display: block; +} + +.aa-ItemContent--indented { + padding-left: calc(20px + (16 * 1 * 1px)); + padding-left: calc(20px + calc(16 * 1 * 1px)); + padding-left: calc(var(--aa-icon-size) + var(--aa-spacing)); +} + +.aa-ItemContentBody { + display: grid; + gap: calc(((16 * 1 * 1px) / 2) / 2); + gap: calc(calc(calc(16 * 1 * 1px) / 2) / 2); + grid-gap: calc(((16 * 1 * 1px) / 2) / 2); + grid-gap: calc(calc(calc(16 * 1 * 1px) / 2) / 2); + grid-gap: calc(var(--aa-spacing-half) / 2); + gap: calc(var(--aa-spacing-half) / 2); +} + +.aa-ItemContentTitle { + display: inline-block; + margin: 0 0.5em 0 0; + max-width: 100%; + overflow: hidden; + padding: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.aa-ItemContentSubtitle { + font-size: 0.92em; +} +.aa-ItemContentSubtitleIcon::before { + border-color: var(--bs-tertiary-color); + border-style: solid; + content: ""; + display: inline-block; + left: 1px; + position: relative; + top: -3px; +} + +.aa-ItemContentSubtitle--inline .aa-ItemContentSubtitleIcon::before { + border-width: 0 0 1.5px; + margin-left: calc((16 * 1 * 1px) / 2); + margin-left: calc(calc(16 * 1 * 1px) / 2); + margin-left: var(--aa-spacing-half); + margin-right: calc(((16 * 1 * 1px) / 2) / 2); + margin-right: calc(calc(calc(16 * 1 * 1px) / 2) / 2); + margin-right: calc(var(--aa-spacing-half) / 2); + width: calc(((16 * 1 * 1px) / 2) + 2px); + width: calc(calc(calc(16 * 1 * 1px) / 2) + 2px); + width: calc(var(--aa-spacing-half) + 2px); +} + +.aa-ItemContentSubtitle--standalone { + align-items: center; + color: var(--bs-body-color); + display: grid; + gap: calc((16 * 1 * 1px) / 2); + gap: calc(calc(16 * 1 * 1px) / 2); + grid-gap: calc((16 * 1 * 1px) / 2); + grid-gap: calc(calc(16 * 1 * 1px) / 2); + grid-gap: var(--aa-spacing-half); + gap: var(--aa-spacing-half); + grid-auto-flow: column; + justify-content: start; +} +.aa-ItemContentSubtitle--standalone .aa-ItemContentSubtitleIcon::before { + border-radius: 0 0 0 3px; + border-width: 0 0 1.5px 1.5px; + height: calc((16 * 1 * 1px) / 2); + height: calc(calc(16 * 1 * 1px) / 2); + height: var(--aa-spacing-half); + width: calc((16 * 1 * 1px) / 2); + width: calc(calc(16 * 1 * 1px) / 2); + width: var(--aa-spacing-half); +} + +.aa-ItemContentSubtitleCategory { + color: var(--bs-secondary-color); + font-weight: 500; +} + +.aa-ItemContentDescription { + color: var(--bs-body-color); + font-size: 0.85em; + max-width: 100%; + overflow-x: hidden; + text-overflow: ellipsis; +} +.aa-ItemContentDescription:empty { + display: none; +} +.aa-ItemContentDescription mark { + background: rgba(245, 223, 77, 0.5); + background: rgba(var(--aa-description-highlight-background-color-rgb), var(--aa-description-highlight-background-color-alpha)); + color: rgba(38, 38, 39, 1); + color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha)); + font-style: normal; + font-weight: 500; + font-weight: var(--aa-font-weight-medium); +} + +.aa-ItemContentDash { + color: var(--bs-secondary-color); + display: none; + opacity: 0.4; +} + +.aa-ItemContentTag { + color: rgba(var(--bs-primary-rgb), 1.0);; + border-radius: 3px; + margin: 0 0.4em 0 0; + padding: 0.08em 0.3em; +} + +.aa-ItemWrapper, +.aa-ItemLink { + align-items: center; + color: inherit; + display: grid; + gap: calc(((16 * 1 * 1px) / 2) / 2); + gap: calc(calc(calc(16 * 1 * 1px) / 2) / 2); + grid-gap: calc(((16 * 1 * 1px) / 2) / 2); + grid-gap: calc(calc(calc(16 * 1 * 1px) / 2) / 2); + grid-gap: calc(var(--aa-spacing-half) / 2); + gap: calc(var(--aa-spacing-half) / 2); + grid-auto-flow: column; + justify-content: space-between; + width: 100%; +} + +.aa-ItemLink { + color: inherit; + -webkit-text-decoration: none; + text-decoration: none; +} + +.aa-ItemActions { + display: grid; + grid-auto-flow: column; + height: 100%; + justify-self: end; + margin: 0 calc((16 * 1 * 1px) / -3); + margin: 0 calc(calc(16 * 1 * 1px) / -3); + margin: 0 calc(var(--aa-spacing) / -3); + padding: 0 2px 0 0; +} + +.aa-ItemActionButton { + align-items: center; + background: none; + border: 0; + color: var(--bs-secondary-color); + cursor: pointer; + display: flex; + flex-shrink: 0; + padding: 0; +} +.aa-ItemActionButton:hover svg, .aa-ItemActionButton:focus svg { + color: var(--bs-body-color); +} +@media (hover: none) and (pointer: coarse) { + .aa-ItemActionButton:hover svg, .aa-ItemActionButton:focus svg { + color: inherit; + } +} +.aa-ItemActionButton svg { + color: var(--bs-secondary-color); + margin: 0; + margin: calc(calc(16 * 1 * 1px) / 3); + margin: calc(var(--aa-spacing) / 3); + stroke-width: 1.6; + stroke-width: var(--aa-icon-stroke-width); + width: 20px; + width: var(--aa-action-icon-size); +} + +.aa-ActiveOnly { + visibility: hidden; +} + +/*----------------*/ +/* 7. Panel Header*/ +/*----------------*/ +.aa-PanelHeader { + align-items: center; + background: var(--bs-primary-bg-subtle); + color: #fff; + display: grid; + height: var(--aa-modal-header-height); + margin: 0; + padding: calc((16 * 1 * 1px) / 2) calc(16 * 1 * 1px); + padding: calc(calc(16 * 1 * 1px) / 2) calc(16 * 1 * 1px); + padding: var(--aa-spacing-half) var(--aa-spacing); + position: relative; +} +.aa-PanelHeader::after { + background-image: linear-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + background-image: linear-gradient(rgba(var(--aa-background-color-rgb), 1), rgba(var(--aa-background-color-rgb), 0)); + bottom: calc(((16 * 1 * 1px) / 2) * -1); + bottom: calc(calc(calc(16 * 1 * 1px) / 2) * -1); + bottom: calc(var(--aa-spacing-half) * -1); + content: ""; + height: calc((16 * 1 * 1px) / 2); + height: calc(calc(16 * 1 * 1px) / 2); + height: var(--aa-spacing-half); + left: 0; + pointer-events: none; + position: absolute; + right: 0; + z-index: 9999; + z-index: var(--aa-base-z-index); +} + +/*----------------*/ +/* 8. Panel Footer*/ +/*----------------*/ +.aa-PanelFooter { + background-color: var(--bs-body-bg); + box-shadow: inset 0 1px 0 var(--bs-dropdown-border-color); + display: flex; + justify-content: space-between; + margin: 0; + padding: calc(16 * 1 * 1px); + padding: var(--aa-spacing); + position: relative; + z-index: 9999; + z-index: var(--aa-base-z-index); +} +.aa-PanelFooter::after { + background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(128, 126, 163, 0.6)); + background-image: linear-gradient(rgba(var(--aa-background-color-rgb), 0), rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha))); + content: ""; + height: calc(16 * 1 * 1px); + height: var(--aa-spacing); + left: 0; + opacity: 0.12; + pointer-events: none; + position: absolute; + right: 0; + top: calc((16 * 1 * 1px) * -1); + top: calc(calc(16 * 1 * 1px) * -1); + top: calc(var(--aa-spacing) * -1); + z-index: calc(9999 - 1); + z-index: calc(var(--aa-base-z-index) - 1); +} + +/*----------------*/ +/* 9. Detached Mode*/ +/*----------------*/ +.aa-DetachedContainer { + background: var(--bs-body-bg); + bottom: 0; + box-shadow: 0 0 0 1px rgba(35, 38, 59, 0.1), + 0 6px 16px -4px rgba(35, 38, 59, 0.15); + box-shadow: var(--aa-panel-shadow); + display: flex; + flex-direction: column; + left: 0; + margin: 0; + overflow: hidden; + padding: 0; + position: fixed; + right: 0; + top: 0; + z-index: 9999; + z-index: var(--aa-base-z-index); +} +.aa-DetachedContainer::after { + height: 32px; +} +.aa-DetachedContainer .aa-SourceHeader { + margin: calc((16 * 1 * 1px) / 2) 0 calc((16 * 1 * 1px) / 2) 2px; + margin: calc(calc(16 * 1 * 1px) / 2) 0 calc(calc(16 * 1 * 1px) / 2) 2px; + margin: var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px; +} +.aa-DetachedContainer .aa-Panel { + background-color: var(--bs-body-bg); + border-radius: 0; + box-shadow: none; + flex-grow: 1; + margin: 0; + padding: 0; + position: relative; +} +.aa-DetachedContainer .aa-PanelLayout { + bottom: 0; + box-shadow: none; + left: 0; + margin: 0; + max-height: none; + overflow-y: auto; + position: absolute; + right: 0; + top: 0; + width: 100%; +} +.aa-DetachedFormContainer { + border-bottom: solid 1px rgba(128, 126, 163, 0.3); + border-bottom: solid 1px rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha)); + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 0; + padding: calc((16 * 1 * 1px) / 2); + padding: calc(calc(16 * 1 * 1px) / 2); + padding: var(--aa-spacing-half); +} +.aa-DetachedCancelButton { + background: none; + border: 0; + border-radius: 3px; + color: var(--bs-body-color); + cursor: pointer; + font: inherit; + margin: 0 0 0 calc((16 * 1 * 1px) / 2); + margin: 0 0 0 calc(calc(16 * 1 * 1px) / 2); + margin: 0 0 0 var(--aa-spacing-half); + padding: 0 calc((16 * 1 * 1px) / 2); + padding: 0 calc(calc(16 * 1 * 1px) / 2); + padding: 0 var(--aa-spacing-half); +} +.aa-DetachedCancelButton:hover, .aa-DetachedCancelButton:focus { + box-shadow: inset 0 0 0 1px rgba(128, 126, 163, 0.3); + box-shadow: inset 0 0 0 1px rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha)); +} + +.aa-DetachedContainer--modal { + border-radius: 6px; + bottom: inherit; + height: auto; + margin: 0 auto; + max-width: 680px; + max-width: var(--aa-detached-modal-max-width); + position: absolute; + top: 3%; +} +.aa-DetachedContainer--modal .aa-PanelLayout { + max-height: 500px; + max-height: var(--aa-detached-modal-max-height); + padding-bottom: calc((16 * 1 * 1px) / 2); + padding-bottom: calc(calc(16 * 1 * 1px) / 2); + padding-bottom: var(--aa-spacing-half); + position: static; +} +.aa-DetachedContainer--modal .aa-PanelLayout:empty { + display: none; +} + +/* Search Button*/ +.aa-DetachedSearchButton { + align-items: center; + background-color: var(--bs-body-bg); + border: 1px solid var(--bs-secondary-border-subtle); + border-radius: 3px; + color: var(--bs-secondary-color); + cursor: pointer; + display: flex; + font: inherit; + font-family: inherit; + font-family: var(--aa-font-family); + font-size: calc(16 * 1px); + font-size: var(--aa-font-size); + height: 44px; + height: var(--aa-search-input-height); + margin: 0; + padding: 0 calc(44px / 8); + padding: 0 calc(var(--aa-search-input-height) / 8); + position: relative; + text-align: left; + width: 100%; +} +.aa-DetachedSearchButton:focus { + border-color: var(--bs-primary-border-subtle); + box-shadow: rgba(62, 52, 211, 0.2) 0 0 0 3px, inset rgba(62, 52, 211, 0.2) 0 0 0 2px; + box-shadow: var(--bs-primary-border-subtle) 0 0 0 3px, inset var(--bs-primary-border-subtle) 0 0 0 2px; + outline: currentColor none medium; +} +.aa-DetachedSearchButtonIcon { + align-items: center; + color: rgba(var(--bs-primary-rgb), 1.0); + cursor: auto; + cursor: initial; + display: flex; + flex-shrink: 0; + height: 100%; + justify-content: center; + width: calc(20px + (16 * 1 * 1px)); + width: calc(20px + calc(16 * 1 * 1px)); + width: calc(var(--aa-icon-size) + var(--aa-spacing)); +} + +.aa-DetachedSearchButtonQuery { + color: var(--bs-body-color); + line-height: 1.25em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.aa-DetachedSearchButtonPlaceholder[hidden] { + display: none; +} + +/* Remove scroll on `body`*/ +.aa-Detached { + height: 100vh; + overflow: hidden; +} + +.aa-DetachedOverlay { + background-color: rgba(115, 114, 129, 0.4); + background-color: rgba(var(--aa-overlay-color-rgb), var(--aa-overlay-color-alpha)); + height: 100vh; + left: 0; + margin: 0; + padding: 0; + position: fixed; + right: 0; + top: 0; + z-index: calc(9999 - 1); + z-index: calc(var(--aa-base-z-index) - 1); +} + +/*----------------*/ +/* 10. Gradients*/ +/*----------------*/ +.aa-GradientTop, +.aa-GradientBottom { + height: calc((16 * 1 * 1px) / 2); + height: calc(calc(16 * 1 * 1px) / 2); + height: var(--aa-spacing-half); + left: 0; + pointer-events: none; + position: absolute; + right: 0; + z-index: 9999; + z-index: var(--aa-base-z-index); +} + +.aa-GradientTop { + background-image: linear-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + background-image: linear-gradient(rgba(var(--aa-background-color-rgb), 1), rgba(var(--aa-background-color-rgb), 0)); + top: 0; +} + +.aa-GradientBottom { + background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + background-image: linear-gradient(rgba(var(--aa-background-color-rgb), 0), rgba(var(--aa-background-color-rgb), 1)); + border-bottom-left-radius: calc((16 * 1 * 1px) / 4); + border-bottom-left-radius: calc(calc(16 * 1 * 1px) / 4); + border-bottom-left-radius: calc(var(--aa-spacing) / 4); + border-bottom-right-radius: calc((16 * 1 * 1px) / 4); + border-bottom-right-radius: calc(calc(16 * 1 * 1px) / 4); + border-bottom-right-radius: calc(var(--aa-spacing) / 4); + bottom: 0; +} + +/*----------------*/ +/* 11. Utilities*/ +/*----------------*/ +@media (hover: none) and (pointer: coarse) { + .aa-DesktopOnly { + display: none; + } +} + +@media (hover: hover) { + .aa-TouchOnly { + display: none; + } +} \ No newline at end of file diff --git a/assets/css/components/ckeditor.css b/assets/css/components/ckeditor.css index 1904e701..d6b3def4 100644 --- a/assets/css/components/ckeditor.css +++ b/assets/css/components/ckeditor.css @@ -24,9 +24,8 @@ /** Should be the same settings, as in label_style.css */ .ck-html-label .ck-content { font-family: "DejaVu Sans Mono", monospace; - font-size: 12px; + font-size: 12pt; line-height: 1.0; - font-size-adjust: 1.5; } .ck-html-label .ck-content p { @@ -36,3 +35,42 @@ .ck-html-label .ck-content hr { 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) + +} \ No newline at end of file diff --git a/assets/css/components/datatables_select_bs5.css b/assets/css/components/datatables_select_bs5.css new file mode 100644 index 00000000..7c717bf4 --- /dev/null +++ b/assets/css/components/datatables_select_bs5.css @@ -0,0 +1,69 @@ +/****************************************************************************************** +* 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; +} + diff --git a/assets/css/components/tom-select_extensions.css b/assets/css/components/tom-select_extensions.css index 326731c9..b0529c25 100644 --- a/assets/css/components/tom-select_extensions.css +++ b/assets/css/components/tom-select_extensions.css @@ -18,6 +18,29 @@ */ .tagsinput.ts-wrapper.multi .ts-control > div { - background: var(--bs-secondary); - color: var(--bs-white); -} \ No newline at end of file + background: var(--bs-secondary-bg); + color: var(--bs-body-color); +} + +/********* + * 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; +} diff --git a/assets/css/email/foundation-emails.css b/assets/css/email/foundation-emails.css index 723728d3..2b62bc09 100644 --- a/assets/css/email/foundation-emails.css +++ b/assets/css/email/foundation-emails.css @@ -1,594 +1,708 @@ -/* - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). +/** + * Copyright (c) 2017 ZURB, inc. * - * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * MIT License * - * 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. + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions: * - * 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. + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ .wrapper { - width: 100%; } + width: 100%; +} #outlook a { - padding: 0; } + padding: 0; +} body { - width: 100% !important; - min-width: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; - margin: 0; - Margin: 0; - padding: 0; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; } + width: 100% !important; + min-width: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + margin: 0; + Margin: 0; + padding: 0; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} .ExternalClass { - width: 100%; } - .ExternalClass, - .ExternalClass p, - .ExternalClass span, - .ExternalClass font, - .ExternalClass td, - .ExternalClass div { - line-height: 100%; } + width: 100%; +} + +.ExternalClass, +.ExternalClass p, +.ExternalClass span, +.ExternalClass font, +.ExternalClass td, +.ExternalClass div { + line-height: 100%; +} #backgroundTable { - margin: 0; - Margin: 0; - padding: 0; - width: 100% !important; - line-height: 100% !important; } + margin: 0; + Margin: 0; + padding: 0; + width: 100% !important; + line-height: 100% !important; +} img { - outline: none; - text-decoration: none; - -ms-interpolation-mode: bicubic; - width: auto; - max-width: 100%; - clear: both; - display: block; } + outline: none; + text-decoration: none; + -ms-interpolation-mode: bicubic; + width: auto; + max-width: 100%; + clear: both; + display: block; +} center { - width: 100%; - min-width: 580px; } + width: 100%; + min-width: 580px; +} a img { - border: none; } + border: none; +} p { - margin: 0 0 0 10px; - Margin: 0 0 0 10px; } + margin: 0 0 0 10px; + Margin: 0 0 0 10px; +} table { - border-spacing: 0; - border-collapse: collapse; } + border-spacing: 0; + border-collapse: collapse; +} td { - word-wrap: break-word; - -webkit-hyphens: auto; - -moz-hyphens: auto; - hyphens: auto; - border-collapse: collapse !important; } + word-wrap: break-word; + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; + border-collapse: collapse !important; +} table, tr, td { - padding: 0; - vertical-align: top; - text-align: left; } + padding: 0; + vertical-align: top; + text-align: left; +} @media only screen { - html { - min-height: 100%; - background: #f3f3f3; } } + html { + min-height: 100%; + background: #f3f3f3; + } +} table.body { - background: #f3f3f3; - height: 100%; - width: 100%; } + background: #f3f3f3; + height: 100%; + width: 100%; +} table.container { - background: #fefefe; - width: 580px; - margin: 0 auto; - Margin: 0 auto; - text-align: inherit; } + background: #fefefe; + width: 580px; + margin: 0 auto; + Margin: 0 auto; + text-align: inherit; +} table.row { - padding: 0; - width: 100%; - position: relative; } + padding: 0; + width: 100%; + position: relative; +} table.spacer { - width: 100%; } - table.spacer td { - mso-line-height-rule: exactly; } + width: 100%; +} + +table.spacer td { + mso-line-height-rule: exactly; +} table.container table.row { - display: table; } + display: table; +} td.columns, td.column, th.columns, th.column { - margin: 0 auto; - Margin: 0 auto; - padding-left: 16px; - padding-bottom: 16px; } - td.columns .column, - td.columns .columns, - td.column .column, - td.column .columns, - th.columns .column, - th.columns .columns, - th.column .column, - th.column .columns { + margin: 0 auto; + Margin: 0 auto; + padding-left: 16px; + padding-bottom: 16px; +} + +td.columns .column, +td.columns .columns, +td.column .column, +td.column .columns, +th.columns .column, +th.columns .columns, +th.column .column, +th.column .columns { padding-left: 0 !important; - padding-right: 0 !important; } - td.columns .column center, - td.columns .columns center, - td.column .column center, - td.column .columns center, - th.columns .column center, - th.columns .columns center, - th.column .column center, - th.column .columns center { - min-width: none !important; } + padding-right: 0 !important; +} + +td.columns .column center, +td.columns .columns center, +td.column .column center, +td.column .columns center, +th.columns .column center, +th.columns .columns center, +th.column .column center, +th.column .columns center { + min-width: none !important; +} td.columns.last, td.column.last, th.columns.last, th.column.last { - padding-right: 16px; } + padding-right: 16px; +} td.columns table:not(.button), td.column table:not(.button), th.columns table:not(.button), th.column table:not(.button) { - width: 100%; } + width: 100%; +} td.large-1, th.large-1 { - width: 32.33333px; - padding-left: 8px; - padding-right: 8px; } + width: 32.33333px; + padding-left: 8px; + padding-right: 8px; +} td.large-1.first, th.large-1.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-1.last, th.large-1.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-1, .collapse > tbody > tr > th.large-1 { - padding-right: 0; - padding-left: 0; - width: 48.33333px; } + padding-right: 0; + padding-left: 0; + width: 48.33333px; +} .collapse td.large-1.first, .collapse th.large-1.first, .collapse td.large-1.last, .collapse th.large-1.last { - width: 56.33333px; } + width: 56.33333px; +} td.large-1 center, th.large-1 center { - min-width: 0.33333px; } + min-width: 0.33333px; +} .body .columns td.large-1, .body .column td.large-1, .body .columns th.large-1, .body .column th.large-1 { - width: 8.33333%; } + width: 8.33333%; +} td.large-2, th.large-2 { - width: 80.66667px; - padding-left: 8px; - padding-right: 8px; } + width: 80.66667px; + padding-left: 8px; + padding-right: 8px; +} td.large-2.first, th.large-2.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-2.last, th.large-2.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-2, .collapse > tbody > tr > th.large-2 { - padding-right: 0; - padding-left: 0; - width: 96.66667px; } + padding-right: 0; + padding-left: 0; + width: 96.66667px; +} .collapse td.large-2.first, .collapse th.large-2.first, .collapse td.large-2.last, .collapse th.large-2.last { - width: 104.66667px; } + width: 104.66667px; +} td.large-2 center, th.large-2 center { - min-width: 48.66667px; } + min-width: 48.66667px; +} .body .columns td.large-2, .body .column td.large-2, .body .columns th.large-2, .body .column th.large-2 { - width: 16.66667%; } + width: 16.66667%; +} td.large-3, th.large-3 { - width: 129px; - padding-left: 8px; - padding-right: 8px; } + width: 129px; + padding-left: 8px; + padding-right: 8px; +} td.large-3.first, th.large-3.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-3.last, th.large-3.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-3, .collapse > tbody > tr > th.large-3 { - padding-right: 0; - padding-left: 0; - width: 145px; } + padding-right: 0; + padding-left: 0; + width: 145px; +} .collapse td.large-3.first, .collapse th.large-3.first, .collapse td.large-3.last, .collapse th.large-3.last { - width: 153px; } + width: 153px; +} td.large-3 center, th.large-3 center { - min-width: 97px; } + min-width: 97px; +} .body .columns td.large-3, .body .column td.large-3, .body .columns th.large-3, .body .column th.large-3 { - width: 25%; } + width: 25%; +} td.large-4, th.large-4 { - width: 177.33333px; - padding-left: 8px; - padding-right: 8px; } + width: 177.33333px; + padding-left: 8px; + padding-right: 8px; +} td.large-4.first, th.large-4.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-4.last, th.large-4.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-4, .collapse > tbody > tr > th.large-4 { - padding-right: 0; - padding-left: 0; - width: 193.33333px; } + padding-right: 0; + padding-left: 0; + width: 193.33333px; +} .collapse td.large-4.first, .collapse th.large-4.first, .collapse td.large-4.last, .collapse th.large-4.last { - width: 201.33333px; } + width: 201.33333px; +} td.large-4 center, th.large-4 center { - min-width: 145.33333px; } + min-width: 145.33333px; +} .body .columns td.large-4, .body .column td.large-4, .body .columns th.large-4, .body .column th.large-4 { - width: 33.33333%; } + width: 33.33333%; +} td.large-5, th.large-5 { - width: 225.66667px; - padding-left: 8px; - padding-right: 8px; } + width: 225.66667px; + padding-left: 8px; + padding-right: 8px; +} td.large-5.first, th.large-5.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-5.last, th.large-5.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-5, .collapse > tbody > tr > th.large-5 { - padding-right: 0; - padding-left: 0; - width: 241.66667px; } + padding-right: 0; + padding-left: 0; + width: 241.66667px; +} .collapse td.large-5.first, .collapse th.large-5.first, .collapse td.large-5.last, .collapse th.large-5.last { - width: 249.66667px; } + width: 249.66667px; +} td.large-5 center, th.large-5 center { - min-width: 193.66667px; } + min-width: 193.66667px; +} .body .columns td.large-5, .body .column td.large-5, .body .columns th.large-5, .body .column th.large-5 { - width: 41.66667%; } + width: 41.66667%; +} td.large-6, th.large-6 { - width: 274px; - padding-left: 8px; - padding-right: 8px; } + width: 274px; + padding-left: 8px; + padding-right: 8px; +} td.large-6.first, th.large-6.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-6.last, th.large-6.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-6, .collapse > tbody > tr > th.large-6 { - padding-right: 0; - padding-left: 0; - width: 290px; } + padding-right: 0; + padding-left: 0; + width: 290px; +} .collapse td.large-6.first, .collapse th.large-6.first, .collapse td.large-6.last, .collapse th.large-6.last { - width: 298px; } + width: 298px; +} td.large-6 center, th.large-6 center { - min-width: 242px; } + min-width: 242px; +} .body .columns td.large-6, .body .column td.large-6, .body .columns th.large-6, .body .column th.large-6 { - width: 50%; } + width: 50%; +} td.large-7, th.large-7 { - width: 322.33333px; - padding-left: 8px; - padding-right: 8px; } + width: 322.33333px; + padding-left: 8px; + padding-right: 8px; +} td.large-7.first, th.large-7.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-7.last, th.large-7.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-7, .collapse > tbody > tr > th.large-7 { - padding-right: 0; - padding-left: 0; - width: 338.33333px; } + padding-right: 0; + padding-left: 0; + width: 338.33333px; +} .collapse td.large-7.first, .collapse th.large-7.first, .collapse td.large-7.last, .collapse th.large-7.last { - width: 346.33333px; } + width: 346.33333px; +} td.large-7 center, th.large-7 center { - min-width: 290.33333px; } + min-width: 290.33333px; +} .body .columns td.large-7, .body .column td.large-7, .body .columns th.large-7, .body .column th.large-7 { - width: 58.33333%; } + width: 58.33333%; +} td.large-8, th.large-8 { - width: 370.66667px; - padding-left: 8px; - padding-right: 8px; } + width: 370.66667px; + padding-left: 8px; + padding-right: 8px; +} td.large-8.first, th.large-8.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-8.last, th.large-8.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-8, .collapse > tbody > tr > th.large-8 { - padding-right: 0; - padding-left: 0; - width: 386.66667px; } + padding-right: 0; + padding-left: 0; + width: 386.66667px; +} .collapse td.large-8.first, .collapse th.large-8.first, .collapse td.large-8.last, .collapse th.large-8.last { - width: 394.66667px; } + width: 394.66667px; +} td.large-8 center, th.large-8 center { - min-width: 338.66667px; } + min-width: 338.66667px; +} .body .columns td.large-8, .body .column td.large-8, .body .columns th.large-8, .body .column th.large-8 { - width: 66.66667%; } + width: 66.66667%; +} td.large-9, th.large-9 { - width: 419px; - padding-left: 8px; - padding-right: 8px; } + width: 419px; + padding-left: 8px; + padding-right: 8px; +} td.large-9.first, th.large-9.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-9.last, th.large-9.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-9, .collapse > tbody > tr > th.large-9 { - padding-right: 0; - padding-left: 0; - width: 435px; } + padding-right: 0; + padding-left: 0; + width: 435px; +} .collapse td.large-9.first, .collapse th.large-9.first, .collapse td.large-9.last, .collapse th.large-9.last { - width: 443px; } + width: 443px; +} td.large-9 center, th.large-9 center { - min-width: 387px; } + min-width: 387px; +} .body .columns td.large-9, .body .column td.large-9, .body .columns th.large-9, .body .column th.large-9 { - width: 75%; } + width: 75%; +} td.large-10, th.large-10 { - width: 467.33333px; - padding-left: 8px; - padding-right: 8px; } + width: 467.33333px; + padding-left: 8px; + padding-right: 8px; +} td.large-10.first, th.large-10.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-10.last, th.large-10.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-10, .collapse > tbody > tr > th.large-10 { - padding-right: 0; - padding-left: 0; - width: 483.33333px; } + padding-right: 0; + padding-left: 0; + width: 483.33333px; +} .collapse td.large-10.first, .collapse th.large-10.first, .collapse td.large-10.last, .collapse th.large-10.last { - width: 491.33333px; } + width: 491.33333px; +} td.large-10 center, th.large-10 center { - min-width: 435.33333px; } + min-width: 435.33333px; +} .body .columns td.large-10, .body .column td.large-10, .body .columns th.large-10, .body .column th.large-10 { - width: 83.33333%; } + width: 83.33333%; +} td.large-11, th.large-11 { - width: 515.66667px; - padding-left: 8px; - padding-right: 8px; } + width: 515.66667px; + padding-left: 8px; + padding-right: 8px; +} td.large-11.first, th.large-11.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-11.last, th.large-11.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-11, .collapse > tbody > tr > th.large-11 { - padding-right: 0; - padding-left: 0; - width: 531.66667px; } + padding-right: 0; + padding-left: 0; + width: 531.66667px; +} .collapse td.large-11.first, .collapse th.large-11.first, .collapse td.large-11.last, .collapse th.large-11.last { - width: 539.66667px; } + width: 539.66667px; +} td.large-11 center, th.large-11 center { - min-width: 483.66667px; } + min-width: 483.66667px; +} .body .columns td.large-11, .body .column td.large-11, .body .columns th.large-11, .body .column th.large-11 { - width: 91.66667%; } + width: 91.66667%; +} td.large-12, th.large-12 { - width: 564px; - padding-left: 8px; - padding-right: 8px; } + width: 564px; + padding-left: 8px; + padding-right: 8px; +} td.large-12.first, th.large-12.first { - padding-left: 16px; } + padding-left: 16px; +} td.large-12.last, th.large-12.last { - padding-right: 16px; } + padding-right: 16px; +} .collapse > tbody > tr > td.large-12, .collapse > tbody > tr > th.large-12 { - padding-right: 0; - padding-left: 0; - width: 580px; } + padding-right: 0; + padding-left: 0; + width: 580px; +} .collapse td.large-12.first, .collapse th.large-12.first, .collapse td.large-12.last, .collapse th.large-12.last { - width: 588px; } + width: 588px; +} td.large-12 center, th.large-12 center { - min-width: 532px; } + min-width: 532px; +} .body .columns td.large-12, .body .column td.large-12, .body .columns th.large-12, .body .column th.large-12 { - width: 100%; } + width: 100%; +} td.large-offset-1, td.large-offset-1.first, @@ -596,7 +710,8 @@ td.large-offset-1.last, th.large-offset-1, th.large-offset-1.first, th.large-offset-1.last { - padding-left: 64.33333px; } + padding-left: 64.33333px; +} td.large-offset-2, td.large-offset-2.first, @@ -604,7 +719,8 @@ td.large-offset-2.last, th.large-offset-2, th.large-offset-2.first, th.large-offset-2.last { - padding-left: 112.66667px; } + padding-left: 112.66667px; +} td.large-offset-3, td.large-offset-3.first, @@ -612,7 +728,8 @@ td.large-offset-3.last, th.large-offset-3, th.large-offset-3.first, th.large-offset-3.last { - padding-left: 161px; } + padding-left: 161px; +} td.large-offset-4, td.large-offset-4.first, @@ -620,7 +737,8 @@ td.large-offset-4.last, th.large-offset-4, th.large-offset-4.first, th.large-offset-4.last { - padding-left: 209.33333px; } + padding-left: 209.33333px; +} td.large-offset-5, td.large-offset-5.first, @@ -628,7 +746,8 @@ td.large-offset-5.last, th.large-offset-5, th.large-offset-5.first, th.large-offset-5.last { - padding-left: 257.66667px; } + padding-left: 257.66667px; +} td.large-offset-6, td.large-offset-6.first, @@ -636,7 +755,8 @@ td.large-offset-6.last, th.large-offset-6, th.large-offset-6.first, th.large-offset-6.last { - padding-left: 306px; } + padding-left: 306px; +} td.large-offset-7, td.large-offset-7.first, @@ -644,7 +764,8 @@ td.large-offset-7.last, th.large-offset-7, th.large-offset-7.first, th.large-offset-7.last { - padding-left: 354.33333px; } + padding-left: 354.33333px; +} td.large-offset-8, td.large-offset-8.first, @@ -652,7 +773,8 @@ td.large-offset-8.last, th.large-offset-8, th.large-offset-8.first, th.large-offset-8.last { - padding-left: 402.66667px; } + padding-left: 402.66667px; +} td.large-offset-9, td.large-offset-9.first, @@ -660,7 +782,8 @@ td.large-offset-9.last, th.large-offset-9, th.large-offset-9.first, th.large-offset-9.last { - padding-left: 451px; } + padding-left: 451px; +} td.large-offset-10, td.large-offset-10.first, @@ -668,7 +791,8 @@ td.large-offset-10.last, th.large-offset-10, th.large-offset-10.first, th.large-offset-10.last { - padding-left: 499.33333px; } + padding-left: 499.33333px; +} td.large-offset-11, td.large-offset-11.first, @@ -676,45 +800,58 @@ td.large-offset-11.last, th.large-offset-11, th.large-offset-11.first, th.large-offset-11.last { - padding-left: 547.66667px; } + padding-left: 547.66667px; +} td.expander, th.expander { - visibility: hidden; - width: 0; - padding: 0 !important; } + visibility: hidden; + width: 0; + padding: 0 !important; +} table.container.radius { - border-radius: 0; - border-collapse: separate; } + border-radius: 0; + border-collapse: separate; +} .block-grid { - width: 100%; - max-width: 580px; } - .block-grid td { + width: 100%; + max-width: 580px; +} + +.block-grid td { display: inline-block; - padding: 8px; } + padding: 8px; +} .up-2 td { - width: 274px !important; } + width: 274px !important; +} .up-3 td { - width: 177px !important; } + width: 177px !important; +} .up-4 td { - width: 129px !important; } + width: 129px !important; +} .up-5 td { - width: 100px !important; } + width: 100px !important; +} .up-6 td { - width: 80px !important; } + width: 80px !important; +} .up-7 td { - width: 66px !important; } + width: 66px !important; +} .up-8 td { - width: 56px !important; } + width: 56px !important; +} table.text-center, th.text-center, @@ -727,7 +864,8 @@ h5.text-center, h6.text-center, p.text-center, span.text-center { - text-align: center; } + text-align: center; +} table.text-left, th.text-left, @@ -740,7 +878,8 @@ h5.text-left, h6.text-left, p.text-left, span.text-left { - text-align: left; } + text-align: left; +} table.text-right, th.text-right, @@ -753,85 +892,110 @@ h5.text-right, h6.text-right, p.text-right, span.text-right { - text-align: right; } + text-align: right; +} span.text-center { - display: block; - width: 100%; - text-align: center; } + display: block; + width: 100%; + text-align: center; +} @media only screen and (max-width: 596px) { - .small-float-center { - margin: 0 auto !important; - float: none !important; - text-align: center !important; } - .small-text-center { - text-align: center !important; } - .small-text-left { - text-align: left !important; } - .small-text-right { - text-align: right !important; } } + .small-float-center { + margin: 0 auto !important; + float: none !important; + text-align: center !important; + } + + .small-text-center { + text-align: center !important; + } + + .small-text-left { + text-align: left !important; + } + + .small-text-right { + text-align: right !important; + } +} img.float-left { - float: left; - text-align: left; } + float: left; + text-align: left; +} img.float-right { - float: right; - text-align: right; } + float: right; + text-align: right; +} img.float-center, img.text-center { - margin: 0 auto; - Margin: 0 auto; - float: none; - text-align: center; } + margin: 0 auto; + Margin: 0 auto; + float: none; + text-align: center; +} table.float-center, td.float-center, th.float-center { - margin: 0 auto; - Margin: 0 auto; - float: none; - text-align: center; } + margin: 0 auto; + Margin: 0 auto; + float: none; + text-align: center; +} .hide-for-large { - display: none !important; - mso-hide: all; - overflow: hidden; - max-height: 0; - font-size: 0; - width: 0; - line-height: 0; } - @media only screen and (max-width: 596px) { + display: none !important; + mso-hide: all; + overflow: hidden; + max-height: 0; + font-size: 0; + width: 0; + line-height: 0; +} + +@media only screen and (max-width: 596px) { .hide-for-large { - display: block !important; - width: auto !important; - overflow: visible !important; - max-height: none !important; - font-size: inherit !important; - line-height: inherit !important; } } + display: block !important; + width: auto !important; + overflow: visible !important; + max-height: none !important; + font-size: inherit !important; + line-height: inherit !important; + } +} table.body table.container .hide-for-large * { - mso-hide: all; } - -@media only screen and (max-width: 596px) { - table.body table.container .hide-for-large, - table.body table.container .row.hide-for-large { - display: table !important; - width: 100% !important; } } - -@media only screen and (max-width: 596px) { - table.body table.container .callout-inner.hide-for-large { - display: table-cell !important; - width: 100% !important; } } - -@media only screen and (max-width: 596px) { - table.body table.container .show-for-large { - display: none !important; - width: 0; mso-hide: all; - overflow: hidden; } } +} + +@media only screen and (max-width: 596px) { + table.body table.container .hide-for-large, + table.body table.container .row.hide-for-large { + display: table !important; + width: 100% !important; + } +} + +@media only screen and (max-width: 596px) { + table.body table.container .callout-inner.hide-for-large { + display: table-cell !important; + width: 100% !important; + } +} + +@media only screen and (max-width: 596px) { + table.body table.container .show-for-large { + display: none !important; + width: 0; + mso-hide: all; + overflow: hidden; + } +} body, table.body, @@ -845,14 +1009,15 @@ p, td, th, a { - color: #0a0a0a; - font-family: Helvetica, Arial, sans-serif; - font-weight: normal; - padding: 0; - margin: 0; - Margin: 0; - text-align: left; - line-height: 1.3; } + color: #0a0a0a; + font-family: Helvetica, Arial, sans-serif; + font-weight: normal; + padding: 0; + margin: 0; + Margin: 0; + text-align: left; + line-height: 1.3; +} h1, h2, @@ -860,67 +1025,88 @@ h3, h4, h5, h6 { - color: inherit; - word-wrap: normal; - font-family: Helvetica, Arial, sans-serif; - font-weight: normal; - margin-bottom: 10px; - Margin-bottom: 10px; } + color: inherit; + word-wrap: normal; + font-family: Helvetica, Arial, sans-serif; + font-weight: normal; + margin-bottom: 10px; + Margin-bottom: 10px; +} h1 { - font-size: 34px; } + font-size: 34px; +} h2 { - font-size: 30px; } + font-size: 30px; +} h3 { - font-size: 28px; } + font-size: 28px; +} h4 { - font-size: 24px; } + font-size: 24px; +} h5 { - font-size: 20px; } + font-size: 20px; +} h6 { - font-size: 18px; } + font-size: 18px; +} body, table.body, p, td, th { - font-size: 16px; - line-height: 1.3; } + font-size: 16px; + line-height: 1.3; +} p { - margin-bottom: 10px; - Margin-bottom: 10px; } - p.lead { + margin-bottom: 10px; + Margin-bottom: 10px; +} + +p.lead { font-size: 20px; - line-height: 1.6; } - p.subheader { + line-height: 1.6; +} + +p.subheader { margin-top: 4px; margin-bottom: 8px; Margin-top: 4px; Margin-bottom: 8px; font-weight: normal; line-height: 1.4; - color: #8a8a8a; } + color: #8a8a8a; +} small { - font-size: 80%; - color: #cacaca; } + font-size: 80%; + color: #cacaca; +} a { - color: #2199e8; - text-decoration: none; } - a:hover { - color: #147dc2; } - a:active { - color: #147dc2; } - a:visited { - color: #2199e8; } + color: #2199e8; + text-decoration: none; +} + +a:hover { + color: #147dc2; +} + +a:active { + color: #147dc2; +} + +a:visited { + color: #2199e8; +} h1 a, h1 a:visited, @@ -934,24 +1120,34 @@ h5 a, h5 a:visited, h6 a, h6 a:visited { - color: #2199e8; } + color: #2199e8; +} pre { - background: #f3f3f3; - margin: 30px 0; - Margin: 30px 0; } - pre code { - color: #cacaca; } - pre code span.callout { - color: #8a8a8a; - font-weight: bold; } - pre code span.callout-strong { - color: #ff6908; - font-weight: bold; } + background: #f3f3f3; + margin: 30px 0; + Margin: 30px 0; +} + +pre code { + color: #cacaca; +} + +pre code span.callout { + color: #8a8a8a; + font-weight: bold; +} + +pre code span.callout-strong { + color: #ff6908; + font-weight: bold; +} table.hr { - width: 100%; } - table.hr th { + width: 100%; +} + +table.hr th { height: 0; max-width: 580px; border-top: 0; @@ -960,52 +1156,66 @@ table.hr { border-left: 0; margin: 20px auto; Margin: 20px auto; - clear: both; } + clear: both; +} .stat { - font-size: 40px; - line-height: 1; } - p + .stat { + font-size: 40px; + line-height: 1; +} + +p + .stat { margin-top: -16px; - Margin-top: -16px; } + Margin-top: -16px; +} span.preheader { - display: none !important; - visibility: hidden; - mso-hide: all !important; - font-size: 1px; - color: #f3f3f3; - line-height: 1px; - max-height: 0px; - max-width: 0px; - opacity: 0; - overflow: hidden; } + display: none !important; + visibility: hidden; + mso-hide: all !important; + font-size: 1px; + color: #f3f3f3; + line-height: 1px; + max-height: 0px; + max-width: 0px; + opacity: 0; + overflow: hidden; +} table.button { - width: auto; - margin: 0 0 16px 0; - Margin: 0 0 16px 0; } - table.button table td { + width: auto; + margin: 0 0 16px 0; + Margin: 0 0 16px 0; +} + +table.button table td { text-align: left; color: #fefefe; background: #2199e8; - border: 2px solid #2199e8; } - table.button table td a { - font-family: Helvetica, Arial, sans-serif; - font-size: 16px; - font-weight: bold; - color: #fefefe; - text-decoration: none; - display: inline-block; - padding: 8px 16px 8px 16px; - border: 0 solid #2199e8; - border-radius: 3px; } - table.button.radius table td { + border: 2px solid #2199e8; +} + +table.button table td a { + font-family: Helvetica, Arial, sans-serif; + font-size: 16px; + font-weight: bold; + color: #fefefe; + text-decoration: none; + display: inline-block; + padding: 8px 16px 8px 16px; + border: 0 solid #2199e8; border-radius: 3px; - border: none; } - table.button.rounded table td { +} + +table.button.radius table td { + border-radius: 3px; + border: none; +} + +table.button.rounded table td { border-radius: 500px; - border: none; } + border: none; +} table.button:hover table tr td a, table.button:active table tr td a, @@ -1019,349 +1229,491 @@ table.button.small table tr td a:visited, table.button.large:hover table tr td a, table.button.large:active table tr td a, table.button.large table tr td a:visited { - color: #fefefe; } + color: #fefefe; +} table.button.tiny table td, table.button.tiny table a { - padding: 4px 8px 4px 8px; } + padding: 4px 8px 4px 8px; +} table.button.tiny table a { - font-size: 10px; - font-weight: normal; } + font-size: 10px; + font-weight: normal; +} table.button.small table td, table.button.small table a { - padding: 5px 10px 5px 10px; - font-size: 12px; } + padding: 5px 10px 5px 10px; + font-size: 12px; +} table.button.large table a { - padding: 10px 20px 10px 20px; - font-size: 20px; } + padding: 10px 20px 10px 20px; + font-size: 20px; +} table.button.expand, table.button.expanded { - width: 100% !important; } - table.button.expand table, - table.button.expanded table { - width: 100%; } - table.button.expand table a, - table.button.expanded table a { - text-align: center; - width: 100%; - padding-left: 0; - padding-right: 0; } - table.button.expand center, - table.button.expanded center { - min-width: 0; } + width: 100% !important; +} + +table.button.expand table, +table.button.expanded table { + width: 100%; +} + +table.button.expand table a, +table.button.expanded table a { + text-align: center; + width: 100%; + padding-left: 0; + padding-right: 0; +} + +table.button.expand center, +table.button.expanded center { + min-width: 0; +} table.button:hover table td, table.button:visited table td, table.button:active table td { - background: #147dc2; - color: #fefefe; } + background: #147dc2; + color: #fefefe; +} table.button:hover table a, table.button:visited table a, table.button:active table a { - border: 0 solid #147dc2; } + border: 0 solid #147dc2; +} table.button.secondary table td { - background: #777777; - color: #fefefe; - border: 0px solid #777777; } + background: #777777; + color: #fefefe; + border: 0px solid #777777; +} table.button.secondary table a { - color: #fefefe; - border: 0 solid #777777; } + color: #fefefe; + border: 0 solid #777777; +} table.button.secondary:hover table td { - background: #919191; - color: #fefefe; } + background: #919191; + color: #fefefe; +} table.button.secondary:hover table a { - border: 0 solid #919191; } + border: 0 solid #919191; +} table.button.secondary:hover table td a { - color: #fefefe; } + color: #fefefe; +} table.button.secondary:active table td a { - color: #fefefe; } + color: #fefefe; +} table.button.secondary table td a:visited { - color: #fefefe; } + color: #fefefe; +} table.button.success table td { - background: #3adb76; - border: 0px solid #3adb76; } + background: #3adb76; + border: 0px solid #3adb76; +} table.button.success table a { - border: 0 solid #3adb76; } + border: 0 solid #3adb76; +} table.button.success:hover table td { - background: #23bf5d; } + background: #23bf5d; +} table.button.success:hover table a { - border: 0 solid #23bf5d; } + border: 0 solid #23bf5d; +} table.button.alert table td { - background: #ec5840; - border: 0px solid #ec5840; } + background: #ec5840; + border: 0px solid #ec5840; +} table.button.alert table a { - border: 0 solid #ec5840; } + border: 0 solid #ec5840; +} table.button.alert:hover table td { - background: #e23317; } + background: #e23317; +} table.button.alert:hover table a { - border: 0 solid #e23317; } + border: 0 solid #e23317; +} table.button.warning table td { - background: #ffae00; - border: 0px solid #ffae00; } + background: #ffae00; + border: 0px solid #ffae00; +} table.button.warning table a { - border: 0px solid #ffae00; } + border: 0px solid #ffae00; +} table.button.warning:hover table td { - background: #cc8b00; } + background: #cc8b00; +} table.button.warning:hover table a { - border: 0px solid #cc8b00; } + border: 0px solid #cc8b00; +} table.callout { - margin-bottom: 16px; - Margin-bottom: 16px; } + margin-bottom: 16px; + Margin-bottom: 16px; +} th.callout-inner { - width: 100%; - border: 1px solid #cbcbcb; - padding: 10px; - background: #fefefe; } - th.callout-inner.primary { + width: 100%; + border: 1px solid #cbcbcb; + padding: 10px; + background: #fefefe; +} + +th.callout-inner.primary { background: #def0fc; border: 1px solid #444444; - color: #0a0a0a; } - th.callout-inner.secondary { + color: #0a0a0a; +} + +th.callout-inner.secondary { background: #ebebeb; border: 1px solid #444444; - color: #0a0a0a; } - th.callout-inner.success { + color: #0a0a0a; +} + +th.callout-inner.success { background: #e1faea; border: 1px solid #1b9448; - color: #fefefe; } - th.callout-inner.warning { + color: #fefefe; +} + +th.callout-inner.warning { background: #fff3d9; border: 1px solid #996800; - color: #fefefe; } - th.callout-inner.alert { + color: #fefefe; +} + +th.callout-inner.alert { background: #fce6e2; border: 1px solid #b42912; - color: #fefefe; } + color: #fefefe; +} .thumbnail { - border: solid 4px #fefefe; - box-shadow: 0 0 0 1px rgba(10, 10, 10, 0.2); - display: inline-block; - line-height: 0; - max-width: 100%; - transition: box-shadow 200ms ease-out; - border-radius: 3px; - margin-bottom: 16px; } - .thumbnail:hover, .thumbnail:focus { - box-shadow: 0 0 6px 1px rgba(33, 153, 232, 0.5); } + border: solid 4px #fefefe; + box-shadow: 0 0 0 1px rgba(10, 10, 10, 0.2); + display: inline-block; + line-height: 0; + max-width: 100%; + transition: box-shadow 200ms ease-out; + border-radius: 3px; + margin-bottom: 16px; +} + +.thumbnail:hover, .thumbnail:focus { + box-shadow: 0 0 6px 1px rgba(33, 153, 232, 0.5); +} table.menu { - width: 580px; } - table.menu td.menu-item, - table.menu th.menu-item { + width: 580px; +} + +table.menu td.menu-item, +table.menu th.menu-item { padding: 10px; - padding-right: 10px; } - table.menu td.menu-item a, - table.menu th.menu-item a { - color: #2199e8; } + padding-right: 10px; +} + +table.menu td.menu-item a, +table.menu th.menu-item a { + color: #2199e8; +} table.menu.vertical td.menu-item, table.menu.vertical th.menu-item { - padding: 10px; - padding-right: 0; - display: block; } - table.menu.vertical td.menu-item a, - table.menu.vertical th.menu-item a { - width: 100%; } + padding: 10px; + padding-right: 0; + display: block; +} + +table.menu.vertical td.menu-item a, +table.menu.vertical th.menu-item a { + width: 100%; +} table.menu.vertical td.menu-item table.menu.vertical td.menu-item, table.menu.vertical td.menu-item table.menu.vertical th.menu-item, table.menu.vertical th.menu-item table.menu.vertical td.menu-item, table.menu.vertical th.menu-item table.menu.vertical th.menu-item { - padding-left: 10px; } + padding-left: 10px; +} table.menu.text-center a { - text-align: center; } + text-align: center; +} .menu[align="center"] { - width: auto !important; } + width: auto !important; +} body.outlook p { - display: inline !important; } + display: inline !important; +} @media only screen and (max-width: 596px) { - table.body img { - width: auto; - height: auto; } - table.body center { - min-width: 0 !important; } - table.body .container { - width: 95% !important; } - table.body .columns, - table.body .column { - height: auto !important; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - padding-left: 16px !important; - padding-right: 16px !important; } + table.body img { + width: auto; + height: auto; + } + + table.body center { + min-width: 0 !important; + } + + table.body .container { + width: 95% !important; + } + + table.body .columns, + table.body .column { + height: auto !important; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding-left: 16px !important; + padding-right: 16px !important; + } + table.body .columns .column, table.body .columns .columns, table.body .column .column, table.body .column .columns { - padding-left: 0 !important; - padding-right: 0 !important; } - table.body .collapse .columns, - table.body .collapse .column { - padding-left: 0 !important; - padding-right: 0 !important; } - td.small-1, - th.small-1 { - display: inline-block !important; - width: 8.33333% !important; } - td.small-2, - th.small-2 { - display: inline-block !important; - width: 16.66667% !important; } - td.small-3, - th.small-3 { - display: inline-block !important; - width: 25% !important; } - td.small-4, - th.small-4 { - display: inline-block !important; - width: 33.33333% !important; } - td.small-5, - th.small-5 { - display: inline-block !important; - width: 41.66667% !important; } - td.small-6, - th.small-6 { - display: inline-block !important; - width: 50% !important; } - td.small-7, - th.small-7 { - display: inline-block !important; - width: 58.33333% !important; } - td.small-8, - th.small-8 { - display: inline-block !important; - width: 66.66667% !important; } - td.small-9, - th.small-9 { - display: inline-block !important; - width: 75% !important; } - td.small-10, - th.small-10 { - display: inline-block !important; - width: 83.33333% !important; } - td.small-11, - th.small-11 { - display: inline-block !important; - width: 91.66667% !important; } - td.small-12, - th.small-12 { - display: inline-block !important; - width: 100% !important; } - .columns td.small-12, - .column td.small-12, - .columns th.small-12, - .column th.small-12 { - display: block !important; - width: 100% !important; } - table.body td.small-offset-1, - table.body th.small-offset-1 { - margin-left: 8.33333% !important; - Margin-left: 8.33333% !important; } - table.body td.small-offset-2, - table.body th.small-offset-2 { - margin-left: 16.66667% !important; - Margin-left: 16.66667% !important; } - table.body td.small-offset-3, - table.body th.small-offset-3 { - margin-left: 25% !important; - Margin-left: 25% !important; } - table.body td.small-offset-4, - table.body th.small-offset-4 { - margin-left: 33.33333% !important; - Margin-left: 33.33333% !important; } - table.body td.small-offset-5, - table.body th.small-offset-5 { - margin-left: 41.66667% !important; - Margin-left: 41.66667% !important; } - table.body td.small-offset-6, - table.body th.small-offset-6 { - margin-left: 50% !important; - Margin-left: 50% !important; } - table.body td.small-offset-7, - table.body th.small-offset-7 { - margin-left: 58.33333% !important; - Margin-left: 58.33333% !important; } - table.body td.small-offset-8, - table.body th.small-offset-8 { - margin-left: 66.66667% !important; - Margin-left: 66.66667% !important; } - table.body td.small-offset-9, - table.body th.small-offset-9 { - margin-left: 75% !important; - Margin-left: 75% !important; } - table.body td.small-offset-10, - table.body th.small-offset-10 { - margin-left: 83.33333% !important; - Margin-left: 83.33333% !important; } - table.body td.small-offset-11, - table.body th.small-offset-11 { - margin-left: 91.66667% !important; - Margin-left: 91.66667% !important; } - table.body table.columns td.expander, - table.body table.columns th.expander { - display: none !important; } - table.body .right-text-pad, - table.body .text-pad-right { - padding-left: 10px !important; } - table.body .left-text-pad, - table.body .text-pad-left { - padding-right: 10px !important; } - table.menu { - width: 100% !important; } + padding-left: 0 !important; + padding-right: 0 !important; + } + + table.body .collapse .columns, + table.body .collapse .column { + padding-left: 0 !important; + padding-right: 0 !important; + } + + td.small-1, + th.small-1 { + display: inline-block !important; + width: 8.33333% !important; + } + + td.small-2, + th.small-2 { + display: inline-block !important; + width: 16.66667% !important; + } + + td.small-3, + th.small-3 { + display: inline-block !important; + width: 25% !important; + } + + td.small-4, + th.small-4 { + display: inline-block !important; + width: 33.33333% !important; + } + + td.small-5, + th.small-5 { + display: inline-block !important; + width: 41.66667% !important; + } + + td.small-6, + th.small-6 { + display: inline-block !important; + width: 50% !important; + } + + td.small-7, + th.small-7 { + display: inline-block !important; + width: 58.33333% !important; + } + + td.small-8, + th.small-8 { + display: inline-block !important; + width: 66.66667% !important; + } + + td.small-9, + th.small-9 { + display: inline-block !important; + width: 75% !important; + } + + td.small-10, + th.small-10 { + display: inline-block !important; + width: 83.33333% !important; + } + + td.small-11, + th.small-11 { + display: inline-block !important; + width: 91.66667% !important; + } + + td.small-12, + th.small-12 { + display: inline-block !important; + width: 100% !important; + } + + .columns td.small-12, + .column td.small-12, + .columns th.small-12, + .column th.small-12 { + display: block !important; + width: 100% !important; + } + + table.body td.small-offset-1, + table.body th.small-offset-1 { + margin-left: 8.33333% !important; + Margin-left: 8.33333% !important; + } + + table.body td.small-offset-2, + table.body th.small-offset-2 { + margin-left: 16.66667% !important; + Margin-left: 16.66667% !important; + } + + table.body td.small-offset-3, + table.body th.small-offset-3 { + margin-left: 25% !important; + Margin-left: 25% !important; + } + + table.body td.small-offset-4, + table.body th.small-offset-4 { + margin-left: 33.33333% !important; + Margin-left: 33.33333% !important; + } + + table.body td.small-offset-5, + table.body th.small-offset-5 { + margin-left: 41.66667% !important; + Margin-left: 41.66667% !important; + } + + table.body td.small-offset-6, + table.body th.small-offset-6 { + margin-left: 50% !important; + Margin-left: 50% !important; + } + + table.body td.small-offset-7, + table.body th.small-offset-7 { + margin-left: 58.33333% !important; + Margin-left: 58.33333% !important; + } + + table.body td.small-offset-8, + table.body th.small-offset-8 { + margin-left: 66.66667% !important; + Margin-left: 66.66667% !important; + } + + table.body td.small-offset-9, + table.body th.small-offset-9 { + margin-left: 75% !important; + Margin-left: 75% !important; + } + + table.body td.small-offset-10, + table.body th.small-offset-10 { + margin-left: 83.33333% !important; + Margin-left: 83.33333% !important; + } + + table.body td.small-offset-11, + table.body th.small-offset-11 { + margin-left: 91.66667% !important; + Margin-left: 91.66667% !important; + } + + table.body table.columns td.expander, + table.body table.columns th.expander { + display: none !important; + } + + table.body .right-text-pad, + table.body .text-pad-right { + padding-left: 10px !important; + } + + table.body .left-text-pad, + table.body .text-pad-left { + padding-right: 10px !important; + } + + table.menu { + width: 100% !important; + } + table.menu td, table.menu th { - width: auto !important; - display: inline-block !important; } + width: auto !important; + display: inline-block !important; + } + table.menu.vertical td, table.menu.vertical th, table.menu.small-vertical td, table.menu.small-vertical th { - display: block !important; } - table.menu[align="center"] { - width: auto !important; } - table.button.small-expand, - table.button.small-expanded { - width: 100% !important; } + display: block !important; + } + + table.menu[align="center"] { + width: auto !important; + } + + table.button.small-expand, + table.button.small-expanded { + width: 100% !important; + } + table.button.small-expand table, table.button.small-expanded table { - width: 100%; } - table.button.small-expand table a, - table.button.small-expanded table a { + width: 100%; + } + + table.button.small-expand table a, + table.button.small-expanded table a { text-align: center !important; width: 100% !important; padding-left: 0 !important; - padding-right: 0 !important; } + padding-right: 0 !important; + } + table.button.small-expand center, table.button.small-expanded center { - min-width: 0; } } + min-width: 0; + } +} diff --git a/assets/fonts/dompdf/.gitignore b/assets/fonts/dompdf/.gitignore new file mode 100644 index 00000000..085ad9f3 --- /dev/null +++ b/assets/fonts/dompdf/.gitignore @@ -0,0 +1,3 @@ +# Ignore font files +*.otf +*.ttf \ No newline at end of file diff --git a/assets/fonts/dompdf/README.md b/assets/fonts/dompdf/README.md new file mode 100644 index 00000000..d80c64f4 --- /dev/null +++ b/assets/fonts/dompdf/README.md @@ -0,0 +1 @@ +Put your font ttf files in this folder to make them available to the label generator. \ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js index 8242331f..43acec5d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -22,7 +22,6 @@ import '../css/app/layout.css'; import '../css/app/helpers.css'; -import '../css/app/darkmode.css'; import '../css/app/tables.css'; import '../css/app/bs-overrides.css'; import '../css/app/treeview.css'; @@ -45,4 +44,18 @@ import "./register_events"; import "./tristate_checkboxes"; //Define jquery globally -window.$ = window.jQuery = require("jquery") \ No newline at end of file +window.$ = window.jQuery = require("jquery"); + +//Use the local WASM file for the ZXing library +import { + setZXingModuleOverrides, +} from "barcode-detector/pure"; +import wasmFile from "../../node_modules/zxing-wasm/dist/reader/zxing_reader.wasm"; +setZXingModuleOverrides({ + locateFile: (path, prefix) => { + if (path.endsWith(".wasm")) { + return wasmFile; + } + return prefix + path; + }, +}); \ No newline at end of file diff --git a/assets/js/lib/dataTables.select.mjs b/assets/js/lib/dataTables.select.mjs new file mode 100644 index 00000000..bba97692 --- /dev/null +++ b/assets/js/lib/dataTables.select.mjs @@ -0,0 +1,1538 @@ +/********************* + * This is the fixed version of the select extension for DataTables with the fix for the issue with the select extension + * (https://github.com/DataTables/Select/issues/51) + * We use this instead of the yarn version until the PR (https://github.com/DataTables/Select/pull/52) is merged and released + * /*******************/ + + +/*! Select for DataTables 2.0.0 + * © SpryMedia Ltd - datatables.net/license/mit + */ + +import jQuery from 'jquery'; +import DataTable from 'datatables.net'; + +// Allow reassignment of the $ variable +let $ = jQuery; + + +// Version information for debugger +DataTable.select = {}; + +DataTable.select.version = '2.0.0'; + +DataTable.select.init = function (dt) { + var ctx = dt.settings()[0]; + + if (!DataTable.versionCheck('2')) { + throw 'Warning: Select requires DataTables 2 or newer'; + } + + if (ctx._select) { + return; + } + + var savedSelected = dt.state.loaded(); + + var selectAndSave = function (e, settings, data) { + if (data === null || data.select === undefined) { + return; + } + + // Clear any currently selected rows, before restoring state + // None will be selected on first initialisation + if (dt.rows({ selected: true }).any()) { + dt.rows().deselect(); + } + if (data.select.rows !== undefined) { + dt.rows(data.select.rows).select(); + } + + if (dt.columns({ selected: true }).any()) { + dt.columns().deselect(); + } + if (data.select.columns !== undefined) { + dt.columns(data.select.columns).select(); + } + + if (dt.cells({ selected: true }).any()) { + dt.cells().deselect(); + } + if (data.select.cells !== undefined) { + for (var i = 0; i < data.select.cells.length; i++) { + dt.cell(data.select.cells[i].row, data.select.cells[i].column).select(); + } + } + + dt.state.save(); + }; + + dt.on('stateSaveParams', function (e, settings, data) { + data.select = {}; + data.select.rows = dt.rows({ selected: true }).ids(true).toArray(); + data.select.columns = dt.columns({ selected: true })[0]; + data.select.cells = dt.cells({ selected: true })[0].map(function (coords) { + return { row: dt.row(coords.row).id(true), column: coords.column }; + }); + }) + .on('stateLoadParams', selectAndSave) + .one('init', function () { + selectAndSave(undefined, undefined, savedSelected); + }); + + var init = ctx.oInit.select; + var defaults = DataTable.defaults.select; + var opts = init === undefined ? defaults : init; + + // Set defaults + var items = 'row'; + var style = 'api'; + var blurable = false; + var toggleable = true; + var info = true; + var selector = 'td, th'; + var className = 'selected'; + var headerCheckbox = true; + var setStyle = false; + + ctx._select = { + infoEls: [] + }; + + // Initialisation customisations + if (opts === true) { + style = 'os'; + setStyle = true; + } + else if (typeof opts === 'string') { + style = opts; + setStyle = true; + } + else if ($.isPlainObject(opts)) { + if (opts.blurable !== undefined) { + blurable = opts.blurable; + } + + if (opts.toggleable !== undefined) { + toggleable = opts.toggleable; + } + + if (opts.info !== undefined) { + info = opts.info; + } + + if (opts.items !== undefined) { + items = opts.items; + } + + if (opts.style !== undefined) { + style = opts.style; + setStyle = true; + } + else { + style = 'os'; + setStyle = true; + } + + if (opts.selector !== undefined) { + selector = opts.selector; + } + + if (opts.className !== undefined) { + className = opts.className; + } + + if (opts.headerCheckbox !== undefined) { + headerCheckbox = opts.headerCheckbox; + } + } + + dt.select.selector(selector); + dt.select.items(items); + dt.select.style(style); + dt.select.blurable(blurable); + dt.select.toggleable(toggleable); + dt.select.info(info); + ctx._select.className = className; + + // If the init options haven't enabled select, but there is a selectable + // class name, then enable + if (!setStyle && $(dt.table().node()).hasClass('selectable')) { + dt.select.style('os'); + } + + // Insert a checkbox into the header if needed - might need to wait + // for init complete, or it might already be done + if (headerCheckbox) { + initCheckboxHeader(dt); + + dt.on('init', function () { + initCheckboxHeader(dt); + }); + } +}; + +/* + +Select is a collection of API methods, event handlers, event emitters and +buttons (for the `Buttons` extension) for DataTables. It provides the following +features, with an overview of how they are implemented: + +## Selection of rows, columns and cells. Whether an item is selected or not is + stored in: + +* rows: a `_select_selected` property which contains a boolean value of the + DataTables' `aoData` object for each row +* columns: a `_select_selected` property which contains a boolean value of the + DataTables' `aoColumns` object for each column +* cells: a `_selected_cells` property which contains an array of boolean values + of the `aoData` object for each row. The array is the same length as the + columns array, with each element of it representing a cell. + +This method of using boolean flags allows Select to operate when nodes have not +been created for rows / cells (DataTables' defer rendering feature). + +## API methods + +A range of API methods are available for triggering selection and de-selection +of rows. Methods are also available to configure the selection events that can +be triggered by an end user (such as which items are to be selected). To a large +extent, these of API methods *is* Select. It is basically a collection of helper +functions that can be used to select items in a DataTable. + +Configuration of select is held in the object `_select` which is attached to the +DataTables settings object on initialisation. Select being available on a table +is not optional when Select is loaded, but its default is for selection only to +be available via the API - so the end user wouldn't be able to select rows +without additional configuration. + +The `_select` object contains the following properties: + +``` +{ + items:string - Can be `rows`, `columns` or `cells`. Defines what item + will be selected if the user is allowed to activate row + selection using the mouse. + style:string - Can be `none`, `single`, `multi` or `os`. Defines the + interaction style when selecting items + blurable:boolean - If row selection can be cleared by clicking outside of + the table + toggleable:boolean - If row selection can be cancelled by repeated clicking + on the row + info:boolean - If the selection summary should be shown in the table + information elements + infoEls:element[] - List of HTML elements with info elements for a table +} +``` + +In addition to the API methods, Select also extends the DataTables selector +options for rows, columns and cells adding a `selected` option to the selector +options object, allowing the developer to select only selected items or +unselected items. + +## Mouse selection of items + +Clicking on items can be used to select items. This is done by a simple event +handler that will select the items using the API methods. + + */ + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Local functions + */ + +/** + * Add one or more cells to the selection when shift clicking in OS selection + * style cell selection. + * + * Cell range is more complicated than row and column as we want to select + * in the visible grid rather than by index in sequence. For example, if you + * click first in cell 1-1 and then shift click in 2-2 - cells 1-2 and 2-1 + * should also be selected (and not 1-3, 1-4. etc) + * + * @param {DataTable.Api} dt DataTable + * @param {object} idx Cell index to select to + * @param {object} last Cell index to select from + * @private + */ +function cellRange(dt, idx, last) { + var indexes; + var columnIndexes; + var rowIndexes; + var selectColumns = function (start, end) { + if (start > end) { + var tmp = end; + end = start; + start = tmp; + } + + var record = false; + return dt + .columns(':visible') + .indexes() + .filter(function (i) { + if (i === start) { + record = true; + } + + if (i === end) { + // not else if, as start might === end + record = false; + return true; + } + + return record; + }); + }; + + var selectRows = function (start, end) { + var indexes = dt.rows({ search: 'applied' }).indexes(); + + // Which comes first - might need to swap + if (indexes.indexOf(start) > indexes.indexOf(end)) { + var tmp = end; + end = start; + start = tmp; + } + + var record = false; + return indexes.filter(function (i) { + if (i === start) { + record = true; + } + + if (i === end) { + record = false; + return true; + } + + return record; + }); + }; + + if (!dt.cells({ selected: true }).any() && !last) { + // select from the top left cell to this one + columnIndexes = selectColumns(0, idx.column); + rowIndexes = selectRows(0, idx.row); + } + else { + // Get column indexes between old and new + columnIndexes = selectColumns(last.column, idx.column); + rowIndexes = selectRows(last.row, idx.row); + } + + indexes = dt.cells(rowIndexes, columnIndexes).flatten(); + + if (!dt.cells(idx, { selected: true }).any()) { + // Select range + dt.cells(indexes).select(); + } + else { + // Deselect range + dt.cells(indexes).deselect(); + } +} + +/** + * Disable mouse selection by removing the selectors + * + * @param {DataTable.Api} dt DataTable to remove events from + * @private + */ +function disableMouseSelection(dt) { + var ctx = dt.settings()[0]; + var selector = ctx._select.selector; + + $(dt.table().container()) + .off('mousedown.dtSelect', selector) + .off('mouseup.dtSelect', selector) + .off('click.dtSelect', selector); + + $('body').off('click.dtSelect' + _safeId(dt.table().node())); +} + +/** + * Attach mouse listeners to the table to allow mouse selection of items + * + * @param {DataTable.Api} dt DataTable to remove events from + * @private + */ +function enableMouseSelection(dt) { + var container = $(dt.table().container()); + var ctx = dt.settings()[0]; + var selector = ctx._select.selector; + var matchSelection; + + container + .on('mousedown.dtSelect', selector, function (e) { + // Disallow text selection for shift clicking on the table so multi + // element selection doesn't look terrible! + if (e.shiftKey || e.metaKey || e.ctrlKey) { + container + .css('-moz-user-select', 'none') + .one('selectstart.dtSelect', selector, function () { + return false; + }); + } + + if (window.getSelection) { + matchSelection = window.getSelection(); + } + }) + .on('mouseup.dtSelect', selector, function () { + // Allow text selection to occur again, Mozilla style (tested in FF + // 35.0.1 - still required) + container.css('-moz-user-select', ''); + }) + .on('click.dtSelect', selector, function (e) { + var items = dt.select.items(); + var idx; + + // If text was selected (click and drag), then we shouldn't change + // the row's selected state + if (matchSelection) { + var selection = window.getSelection(); + + // If the element that contains the selection is not in the table, we can ignore it + // This can happen if the developer selects text from the click event + if ( + !selection.anchorNode || + $(selection.anchorNode).closest('table')[0] === dt.table().node() + ) { + if (selection !== matchSelection) { + return; + } + } + } + + var ctx = dt.settings()[0]; + var container = dt.table().container(); + + // Ignore clicks inside a sub-table + if ($(e.target).closest('div.dt-container')[0] != container) { + return; + } + + var cell = dt.cell($(e.target).closest('td, th')); + + // Check the cell actually belongs to the host DataTable (so child + // rows, etc, are ignored) + if (!cell.any()) { + return; + } + + var event = $.Event('user-select.dt'); + eventTrigger(dt, event, [items, cell, e]); + + if (event.isDefaultPrevented()) { + return; + } + + var cellIndex = cell.index(); + if (items === 'row') { + idx = cellIndex.row; + typeSelect(e, dt, ctx, 'row', idx); + } + else if (items === 'column') { + idx = cell.index().column; + typeSelect(e, dt, ctx, 'column', idx); + } + else if (items === 'cell') { + idx = cell.index(); + typeSelect(e, dt, ctx, 'cell', idx); + } + + ctx._select_lastCell = cellIndex; + }); + + // Blurable + $('body').on('click.dtSelect' + _safeId(dt.table().node()), function (e) { + if (ctx._select.blurable) { + // If the click was inside the DataTables container, don't blur + if ($(e.target).parents().filter(dt.table().container()).length) { + return; + } + + // Ignore elements which have been removed from the DOM (i.e. paging + // buttons) + if ($(e.target).parents('html').length === 0) { + return; + } + + // Don't blur in Editor form + if ($(e.target).parents('div.DTE').length) { + return; + } + + var event = $.Event('select-blur.dt'); + eventTrigger(dt, event, [e.target, e]); + + if (event.isDefaultPrevented()) { + return; + } + + clear(ctx, true); + } + }); +} + +/** + * Trigger an event on a DataTable + * + * @param {DataTable.Api} api DataTable to trigger events on + * @param {boolean} selected true if selected, false if deselected + * @param {string} type Item type acting on + * @param {boolean} any Require that there are values before + * triggering + * @private + */ +function eventTrigger(api, type, args, any) { + if (any && !api.flatten().length) { + return; + } + + if (typeof type === 'string') { + type = type + '.dt'; + } + + args.unshift(api); + + $(api.table().node()).trigger(type, args); +} + +/** + * Update the information element of the DataTable showing information about the + * items selected. This is done by adding tags to the existing text + * + * @param {DataTable.Api} api DataTable to update + * @private + */ +function info(api, node) { + if (api.select.style() === 'api' || api.select.info() === false) { + return; + } + + var rows = api.rows({ selected: true }).flatten().length; + var columns = api.columns({ selected: true }).flatten().length; + var cells = api.cells({ selected: true }).flatten().length; + + var add = function (el, name, num) { + el.append( + $('').append( + api.i18n( + 'select.' + name + 's', + { _: '%d ' + name + 's selected', 0: '', 1: '1 ' + name + ' selected' }, + num + ) + ) + ); + }; + + var el = $(node); + var output = $(''); + + add(output, 'row', rows); + add(output, 'column', columns); + add(output, 'cell', cells); + + var existing = el.children('span.select-info'); + + if (existing.length) { + existing.remove(); + } + + if (output.text() !== '') { + el.append(output); + } +} + +/** + * Add a checkbox to the header for checkbox columns, allowing all rows to + * be selected, deselected or just to show the state. + * + * @param {*} dt API + */ +function initCheckboxHeader( dt ) { + // Find any checkbox column(s) + dt.columns('.dt-select').every(function () { + var header = this.header(); + + if (! $('input', header).length) { + // If no checkbox yet, insert one + var input = $('') + .attr({ + class: 'dt-select-checkbox', + type: 'checkbox', + 'aria-label': dt.i18n('select.aria.headerCheckbox') || 'Select all rows' + }) + .appendTo(header) + .on('change', function () { + if (this.checked) { + dt.rows({search: 'applied'}).select(); + } + else { + dt.rows({selected: true}).deselect(); + } + }) + .on('click', function (e) { + e.stopPropagation(); + }); + + // Update the header checkbox's state when the selection in the + // table changes + dt.on('draw select deselect', function (e, pass, type) { + if (type === 'row' || ! type) { + var count = dt.rows({selected: true}).count(); + var search = dt.rows({search: 'applied', selected: true}).count(); + var available = dt.rows({search: 'applied'}).count(); + + if (search && search <= count && search === available) { + input + .prop('checked', true) + .prop('indeterminate', false); + } + else if (search === 0 && count === 0) { + input + .prop('checked', false) + .prop('indeterminate', false); + } + else { + input + .prop('checked', false) + .prop('indeterminate', true); + } + } + }); + } + }); +} + +/** + * Initialisation of a new table. Attach event handlers and callbacks to allow + * Select to operate correctly. + * + * This will occur _after_ the initial DataTables initialisation, although + * before Ajax data is rendered, if there is ajax data + * + * @param {DataTable.settings} ctx Settings object to operate on + * @private + */ +function init(ctx) { + var api = new DataTable.Api(ctx); + ctx._select_init = true; + + // Row callback so that classes can be added to rows and cells if the item + // was selected before the element was created. This will happen with the + // `deferRender` option enabled. + // + // This method of attaching to `aoRowCreatedCallback` is a hack until + // DataTables has proper events for row manipulation If you are reviewing + // this code to create your own plug-ins, please do not do this! + ctx.aoRowCreatedCallback.push(function (row, data, index) { + var i, ien; + var d = ctx.aoData[index]; + + // Row + if (d._select_selected) { + $(row).addClass(ctx._select.className); + } + + // Cells and columns - if separated out, we would need to do two + // loops, so it makes sense to combine them into a single one + for (i = 0, ien = ctx.aoColumns.length; i < ien; i++) { + if ( + ctx.aoColumns[i]._select_selected || + (d._selected_cells && d._selected_cells[i]) + ) { + $(d.anCells[i]).addClass(ctx._select.className); + } + } + } + ); + + // On Ajax reload we want to reselect all rows which are currently selected, + // if there is an rowId (i.e. a unique value to identify each row with) + api.on('preXhr.dt.dtSelect', function (e, settings) { + if (settings !== api.settings()[0]) { + // Not triggered by our DataTable! + return; + } + + // note that column selection doesn't need to be cached and then + // reselected, as they are already selected + var rows = api + .rows({ selected: true }) + .ids(true) + .filter(function (d) { + return d !== undefined; + }); + + var cells = api + .cells({ selected: true }) + .eq(0) + .map(function (cellIdx) { + var id = api.row(cellIdx.row).id(true); + return id ? { row: id, column: cellIdx.column } : undefined; + }) + .filter(function (d) { + return d !== undefined; + }); + + // On the next draw, reselect the currently selected items + api.one('draw.dt.dtSelect', function () { + api.rows(rows).select(); + + // `cells` is not a cell index selector, so it needs a loop + if (cells.any()) { + cells.each(function (id) { + api.cells(id.row, id.column).select(); + }); + } + }); + }); + + // Update the table information element with selected item summary + api.on('info.dt', function (e, ctx, node) { + // Store the info node for updating on select / deselect + if (!ctx._select.infoEls.includes(node)) { + ctx._select.infoEls.push(node); + } + + info(api, node); + }); + + api.on('select.dtSelect.dt deselect.dtSelect.dt', function () { + ctx._select.infoEls.forEach(function (el) { + info(api, el); + }); + + api.state.save(); + }); + + // Clean up and release + api.on('destroy.dtSelect', function () { + // Remove class directly rather than calling deselect - which would trigger events + $(api.rows({ selected: true }).nodes()).removeClass(api.settings()[0]._select.className); + + disableMouseSelection(api); + api.off('.dtSelect'); + $('body').off('.dtSelect' + _safeId(api.table().node())); + }); +} + +/** + * Add one or more items (rows or columns) to the selection when shift clicking + * in OS selection style + * + * @param {DataTable.Api} dt DataTable + * @param {string} type Row or column range selector + * @param {object} idx Item index to select to + * @param {object} last Item index to select from + * @private + */ +function rowColumnRange(dt, type, idx, last) { + // Add a range of rows from the last selected row to this one + var indexes = dt[type + 's']({ search: 'applied' }).indexes(); + var idx1 = indexes.indexOf(last); + var idx2 = indexes.indexOf(idx); + + if (!dt[type + 's']({ selected: true }).any() && idx1 === -1) { + // select from top to here - slightly odd, but both Windows and Mac OS + // do this + indexes.splice(indexes.indexOf(idx) + 1, indexes.length); + } + else { + // reverse so we can shift click 'up' as well as down + if (idx1 > idx2) { + var tmp = idx2; + idx2 = idx1; + idx1 = tmp; + } + + indexes.splice(idx2 + 1, indexes.length); + indexes.splice(0, idx1); + } + + if (!dt[type](idx, { selected: true }).any()) { + // Select range + dt[type + 's'](indexes).select(); + } + else { + // Deselect range - need to keep the clicked on row selected + indexes.splice(indexes.indexOf(idx), 1); + dt[type + 's'](indexes).deselect(); + } +} + +/** + * Clear all selected items + * + * @param {DataTable.settings} ctx Settings object of the host DataTable + * @param {boolean} [force=false] Force the de-selection to happen, regardless + * of selection style + * @private + */ +function clear(ctx, force) { + if (force || ctx._select.style === 'single') { + var api = new DataTable.Api(ctx); + + api.rows({ selected: true }).deselect(); + api.columns({ selected: true }).deselect(); + api.cells({ selected: true }).deselect(); + } +} + +/** + * Select items based on the current configuration for style and items. + * + * @param {object} e Mouse event object + * @param {DataTables.Api} dt DataTable + * @param {DataTable.settings} ctx Settings object of the host DataTable + * @param {string} type Items to select + * @param {int|object} idx Index of the item to select + * @private + */ +function typeSelect(e, dt, ctx, type, idx) { + var style = dt.select.style(); + var toggleable = dt.select.toggleable(); + var isSelected = dt[type](idx, { selected: true }).any(); + + if (isSelected && !toggleable) { + return; + } + + if (style === 'os') { + if (e.ctrlKey || e.metaKey) { + // Add or remove from the selection + dt[type](idx).select(!isSelected); + } + else if (e.shiftKey) { + if (type === 'cell') { + cellRange(dt, idx, ctx._select_lastCell || null); + } + else { + rowColumnRange( + dt, + type, + idx, + ctx._select_lastCell ? ctx._select_lastCell[type] : null + ); + } + } + else { + // No cmd or shift click - deselect if selected, or select + // this row only + var selected = dt[type + 's']({ selected: true }); + + if (isSelected && selected.flatten().length === 1) { + dt[type](idx).deselect(); + } + else { + selected.deselect(); + dt[type](idx).select(); + } + } + } + else if (style == 'multi+shift') { + if (e.shiftKey) { + if (type === 'cell') { + cellRange(dt, idx, ctx._select_lastCell || null); + } + else { + rowColumnRange( + dt, + type, + idx, + ctx._select_lastCell ? ctx._select_lastCell[type] : null + ); + } + } + else { + dt[type](idx).select(!isSelected); + } + } + else { + dt[type](idx).select(!isSelected); + } +} + +function _safeId(node) { + return node.id.replace(/[^a-zA-Z0-9\-\_]/g, '-'); +} + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * DataTables selectors + */ + +// row and column are basically identical just assigned to different properties +// and checking a different array, so we can dynamically create the functions to +// reduce the code size +$.each( + [ + { type: 'row', prop: 'aoData' }, + { type: 'column', prop: 'aoColumns' } + ], + function (i, o) { + DataTable.ext.selector[o.type].push(function (settings, opts, indexes) { + var selected = opts.selected; + var data; + var out = []; + + if (selected !== true && selected !== false) { + return indexes; + } + + for (var i = 0, ien = indexes.length; i < ien; i++) { + data = settings[o.prop][indexes[i]]; + + if ( + data && ( + (selected === true && data._select_selected === true) || + (selected === false && !data._select_selected) + ) + ) { + out.push(indexes[i]); + } + } + + return out; + }); + } +); + +DataTable.ext.selector.cell.push(function (settings, opts, cells) { + var selected = opts.selected; + var rowData; + var out = []; + + if (selected === undefined) { + return cells; + } + + for (var i = 0, ien = cells.length; i < ien; i++) { + rowData = settings.aoData[cells[i].row]; + + if ( + rowData && ( + (selected === true && + rowData._selected_cells && + rowData._selected_cells[cells[i].column] === true) || + (selected === false && + (!rowData._selected_cells || !rowData._selected_cells[cells[i].column])) + ) + ) { + out.push(cells[i]); + } + } + + return out; +}); + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * DataTables API + * + * For complete documentation, please refer to the docs/api directory or the + * DataTables site + */ + +// Local variables to improve compression +var apiRegister = DataTable.Api.register; +var apiRegisterPlural = DataTable.Api.registerPlural; + +apiRegister('select()', function () { + return this.iterator('table', function (ctx) { + DataTable.select.init(new DataTable.Api(ctx)); + }); +}); + +apiRegister('select.blurable()', function (flag) { + if (flag === undefined) { + return this.context[0]._select.blurable; + } + + return this.iterator('table', function (ctx) { + ctx._select.blurable = flag; + }); +}); + +apiRegister('select.toggleable()', function (flag) { + if (flag === undefined) { + return this.context[0]._select.toggleable; + } + + return this.iterator('table', function (ctx) { + ctx._select.toggleable = flag; + }); +}); + +apiRegister('select.info()', function (flag) { + if (flag === undefined) { + return this.context[0]._select.info; + } + + return this.iterator('table', function (ctx) { + ctx._select.info = flag; + }); +}); + +apiRegister('select.items()', function (items) { + if (items === undefined) { + return this.context[0]._select.items; + } + + return this.iterator('table', function (ctx) { + ctx._select.items = items; + + eventTrigger(new DataTable.Api(ctx), 'selectItems', [items]); + }); +}); + +// Takes effect from the _next_ selection. None disables future selection, but +// does not clear the current selection. Use the `deselect` methods for that +apiRegister('select.style()', function (style) { + if (style === undefined) { + return this.context[0]._select.style; + } + + return this.iterator('table', function (ctx) { + if (!ctx._select) { + DataTable.select.init(new DataTable.Api(ctx)); + } + + if (!ctx._select_init) { + init(ctx); + } + + ctx._select.style = style; + + // Add / remove mouse event handlers. They aren't required when only + // API selection is available + var dt = new DataTable.Api(ctx); + disableMouseSelection(dt); + + if (style !== 'api') { + enableMouseSelection(dt); + } + + eventTrigger(new DataTable.Api(ctx), 'selectStyle', [style]); + }); +}); + +apiRegister('select.selector()', function (selector) { + if (selector === undefined) { + return this.context[0]._select.selector; + } + + return this.iterator('table', function (ctx) { + disableMouseSelection(new DataTable.Api(ctx)); + + ctx._select.selector = selector; + + if (ctx._select.style !== 'api') { + enableMouseSelection(new DataTable.Api(ctx)); + } + }); +}); + +apiRegister('select.last()', function (set) { + let ctx = this.context[0]; + + if (set) { + ctx._select_lastCell = set; + return this; + } + + return ctx._select_lastCell; +}); + +apiRegisterPlural('rows().select()', 'row().select()', function (select) { + var api = this; + + if (select === false) { + return this.deselect(); + } + + this.iterator('row', function (ctx, idx) { + clear(ctx); + + // There is a good amount of knowledge of DataTables internals in + // this function. It _could_ be done without that, but it would hurt + // performance (or DT would need new APIs for this work) + var dtData = ctx.aoData[idx]; + var dtColumns = ctx.aoColumns; + + $(dtData.nTr).addClass(ctx._select.className); + dtData._select_selected = true; + + for (var i=0 ; i 0); + }); + + this.disable(); + }, + destroy: function (dt, node, config) { + dt.off(config._eventNamespace); + } + }, + showSelected: { + text: i18n('showSelected', 'Show only selected'), + className: 'buttons-show-selected', + action: function (e, dt) { + if (dt.search.fixed('dt-select')) { + // Remove existing function + dt.search.fixed('dt-select', null); + + this.active(false); + } + else { + // Use a fixed filtering function to match on selected rows + // This needs to reference the internal aoData since that is + // where Select stores its reference for the selected state + var dataSrc = dt.settings()[0].aoData; + + dt.search.fixed('dt-select', function (text, data, idx) { + // _select_selected is set by Select on the data object for the row + return dataSrc[idx]._select_selected; + }); + + this.active(true); + } + + dt.draw(); + } + } +}); + +$.each(['Row', 'Column', 'Cell'], function (i, item) { + var lc = item.toLowerCase(); + + DataTable.ext.buttons['select' + item + 's'] = { + text: i18n('select' + item + 's', 'Select ' + lc + 's'), + className: 'buttons-select-' + lc + 's', + action: function () { + this.select.items(lc); + }, + init: function (dt) { + var that = this; + + dt.on('selectItems.dt.DT', function (e, ctx, items) { + that.active(items === lc); + }); + } + }; +}); + +DataTable.type('select-checkbox', { + className: 'dt-select', + detect: function (data) { + // Rendering function will tell us if it is a checkbox type + return data === 'select-checkbox' ? data : false; + }, + order: { + pre: function (d) { + return d === 'X' ? -1 : 0; + } + } +}); + +$.extend(true, DataTable.defaults.oLanguage, { + select: { + aria: { + rowCheckbox: 'Select row' + } + } +}); + +DataTable.render.select = function (valueProp, nameProp) { + var valueFn = valueProp ? DataTable.util.get(valueProp) : null; + var nameFn = nameProp ? DataTable.util.get(nameProp) : null; + + return function (data, type, row, meta) { + var dtRow = meta.settings.aoData[meta.row]; + var selected = dtRow._select_selected; + var ariaLabel = meta.settings.oLanguage.select.aria.rowCheckbox; + + if (type === 'display') { + return $('') + .attr({ + 'aria-label': ariaLabel, + class: 'dt-select-checkbox', + name: nameFn ? nameFn(row) : null, + type: 'checkbox', + value: valueFn ? valueFn(row) : null, + checked: selected + })[0]; + } + else if (type === 'type') { + return 'select-checkbox'; + } + else if (type === 'filter') { + return ''; + } + + return selected ? 'X' : ''; + } +} + +// Legacy checkbox ordering +DataTable.ext.order['select-checkbox'] = function (settings, col) { + return this.api() + .column(col, { order: 'index' }) + .nodes() + .map(function (td) { + if (settings._select.items === 'row') { + return $(td).parent().hasClass(settings._select.className); + } + else if (settings._select.items === 'cell') { + return $(td).hasClass(settings._select.className); + } + return false; + }); +}; + +$.fn.DataTable.select = DataTable.select; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Initialisation + */ + +// DataTables creation - check if select has been defined in the options. Note +// this required that the table be in the document! If it isn't then something +// needs to trigger this method unfortunately. The next major release of +// DataTables will rework the events and address this. +$(document).on('preInit.dt.dtSelect', function (e, ctx) { + if (e.namespace !== 'dt') { + return; + } + + DataTable.select.init(new DataTable.Api(ctx)); +}); + + +export default DataTable; diff --git a/assets/js/lib/datatables.js b/assets/js/lib/datatables.js index 5c94799a..8e39548b 100644 --- a/assets/js/lib/datatables.js +++ b/assets/js/lib/datatables.js @@ -47,7 +47,8 @@ method: config.method, data: { _dt: config.name, - _init: true + _init: true, + order: config.initial_order ?? undefined, } }).done(function(data) { var baseState; @@ -72,6 +73,17 @@ } } else { request._dt = config.name; + + //Try to resolve the original column index when the column was reordered (using the ColReorder plugin) + //Only do this when _ColReorder_iOrigCol is available + if (settings.aoColumns && settings.aoColumns.length && settings.aoColumns[0]._ColReorder_iOrigCol !== undefined) { + if (request.order && request.order.length) { + request.order.forEach(function (order) { + order.column = settings.aoColumns[order.column]._ColReorder_iOrigCol; + }); + } + } + $.ajax(typeof config.url === 'function' ? config.url(dt) : config.url, { method: config.method, data: request @@ -86,6 +98,15 @@ dtOpts = config.options(dtOpts); } + //Choose the column where the className contains "select-column" and apply the select extension to its render field + //Added for Part-DB + for (let column of dtOpts.columns) { + if (column.className && column.className.includes('dt-select')) { + column.render = $.fn.dataTable.render.select(); + } + } + + root.html(data.template); dt = $('table', root).DataTable(dtOpts); if (config.state !== 'none') { diff --git a/assets/js/register_events.js b/assets/js/register_events.js index 9fcbd6cc..9732c0c1 100644 --- a/assets/js/register_events.js +++ b/assets/js/register_events.js @@ -20,6 +20,8 @@ 'use strict'; import {Dropdown} from "bootstrap"; +import ClipboardJS from "clipboard"; +import {Modal} from "bootstrap"; class RegisterEventHelper { constructor() { @@ -27,7 +29,14 @@ class RegisterEventHelper { this.configureDropdowns(); this.registerSpecialCharInput(); + //Initialize ClipboardJS + this.registerLoadHandler(() => { + new ClipboardJS('.btn'); + }); + this.registerModalDropRemovalOnFormSubmit(); + + } registerModalDropRemovalOnFormSubmit() { @@ -37,6 +46,15 @@ class RegisterEventHelper { if (back_drop) { back_drop.remove(); } + + //Remove scroll-lock if it is still active + if (document.body.classList.contains('modal-open')) { + document.body.classList.remove('modal-open'); + + //Remove the padding-right and overflow:hidden from the body + document.body.style.paddingRight = ''; + document.body.style.overflow = ''; + } }); } @@ -59,76 +77,239 @@ class RegisterEventHelper { } registerTooltips() { - this.registerLoadHandler(() => { + const handler = () => { $(".tooltip").remove(); //Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.) - $('a[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i.fas[title]') + $('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title]') //@ts-ignore .tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'}); - }); + }; + + this.registerLoadHandler(handler); + document.addEventListener('dt:loaded', handler); } registerSpecialCharInput() { this.registerLoadHandler(() => { //@ts-ignore $("input[type=text], input[type=search]").unbind("keydown").keydown(function (event) { - let greek = event.altKey; + let use_special_char = event.altKey; let greek_char = ""; - if (greek){ + if (use_special_char){ + //Use the key property to determine the greek letter (as it is independent of the keyboard layout) switch(event.key) { - case "w": //Omega - greek_char = '\u2126'; - break; - case "u": - case "m": //Micro - greek_char = "\u00B5"; - break; - case "p": //Phi - greek_char = "\u03C6"; - break; - case "a": //Alpha + //Greek letters + case "a": //Alpha (lowercase) greek_char = "\u03B1"; break; - case "b": //Beta + case "A": //Alpha (uppercase) + greek_char = "\u0391"; + break; + case "b": //Beta (lowercase) greek_char = "\u03B2"; break; - case "c": //Gamma + case "B": //Beta (uppercase) + greek_char = "\u0392"; + break; + case "g": //Gamma (lowercase) greek_char = "\u03B3"; break; - case "d": //Delta + case "G": //Gamma (uppercase) + greek_char = "\u0393"; + break; + case "d": //Delta (lowercase) greek_char = "\u03B4"; break; - case "l": //Pound - greek_char = "\u00A3"; + case "D": //Delta (uppercase) + greek_char = "\u0394"; break; - case "y": //Yen - greek_char = "\u00A5"; + case "e": //Epsilon (lowercase) + greek_char = "\u03B5"; break; - case "o": //Yen - greek_char = "\u00A4"; + case "E": //Epsilon (uppercase) + greek_char = "\u0395"; break; - case "1": //Sum symbol - greek_char = "\u2211"; + case "z": //Zeta (lowercase) + greek_char = "\u03B6"; break; - case "2": //Integral - greek_char = "\u222B"; + case "Z": //Zeta (uppercase) + greek_char = "\u0396"; break; - case "3": //Less-than or equal - greek_char = "\u2264"; + case "h": //Eta (lowercase) + greek_char = "\u03B7"; break; - case "4": //Greater than or equal - greek_char = "\u2265"; + case "H": //Eta (uppercase) + greek_char = "\u0397"; break; - case "5": //PI - greek_char = "\u03c0"; + case "q": //Theta (lowercase) + greek_char = "\u03B8"; break; - case "q": //Copyright - greek_char = "\u00A9"; + case "Q": //Theta (uppercase) + greek_char = "\u0398"; break; - case "e": //Euro - greek_char = "\u20AC"; + case "i": //Iota (lowercase) + greek_char = "\u03B9"; break; + case "I": //Iota (uppercase) + greek_char = "\u0399"; + break; + case "k": //Kappa (lowercase) + greek_char = "\u03BA"; + break; + case "K": //Kappa (uppercase) + greek_char = "\u039A"; + break; + case "l": //Lambda (lowercase) + greek_char = "\u03BB"; + break; + case "L": //Lambda (uppercase) + greek_char = "\u039B"; + break; + case "m": //Mu (lowercase) + greek_char = "\u03BC"; + break; + case "M": //Mu (uppercase) + greek_char = "\u039C"; + break; + case "n": //Nu (lowercase) + greek_char = "\u03BD"; + break; + case "N": //Nu (uppercase) + greek_char = "\u039D"; + break; + case "x": //Xi (lowercase) + greek_char = "\u03BE"; + break; + case "X": //Xi (uppercase) + greek_char = "\u039E"; + break; + case "o": //Omicron (lowercase) + greek_char = "\u03BF"; + break; + case "O": //Omicron (uppercase) + greek_char = "\u039F"; + break; + case "p": //Pi (lowercase) + greek_char = "\u03C0"; + break; + case "P": //Pi (uppercase) + greek_char = "\u03A0"; + break; + case "r": //Rho (lowercase) + greek_char = "\u03C1"; + break; + case "R": //Rho (uppercase) + greek_char = "\u03A1"; + break; + case "s": //Sigma (lowercase) + greek_char = "\u03C3"; + break; + case "S": //Sigma (uppercase) + greek_char = "\u03A3"; + break; + case "t": //Tau (lowercase) + greek_char = "\u03C4"; + break; + case "T": //Tau (uppercase) + greek_char = "\u03A4"; + break; + case "u": //Upsilon (lowercase) + greek_char = "\u03C5"; + break; + case "U": //Upsilon (uppercase) + greek_char = "\u03A5"; + break; + case "f": //Phi (lowercase) + greek_char = "\u03C6"; + break; + case "F": //Phi (uppercase) + greek_char = "\u03A6"; + break; + case "c": //Chi (lowercase) + greek_char = "\u03C7"; + break; + case "C": //Chi (uppercase) + greek_char = "\u03A7"; + break; + case "y": //Psi (lowercase) + greek_char = "\u03C8"; + break; + case "Y": //Psi (uppercase) + greek_char = "\u03A8"; + break; + case "w": //Omega (lowercase) + greek_char = "\u03C9"; + break; + case "W": //Omega (uppercase) + greek_char = "\u03A9"; + break; + } + + //Use keycodes for special characters as the shift char on the number keys are layout dependent + switch (event.keyCode) { + case 49: //1 key + //Product symbol on shift, sum on no shift + greek_char = event.shiftKey ? "\u220F" : "\u2211"; + break; + case 50: //2 key + //Integral on no shift, partial derivative on shift + greek_char = event.shiftKey ? "\u2202" : "\u222B"; + break; + case 51: //3 key + //Less than or equal on no shift, greater than or equal on shift + greek_char = event.shiftKey ? "\u2265" : "\u2264"; + break; + case 52: //4 key + //Empty set on shift, infinity on no shift + greek_char = event.shiftKey ? "\u2205" : "\u221E"; + break; + case 53: //5 key + //Not equal on shift, approx equal on no shift + greek_char = event.shiftKey ? "\u2260" : "\u2248"; + break; + case 54: //6 key + //Element of on no shift, not element of on shift + greek_char = event.shiftKey ? "\u2209" : "\u2208"; + break; + case 55: //7 key + //And on shift, or on no shift + greek_char = event.shiftKey ? "\u2227" : "\u2228"; + break; + case 56: //8 key + //Proportional to on shift, angle on no shift + greek_char = event.shiftKey ? "\u221D" : "\u2220"; + break; + case 57: //9 key + //Cube root on shift, square root on no shift + greek_char = event.shiftKey ? "\u221B" : "\u221A"; + break; + case 48: //0 key + //Minus-Plus on shift, plus-minus on no shift + greek_char = event.shiftKey ? "\u2213" : "\u00B1"; + break; + + //Special characters + case 219: //hyphen (or ß on german layout) + //Copyright on no shift, TM on shift + greek_char = event.shiftKey ? "\u2122" : "\u00A9"; + break; + case 191: //forward slash (or # on german layout) + //Generic currency on no shift, paragraph on shift + greek_char = event.shiftKey ? "\u00B6" : "\u00A4"; + break; + + //Currency symbols + case 192: //: or (ö on german layout) + //Euro on no shift, pound on shift + greek_char = event.shiftKey ? "\u00A3" : "\u20AC"; + break; + case 221: //; or (ä on german layout) + //Yen on no shift, dollar on shift + greek_char = event.shiftKey ? "\u0024" : "\u00A5"; + break; + + } if(greek_char=="") return; diff --git a/assets/js/tab_remember.js b/assets/js/tab_remember.js index 9ecd71c5..1bf35db5 100644 --- a/assets/js/tab_remember.js +++ b/assets/js/tab_remember.js @@ -19,7 +19,7 @@ "use strict"; -import {Tab, Dropdown} from "bootstrap"; +import {Tab, Dropdown, Collapse} from "bootstrap"; import tab from "bootstrap/js/src/tab"; /** @@ -54,6 +54,7 @@ class TabRememberHelper { const first_element = merged[0] ?? null; if(first_element) { this.revealElementOnTab(first_element); + this.revealElementInCollapse(first_element); } } @@ -62,10 +63,20 @@ class TabRememberHelper { * @param event */ onInvalid(event) { + this.revealElementInCollapse(event.target); this.revealElementOnTab(event.target); this.revealElementInDropdown(event.target); } + revealElementInCollapse(element) { + let collapse = element.closest('.collapse'); + + if(collapse) { + let bs_collapse = Collapse.getOrCreateInstance(collapse); + bs_collapse.show(); + } + } + revealElementInDropdown(element) { let dropdown = element.closest('.dropdown-menu'); diff --git a/assets/js/webauthn_tfa.js b/assets/js/webauthn_tfa.js index a2e00595..4d54efc0 100644 --- a/assets/js/webauthn_tfa.js +++ b/assets/js/webauthn_tfa.js @@ -21,8 +21,13 @@ class WebauthnTFA { -// Decodes a Base64Url string - _base64UrlDecode = (input) => { + _b64UrlSafeEncode = (str) => { + const b64 = btoa(str); + return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + } + + // Decodes a Base64Url string + _b64UrlSafeDecode = (input) => { input = input .replace(/-/g, '+') .replace(/_/g, '/'); @@ -39,13 +44,16 @@ class WebauthnTFA { }; // Converts an array of bytes into a Base64Url string - _arrayToBase64String = (a) => btoa(String.fromCharCode(...a)); + _arrayToBase64String = (a) => { + const str = String.fromCharCode(...a); + return this._b64UrlSafeEncode(str); + } // Prepares the public key options object returned by the Webauthn Framework _preparePublicKeyOptions = publicKey => { //Convert challenge from Base64Url string to Uint8Array publicKey.challenge = Uint8Array.from( - this._base64UrlDecode(publicKey.challenge), + this._b64UrlSafeDecode(publicKey.challenge), c => c.charCodeAt(0) ); @@ -67,7 +75,7 @@ class WebauthnTFA { return { ...data, id: Uint8Array.from( - this._base64UrlDecode(data.id), + this._b64UrlSafeDecode(data.id), c => c.charCodeAt(0) ), }; @@ -81,7 +89,7 @@ class WebauthnTFA { return { ...data, id: Uint8Array.from( - this._base64UrlDecode(data.id), + this._b64UrlSafeDecode(data.id), c => c.charCodeAt(0) ), }; diff --git a/assets/tomselect/autoselect_typed/autoselect_typed.js b/assets/tomselect/autoselect_typed/autoselect_typed.js new file mode 100644 index 00000000..8a426be7 --- /dev/null +++ b/assets/tomselect/autoselect_typed/autoselect_typed.js @@ -0,0 +1,63 @@ +/** + * Autoselect Typed plugin for Tomselect + * + * This plugin allows automatically selecting an option matching the typed text when the Tomselect element goes out of + * focus (is blurred) and/or when the delimiter is typed. + * + * #select_on_blur option + * Tomselect natively supports the "createOnBlur" option. This option picks up any remaining text in the input field + * and uses it to create a new option and selects that option. It does behave a bit strangely though, in that it will + * not select an already existing option when the input is blurred, so if you typed something that matches an option in + * the list and then click outside the box (without pressing enter) the entered text is just removed (unless you have + * allow duplicates on in which case it will create a new option). + * This plugin fixes that, such that Tomselect will first try to select an option matching the remaining uncommitted + * text and only when no matching option is found tries to create a new one (if createOnBlur and create is on) + * + * #select_on_delimiter option + * Normally when typing the delimiter (space by default) Tomselect will try to create a new option (and select it) (if + * create is on), but if the typed text matches an option (and allow duplicates is off) it refuses to react at all until + * you press enter. With this option, the delimiter will also allow selecting an option, not just creating it. + */ +function select_current_input(self){ + if(self.isLocked){ + return + } + + const val = self.inputValue() + //Do nothing if the input is empty + if (!val) { + return + } + + if (self.options[val]) { + self.addItem(val) + self.setTextboxValue() + } +} + +export default function(plugin_options_) { + const plugin_options = Object.assign({ + //Autoselect the typed text when the input element goes out of focus + select_on_blur: true, + //Autoselect the typed text when the delimiter is typed + select_on_delimiter: true, + }, plugin_options_); + + const self = this + + if(plugin_options.select_on_blur) { + this.hook("before", "onBlur", function () { + select_current_input(self) + }) + } + + if(plugin_options.select_on_delimiter) { + this.hook("before", "onKeyPress", function (e) { + const character = String.fromCharCode(e.keyCode || e.which); + if (self.settings.mode === 'multi' && character === self.settings.delimiter) { + select_current_input(self) + } + }) + } + +} \ No newline at end of file diff --git a/assets/tomselect/click_to_edit/click_to_edit.js b/assets/tomselect/click_to_edit/click_to_edit.js new file mode 100644 index 00000000..b7dcab03 --- /dev/null +++ b/assets/tomselect/click_to_edit/click_to_edit.js @@ -0,0 +1,93 @@ +/** + * click_to_edit plugin for Tomselect + * + * This plugin allows editing (and selecting text in) any selected item by clicking it. + * + * Usually, when the user typed some text and created an item in Tomselect that item cannot be edited anymore. To make + * a change, the item has to be deleted and retyped completely. There is also generally no way to copy text out of a + * tomselect item. The "restore_on_backspace" plugin improves that somewhat, by allowing the user to edit an item after + * pressing backspace. However, it is somewhat confusing to first have to focus the field an then hit backspace in order + * to copy a piece of text. It may also not be immediately obvious for editing. + * This plugin transforms an item into editable text when it is clicked, e.g. when the user tries to place the caret + * within an item or when they try to drag across the text to highlight it. + * It also plays nice with the remove_button plugin which still removes (deselects) an option entirely. + * + * It is recommended to also enable the autoselect_typed plugin when using this plugin. Without it, the text in the + * input field (i.e. the item that was just clicked) is lost when the user clicks outside the field. Also, when the user + * clicks an option (making it text) and then tries to enter another one by entering the delimiter (e.g. space) nothing + * happens until enter is pressed or the text is changed from what it was. + */ + +/** + * Return a dom element from either a dom query string, jQuery object, a dom element or html string + * https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518 + * + * param query should be {} + */ +const getDom = query => { + if (query.jquery) { + return query[0]; + } + if (query instanceof HTMLElement) { + return query; + } + if (isHtmlString(query)) { + var tpl = document.createElement('template'); + tpl.innerHTML = query.trim(); // Never return a text node of whitespace as the result + return tpl.content.firstChild; + } + return document.querySelector(query); +}; +const isHtmlString = arg => { + if (typeof arg === 'string' && arg.indexOf('<') > -1) { + return true; + } + return false; +}; + +function plugin(plugin_options_) { + const self = this + + const plugin_options = Object.assign({ + //If there is unsubmitted text in the input field, should that text be automatically used to select a matching + //element? If this is off, clicking on item1 and then clicking on item2 will result in item1 being deselected + auto_select_before_edit: true, + //If there is unsubmitted text in the input field, should that text be automatically used to create a matching + //element if no matching element was found or auto_select_before_edit is off? + auto_create_before_edit: true, + //customize this function to change which text the item is replaced with when clicking on it + text: option => { + return option[self.settings.labelField]; + } + }, plugin_options_); + + + self.hook('after', 'setupTemplates', () => { + const orig_render_item = self.settings.render.item; + self.settings.render.item = (data, escape) => { + const item = getDom(orig_render_item.call(self, data, escape)); + + item.addEventListener('click', evt => { + if (self.isLocked) { + return; + } + const val = self.inputValue(); + + if (self.options[val]) { + self.addItem(val) + } else if (self.settings.create) { + self.createItem(); + } + const option = self.options[item.dataset.value] + self.setTextboxValue(plugin_options.text.call(self, option)); + self.focus(); + self.removeItem(item); + } + ); + + return item; + } + }); + +} +export { plugin as default }; \ No newline at end of file diff --git a/assets/translator.js b/assets/translator.js new file mode 100644 index 00000000..0d5ae86b --- /dev/null +++ b/assets/translator.js @@ -0,0 +1,16 @@ +import { localeFallbacks } from '../var/translations/configuration'; +import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator'; +/* + * This file is part of the Symfony UX Translator package. + * + * If folder "../var/translations" does not exist, or some translations are missing, + * you must warmup your Symfony cache to refresh JavaScript translations. + * + * If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking. + */ + +setLocaleFallbacks(localeFallbacks); + +export { trans }; + +export * from '../var/translations'; \ No newline at end of file diff --git a/bin/console b/bin/console index c933dc53..256c0a60 100755 --- a/bin/console +++ b/bin/console @@ -4,6 +4,17 @@ use App\Kernel; use Symfony\Bundle\FrameworkBundle\Console\Application; +if (!is_dir(dirname(__DIR__).'/vendor')) { + throw new LogicException('Dependencies are missing. Try running "composer install".'); +} + +//Increase xdebug.max_nesting_level to 1000 if required (see issue #411) +//Check if xdebug extension is active, and xdebug.max_nesting_level is set to 256 or lower +if (extension_loaded('xdebug') && ((int) ini_get('xdebug.max_nesting_level')) <= 256) { + //Increase xdebug.max_nesting_level to 1000 + ini_set('xdebug.max_nesting_level', '1000'); +} + if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) { throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".'); } diff --git a/bin/phpunit b/bin/phpunit index f26f2c72..692baccb 100755 --- a/bin/phpunit +++ b/bin/phpunit @@ -6,9 +6,13 @@ if (!ini_get('date.timezone')) { } if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) { - define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); - require PHPUNIT_COMPOSER_INSTALL; - PHPUnit\TextUI\Command::main(); + if (PHP_VERSION_ID >= 80000) { + require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; + } else { + define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); + require PHPUNIT_COMPOSER_INSTALL; + PHPUnit\TextUI\Command::main(); + } } else { if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; diff --git a/codecov.yml b/codecov.yml index 34acb532..6ac0375c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,4 +5,5 @@ coverage: status: project: default: - threshold: 5% \ No newline at end of file + threshold: 10% + target: 40% diff --git a/composer.json b/composer.json index af991b44..e57ce652 100644 --- a/composer.json +++ b/composer.json @@ -1,104 +1,118 @@ { + "name": "part-db/part-db-server", "type": "project", "license": "AGPL-3.0-or-later", "require": { - "php": "^7.4 || ^8.0", + "php": "^8.1", "ext-ctype": "*", + "ext-dom": "*", "ext-gd": "*", "ext-iconv": "*", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "ext-dom": "*", + "amphp/http-client": "^5.1", + "api-platform/core": "^3.1", "beberlei/doctrineextensions": "^1.2", - "brick/math": "^0.8.15", - "composer/package-versions-deprecated": "1.11.99.4", - "doctrine/annotations": "^1.6", - "doctrine/dbal": "^3.4.6", + "brick/math": "0.12.1 as 0.11.0", + "composer/ca-bundle": "^1.5", + "composer/package-versions-deprecated": "^1.11.99.5", + "doctrine/data-fixtures": "^2.0.0", + "doctrine/dbal": "^4.0.0", "doctrine/doctrine-bundle": "^2.0", "doctrine/doctrine-migrations-bundle": "^3.0", - "doctrine/orm": "^2.9", - "dompdf/dompdf": "^2.0.0", + "doctrine/orm": "^3.2.0", + "dompdf/dompdf": "^v3.0.0", "erusev/parsedown": "^1.7", "florianv/swap": "^4.0", "florianv/swap-bundle": "dev-master", "gregwar/captcha-bundle": "^2.1.0", - "hslavich/oneloginsaml-bundle": "^2.10", - "jbtronics/2fa-webauthn": "^1.0.0", + "hshn/base64-encoded-file": "^5.0", + "jbtronics/2fa-webauthn": "^v2.2.0", + "jbtronics/dompdf-font-loader-bundle": "^1.0.0", + "jfcherng/php-diff": "^6.14", + "knpuniversity/oauth2-client-bundle": "^2.15", "league/csv": "^9.8.0", "league/html-to-markdown": "^5.0.1", "liip/imagine-bundle": "^2.2", + "nbgrp/onelogin-saml-bundle": "^1.3", "nelexa/zip": "^4.0", + "nelmio/cors-bundle": "^2.3", "nelmio/security-bundle": "^3.0", "nyholm/psr7": "^1.1", - "ocramius/proxy-manager": "2.2.*", - "omines/datatables-bundle": "^0.5.0", - "php-translation/symfony-bundle": "^0.12.0", - "phpdocumentor/reflection-docblock": "^5.2", + "omines/datatables-bundle": "^0.9.1", + "paragonie/sodium_compat": "^1.21", + "part-db/label-fonts": "^1.0", + "rhukster/dom-sanitizer": "^1.0", + "runtime/frankenphp-symfony": "^0.2.0", "s9e/text-formatter": "^2.1", - "scheb/2fa-backup-code": "^5.13", - "scheb/2fa-bundle": "^5.13", - "scheb/2fa-google-authenticator": "^5.13", - "scheb/2fa-trusted-device": "^5.13", - "sensio/framework-extra-bundle": "^6.1.1", + "scheb/2fa-backup-code": "^6.8.0", + "scheb/2fa-bundle": "^6.8.0", + "scheb/2fa-google-authenticator": "^6.8.0", + "scheb/2fa-trusted-device": "^6.8.0", "shivas/versioning-bundle": "^4.0", - "spatie/db-dumper": "^2.21", + "spatie/db-dumper": "^3.3.1", "symfony/apache-pack": "^1.0", - "symfony/asset": "5.4.*", - "symfony/console": "5.4.*", - "symfony/dotenv": "5.4.*", - "symfony/expression-language": "5.4.*", - "symfony/flex": "^1.1", - "symfony/form": "5.4.*", - "symfony/framework-bundle": "5.4.*", - "symfony/http-client": "5.4.*", - "symfony/http-kernel": "5.4.*", - "symfony/mailer": "5.4.*", + "symfony/asset": "6.4.*", + "symfony/console": "6.4.*", + "symfony/css-selector": "6.4.*", + "symfony/dom-crawler": "6.4.*", + "symfony/dotenv": "6.4.*", + "symfony/expression-language": "6.4.*", + "symfony/flex": "^v2.3.1", + "symfony/form": "6.4.*", + "symfony/framework-bundle": "6.4.*", + "symfony/http-client": "6.4.*", + "symfony/http-kernel": "6.4.*", + "symfony/mailer": "6.4.*", "symfony/monolog-bundle": "^3.1", - "symfony/process": "5.4.*", - "symfony/property-access": "5.4.*", - "symfony/property-info": "5.4.*", - "symfony/proxy-manager-bridge": "5.4.*", - "symfony/rate-limiter": "5.4.*", - "symfony/runtime": "5.4.*", - "symfony/security-bundle": "5.4.*", - "symfony/serializer": "5.4.*", - "symfony/translation": "5.4.*", - "symfony/twig-bundle": "5.4.*", + "symfony/polyfill-php82": "^1.28", + "symfony/process": "6.4.*", + "symfony/property-access": "6.4.*", + "symfony/property-info": "6.4.*", + "symfony/rate-limiter": "6.4.*", + "symfony/runtime": "6.4.*", + "symfony/security-bundle": "6.4.*", + "symfony/serializer": "6.4.*", + "symfony/string": "6.4.*", + "symfony/translation": "6.4.*", + "symfony/twig-bundle": "6.4.*", + "symfony/ux-translator": "^2.10", "symfony/ux-turbo": "^2.0", - "symfony/validator": "5.4.*", - "symfony/web-link": "5.4.*", - "symfony/webpack-encore-bundle": "^1.1", - "symfony/yaml": "5.4.*", - "tecnickcom/tc-lib-barcode": "^1.15", + "symfony/validator": "6.4.*", + "symfony/web-link": "6.4.*", + "symfony/webpack-encore-bundle": "^v2.0.1", + "symfony/yaml": "6.4.*", + "tecnickcom/tc-lib-barcode": "^2.1.4", "twig/cssinliner-extra": "^3.0", - "twig/extra-bundle": "^3.0", - "twig/html-extra": "^3.0", + "twig/extra-bundle": "^3.8", + "twig/html-extra": "^3.8", "twig/inky-extra": "^3.0", - "twig/intl-extra": "^3.0", - "twig/markdown-extra": "^3.0", - "web-auth/webauthn-symfony-bundle": "^3.3", - "webmozart/assert": "^1.4" + "twig/intl-extra": "^3.8", + "twig/markdown-extra": "^3.8", + "twig/string-extra": "^3.8", + "web-auth/webauthn-symfony-bundle": "^4.0.0" }, "require-dev": { - "dama/doctrine-test-bundle": "^7.0", - "doctrine/doctrine-fixtures-bundle": "^3.2", - "ekino/phpstan-banned-code": "^v1.0.0", + "dama/doctrine-test-bundle": "^v8.0.0", + "doctrine/doctrine-fixtures-bundle": "^4.0.0", + "ekino/phpstan-banned-code": "^v3.0.0", + "jbtronics/translation-editor-bundle": "^1.0", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.4.7", - "phpstan/phpstan-doctrine": "^1.2.11", - "phpstan/phpstan-symfony": "^1.1.7", - "psalm/plugin-symfony": "^v5.0.1", + "phpstan/phpstan": "^2.0.4", + "phpstan/phpstan-doctrine": "^2.0.1", + "phpstan/phpstan-strict-rules": "^2.0.1", + "phpstan/phpstan-symfony": "^2.0.0", + "phpunit/phpunit": "^9.5", + "rector/rector": "^2.0.4", "roave/security-advisories": "dev-latest", - "symfony/browser-kit": "^5.2", - "symfony/css-selector": "^5.2", - "symfony/debug-bundle": "^5.2", + "symfony/browser-kit": "6.4.*", + "symfony/debug-bundle": "6.4.*", "symfony/maker-bundle": "^1.13", - "symfony/phpunit-bridge": "5.4.*", - "symfony/stopwatch": "^5.2", - "symfony/web-profiler-bundle": "^5.2", - "symplify/easy-coding-standard": "^11.0", - "vimeo/psalm": "^5.6.0" + "symfony/phpunit-bridge": "6.4.*", + "symfony/stopwatch": "6.4.*", + "symfony/web-profiler-bundle": "6.4.*", + "symplify/easy-coding-standard": "^12.0" }, "suggest": { "ext-bcmath": "Used to improve price calculation performance", @@ -109,7 +123,7 @@ "*": "dist" }, "platform": { - "php": "7.4.0" + "php": "8.1.0" }, "sort-packages": true, "allow-plugins": { @@ -141,7 +155,7 @@ "post-update-cmd": [ "@auto-scripts" ], - "phpstan": "vendor/bin/phpstan analyse src --level 2 --memory-limit 1G" + "phpstan": "vendor/bin/phpstan analyse src --level 5 --memory-limit 1G" }, "conflict": { "symfony/symfony": "*" @@ -149,9 +163,8 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "5.4.*" + "require": "6.4.*", + "docker": true } - }, - "repositories": [ - ] + } } diff --git a/composer.lock b/composer.lock index db97b11f..4d658092 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,1201 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2333fbbcf1b0400f8dfd6ca119f55195", + "content-hash": "27cd0d915eab5e7cb57215a4c0b529fb", "packages": [ { - "name": "beberlei/assert", - "version": "v3.3.2", + "name": "amphp/amp", + "version": "v3.1.0", "source": { "type": "git", - "url": "https://github.com/beberlei/assert.git", - "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655" + "url": "https://github.com/amphp/amp.git", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/beberlei/assert/zipball/cb70015c04be1baee6f5f5c953703347c0ac1655", - "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655", + "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-26T16:07:39+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" + }, + { + "name": "amphp/hpack", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/hpack.git", + "reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/hpack/zipball/4f293064b15682a2b178b1367ddf0b8b5feb0239", + "reference": "4f293064b15682a2b178b1367ddf0b8b5feb0239", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "http2jp/hpack-test-case": "^1", + "nikic/php-fuzzer": "^0.0.10", + "phpunit/phpunit": "^7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "HTTP/2 HPack implementation.", + "homepage": "https://github.com/amphp/hpack", + "keywords": [ + "headers", + "hpack", + "http-2" + ], + "support": { + "issues": "https://github.com/amphp/hpack/issues", + "source": "https://github.com/amphp/hpack/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:00:16+00:00" + }, + { + "name": "amphp/http", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/http.git", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http/zipball/3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "shasum": "" + }, + "require": { + "amphp/hpack": "^3", + "amphp/parser": "^1.1", + "league/uri-components": "^2.4.2 | ^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "league/uri": "^6.8 | ^7.1", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.26.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/constants.php" + ], + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Basic HTTP primitives which can be shared by servers and clients.", + "support": { + "issues": "https://github.com/amphp/http/issues", + "source": "https://github.com/amphp/http/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-11-23T14:57:26+00:00" + }, + { + "name": "amphp/http-client", + "version": "v5.3.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-client.git", + "reference": "cf885bf00550d645a27605626528a3b2ce5563ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-client/zipball/cf885bf00550d645a27605626528a3b2ce5563ae", + "reference": "cf885bf00550d645a27605626528a3b2ce5563ae", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/hpack": "^3", + "amphp/http": "^2", + "amphp/pipeline": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "league/uri": "^7", + "league/uri-components": "^7", + "league/uri-interfaces": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2", + "revolt/event-loop": "^1" + }, + "conflict": { + "amphp/file": "<3 | >=5" + }, + "require-dev": { + "amphp/file": "^3 | ^4", + "amphp/http-server": "^3", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "ext-json": "*", + "kelunik/link-header-rfc5988": "^1", + "laminas/laminas-diactoros": "^2.3", + "phpunit/phpunit": "^9", + "psalm/phar": "~5.23" + }, + "suggest": { + "amphp/file": "Required for file request bodies and HTTP archive logging", + "ext-json": "Required for logging HTTP archives", + "ext-zlib": "Allows using compression for response bodies." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\Http\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "An advanced async HTTP client library for PHP, enabling efficient, non-blocking, and concurrent requests and responses.", + "homepage": "https://amphp.org/http-client", + "keywords": [ + "async", + "client", + "concurrent", + "http", + "non-blocking", + "rest" + ], + "support": { + "issues": "https://github.com/amphp/http-client/issues", + "source": "https://github.com/amphp/http-client/tree/v5.3.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-05-18T18:27:44+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "api-platform/core", + "version": "v3.4.17", + "source": { + "type": "git", + "url": "https://github.com/api-platform/core.git", + "reference": "c5fb664d17ed9ae919394514ea69a5039d2ad9ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/core/zipball/c5fb664d17ed9ae919394514ea69a5039d2ad9ab", + "reference": "c5fb664d17ed9ae919394514ea69a5039d2ad9ab", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.0 || ^2.0", + "php": ">=8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^3.1", + "symfony/http-foundation": "^6.4 || ^7.1", + "symfony/http-kernel": "^6.4 || ^7.1", + "symfony/property-access": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/translation-contracts": "^3.3", + "symfony/web-link": "^6.4 || ^7.1", + "willdurand/negotiation": "^3.0" + }, + "conflict": { + "doctrine/common": "<3.2.2", + "doctrine/dbal": "<2.10", + "doctrine/mongodb-odm": "<2.4", + "doctrine/orm": "<2.14.0", + "doctrine/persistence": "<1.3", + "elasticsearch/elasticsearch": ">=8.0,<8.4", + "phpspec/prophecy": "<1.15", + "phpunit/phpunit": "<9.5", + "symfony/framework-bundle": "6.4.6 || 7.0.6", + "symfony/var-exporter": "<6.1.1" + }, + "replace": { + "api-platform/doctrine-common": "self.version", + "api-platform/doctrine-odm": "self.version", + "api-platform/doctrine-orm": "self.version", + "api-platform/documentation": "self.version", + "api-platform/elasticsearch": "self.version", + "api-platform/graphql": "self.version", + "api-platform/http-cache": "self.version", + "api-platform/hydra": "self.version", + "api-platform/json-api": "self.version", + "api-platform/json-hal": "self.version", + "api-platform/json-schema": "self.version", + "api-platform/jsonld": "self.version", + "api-platform/laravel": "self.version", + "api-platform/metadata": "self.version", + "api-platform/openapi": "self.version", + "api-platform/parameter-validator": "self.version", + "api-platform/ramsey-uuid": "self.version", + "api-platform/serializer": "self.version", + "api-platform/state": "self.version", + "api-platform/symfony": "self.version", + "api-platform/validator": "self.version" + }, + "require-dev": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/doctrine-odm": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0", + "api-platform/documentation": "^3.4 || ^4.0", + "api-platform/elasticsearch": "^3.4 || ^4.0", + "api-platform/graphql": "^3.4 || ^4.0", + "api-platform/http-cache": "^3.4 || ^4.0", + "api-platform/hydra": "^3.4 || ^4.0", + "api-platform/json-api": "^3.3 || ^4.0", + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/jsonld": "^3.4 || ^4.0", + "api-platform/metadata": "^3.4 || ^4.0", + "api-platform/openapi": "^3.4 || ^4.0", + "api-platform/parameter-validator": "^3.4", + "api-platform/ramsey-uuid": "^3.4 || ^4.0", + "api-platform/serializer": "^3.4 || ^4.0", + "api-platform/state": "^3.4 || ^4.0", + "api-platform/validator": "^3.4 || ^4.0", + "behat/behat": "^3.11", + "behat/mink": "^1.9", + "doctrine/cache": "^1.11 || ^2.1", + "doctrine/common": "^3.2.2", + "doctrine/dbal": "^3.4.0 || ^4.0", + "doctrine/doctrine-bundle": "^1.12 || ^2.0", + "doctrine/mongodb-odm": "^2.2", + "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", + "doctrine/orm": "^2.14 || ^3.0", + "elasticsearch/elasticsearch": "^7.11 || ^8.4", + "friends-of-behat/mink-browserkit-driver": "^1.3.1", + "friends-of-behat/mink-extension": "^2.2", + "friends-of-behat/symfony-extension": "^2.1", + "guzzlehttp/guzzle": "^6.0 || ^7.1", + "jangregor/phpstan-prophecy": "^1.0", + "justinrainbow/json-schema": "^5.2.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpdoc-parser": "^1.13|^2.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-symfony": "^1.0", + "phpunit/phpunit": "^9.6", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^3.9.7 || ^4.0", + "ramsey/uuid-doctrine": "^1.4 || ^2.0 || ^3.0", + "sebastian/comparator": "<5.0", + "soyuka/contexts": "v3.3.9", + "soyuka/pmu": "^0.0.12", + "soyuka/stubs-mongodb": "^1.0", + "symfony/asset": "^6.4 || ^7.1", + "symfony/browser-kit": "^6.4 || ^7.1", + "symfony/cache": "^6.4 || ^7.1", + "symfony/config": "^6.4 || ^7.1", + "symfony/console": "^6.4 || ^7.1", + "symfony/css-selector": "^6.4 || ^7.1", + "symfony/dependency-injection": "^6.4 || ^7.1", + "symfony/doctrine-bridge": "^6.4 || ^7.1", + "symfony/dom-crawler": "^6.4 || ^7.1", + "symfony/error-handler": "^6.4 || ^7.1", + "symfony/event-dispatcher": "^6.4 || ^7.1", + "symfony/expression-language": "^6.4 || ^7.1", + "symfony/finder": "^6.4 || ^7.1", + "symfony/form": "^6.4 || ^7.1", + "symfony/framework-bundle": "^6.4 || ^7.1", + "symfony/http-client": "^6.4 || ^7.1", + "symfony/intl": "^6.4 || ^7.1", + "symfony/maker-bundle": "^1.24", + "symfony/mercure-bundle": "*", + "symfony/messenger": "^6.4 || ^7.1", + "symfony/phpunit-bridge": "^6.4.1 || ^7.1", + "symfony/routing": "^6.4 || ^7.1", + "symfony/security-bundle": "^6.4 || ^7.1", + "symfony/security-core": "^6.4 || ^7.1", + "symfony/stopwatch": "^6.4 || ^7.1", + "symfony/string": "^6.4 || ^7.1", + "symfony/twig-bundle": "^6.4 || ^7.1", + "symfony/uid": "^6.4 || ^7.1", + "symfony/validator": "^6.4 || ^7.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.1", + "symfony/yaml": "^6.4 || ^7.1", + "twig/twig": "^1.42.3 || ^2.12 || ^3.0", + "webonyx/graphql-php": "^14.0 || ^15.0" + }, + "suggest": { + "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", + "elasticsearch/elasticsearch": "To support Elasticsearch.", + "ocramius/package-versions": "To display the API Platform's version in the debug bar.", + "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", + "psr/cache-implementation": "To use metadata caching.", + "ramsey/uuid": "To support Ramsey's UUID identifiers.", + "symfony/cache": "To have metadata caching when using Symfony integration.", + "symfony/config": "To load XML configuration files.", + "symfony/expression-language": "To use authorization features.", + "symfony/http-client": "To use the HTTP cache invalidation system.", + "symfony/messenger": "To support messenger integration.", + "symfony/security": "To use authorization features.", + "symfony/twig-bundle": "To use the Swagger UI integration.", + "symfony/uid": "To support Symfony UUID/ULID identifiers.", + "symfony/web-profiler-bundle": "To use the data collector.", + "webonyx/graphql-php": "To support GraphQL." + }, + "type": "library", + "extra": { + "pmu": { + "projects": [ + "./src/*/composer.json", + "src/Doctrine/*/composer.json" + ] + }, + "thanks": { + "url": "https://github.com/api-platform/api-platform", + "name": "api-platform/api-platform" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-main": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + } + ], + "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", + "homepage": "https://api-platform.com", + "keywords": [ + "Hydra", + "JSON-LD", + "api", + "graphql", + "hal", + "jsonapi", + "openapi", + "rest", + "swagger" + ], + "support": { + "issues": "https://github.com/api-platform/core/issues", + "source": "https://github.com/api-platform/core/tree/v3.4.17" + }, + "time": "2025-04-07T08:40:57+00:00" + }, + { + "name": "beberlei/assert", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd", "shasum": "" }, "require": { @@ -25,7 +1206,7 @@ "ext-json": "*", "ext-mbstring": "*", "ext-simplexml": "*", - "php": "^7.0 || ^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", @@ -69,33 +1250,38 @@ ], "support": { "issues": "https://github.com/beberlei/assert/issues", - "source": "https://github.com/beberlei/assert/tree/v3.3.2" + "source": "https://github.com/beberlei/assert/tree/v3.3.3" }, - "time": "2021-12-16T21:41:27+00:00" + "time": "2024-07-15T13:18:35+00:00" }, { "name": "beberlei/doctrineextensions", - "version": "v1.3.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/beberlei/DoctrineExtensions.git", - "reference": "008f162f191584a6c37c03a803f718802ba9dd9a" + "reference": "281f1650641c2f438b0a54d8eaa7ba50ac7e3eb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/008f162f191584a6c37c03a803f718802ba9dd9a", - "reference": "008f162f191584a6c37c03a803f718802ba9dd9a", + "url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/281f1650641c2f438b0a54d8eaa7ba50ac7e3eb6", + "reference": "281f1650641c2f438b0a54d8eaa7ba50ac7e3eb6", "shasum": "" }, "require": { - "doctrine/orm": "^2.7", + "doctrine/orm": "^2.19 || ^3.0", "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.14", - "nesbot/carbon": "*", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "symfony/yaml": "^4.2 || ^5.0", + "doctrine/annotations": "^1.14 || ^2", + "doctrine/coding-standard": "^9.0.2 || ^12.0", + "nesbot/carbon": "^2.72 || ^3", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5 || ^9.6", + "squizlabs/php_codesniffer": "^3.8", + "symfony/cache": "^5.4 || ^6.4 || ^7.0", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0", + "vimeo/psalm": "^3.18 || ^5.22", "zf1/zend-date": "^1.12", "zf1/zend-registry": "^1.12" }, @@ -126,32 +1312,31 @@ "orm" ], "support": { - "source": "https://github.com/beberlei/DoctrineExtensions/tree/v1.3.0" + "source": "https://github.com/beberlei/DoctrineExtensions/tree/v1.5.0" }, - "time": "2020-11-29T07:37:23+00:00" + "time": "2024-03-03T17:55:15+00:00" }, { "name": "brick/math", - "version": "0.8.17", + "version": "0.12.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "e6f8e7d04346a95be89580f8c2c22d6c3fa65556" + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/e6f8e7d04346a95be89580f8c2c22d6c3fa65556", - "reference": "e6f8e7d04346a95be89580f8c2c22d6c3fa65556", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.1|^8.0" + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^7.5.15|^8.5", - "vimeo/psalm": "^3.5" + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" }, "type": "library", "autoload": { @@ -171,45 +1356,50 @@ "arithmetic", "bigdecimal", "bignum", + "bignumber", "brick", - "math" + "decimal", + "integer", + "math", + "mathematics", + "rational" ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/v0.8" + "source": "https://github.com/brick/math/tree/0.12.1" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/brick/math", - "type": "tidelift" + "url": "https://github.com/BenMorel", + "type": "github" } ], - "time": "2020-08-18T23:41:20+00:00" + "time": "2023-11-29T23:19:16+00:00" }, { "name": "composer/ca-bundle", - "version": "1.3.5", + "version": "1.5.6", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd" + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/74780ccf8c19d6acb8d65c5f39cd72110e132bbd", - "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -244,7 +1434,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.5" + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" }, "funding": [ { @@ -260,20 +1450,20 @@ "type": "tidelift" } ], - "time": "2023-01-11T08:27:00+00:00" + "time": "2025-03-06T14:30:56+00:00" }, { "name": "composer/package-versions-deprecated", - "version": "1.11.99.4", + "version": "1.11.99.5", "source": { "type": "git", "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "b174585d1fe49ceed21928a945138948cb394600" + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600", - "reference": "b174585d1fe49ceed21928a945138948cb394600", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", "shasum": "" }, "require": { @@ -317,7 +1507,7 @@ "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", "support": { "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4" + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" }, "funding": [ { @@ -333,205 +1523,82 @@ "type": "tidelift" } ], - "time": "2021-09-13T08:41:34+00:00" + "time": "2022-01-17T14:14:24+00:00" }, { - "name": "doctrine/annotations", - "version": "1.14.3", + "name": "daverandom/libdns", + "version": "v2.1.0", "source": { "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af" + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", - "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", "shasum": "" }, "require": { - "doctrine/lexer": "^1 || ^2", - "ext-tokenizer": "*", - "php": "^7.1 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" - }, - "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "~1.4.10 || ^1.8.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "vimeo/psalm": "^4.10" + "ext-ctype": "*", + "php": ">=7.1" }, "suggest": { - "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + "ext-intl": "Required for IDN support" }, "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + "LibDNS\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "description": "DNS protocol implementation written in pure PHP", "keywords": [ - "annotations", - "docblock", - "parser" + "dns" ], "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.14.3" + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" }, - "time": "2023-02-01T09:20:38+00:00" - }, - { - "name": "doctrine/cache", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", - "shasum": "" - }, - "require": { - "php": "~7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" - }, - "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", - "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" - ], - "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/2.2.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", - "type": "tidelift" - } - ], - "time": "2022-05-20T20:07:39+00:00" + "time": "2024-04-12T12:12:48+00:00" }, { "name": "doctrine/collections", - "version": "1.8.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e" + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/2b44dd4cbca8b5744327de78bafef5945c7e7b5e", - "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3 || ^1", - "php": "^7.1.3 || ^8.0" + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" }, "require-dev": { - "doctrine/coding-standard": "^9.0 || ^10.0", - "phpstan/phpstan": "^1.4.8", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", - "vimeo/psalm": "^4.22" + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + "Doctrine\\Common\\Collections\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -570,84 +1637,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/1.8.0" - }, - "time": "2022-09-01T20:12:10+00:00" - }, - { - "name": "doctrine/common", - "version": "3.4.3", - "source": { - "type": "git", - "url": "https://github.com/doctrine/common.git", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", - "shasum": "" - }, - "require": { - "doctrine/persistence": "^2.0 || ^3.0", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0 || ^10.0", - "doctrine/collections": "^1", - "phpstan/phpstan": "^1.4.1", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.0", - "symfony/phpunit-bridge": "^6.1", - "vimeo/psalm": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", - "homepage": "https://www.doctrine-project.org/projects/common.html", - "keywords": [ - "common", - "doctrine", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.3" + "source": "https://github.com/doctrine/collections/tree/2.3.0" }, "funding": [ { @@ -659,54 +1649,131 @@ "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", "type": "tidelift" } ], - "time": "2022-10-09T11:47:59+00:00" + "time": "2025-03-22T10:17:19+00:00" }, { - "name": "doctrine/dbal", - "version": "3.6.1", + "name": "doctrine/data-fixtures", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/doctrine/dbal.git", - "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e" + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/57815c7bbcda3cd18871d253c1dd8cbe56f8526e", - "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "reference": "f7f1e12d6bceb58c204b3e77210a103c1c57601e", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^3.1 || ^4.0", + "php": "^8.1", + "psr/log": "^1.1 || ^2 || ^3" + }, + "conflict": { + "doctrine/dbal": "<3.5 || >=5", + "doctrine/orm": "<2.14 || >=4", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/dbal": "^3.5 || ^4", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.14 || ^3", + "ext-sqlite3": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5.3", + "symfony/cache": "^6.4 || ^7", + "symfony/var-exporter": "^6.4 || ^7" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/2.0.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2025-01-21T13:21:31+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e", "shasum": "" }, "require": { - "composer-runtime-api": "^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/deprecations": "^0.5.3|^1", - "doctrine/event-manager": "^1|^2", - "php": "^7.4 || ^8.0", + "php": "^8.1", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "11.1.0", + "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", - "jetbrains/phpstorm-stubs": "2022.3", - "phpstan/phpstan": "1.10.3", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.4", - "psalm/plugin-phpunit": "0.18.4", - "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^5.4|^6.0", - "symfony/console": "^4.4|^5.4|^6.0", - "vimeo/psalm": "4.30.0" + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "10.5.39", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." }, - "bin": [ - "bin/doctrine-dbal" - ], "type": "library", "autoload": { "psr-4": { @@ -759,7 +1826,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.1" + "source": "https://github.com/doctrine/dbal/tree/4.2.3" }, "funding": [ { @@ -775,29 +1842,34 @@ "type": "tidelift" } ], - "time": "2023-03-02T19:26:24+00:00" + "time": "2025-03-07T18:29:05+00:00" }, { "name": "doctrine/deprecations", - "version": "v1.0.0", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5|^8.5|^9.5", - "psr/log": "^1|^2|^3" + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -805,7 +1877,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -816,64 +1888,70 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" }, - "time": "2022-05-02T15:47:09+00:00" + "time": "2025-04-07T20:06:18+00:00" }, { "name": "doctrine/doctrine-bundle", - "version": "2.9.0", + "version": "2.14.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "9819c00c2eea750b99902f244309b824911b72b2" + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/9819c00c2eea750b99902f244309b824911b72b2", - "reference": "9819c00c2eea750b99902f244309b824911b72b2", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca6a7350b421baf7fbdefbf9f4993292ed18effb", + "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb", "shasum": "" }, "require": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/dbal": "^3.6.0", - "doctrine/persistence": "^2.2 || ^3", + "doctrine/dbal": "^3.7.0 || ^4.0", + "doctrine/persistence": "^3.1 || ^4", "doctrine/sql-formatter": "^1.0.1", - "php": "^7.4 || ^8.0", - "symfony/cache": "^5.4 || ^6.0", - "symfony/config": "^5.4 || ^6.0", - "symfony/console": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", + "php": "^8.1", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/doctrine-bridge": "^5.4.19 || ^6.0.7", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/service-contracts": "^1.1.1 || ^2.0 || ^3" + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/service-contracts": "^2.5 || ^3" }, "conflict": { "doctrine/annotations": ">=3.0", - "doctrine/orm": "<2.11 || >=3.0", - "twig/twig": "<1.34 || >=2.0 <2.4" + "doctrine/cache": "< 1.11", + "doctrine/orm": "<2.17 || >=4.0", + "symfony/var-exporter": "< 6.4.1 || 7.0.0", + "twig/twig": "<2.13 || >=3.0 <3.0.4" }, "require-dev": { "doctrine/annotations": "^1 || ^2", - "doctrine/coding-standard": "^9.0", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^12", "doctrine/deprecations": "^1.0", - "doctrine/orm": "^2.11 || ^3.0", + "doctrine/orm": "^2.17 || ^3.0", "friendsofphp/proxy-manager-lts": "^1.0", - "phpunit/phpunit": "^9.5.26 || ^10.0", - "psalm/plugin-phpunit": "^0.18.4", - "psalm/plugin-symfony": "^4", + "phpstan/phpstan": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9.6.22", "psr/log": "^1.1.4 || ^2.0 || ^3.0", - "symfony/phpunit-bridge": "^6.1", - "symfony/property-info": "^5.4 || ^6.0", - "symfony/proxy-manager-bridge": "^5.4 || ^6.0", - "symfony/security-bundle": "^5.4 || ^6.0", - "symfony/twig-bridge": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0", - "symfony/web-profiler-bundle": "^5.4 || ^6.0", - "symfony/yaml": "^5.4 || ^6.0", - "twig/twig": "^1.34 || ^2.12 || ^3.0", - "vimeo/psalm": "^4.30" + "symfony/doctrine-messenger": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^7.2", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", + "symfony/twig-bridge": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/var-exporter": "^6.4.1 || ^7.0.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^2.13 || ^3.0.4" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -883,7 +1961,7 @@ "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\DoctrineBundle\\": "" + "Doctrine\\Bundle\\DoctrineBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -918,7 +1996,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.9.0" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.14.0" }, "funding": [ { @@ -934,47 +2012,47 @@ "type": "tidelift" } ], - "time": "2023-03-23T20:02:57+00:00" + "time": "2025-03-22T17:28:21+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.2.2", + "version": "3.4.2", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "3393f411ba25ade21969c33f2053220044854d01" + "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/3393f411ba25ade21969c33f2053220044854d01", - "reference": "3393f411ba25ade21969c33f2053220044854d01", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/5a6ac7120c2924c4c070a869d08b11ccf9e277b9", + "reference": "5a6ac7120c2924c4c070a869d08b11ccf9e277b9", "shasum": "" }, "require": { - "doctrine/doctrine-bundle": "~1.0|~2.0", + "doctrine/doctrine-bundle": "^2.4", "doctrine/migrations": "^3.2", - "php": "^7.2|^8.0", - "symfony/framework-bundle": "~3.4|~4.0|~5.0|~6.0" + "php": "^7.2 || ^8.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "doctrine/coding-standard": "^8.0", - "doctrine/orm": "^2.6", - "doctrine/persistence": "^1.3||^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^8.0|^9.0", - "vimeo/psalm": "^4.11" + "composer/semver": "^3.0", + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.6 || ^3", + "phpstan/phpstan": "^1.4 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpstan/phpstan-symfony": "^1.3 || ^2", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/phpunit-bridge": "^6.3 || ^7", + "symfony/var-exporter": "^5.4 || ^6 || ^7" }, "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\MigrationsBundle\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1003,7 +2081,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.2.2" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.2" }, "funding": [ { @@ -1019,34 +2097,33 @@ "type": "tidelift" } ], - "time": "2022-02-01T18:08:07+00:00" + "time": "2025-03-11T17:36:26+00:00" }, { "name": "doctrine/event-manager", - "version": "1.2.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520" + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/95aa4cb529f1e96576f3fda9f5705ada4056a520", - "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3 || ^1", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "conflict": { "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "~1.4.10 || ^1.8.8", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.24" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "type": "library", "autoload": { @@ -1095,7 +2172,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/1.2.0" + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" }, "funding": [ { @@ -1111,32 +2188,32 @@ "type": "tidelift" } ], - "time": "2022-10-12T20:51:15+00:00" + "time": "2024-05-22T20:47:39+00:00" }, { "name": "doctrine/inflector", - "version": "2.0.6", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024" + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", - "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^10", + "doctrine/coding-standard": "^11.0", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.3", "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25" + "vimeo/psalm": "^4.25 || ^5.4" }, "type": "library", "autoload": { @@ -1186,7 +2263,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.6" + "source": "https://github.com/doctrine/inflector/tree/2.0.10" }, "funding": [ { @@ -1202,34 +2279,34 @@ "type": "tidelift" } ], - "time": "2022-10-20T09:10:12+00:00" + "time": "2024-02-18T20:23:39+00:00" }, { "name": "doctrine/instantiator", - "version": "1.5.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -1256,7 +2333,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -1272,32 +2349,31 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "doctrine/lexer", - "version": "2.1.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.0" + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { @@ -1334,7 +2410,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.0" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -1350,51 +2426,52 @@ "type": "tidelift" } ], - "time": "2022-12-14T08:49:07+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "doctrine/migrations", - "version": "3.5.5", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "4b1e2b6ba71d21d0c5be22ed03b6fc954d20b204" + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/4b1e2b6ba71d21d0c5be22ed03b6fc954d20b204", - "reference": "4b1e2b6ba71d21d0c5be22ed03b6fc954d20b204", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/325b61e41d032f5f7d7e2d11cbefff656eadc9ab", + "reference": "325b61e41d032f5f7d7e2d11cbefff656eadc9ab", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/dbal": "^3.5.1", + "doctrine/dbal": "^3.6 || ^4", "doctrine/deprecations": "^0.5.3 || ^1", "doctrine/event-manager": "^1.2 || ^2.0", - "friendsofphp/proxy-manager-lts": "^1.0", - "php": "^7.4 || ^8.0", + "php": "^8.1", "psr/log": "^1.1.3 || ^2 || ^3", - "symfony/console": "^4.4.16 || ^5.4 || ^6.0", - "symfony/stopwatch": "^4.4 || ^5.4 || ^6.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.2 || ^7.0" }, "conflict": { - "doctrine/orm": "<2.12" + "doctrine/orm": "<2.12 || >=4" }, "require-dev": { - "doctrine/coding-standard": "^9", - "doctrine/orm": "^2.13", - "doctrine/persistence": "^2 || ^3", + "doctrine/coding-standard": "^12", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", "doctrine/sql-formatter": "^1.0", "ext-pdo_sqlite": "*", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-deprecation-rules": "^1", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.1", - "phpstan/phpstan-symfony": "^1.1", - "phpunit/phpunit": "^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "symfony/process": "^4.4 || ^5.4 || ^6.0", - "symfony/yaml": "^4.4 || ^5.4 || ^6.0" + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.4", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^10.3", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "suggest": { "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", @@ -1406,7 +2483,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Migrations\\": "lib/Doctrine/Migrations" + "Doctrine\\Migrations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1436,7 +2513,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.5.5" + "source": "https://github.com/doctrine/migrations/tree/3.9.0" }, "funding": [ { @@ -1452,69 +2529,58 @@ "type": "tidelift" } ], - "time": "2023-01-18T12:44:30+00:00" + "time": "2025-03-26T06:48:45+00:00" }, { "name": "doctrine/orm", - "version": "2.14.1", + "version": "3.3.3", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e" + "reference": "1f1891d3e20ef9881e81c2f32c53e9dc88dfc9a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/de7eee5ed7b1b35c99b118f26f210a8281e6db8e", - "reference": "de7eee5ed7b1b35c99b118f26f210a8281e6db8e", + "url": "https://api.github.com/repos/doctrine/orm/zipball/1f1891d3e20ef9881e81c2f32c53e9dc88dfc9a7", + "reference": "1f1891d3e20ef9881e81c2f32c53e9dc88dfc9a7", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/cache": "^1.12.1 || ^2.1.1", - "doctrine/collections": "^1.5 || ^2.0", - "doctrine/common": "^3.0.3", - "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", "doctrine/deprecations": "^0.5.3 || ^1", "doctrine/event-manager": "^1.2 || ^2", "doctrine/inflector": "^1.4 || ^2.0", - "doctrine/instantiator": "^1.3", - "doctrine/lexer": "^1.2.3 || ^2", - "doctrine/persistence": "^2.4 || ^3", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", "ext-ctype": "*", - "php": "^7.1 || ^8.0", + "php": "^8.1", "psr/cache": "^1 || ^2 || ^3", - "symfony/console": "^4.2 || ^5.0 || ^6.0", - "symfony/polyfill-php72": "^1.23", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "doctrine/annotations": "<1.13 || >= 3.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/var-exporter": "^6.3.9 || ^7.0" }, "require-dev": { - "doctrine/annotations": "^1.13 || ^2", - "doctrine/coding-standard": "^9.0.2 || ^11.0", - "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.9.8", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "doctrine/coding-standard": "^13.0", + "phpbench/phpbench": "^1.0", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.0.3", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.4.0", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.7.1", - "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", - "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.4.0" + "squizlabs/php_codesniffer": "3.12.0", + "symfony/cache": "^5.4 || ^6.2 || ^7.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", - "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0", - "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" }, - "bin": [ - "bin/doctrine" - ], "type": "library", "autoload": { "psr-4": { - "Doctrine\\ORM\\": "lib/Doctrine/ORM" + "Doctrine\\ORM\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1551,42 +2617,39 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.14.1" + "source": "https://github.com/doctrine/orm/tree/3.3.3" }, - "time": "2023-01-16T18:36:59+00:00" + "time": "2025-05-02T17:42:51+00:00" }, { "name": "doctrine/persistence", - "version": "3.1.4", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "8bf8ab15960787f1a49d405f6eb8c787b4841119" + "reference": "45004aca79189474f113cbe3a53847c2115a55fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/8bf8ab15960787f1a49d405f6eb8c787b4841119", - "reference": "8bf8ab15960787f1a49d405f6eb8c787b4841119", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/45004aca79189474f113cbe3a53847c2115a55fa", + "reference": "45004aca79189474f113cbe3a53847c2115a55fa", "shasum": "" }, "require": { "doctrine/event-manager": "^1 || ^2", - "php": "^7.2 || ^8.0", + "php": "^8.1", "psr/cache": "^1.0 || ^2.0 || ^3.0" }, "conflict": { "doctrine/common": "<2.10" }, "require-dev": { - "composer/package-versions-deprecated": "^1.11", - "doctrine/coding-standard": "^11", - "doctrine/common": "^3.0", - "phpstan/phpstan": "1.9.4", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "1.12.7", "phpstan/phpstan-phpunit": "^1", "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.3.0" + "phpunit/phpunit": "^9.6", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" }, "type": "library", "autoload": { @@ -1635,7 +2698,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.1.4" + "source": "https://github.com/doctrine/persistence/tree/4.0.0" }, "funding": [ { @@ -1651,27 +2714,30 @@ "type": "tidelift" } ], - "time": "2023-02-03T11:13:07+00:00" + "time": "2024-11-01T21:49:07+00:00" }, { "name": "doctrine/sql-formatter", - "version": "1.1.3", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5" + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/25a06c7bf4c6b8218f47928654252863ffc890a5", - "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4" + "doctrine/coding-standard": "^12", + "ergebnis/phpunit-slow-test-detector": "^2.14", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5" }, "bin": [ "bin/sql-formatter" @@ -1701,38 +2767,40 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.1.3" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" }, - "time": "2022-05-23T21:33:49+00:00" + "time": "2025-01-24T11:45:48+00:00" }, { "name": "dompdf/dompdf", - "version": "v2.0.3", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "e8d2d5e37e8b0b30f0732a011295ab80680d7e85" + "reference": "a51bd7a063a65499446919286fb18b518177155a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/e8d2d5e37e8b0b30f0732a011295ab80680d7e85", - "reference": "e8d2d5e37e8b0b30f0732a011295ab80680d7e85", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/a51bd7a063a65499446919286fb18b518177155a", + "reference": "a51bd7a063a65499446919286fb18b518177155a", "shasum": "" }, "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", "ext-dom": "*", "ext-mbstring": "*", "masterminds/html5": "^2.0", - "phenx/php-font-lib": ">=0.5.4 <1.0.0", - "phenx/php-svg-lib": ">=0.3.3 <1.0.0", "php": "^7.1 || ^8.0" }, "require-dev": { + "ext-gd": "*", "ext-json": "*", "ext-zip": "*", "mockery/mockery": "^1.3", - "phpunit/phpunit": "^7.5 || ^8 || ^9", - "squizlabs/php_codesniffer": "^3.5" + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" }, "suggest": { "ext-gd": "Needed to process images", @@ -1763,32 +2831,123 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v2.0.3" + "source": "https://github.com/dompdf/dompdf/tree/v3.1.0" }, - "time": "2023-02-07T12:51:48+00:00" + "time": "2025-01-15T14:09:04+00:00" }, { - "name": "egulias/email-validator", - "version": "3.2.5", + "name": "dompdf/php-font-lib", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/egulias/EmailValidator.git", - "reference": "b531a2311709443320c786feb4519cfaf94af796" + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b531a2311709443320c786feb4519cfaf94af796", - "reference": "b531a2311709443320c786feb4519cfaf94af796", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", "shasum": "" }, "require": { - "doctrine/lexer": "^1.2|^2", - "php": ">=7.2", - "symfony/polyfill-intl-idn": "^1.15" + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5.8|^9.3.3", - "vimeo/psalm": "^4" + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" + }, + "time": "2024-12-02T14:37:59+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0" + }, + "time": "2024-04-29T13:26:35+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -1796,7 +2955,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { @@ -1824,7 +2983,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/3.2.5" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -1832,7 +2991,7 @@ "type": "github" } ], - "time": "2023-01-02T17:26:14+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { "name": "erusev/parsedown", @@ -1884,94 +3043,18 @@ }, "time": "2019-12-30T22:54:17+00:00" }, - { - "name": "fgrosse/phpasn1", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/fgrosse/PHPASN1.git", - "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", - "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "~2.0", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" - }, - "suggest": { - "ext-bcmath": "BCmath is the fallback extension for big integer calculations", - "ext-curl": "For loading OID information from the web if they have not bee defined statically", - "ext-gmp": "GMP is the preferred extension for big integer calculations", - "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "FG\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Friedrich Große", - "email": "friedrich.grosse@gmail.com", - "homepage": "https://github.com/FGrosse", - "role": "Author" - }, - { - "name": "All contributors", - "homepage": "https://github.com/FGrosse/PHPASN1/contributors" - } - ], - "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", - "homepage": "https://github.com/FGrosse/PHPASN1", - "keywords": [ - "DER", - "asn.1", - "asn1", - "ber", - "binary", - "decoding", - "encoding", - "x.509", - "x.690", - "x509", - "x690" - ], - "support": { - "issues": "https://github.com/fgrosse/PHPASN1/issues", - "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" - }, - "abandoned": true, - "time": "2022-12-19T11:08:26+00:00" - }, { "name": "florianv/exchanger", - "version": "2.8.0", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/florianv/exchanger.git", - "reference": "be3e4b316a0fd90bac186cc8b8206e995df161ba" + "reference": "9214f51665fb907e7aa2397e21a90c456eb0c448" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/florianv/exchanger/zipball/be3e4b316a0fd90bac186cc8b8206e995df161ba", - "reference": "be3e4b316a0fd90bac186cc8b8206e995df161ba", + "url": "https://api.github.com/repos/florianv/exchanger/zipball/9214f51665fb907e7aa2397e21a90c456eb0c448", + "reference": "9214f51665fb907e7aa2397e21a90c456eb0c448", "shasum": "" }, "require": { @@ -1982,7 +3065,7 @@ "php-http/client-implementation": "^1.0", "php-http/discovery": "^1.6", "php-http/httplug": "^1.0 || ^2.0", - "php-http/message-factory": "^1.0.2", + "psr/http-factory": "^1.0.2", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { @@ -2028,9 +3111,9 @@ ], "support": { "issues": "https://github.com/florianv/exchanger/issues", - "source": "https://github.com/florianv/exchanger/tree/2.8.0" + "source": "https://github.com/florianv/exchanger/tree/2.8.1" }, - "time": "2022-12-11T05:52:51+00:00" + "time": "2023-11-03T17:11:52+00:00" }, { "name": "florianv/swap", @@ -2098,25 +3181,25 @@ "source": { "type": "git", "url": "https://github.com/florianv/symfony-swap.git", - "reference": "fc6154976533e386ac6783c02ff19ab65aed4029" + "reference": "c8cd268ad6e2f636f10b91df9850e3941d7f5807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/florianv/symfony-swap/zipball/fc6154976533e386ac6783c02ff19ab65aed4029", - "reference": "fc6154976533e386ac6783c02ff19ab65aed4029", + "url": "https://api.github.com/repos/florianv/symfony-swap/zipball/c8cd268ad6e2f636f10b91df9850e3941d7f5807", + "reference": "c8cd268ad6e2f636f10b91df9850e3941d7f5807", "shasum": "" }, "require": { "florianv/swap": "^4.0", "php": "^7.1.3|^8.0", - "symfony/framework-bundle": "~3.0|~4.0|~5.0|~6.0" + "symfony/framework-bundle": "~3.0|~4.0|~5.0|~6.0|~7.0" }, "require-dev": { "nyholm/psr7": "^1.1", "php-http/guzzle6-adapter": "^1.0", "php-http/message": "^1.7", "phpunit/phpunit": "~5.7|~6.0|~7.0|~8.0|~9.0", - "symfony/cache": "~3.0|~4.0|~5.0|~6.0" + "symfony/cache": "~3.0|~4.0|~5.0|~6.0|~7.0" }, "suggest": { "symfony/cache": "For caching" @@ -2159,102 +3242,20 @@ "issues": "https://github.com/florianv/symfony-swap/issues", "source": "https://github.com/florianv/symfony-swap/tree/master" }, - "time": "2023-01-12T08:17:02+00:00" - }, - { - "name": "friendsofphp/proxy-manager-lts", - "version": "v1.0.14", - "source": { - "type": "git", - "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", - "reference": "a527c9d9d5348e012bd24482d83a5cd643bcbc9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/a527c9d9d5348e012bd24482d83a5cd643bcbc9e", - "reference": "a527c9d9d5348e012bd24482d83a5cd643bcbc9e", - "shasum": "" - }, - "require": { - "laminas/laminas-code": "~3.4.1|^4.0", - "php": ">=7.1", - "symfony/filesystem": "^4.4.17|^5.0|^6.0" - }, - "conflict": { - "laminas/laminas-stdlib": "<3.2.1", - "zendframework/zend-stdlib": "<3.2.1" - }, - "replace": { - "ocramius/proxy-manager": "^2.1" - }, - "require-dev": { - "ext-phar": "*", - "symfony/phpunit-bridge": "^5.4|^6.0" - }, - "type": "library", - "extra": { - "thanks": { - "name": "ocramius/proxy-manager", - "url": "https://github.com/Ocramius/ProxyManager" - } - }, - "autoload": { - "psr-4": { - "ProxyManager\\": "src/ProxyManager" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - } - ], - "description": "Adding support for a wider range of PHP versions to ocramius/proxy-manager", - "homepage": "https://github.com/FriendsOfPHP/proxy-manager-lts", - "keywords": [ - "aop", - "lazy loading", - "proxy", - "proxy pattern", - "service proxies" - ], - "support": { - "issues": "https://github.com/FriendsOfPHP/proxy-manager-lts/issues", - "source": "https://github.com/FriendsOfPHP/proxy-manager-lts/tree/v1.0.14" - }, - "funding": [ - { - "url": "https://github.com/Ocramius", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager", - "type": "tidelift" - } - ], - "time": "2023-01-30T10:40:19+00:00" + "time": "2024-07-09T13:51:01+00:00" }, { "name": "gregwar/captcha", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/Gregwar/Captcha.git", - "reference": "6e5b61b66ac89885b505153f4ef9a74ffa5b3074" + "reference": "229d3cdfe33d6f1349e0aec94a26e9205a6db08e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/6e5b61b66ac89885b505153f4ef9a74ffa5b3074", - "reference": "6e5b61b66ac89885b505153f4ef9a74ffa5b3074", + "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/229d3cdfe33d6f1349e0aec94a26e9205a6db08e", + "reference": "229d3cdfe33d6f1349e0aec94a26e9205a6db08e", "shasum": "" }, "require": { @@ -2296,36 +3297,37 @@ ], "support": { "issues": "https://github.com/Gregwar/Captcha/issues", - "source": "https://github.com/Gregwar/Captcha/tree/v1.2.0" + "source": "https://github.com/Gregwar/Captcha/tree/v1.2.1" }, - "time": "2023-03-24T22:12:41+00:00" + "time": "2023-09-26T13:45:37+00:00" }, { "name": "gregwar/captcha-bundle", - "version": "v2.2.0", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/Gregwar/CaptchaBundle.git", - "reference": "2b55ba41fd890f1a94d30e53a530c344bf12d6a5" + "reference": "8eb95c0911a1db9e3b2f368f6319e0945b959a6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Gregwar/CaptchaBundle/zipball/2b55ba41fd890f1a94d30e53a530c344bf12d6a5", - "reference": "2b55ba41fd890f1a94d30e53a530c344bf12d6a5", + "url": "https://api.github.com/repos/Gregwar/CaptchaBundle/zipball/8eb95c0911a1db9e3b2f368f6319e0945b959a6c", + "reference": "8eb95c0911a1db9e3b2f368f6319e0945b959a6c", "shasum": "" }, "require": { "ext-gd": "*", - "gregwar/captcha": "^1.1.9", - "php": ">=7.1.3", - "symfony/form": "~5.0|~6.0", - "symfony/framework-bundle": "~5.0|~6.0", - "symfony/translation": "~5.0|^6.0", - "twig/twig": "^2.10|^3.0" + "gregwar/captcha": "^1.2.1", + "php": ">=8.0.2", + "symfony/form": "~6.0|~7.0", + "symfony/framework-bundle": "~6.0|~7.0", + "symfony/translation": "~6.0|^7.0", + "twig/twig": "^3.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.66", - "symplify/easy-coding-standard": "^6.1" + "friendsofphp/php-cs-fixer": "^3.45", + "phpstan/phpstan": "^1.10", + "symplify/easy-coding-standard": "^12" }, "type": "symfony-bundle", "autoload": { @@ -2341,7 +3343,7 @@ { "name": "Grégoire Passault", "email": "g.passault@gmail.com", - "homepage": "http://www.gregwar.com/" + "homepage": "https://www.gregwar.com/" }, { "name": "Jeremy Livingston", @@ -2362,48 +3364,62 @@ ], "support": { "issues": "https://github.com/Gregwar/CaptchaBundle/issues", - "source": "https://github.com/Gregwar/CaptchaBundle/tree/v2.2.0" + "source": "https://github.com/Gregwar/CaptchaBundle/tree/v2.3.0" }, - "time": "2022-01-11T08:28:06+00:00" + "time": "2024-06-06T13:14:57+00:00" }, { - "name": "hslavich/oneloginsaml-bundle", - "version": "v2.10.0", + "name": "guzzlehttp/guzzle", + "version": "7.9.3", "source": { "type": "git", - "url": "https://github.com/hslavich/OneloginSamlBundle.git", - "reference": "aee3450bd36b750a2e61b4a0ca19a09ecab7a086" + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hslavich/OneloginSamlBundle/zipball/aee3450bd36b750a2e61b4a0ca19a09ecab7a086", - "reference": "aee3450bd36b750a2e61b4a0ca19a09ecab7a086", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { - "onelogin/php-saml": "^3.0", - "symfony/dependency-injection": "^5.4", - "symfony/deprecation-contracts": "^2.1 | ^3", - "symfony/event-dispatcher-contracts": "^2.4", - "symfony/framework-bundle": "^5.4", - "symfony/security-bundle": "^5.4" + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" }, "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.2.0", - "doctrine/orm": "~2.3", - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^9.0", - "symfony/event-dispatcher": "^5.4", - "symfony/phpunit-bridge": "^5.4" + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } }, - "type": "symfony-bundle", "autoload": { + "files": [ + "src/functions_include.php" + ], "psr-4": { - "Hslavich\\OneloginSamlBundle\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2411,38 +3427,350 @@ ], "authors": [ { - "name": "hslavich", - "email": "hernan.slavich@gmail.com" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], - "description": "OneLogin SAML Bundle for Symfony", + "description": "Guzzle is a PHP HTTP client library", "keywords": [ - "SSO", - "onelogin", - "saml" + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" ], "support": { - "issues": "https://github.com/hslavich/OneloginSamlBundle/issues", - "source": "https://github.com/hslavich/OneloginSamlBundle/tree/v2.10.0" + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, - "time": "2022-11-23T17:12:47+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" }, { - "name": "imagine/imagine", - "version": "1.3.3", + "name": "guzzlehttp/promises", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/php-imagine/Imagine.git", - "reference": "a6e6da93ea0f76aba33b0e8ed1325523c0413da2" + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/a6e6da93ea0f76aba33b0e8ed1325523c0413da2", - "reference": "a6e6da93ea0f76aba33b0e8ed1325523c0413da2", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "hshn/base64-encoded-file", + "version": "v5.0.1", + "source": { + "type": "git", + "url": "https://github.com/hshn/base64-encoded-file.git", + "reference": "54fa81461ba4fbf5b67ed71d22b43ea5cc8c8748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hshn/base64-encoded-file/zipball/54fa81461ba4fbf5b67ed71d22b43ea5cc8c8748", + "reference": "54fa81461ba4fbf5b67ed71d22b43ea5cc8c8748", + "shasum": "" + }, + "require": { + "php": "^8.1.0", + "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", + "symfony/mime": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0.0", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/form": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/serializer": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "symfony/config": "to use the bundle in a Symfony project", + "symfony/dependency-injection": "to use the bundle in a Symfony project", + "symfony/form": "to use base64_encoded_file type", + "symfony/http-kernel": "to use the bundle in a Symfony project", + "symfony/serializer": "to convert a base64 string to a Base64EncodedFile object" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hshn\\Base64EncodedFile\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Shota Hoshino", + "email": "sht.hshn@gmail.com" + } + ], + "description": "Provides handling base64 encoded files, and the integration of symfony/form", + "support": { + "issues": "https://github.com/hshn/base64-encoded-file/issues", + "source": "https://github.com/hshn/base64-encoded-file/tree/v5.0.1" + }, + "time": "2023-12-24T07:23:07+00:00" + }, + { + "name": "imagine/imagine", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/php-imagine/Imagine.git", + "reference": "80ab21434890dee9ba54969d31c51ac8d4d551e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/80ab21434890dee9ba54969d31c51ac8d4d551e0", + "reference": "80ab21434890dee9ba54969d31c51ac8d4d551e0", + "shasum": "" + }, + "require": { + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4 || ^9.3" @@ -2475,7 +3803,7 @@ "homepage": "http://avalanche123.com" } ], - "description": "Image processing for PHP 5.3", + "description": "Image processing for PHP", "homepage": "http://imagine.readthedocs.org/", "keywords": [ "drawing", @@ -2485,35 +3813,38 @@ ], "support": { "issues": "https://github.com/php-imagine/Imagine/issues", - "source": "https://github.com/php-imagine/Imagine/tree/1.3.3" + "source": "https://github.com/php-imagine/Imagine/tree/1.5.0" }, - "time": "2022-11-16T13:09:11+00:00" + "time": "2024-12-03T14:37:55+00:00" }, { "name": "jbtronics/2fa-webauthn", - "version": "v1.0.0", + "version": "v2.2.3", "source": { "type": "git", "url": "https://github.com/jbtronics/2fa-webauthn.git", - "reference": "c4108d16ba7a3061d977fc92f577c69067e1d003" + "reference": "fda6f39e70784cbf1f93cf758bf798563219d451" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jbtronics/2fa-webauthn/zipball/c4108d16ba7a3061d977fc92f577c69067e1d003", - "reference": "c4108d16ba7a3061d977fc92f577c69067e1d003", + "url": "https://api.github.com/repos/jbtronics/2fa-webauthn/zipball/fda6f39e70784cbf1f93cf758bf798563219d451", + "reference": "fda6f39e70784cbf1f93cf758bf798563219d451", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7": "^1.5", - "php": "^7.4.0|^8.0", - "scheb/2fa-bundle": "^5.0.0|^6.0.0", - "symfony/framework-bundle": "^5.0|^6.0", - "symfony/psr-http-message-bridge": "^2.1", - "web-auth/webauthn-lib": "^3.3" + "php": "^8.1", + "psr/log": "^3.0.0|^2.0.0", + "scheb/2fa-bundle": "^6.0.0|^7.0.0", + "symfony/framework-bundle": "^6.0|^7.0", + "symfony/psr-http-message-bridge": "^2.1|^6.1|^7.0", + "symfony/uid": "^6.0|^7.0", + "web-auth/webauthn-lib": "^4.7" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-latest" }, "type": "symfony-bundle", "autoload": { @@ -2542,103 +3873,449 @@ ], "support": { "issues": "https://github.com/jbtronics/2fa-webauthn/issues", - "source": "https://github.com/jbtronics/2fa-webauthn/tree/v1.0.0" + "source": "https://github.com/jbtronics/2fa-webauthn/tree/v2.2.3" }, - "time": "2022-10-03T22:29:32+00:00" + "time": "2025-03-27T19:23:40+00:00" }, { - "name": "laminas/laminas-code", - "version": "4.7.1", + "name": "jbtronics/dompdf-font-loader-bundle", + "version": "v1.1.3", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-code.git", - "reference": "91aabc066d5620428120800c0eafc0411e441a62" + "url": "https://github.com/jbtronics/dompdf-font-loader-bundle.git", + "reference": "da01d9655826105d53f9d0e8ba4f9d838201dcb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-code/zipball/91aabc066d5620428120800c0eafc0411e441a62", - "reference": "91aabc066d5620428120800c0eafc0411e441a62", + "url": "https://api.github.com/repos/jbtronics/dompdf-font-loader-bundle/zipball/da01d9655826105d53f9d0e8ba4f9d838201dcb2", + "reference": "da01d9655826105d53f9d0e8ba4f9d838201dcb2", "shasum": "" }, "require": { - "php": ">=7.4, <8.2" + "dompdf/dompdf": "^1.0.0|^2.0.0|^3.0.0", + "ext-json": "*", + "php": "^8.1", + "symfony/finder": "^6.0|^7.0", + "symfony/framework-bundle": "^6.0|^7.0" }, "require-dev": { - "doctrine/annotations": "^1.13.2", - "ext-phar": "*", - "laminas/laminas-coding-standard": "^2.3.0", - "laminas/laminas-stdlib": "^3.6.1", - "phpunit/phpunit": "^9.5.10", - "psalm/plugin-phpunit": "^0.17.0", - "vimeo/psalm": "^4.13.1" + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-latest", + "symfony/phpunit-bridge": "^6.0" }, - "suggest": { - "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", - "laminas/laminas-stdlib": "Laminas\\Stdlib component" + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Jbtronics\\DompdfFontLoaderBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Böhmer", + "email": "mail@jan-boehmer.de" + } + ], + "description": "A symfony bundle to easily load custom fonts for dompdf (on cache warming)", + "keywords": [ + "dompdf", + "fonts", + "symfony", + "symfony-bundle" + ], + "support": { + "issues": "https://github.com/jbtronics/dompdf-font-loader-bundle/issues", + "source": "https://github.com/jbtronics/dompdf-font-loader-bundle/tree/v1.1.3" + }, + "time": "2025-02-07T23:21:03+00:00" + }, + { + "name": "jfcherng/php-color-output", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/jfcherng/php-color-output.git", + "reference": "6c7bf16686cc6a291647fcb87491640a2d5edd20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jfcherng/php-color-output/zipball/6c7bf16686cc6a291647fcb87491640a2d5edd20", + "reference": "6c7bf16686cc6a291647fcb87491640a2d5edd20", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "liip/rmt": "^1.6", + "phan/phan": "^2 || ^3 || ^4", + "phpunit/phpunit": ">=7 <10", + "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "autoload": { - "files": [ - "polyfill/ReflectionEnumPolyfill.php" - ], "psr-4": { - "Laminas\\Code\\": "src/" + "Jfcherng\\Utility\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + } + ], + "description": "Make your PHP command-line application colorful.", + "keywords": [ + "ansi-colors", + "color", + "command-line", + "str-color" + ], + "support": { + "issues": "https://github.com/jfcherng/php-color-output/issues", + "source": "https://github.com/jfcherng/php-color-output/tree/3.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" + } + ], + "time": "2021-05-27T02:45:54+00:00" + }, + { + "name": "jfcherng/php-diff", + "version": "6.16.2", + "source": { + "type": "git", + "url": "https://github.com/jfcherng/php-diff.git", + "reference": "7f46bcfc582e81769237d0b3f6b8a548efe8799d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jfcherng/php-diff/zipball/7f46bcfc582e81769237d0b3f6b8a548efe8799d", + "reference": "7f46bcfc582e81769237d0b3f6b8a548efe8799d", + "shasum": "" + }, + "require": { + "jfcherng/php-color-output": "^3", + "jfcherng/php-mb-string": "^1.4.6 || ^2", + "jfcherng/php-sequence-matcher": "^3.2.10 || ^4", + "php": ">=7.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.51", + "liip/rmt": "^1.6", + "phan/phan": "^5", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jfcherng\\Diff\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", - "homepage": "https://laminas.dev", + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + }, + { + "name": "Chris Boulton", + "email": "chris.boulton@interspire.com" + } + ], + "description": "A comprehensive library for generating differences between two strings in multiple formats (unified, side by side HTML etc).", "keywords": [ - "code", - "laminas", - "laminasframework" + "diff", + "udiff", + "unidiff", + "unified diff" ], "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-code/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-code/issues", - "rss": "https://github.com/laminas/laminas-code/releases.atom", - "source": "https://github.com/laminas/laminas-code" + "issues": "https://github.com/jfcherng/php-diff/issues", + "source": "https://github.com/jfcherng/php-diff/tree/6.16.2" }, "funding": [ { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" } ], - "time": "2022-11-21T01:32:31+00:00" + "time": "2024-03-10T17:40:29+00:00" }, { - "name": "lcobucci/clock", - "version": "2.0.0", + "name": "jfcherng/php-mb-string", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/lcobucci/clock.git", - "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3" + "url": "https://github.com/jfcherng/php-mb-string.git", + "reference": "8407bfefde47849c9e7c9594e6de2ac85a0f845d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3", - "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3", + "url": "https://api.github.com/repos/jfcherng/php-mb-string/zipball/8407bfefde47849c9e7c9594e6de2ac85a0f845d", + "reference": "8407bfefde47849c9e7c9594e6de2ac85a0f845d", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "ext-iconv": "*", + "php": ">=8.1" }, "require-dev": { - "infection/infection": "^0.17", - "lcobucci/coding-standard": "^6.0", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/php-code-coverage": "9.1.4", - "phpunit/phpunit": "9.3.7" + "friendsofphp/php-cs-fixer": "^3", + "phan/phan": "^5", + "phpunit/phpunit": "^9 || ^10" + }, + "suggest": { + "ext-iconv": "Either \"ext-iconv\" or \"ext-mbstring\" is requried.", + "ext-mbstring": "Either \"ext-iconv\" or \"ext-mbstring\" is requried." + }, + "type": "library", + "autoload": { + "psr-4": { + "Jfcherng\\Utility\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + } + ], + "description": "A high performance multibytes sting implementation for frequently reading/writing operations.", + "support": { + "issues": "https://github.com/jfcherng/php-mb-string/issues", + "source": "https://github.com/jfcherng/php-mb-string/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" + } + ], + "time": "2023-04-17T14:23:16+00:00" + }, + { + "name": "jfcherng/php-sequence-matcher", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/jfcherng/php-sequence-matcher.git", + "reference": "d2038ac29627340a7458609072a8ba355e80ec5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jfcherng/php-sequence-matcher/zipball/d2038ac29627340a7458609072a8ba355e80ec5b", + "reference": "d2038ac29627340a7458609072a8ba355e80ec5b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "phan/phan": "^5", + "phpunit/phpunit": "^9 || ^10", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jfcherng\\Diff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jack Cherng", + "email": "jfcherng@gmail.com" + }, + { + "name": "Chris Boulton", + "email": "chris.boulton@interspire.com" + } + ], + "description": "A longest sequence matcher. The logic is primarily based on the Python difflib package.", + "support": { + "issues": "https://github.com/jfcherng/php-sequence-matcher/issues", + "source": "https://github.com/jfcherng/php-sequence-matcher/tree/4.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/jfcherng/5usd", + "type": "custom" + } + ], + "time": "2023-05-21T07:57:08+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "knpuniversity/oauth2-client-bundle", + "version": "v2.18.3", + "source": { + "type": "git", + "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", + "reference": "c38ca88a70aae3694ca346a41b13b9a8f6e33ed4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/c38ca88a70aae3694ca346a41b13b9a8f6e33ed4", + "reference": "c38ca88a70aae3694ca346a41b13b9a8f6e33ed4", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": ">=8.1", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "league/oauth2-facebook": "^1.1|^2.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/security-guard": "^5.4", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "suggest": { + "symfony/security-guard": "For integration with Symfony's Guard Security layer" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "KnpU\\OAuth2ClientBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Weaver", + "email": "ryan@symfonycasts.com" + } + ], + "description": "Integration with league/oauth2-client to provide services", + "homepage": "https://symfonycasts.com", + "keywords": [ + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues", + "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.18.3" + }, + "time": "2024-10-02T14:26:09+00:00" + }, + { + "name": "lcobucci/clock", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.26", + "lcobucci/coding-standard": "^9.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-deprecation-rules": "^1.1.1", + "phpstan/phpstan-phpunit": "^1.3.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^9.5.27" }, "type": "library", "autoload": { @@ -2659,7 +4336,7 @@ "description": "Yet another clock abstraction", "support": { "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/2.0.x" + "source": "https://github.com/lcobucci/clock/tree/3.0.0" }, "funding": [ { @@ -2671,43 +4348,42 @@ "type": "patreon" } ], - "time": "2020-08-27T18:56:02+00:00" + "time": "2022-12-19T15:00:24+00:00" }, { "name": "lcobucci/jwt", - "version": "4.3.0", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "4d7de2fe0d51a96418c0d04004986e410e87f6b4" + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/4d7de2fe0d51a96418c0d04004986e410e87f6b4", - "reference": "4d7de2fe0d51a96418c0d04004986e410e87f6b4", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", "shasum": "" }, "require": { - "ext-hash": "*", - "ext-json": "*", - "ext-mbstring": "*", "ext-openssl": "*", "ext-sodium": "*", - "lcobucci/clock": "^2.0 || ^3.0", - "php": "^7.4 || ^8.0" + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.21", - "lcobucci/coding-standard": "^6.0", - "mikey179/vfsstream": "^1.6.7", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/php-invoker": "^3.1", - "phpunit/phpunit": "^9.5" + "infection/infection": "^0.27.0", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2.9", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.2.6" + }, + "suggest": { + "lcobucci/clock": ">= 3.0" }, "type": "library", "autoload": { @@ -2733,7 +4409,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/4.3.0" + "source": "https://github.com/lcobucci/jwt/tree/5.3.0" }, "funding": [ { @@ -2745,7 +4421,7 @@ "type": "patreon" } ], - "time": "2023-01-02T13:28:00+00:00" + "time": "2024-04-11T23:07:54+00:00" }, { "name": "league/csv", @@ -2833,16 +4509,16 @@ }, { "name": "league/html-to-markdown", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "e0fc8cf07bdabbcd3765341ecb50c34c271d64e1" + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e0fc8cf07bdabbcd3765341ecb50c34c271d64e1", - "reference": "e0fc8cf07bdabbcd3765341ecb50c34c271d64e1", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd", "shasum": "" }, "require": { @@ -2852,11 +4528,11 @@ }, "require-dev": { "mikehaertl/php-shellcommand": "^1.1.0", - "phpstan/phpstan": "^0.12.99", + "phpstan/phpstan": "^1.8.8", "phpunit/phpunit": "^8.5 || ^9.2", "scrutinizer/ocular": "^1.6", - "unleashedtech/php-coding-standard": "^2.7", - "vimeo/psalm": "^4.22" + "unleashedtech/php-coding-standard": "^2.7 || ^3.0", + "vimeo/psalm": "^4.22 || ^5.0" }, "bin": [ "bin/html-to-markdown" @@ -2898,7 +4574,7 @@ ], "support": { "issues": "https://github.com/thephpleague/html-to-markdown/issues", - "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.0" + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1" }, "funding": [ { @@ -2918,57 +4594,113 @@ "type": "tidelift" } ], - "time": "2022-03-02T17:24:08+00:00" + "time": "2023-07-12T21:21:09+00:00" }, { - "name": "league/uri", - "version": "6.7.2", + "name": "league/oauth2-client", + "version": "2.8.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "d3b50812dd51f3fbf176344cc2981db03d10fe06" + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/d3b50812dd51f3fbf176344cc2981db03d10fe06", - "reference": "d3b50812dd51f3fbf176344cc2981db03d10fe06", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", "shasum": "" }, "require": { "ext-json": "*", - "league/uri-interfaces": "^2.3", - "php": "^7.4 || ^8.0", - "psr/http-message": "^1.0" + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.5.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" + }, + "time": "2025-02-26T04:37:30+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" }, "conflict": { "league/uri-schemes": "^1.0" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.3.2", - "nyholm/psr7": "^1.5", - "php-http/psr7-integration-tests": "^1.1", - "phpstan/phpstan": "^1.2.0", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0.0", - "phpstan/phpstan-strict-rules": "^1.1.0", - "phpunit/phpunit": "^9.5.10", - "psr/http-factory": "^1.0" - }, "suggest": { - "ext-fileinfo": "Needed to create Data URI from a filepath", - "ext-intl": "Needed to improve host validation", - "league/uri-components": "Needed to easily manipulate URI objects", - "psr/http-factory": "Needed to use the URI factory" + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "src" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3008,8 +4740,8 @@ "support": { "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri/issues", - "source": "https://github.com/thephpleague/uri/tree/6.7.2" + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" }, "funding": [ { @@ -3017,46 +4749,45 @@ "type": "github" } ], - "time": "2022-09-13T19:50:42+00:00" + "time": "2024-12-08T08:40:02+00:00" }, { - "name": "league/uri-interfaces", - "version": "2.3.0", + "name": "league/uri-components", + "version": "7.5.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383" + "url": "https://github.com/thephpleague/uri-components.git", + "reference": "4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", - "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f", + "reference": "4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19", - "phpstan/phpstan": "^0.12.90", - "phpstan/phpstan-phpunit": "^0.12.19", - "phpstan/phpstan-strict-rules": "^0.12.9", - "phpunit/phpunit": "^8.5.15 || ^9.5" + "league/uri": "^7.5", + "php": "^8.1" }, "suggest": { - "ext-intl": "to use the IDNA feature", - "symfony/intl": "to use the IDNA feature via Symfony Polyfill" + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-mbstring": "to use the sorting algorithm of URLSearchParams", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "League\\Uri\\": "src/" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3070,17 +4801,113 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interface for URI representation", - "homepage": "http://github.com/thephpleague/uri-interfaces", + "description": "URI components manipulation library", + "homepage": "http://uri.thephpleague.com", "keywords": [ + "authority", + "components", + "fragment", + "host", + "middleware", + "modifier", + "path", + "port", + "query", "rfc3986", - "rfc3987", + "scheme", "uri", - "url" + "url", + "userinfo" ], "support": { - "issues": "https://github.com/thephpleague/uri-interfaces/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/2.3.0" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-components/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" }, "funding": [ { @@ -3088,55 +4915,56 @@ "type": "github" } ], - "time": "2021-06-28T04:27:21+00:00" + "time": "2024-12-08T08:18:47+00:00" }, { "name": "liip/imagine-bundle", - "version": "2.10.0", + "version": "2.13.3", "source": { "type": "git", "url": "https://github.com/liip/LiipImagineBundle.git", - "reference": "93bfc6dde87f135f9c3e63330cff23122448f4ee" + "reference": "3faccde327f91368e51d05ecad49a9cd915abd81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/93bfc6dde87f135f9c3e63330cff23122448f4ee", - "reference": "93bfc6dde87f135f9c3e63330cff23122448f4ee", + "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/3faccde327f91368e51d05ecad49a9cd915abd81", + "reference": "3faccde327f91368e51d05ecad49a9cd915abd81", "shasum": "" }, "require": { "ext-mbstring": "*", "imagine/imagine": "^1.3.2", - "php": "^7.1|^8.0", - "symfony/filesystem": "^3.4|^4.4|^5.3|^6.0", - "symfony/finder": "^3.4|^4.4|^5.3|^6.0", - "symfony/framework-bundle": "^3.4.23|^4.4|^5.3|^6.0", - "symfony/mime": "^4.4|^5.3|^6.0", - "symfony/options-resolver": "^3.4|^4.4|^5.3|^6.0", - "symfony/process": "^3.4|^4.4|^5.3|^6.0", + "php": "^7.2|^8.0", + "symfony/filesystem": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/finder": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/framework-bundle": "^3.4.23|^4.4|^5.3|^6.0|^7.0", + "symfony/mime": "^4.4|^5.3|^6.0|^7.0", + "symfony/options-resolver": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/process": "^3.4|^4.4|^5.3|^6.0|^7.0", "twig/twig": "^1.44|^2.9|^3.0" }, "require-dev": { "amazonwebservices/aws-sdk-for-php": "^1.0", - "aws/aws-sdk-php": "^2.4", + "aws/aws-sdk-php": "^2.4|^3.0", "doctrine/cache": "^1.11|^2.0", "doctrine/persistence": "^1.3|^2.0", "enqueue/enqueue-bundle": "^0.9|^0.10", "ext-gd": "*", "league/flysystem": "^1.0|^2.0|^3.0", - "phpstan/phpstan": "^0.12.64", + "phpstan/phpstan": "^1.10.0", "psr/cache": "^1.0|^2.0|^3.0", "psr/log": "^1.0", - "symfony/browser-kit": "^3.4|^4.4|^5.3|^6.0", - "symfony/cache": "^3.4|^4.4|^5.3|^6.0", - "symfony/console": "^3.4|^4.4|^5.3|^6.0", - "symfony/dependency-injection": "^3.4|^4.4|^5.3|^6.0", - "symfony/form": "^3.4|^4.4|^5.3|^6.0", - "symfony/messenger": "^4.4|^5.3|^6.0", - "symfony/phpunit-bridge": "^5.3", + "symfony/asset": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/browser-kit": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/cache": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/console": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/dependency-injection": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/form": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/messenger": "^4.4|^5.3|^6.0|^7.0", + "symfony/phpunit-bridge": "^7.0.2", "symfony/templating": "^3.4|^4.4|^5.3|^6.0", - "symfony/validator": "^3.4|^4.4|^5.3|^6.0", - "symfony/yaml": "^3.4|^4.4|^5.3|^6.0" + "symfony/validator": "^3.4|^4.4|^5.3|^6.0|^7.0", + "symfony/yaml": "^3.4|^4.4|^5.3|^6.0|^7.0" }, "suggest": { "alcaeus/mongo-php-adapter": "required for mongodb components", @@ -3148,10 +4976,12 @@ "ext-gd": "required to use gd driver", "ext-gmagick": "required to use gmagick driver", "ext-imagick": "required to use imagick driver", + "ext-json": "required to read JSON manifest versioning", "ext-mongodb": "required for mongodb components", "league/flysystem": "required to use FlySystem data loader or cache resolver", "monolog/monolog": "A psr/log compatible logger is required to enable logging", "rokka/imagine-vips": "required to use 'vips' driver", + "symfony/asset": "If you want to use asset versioning", "symfony/messenger": "If you like to process images in background", "symfony/templating": "required to use deprecated Templating component instead of Twig" }, @@ -3175,7 +5005,7 @@ } ], "description": "This bundle provides an image manipulation abstraction toolkit for Symfony-based projects.", - "homepage": "http://liip.ch", + "homepage": "https://www.liip.ch", "keywords": [ "bundle", "image", @@ -3189,22 +5019,22 @@ ], "support": { "issues": "https://github.com/liip/LiipImagineBundle/issues", - "source": "https://github.com/liip/LiipImagineBundle/tree/2.10.0" + "source": "https://github.com/liip/LiipImagineBundle/tree/2.13.3" }, - "time": "2022-12-01T13:19:59+00:00" + "time": "2024-12-12T09:38:23+00:00" }, { "name": "lorenzo/pinky", - "version": "1.0.9", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/lorenzo/pinky.git", - "reference": "f890472e4a25f89591f176aa03d9588a9d3332a7" + "reference": "e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lorenzo/pinky/zipball/f890472e4a25f89591f176aa03d9588a9d3332a7", - "reference": "f890472e4a25f89591f176aa03d9588a9d3332a7", + "url": "https://api.github.com/repos/lorenzo/pinky/zipball/e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17", + "reference": "e1b1bdb2c132b8a7ba32bca64d2443f646ddbd17", "shasum": "" }, "require": { @@ -3242,32 +5072,30 @@ ], "support": { "issues": "https://github.com/lorenzo/pinky/issues", - "source": "https://github.com/lorenzo/pinky/tree/1.0.9" + "source": "https://github.com/lorenzo/pinky/tree/1.1.0" }, - "time": "2023-01-12T16:15:52+00:00" + "time": "2023-07-31T13:36:50+00:00" }, { "name": "masterminds/html5", - "version": "2.7.6", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947" + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", - "reference": "897eb517a343a2281f11bc5556d6548db7d93947", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-dom": "*", - "ext-libxml": "*", "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" }, "type": "library", "extra": { @@ -3311,48 +5139,49 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" }, - "time": "2022-08-18T16:18:26+00:00" + "time": "2024-03-31T07:05:07+00:00" }, { "name": "monolog/monolog", - "version": "2.9.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1" + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f259e2b15fb95494c83f52d3caad003bbf5ffaa1", - "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { - "php": ">=7.2", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" }, "provide": { - "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + "psr/log-implementation": "3.0.0" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "aws/aws-sdk-php": "^3.0", "doctrine/couchdb": "~1.0@dev", "elasticsearch/elasticsearch": "^7 || ^8", "ext-json": "*", - "graylog2/gelf-php": "^1.4.2 || ^2@dev", - "guzzlehttp/guzzle": "^7.4", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", - "phpspec/prophecy": "^1.15", - "phpstan/phpstan": "^0.12.91", - "phpunit/phpunit": "^8.5.14", - "predis/predis": "^1.1 || ^2.0", - "rollbar/rollbar": "^1.3 || ^2 || ^3", - "ruflin/elastica": "^7", - "swiftmailer/swiftmailer": "^5.3|^6.0", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", "symfony/mailer": "^5.4 || ^6", "symfony/mime": "^5.4 || ^6" }, @@ -3375,7 +5204,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -3403,7 +5232,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.9.1" + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" }, "funding": [ { @@ -3415,7 +5244,70 @@ "type": "tidelift" } ], - "time": "2023-02-06T13:44:46+00:00" + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "nbgrp/onelogin-saml-bundle", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/nbgrp/onelogin-saml-bundle.git", + "reference": "3341544e72b699ab69357ab38cee9c80941ce1c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nbgrp/onelogin-saml-bundle/zipball/3341544e72b699ab69357ab38cee9c80941ce1c6", + "reference": "3341544e72b699ab69357ab38cee9c80941ce1c6", + "shasum": "" + }, + "require": { + "onelogin/php-saml": "^4", + "php": "^8.1", + "psr/log": "^1 || ^2 || ^3", + "symfony/config": "^6.4", + "symfony/dependency-injection": "^6.4", + "symfony/deprecation-contracts": "^3", + "symfony/event-dispatcher-contracts": "^3", + "symfony/http-foundation": "^6.4", + "symfony/http-kernel": "^6.4", + "symfony/routing": "^6.4", + "symfony/security-bundle": "^6.4", + "symfony/security-core": "^6.4", + "symfony/security-http": "^6.4" + }, + "require-dev": { + "doctrine/orm": "^2.3 || ^3", + "symfony/event-dispatcher": "^6.4", + "symfony/phpunit-bridge": "^6.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Nbgrp\\OneloginSamlBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Alexander Menshchikov", + "email": "alexander.menshchikov@yandex.ru" + } + ], + "description": "OneLogin SAML Symfony Bundle", + "keywords": [ + "SSO", + "multiple IdP", + "onelogin", + "saml" + ], + "support": { + "issues": "https://github.com/nbgrp/onelogin-saml-bundle/issues", + "source": "https://github.com/nbgrp/onelogin-saml-bundle/tree/v1.4.0" + }, + "time": "2023-11-29T12:22:32+00:00" }, { "name": "nelexa/zip", @@ -3491,27 +5383,90 @@ "time": "2022-06-17T11:17:46+00:00" }, { - "name": "nelmio/security-bundle", - "version": "v3.0.0", + "name": "nelmio/cors-bundle", + "version": "2.5.0", "source": { "type": "git", - "url": "https://github.com/nelmio/NelmioSecurityBundle.git", - "reference": "34699d40d81b58b6bd256e34489c799620dff2a4" + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioSecurityBundle/zipball/34699d40d81b58b6bd256e34489c799620dff2a4", - "reference": "34699d40d81b58b6bd256e34489c799620dff2a4", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.6", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.5.0" + }, + "time": "2024-06-24T21:25:28+00:00" + }, + { + "name": "nelmio/security-bundle", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioSecurityBundle.git", + "reference": "b1c5e323d71152bc1a61a4f8fbf7d88c6fa3e2e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioSecurityBundle/zipball/b1c5e323d71152bc1a61a4f8fbf7d88c6fa3e2e7", + "reference": "b1c5e323d71152bc1a61a4f8fbf7d88c6fa3e2e7", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0", - "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0", - "symfony/security-core": "^4.4 || ^5.4 || ^6.0", - "symfony/security-csrf": "^4.4 || ^5.4 || ^6.0", - "symfony/security-http": "^4.4 || ^5.4 || ^6.0", - "symfony/yaml": "^4.4 || ^5.4 || ^6.0", + "symfony/deprecation-contracts": "^2.5 || ^3", + "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.3 || ^7.0", + "symfony/security-core": "^5.4 || ^6.3 || ^7.0", + "symfony/security-csrf": "^5.4 || ^6.3 || ^7.0", + "symfony/security-http": "^5.4 || ^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.3 || ^7.0", "ua-parser/uap-php": "^3.4.4" }, "require-dev": { @@ -3522,10 +5477,10 @@ "phpstan/phpstan-symfony": "^1.1", "phpunit/phpunit": "^9.5", "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/browser-kit": "^4.4 || ^5.4 || ^6.0", - "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "symfony/phpunit-bridge": "^6.0", - "symfony/twig-bundle": "^4.4 || ^5.4 || ^6.0", + "symfony/browser-kit": "^5.4 || ^6.3 || ^7.0", + "symfony/cache": "^5.4 || ^6.3 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/twig-bundle": "^5.4 || ^6.3 || ^7.0", "twig/twig": "^2.10 || ^3.0" }, "type": "symfony-bundle", @@ -3559,95 +5514,39 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioSecurityBundle/issues", - "source": "https://github.com/nelmio/NelmioSecurityBundle/tree/v3.0.0" + "source": "https://github.com/nelmio/NelmioSecurityBundle/tree/v3.5.1" }, - "time": "2022-03-17T07:30:15+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.15.4", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" - }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2025-03-13T09:17:16+00:00" }, { "name": "nikolaposa/version", - "version": "4.1.0", + "version": "4.2.1", "source": { "type": "git", "url": "https://github.com/nikolaposa/version.git", - "reference": "0aada6b801962c084ae465f7569397dc2186b6a7" + "reference": "2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikolaposa/version/zipball/0aada6b801962c084ae465f7569397dc2186b6a7", - "reference": "0aada6b801962c084ae465f7569397dc2186b6a7", + "url": "https://api.github.com/repos/nikolaposa/version/zipball/2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428", + "reference": "2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428", "shasum": "" }, "require": { "beberlei/assert": "^3.2", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", - "phpstan/phpstan": "^0.12.10", - "phpstan/phpstan-beberlei-assert": "^0.12.2", - "phpstan/phpstan-phpunit": "^0.12.6", - "phpunit/phpunit": "^8.0" + "friendsofphp/php-cs-fixer": "^3.44", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-beberlei-assert": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "4.2.x-dev" } }, "autoload": { @@ -3676,101 +5575,45 @@ ], "support": { "issues": "https://github.com/nikolaposa/version/issues", - "source": "https://github.com/nikolaposa/version/tree/4.1.0" + "source": "https://github.com/nikolaposa/version/tree/4.2.1" }, - "time": "2020-12-12T10:47:10+00:00" - }, - { - "name": "nyholm/nsa", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/Nyholm/NSA.git", - "reference": "c264c17ed2aa8251c64ad289442ed53f64cdb283" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Nyholm/NSA/zipball/c264c17ed2aa8251c64ad289442ed53f64cdb283", - "reference": "c264c17ed2aa8251c64ad289442ed53f64cdb283", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "webmozart/assert": "^1.1.0" - }, - "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Nyholm\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "description": "See everything and do whatever you want. No privacy rule will stop us. Used in tests, debugging and fixtures to access properties and methods.", - "homepage": "https://tnyholm.se", - "keywords": [ - "Fixture", - "debug", - "reflection", - "test" - ], - "support": { - "issues": "https://github.com/Nyholm/NSA/issues", - "source": "https://github.com/Nyholm/NSA/tree/1.3.0" - }, - "funding": [ - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2021-07-15T18:25:37+00:00" + "time": "2025-03-24T19:12:02+00:00" }, { "name": "nyholm/psr7", - "version": "1.5.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "f734364e38a876a23be4d906a2a089e1315be18a" + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/f734364e38a876a23be4d906a2a089e1315be18a", - "reference": "f734364e38a876a23be4d906a2a089e1315be18a", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { - "php": ">=7.1", - "php-http/message-factory": "^1.0", + "php": ">=7.2", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.1 || ^2.0" }, "provide": { + "php-http/message-factory-implementation": "1.0", "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5 || 8.5 || 9.4", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", "symfony/error-handler": "^4.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.8-dev" } }, "autoload": { @@ -3800,7 +5643,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.5.1" + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, "funding": [ { @@ -3812,58 +5655,73 @@ "type": "github" } ], - "time": "2022-06-22T07:13:36+00:00" + "time": "2024-09-09T07:06:30+00:00" }, { "name": "omines/datatables-bundle", - "version": "0.5.5", + "version": "0.9.2", "source": { "type": "git", "url": "https://github.com/omines/datatables-bundle.git", - "reference": "b8a16c365f9d8e97d1e890e8783249f979f0d7ca" + "reference": "15974fc7dde750f8a3eff32d9ad4d9de6028583f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/omines/datatables-bundle/zipball/b8a16c365f9d8e97d1e890e8783249f979f0d7ca", - "reference": "b8a16c365f9d8e97d1e890e8783249f979f0d7ca", + "url": "https://api.github.com/repos/omines/datatables-bundle/zipball/15974fc7dde750f8a3eff32d9ad4d9de6028583f", + "reference": "15974fc7dde750f8a3eff32d9ad4d9de6028583f", "shasum": "" }, "require": { - "php": ">=7.2", - "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/framework-bundle": "^4.4|^5.0", - "symfony/options-resolver": "^4.4|^5.0", - "symfony/property-access": "^4.4|^5.0", - "symfony/translation": "^4.4|^5.0" + "php": ">=8.1", + "symfony/event-dispatcher": "^6.4|^7.1", + "symfony/framework-bundle": "^6.4|^7.1", + "symfony/options-resolver": "^6.4|^7.1", + "symfony/polyfill-mbstring": "^1.31.0", + "symfony/property-access": "^6.4|^7.1", + "symfony/translation": "^6.4|^7.1" + }, + "conflict": { + "doctrine/orm": "^3.0 <3.3" }, "require-dev": { - "doctrine/common": "^2.6|^3.0", - "doctrine/doctrine-bundle": "^2.3|^3.0", - "doctrine/orm": "^2.6.3", - "doctrine/persistence": "^1.3.4|^2.0", + "doctrine/common": "^3.4.5", + "doctrine/doctrine-bundle": "^2.13.1", + "doctrine/orm": "^2.19.3|^3.3.0", + "doctrine/persistence": "^3.4.0", "ext-curl": "*", "ext-json": "*", + "ext-mbstring": "*", + "ext-mongodb": "*", "ext-pdo_sqlite": "*", "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^2.7", - "mongodb/mongodb": "^1.2", - "ocramius/package-versions": "^1.4", - "phpoffice/phpspreadsheet": "^1.6", - "ruflin/elastica": "^6.0", - "symfony/browser-kit": "^4.4|^5.0", - "symfony/css-selector": "^4.4|^5.0", - "symfony/dom-crawler": "^4.4|^5.0", - "symfony/intl": "^4.4|^5.0", - "symfony/mime": "^4.4|^5.0", - "symfony/phpunit-bridge": "^4.4|^5.0", - "symfony/twig-bundle": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0", - "symfony/yaml": "^4.4|^5.0" + "friendsofphp/php-cs-fixer": "^3.65.0", + "mongodb/mongodb": "^1.20.0", + "ocramius/package-versions": "^2.9", + "openspout/openspout": "^4.23", + "phpoffice/phpspreadsheet": "^2.3.3|^3.5", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.0.3", + "phpstan/phpstan-doctrine": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.1", + "phpstan/phpstan-symfony": "^2.0.0", + "phpunit/phpunit": "^10.5.38|^11.4.4", + "ruflin/elastica": "^6.2|^7.3.2", + "symfony/browser-kit": "^6.4.13|^7.1", + "symfony/css-selector": "^6.4.13|^7.1", + "symfony/doctrine-bridge": "^6.4.13|^7.1", + "symfony/dom-crawler": "^6.4.13|^7.1", + "symfony/intl": "^6.4.13|^7.1", + "symfony/mime": "^6.4.13|^7.1", + "symfony/phpunit-bridge": "^7.2", + "symfony/twig-bundle": "^6.4|^7.1", + "symfony/var-dumper": "^6.4.13|^7.1", + "symfony/yaml": "^6.4.13|^7.1" }, "suggest": { "doctrine/doctrine-bundle": "For integrated access to Doctrine object managers", "doctrine/orm": "For full automated integration with Doctrine entities", "mongodb/mongodb": "For integration with MongoDB collections", + "openspout/openspout": "To use the OpenSpout Excel exporter", "phpoffice/phpspreadsheet": "To export the data from DataTables to Excel", "ruflin/elastica": "For integration with Elasticsearch indexes", "symfony/twig-bundle": "To use default Twig based rendering and TwigColumn" @@ -3871,7 +5729,7 @@ "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-master": "0.5-dev" + "dev-master": "0.9-dev" } }, "autoload": { @@ -3887,12 +5745,12 @@ { "name": "Robbert Beesems", "email": "robbert.beesems@omines.com", - "homepage": "https://omines.nl/" + "homepage": "https://www.omines.nl/" }, { "name": "Niels Keurentjes", "email": "niels.keurentjes@omines.com", - "homepage": "https://omines.nl/" + "homepage": "https://www.omines.nl/" } ], "description": "Symfony DataTables Bundle with native Doctrine ORM, Elastica and MongoDB support", @@ -3909,40 +5767,47 @@ ], "support": { "issues": "https://github.com/omines/datatables-bundle/issues", - "source": "https://github.com/omines/datatables-bundle/tree/0.5.5" + "source": "https://github.com/omines/datatables-bundle/tree/0.9.2" }, - "time": "2021-06-29T08:12:37+00:00" + "funding": [ + { + "url": "https://github.com/curry684", + "type": "github" + } + ], + "time": "2025-01-23T14:53:20+00:00" }, { "name": "onelogin/php-saml", - "version": "3.6.1", + "version": "4.2.0", "source": { "type": "git", - "url": "https://github.com/onelogin/php-saml.git", - "reference": "a7328b11887660ad248ea10952dd67a5aa73ba3b" + "url": "https://github.com/SAML-Toolkits/php-saml.git", + "reference": "d3b5172f137db2f412239432d77253ceaaa1e939" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/onelogin/php-saml/zipball/a7328b11887660ad248ea10952dd67a5aa73ba3b", - "reference": "a7328b11887660ad248ea10952dd67a5aa73ba3b", + "url": "https://api.github.com/repos/SAML-Toolkits/php-saml/zipball/d3b5172f137db2f412239432d77253ceaaa1e939", + "reference": "d3b5172f137db2f412239432d77253ceaaa1e939", "shasum": "" }, "require": { - "php": ">=5.4", - "robrichards/xmlseclibs": ">=3.1.1" + "php": ">=7.3", + "robrichards/xmlseclibs": "^3.1" }, "require-dev": { - "pdepend/pdepend": "^2.5.0", - "php-coveralls/php-coveralls": "^1.0.2 || ^2.0", - "phploc/phploc": "^2.1 || ^3.0 || ^4.0", - "phpunit/phpunit": "<7.5.18", - "sebastian/phpcpd": "^2.0 || ^3.0 || ^4.0", - "squizlabs/php_codesniffer": "^3.1.1" + "pdepend/pdepend": "^2.8.0", + "php-coveralls/php-coveralls": "^2.0", + "phploc/phploc": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "phpunit/phpunit": "^9.5", + "sebastian/phpcpd": "^4.0 || ^5.0 || ^6.0 ", + "squizlabs/php_codesniffer": "^3.5.8" }, "suggest": { "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs", - "ext-gettext": "Install gettext and php5-gettext libs to handle translations", - "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)" + "ext-dom": "Install xml lib", + "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)", + "ext-zlib": "Install zlib" }, "type": "library", "autoload": { @@ -3954,32 +5819,40 @@ "license": [ "MIT" ], - "description": "OneLogin PHP SAML Toolkit", - "homepage": "https://developers.onelogin.com/saml/php", + "description": "PHP SAML Toolkit", + "homepage": "https://github.com/SAML-Toolkits/php-saml", "keywords": [ + "Federation", "SAML2", - "onelogin", + "SSO", + "identity", "saml" ], "support": { - "email": "sixto.garcia@onelogin.com", - "issues": "https://github.com/onelogin/php-saml/issues", - "source": "https://github.com/onelogin/php-saml/" + "email": "sixto.martin.garcia@gmail.com", + "issues": "https://github.com/onelogin/SAML-Toolkits/issues", + "source": "https://github.com/onelogin/SAML-Toolkits/" }, - "time": "2021-03-02T10:13:07+00:00" + "funding": [ + { + "url": "https://github.com/SAML-Toolkits", + "type": "github" + } + ], + "time": "2024-05-30T15:10:40+00:00" }, { "name": "paragonie/constant_time_encoding", - "version": "v2.6.3", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "58c3f47f650c94ec05a151692652a868995d2938" + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", - "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", "shasum": "" }, "require": { @@ -4033,110 +5906,194 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2022-06-14T06:56:20+00:00" + "time": "2024-05-08T12:18:48+00:00" }, { - "name": "phenx/php-font-lib", - "version": "0.5.4", + "name": "paragonie/random_compat", + "version": "v9.99.100", "source": { "type": "git", - "url": "https://github.com/dompdf/php-font-lib.git", - "reference": "dd448ad1ce34c63d09baccd05415e361300c35b4" + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/dd448ad1ce34c63d09baccd05415e361300c35b4", - "reference": "dd448ad1ce34c63d09baccd05415e361300c35b4", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", "shasum": "" }, "require": { - "ext-mbstring": "*" + "php": ">= 7" }, "require-dev": { - "symfony/phpunit-bridge": "^3 || ^4 || ^5" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "psr-4": { - "FontLib\\": "src/FontLib" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "MIT" ], "authors": [ { - "name": "Fabien Ménager", - "email": "fabien.menager@gmail.com" + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" } ], - "description": "A library to read, parse, export and make subsets of different types of font files.", - "homepage": "https://github.com/PhenX/php-font-lib", + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], "support": { - "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/0.5.4" + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" }, - "time": "2021-12-17T19:44:54+00:00" + "time": "2020-10-15T08:29:30+00:00" }, { - "name": "phenx/php-svg-lib", - "version": "0.5.0", + "name": "paragonie/sodium_compat", + "version": "v1.21.1", "source": { "type": "git", - "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "76876c6cf3080bcb6f249d7d59705108166a6685" + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/76876c6cf3080bcb6f249d7d59705108166a6685", - "reference": "76876c6cf3080bcb6f249d7d59705108166a6685", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/bb312875dcdd20680419564fe42ba1d9564b9e37", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": "^7.1 || ^8.0", - "sabberworm/php-css-parser": "^8.4" + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." }, "type": "library", "autoload": { - "psr-4": { - "Svg\\": "src/Svg" - } + "files": [ + "autoload.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "ISC" ], "authors": [ { - "name": "Fabien Ménager", - "email": "fabien.menager@gmail.com" + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" } ], - "description": "A library to read, parse and export to PDF SVG files.", - "homepage": "https://github.com/PhenX/php-svg-lib", + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], "support": { - "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.0" + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v1.21.1" }, - "time": "2022-09-06T12:16:56+00:00" + "time": "2024-04-22T22:05:04+00:00" + }, + { + "name": "part-db/label-fonts", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Part-DB/label-fonts.git", + "reference": "77c84b70ed3bb005df15f30ff835ddec490394b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Part-DB/label-fonts/zipball/77c84b70ed3bb005df15f30ff835ddec490394b9", + "reference": "77c84b70ed3bb005df15f30ff835ddec490394b9", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "The Part-DB Team", + "homepage": "https://github.com/Part-DB/Part-DB-server" + } + ], + "description": "This library bundles the fonts used in Part-DB for label generators. Fonts are work of others.", + "homepage": "https://github.com/Part-DB/Part-DB-server", + "keywords": [ + "font", + "fonts", + "part-db" + ], + "support": { + "issues": "https://github.com/Part-DB/label-fonts/issues", + "source": "https://github.com/Part-DB/label-fonts/tree/v1.1.0" + }, + "time": "2024-02-08T21:44:38+00:00" }, { "name": "php-http/discovery", - "version": "1.15.3", + "version": "1.20.0", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "3ccd28dd9fb34b52db946abea1b538568e34eae8" + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/3ccd28dd9fb34b52db946abea1b538568e34eae8", - "reference": "3ccd28dd9fb34b52db946abea1b538568e34eae8", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", "shasum": "" }, "require": { @@ -4144,7 +6101,8 @@ "php": "^7.1 || ^8.0" }, "conflict": { - "nyholm/psr7": "<1.0" + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" }, "provide": { "php-http/async-client-implementation": "*", @@ -4159,7 +6117,8 @@ "php-http/httplug": "^1.0 || ^2.0", "php-http/message-factory": "^1.0", "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", - "symfony/phpunit-bridge": "^6.2" + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" }, "type": "composer-plugin", "extra": { @@ -4169,7 +6128,10 @@ "autoload": { "psr-4": { "Http\\Discovery\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4195,40 +6157,35 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.15.3" + "source": "https://github.com/php-http/discovery/tree/1.20.0" }, - "time": "2023-03-31T14:40:37+00:00" + "time": "2024-10-02T11:20:13+00:00" }, { "name": "php-http/httplug", - "version": "2.3.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/php-http/httplug.git", - "reference": "f640739f80dfa1152533976e3c112477f69274eb" + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/httplug/zipball/f640739f80dfa1152533976e3c112477f69274eb", - "reference": "f640739f80dfa1152533976e3c112477f69274eb", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", "php-http/promise": "^1.1", "psr/http-client": "^1.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.1", - "phpspec/phpspec": "^5.1 || ^6.0" + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Client\\": "src/" @@ -4257,91 +6214,32 @@ ], "support": { "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/2.3.0" + "source": "https://github.com/php-http/httplug/tree/2.4.1" }, - "time": "2022-02-21T09:52:22+00:00" - }, - { - "name": "php-http/message-factory", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org", - "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" - ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" - }, - "time": "2015-12-19T14:08:53+00:00" + "time": "2024-09-23T11:39:58+00:00" }, { "name": "php-http/promise", - "version": "1.1.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", - "phpspec/phpspec": "^5.1.2 || ^6.2" + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, "autoload": { "psr-4": { "Http\\Promise\\": "src/" @@ -4368,238 +6266,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.1.0" + "source": "https://github.com/php-http/promise/tree/1.3.1" }, - "time": "2020-07-07T09:29:14+00:00" - }, - { - "name": "php-translation/common", - "version": "3.2.0", - "source": { - "type": "git", - "url": "https://github.com/php-translation/common.git", - "reference": "986ddf4e3b2b3458d2a7353658bd40764d8ca1d1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-translation/common/zipball/986ddf4e3b2b3458d2a7353658bd40764d8ca1d1", - "reference": "986ddf4e3b2b3458d2a7353658bd40764d8ca1d1", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "symfony/translation": " ^3.4 || ^4.3 || ^5.0 || ^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^8.4", - "symfony/phpunit-bridge": "^4.3 || ^5.0 || ^6.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Translation\\Common\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "description": "Common translation stuff", - "support": { - "issues": "https://github.com/php-translation/common/issues", - "source": "https://github.com/php-translation/common/tree/3.2.0" - }, - "time": "2022-02-04T11:49:38+00:00" - }, - { - "name": "php-translation/extractor", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/php-translation/extractor.git", - "reference": "09ad2f3654e6badb95a739b0284f5785531f7c8d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-translation/extractor/zipball/09ad2f3654e6badb95a739b0284f5785531f7c8d", - "reference": "09ad2f3654e6badb95a739b0284f5785531f7c8d", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.7 || ^2.0", - "nikic/php-parser": "^3.0 || ^4.0", - "php": "^7.2 || ^8.0", - "symfony/finder": "^3.4 || ^4.4 || ^5.0 || ^6.0", - "twig/twig": "^2.0 || ^3.0" - }, - "require-dev": { - "knplabs/knp-menu": "^3.1", - "symfony/phpunit-bridge": "^5.0 || ^6.0", - "symfony/translation": "^3.4 || ^4.4 || ^5.0 || ^6.0", - "symfony/twig-bridge": "^3.4 || ^4.4 || ^5.0 || ^6.0", - "symfony/validator": "^3.4 || ^4.4 || ^5.0 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Translation\\Extractor\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "description": "Extract translations form the source code", - "support": { - "issues": "https://github.com/php-translation/extractor/issues", - "source": "https://github.com/php-translation/extractor/tree/2.1.1" - }, - "time": "2023-03-28T11:37:22+00:00" - }, - { - "name": "php-translation/symfony-bundle", - "version": "0.12.8", - "source": { - "type": "git", - "url": "https://github.com/php-translation/symfony-bundle.git", - "reference": "9bd3ecace0a4019a7a4327ca9ea8df1c23ff0da3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-translation/symfony-bundle/zipball/9bd3ecace0a4019a7a4327ca9ea8df1c23ff0da3", - "reference": "9bd3ecace0a4019a7a4327ca9ea8df1c23ff0da3", - "shasum": "" - }, - "require": { - "nyholm/nsa": "^1.1", - "php": "^7.2 || ^8.0", - "php-translation/extractor": "^2.0", - "php-translation/symfony-storage": "^2.1", - "symfony/asset": "^4.4.20 || ^5.2.4 || ^6.0", - "symfony/finder": "^4.4.20 || ^5.2.4 || ^6.0", - "symfony/framework-bundle": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/intl": "^4.4.20 || ^5.2.4 || ^6.0", - "symfony/translation": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/twig-bundle": "^4.4.20 || ^5.2.4 || ^6.0", - "symfony/validator": "^4.4.20 || ^5.2.5 || ^6.0", - "twig/twig": "^2.14.4 || ^3.3" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.3", - "matthiasnoback/symfony-config-test": "^4.1", - "matthiasnoback/symfony-dependency-injection-test": "^4.1", - "nyholm/psr7": "^1.1", - "nyholm/symfony-bundle-test": "^2.0", - "php-http/curl-client": "^1.7 || ^2.0", - "php-http/message": "^1.11", - "php-http/message-factory": "^1.0.2", - "php-translation/translator": "^1.0", - "symfony/console": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/dependency-injection": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/phpunit-bridge": "^5.2 || ^6.0", - "symfony/twig-bridge": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.4 || ^6.0" - }, - "suggest": { - "php-http/httplug-bundle": "To easier configure your httplug clients." - }, - "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-master": "0.12-dev" - } - }, - "autoload": { - "psr-4": { - "Translation\\Bundle\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "support": { - "issues": "https://github.com/php-translation/symfony-bundle/issues", - "source": "https://github.com/php-translation/symfony-bundle/tree/0.12.8" - }, - "time": "2022-12-19T12:38:39+00:00" - }, - { - "name": "php-translation/symfony-storage", - "version": "2.3.1", - "source": { - "type": "git", - "url": "https://github.com/php-translation/symfony-storage.git", - "reference": "95d52dd86d41fe0ec2c75e1469b5003956044cc8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-translation/symfony-storage/zipball/95d52dd86d41fe0ec2c75e1469b5003956044cc8", - "reference": "95d52dd86d41fe0ec2c75e1469b5003956044cc8", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "php-translation/common": "^3.0", - "symfony/translation": "^3.4 || ^4.2 || ^5.0 || ^6.0" - }, - "require-dev": { - "phpunit/phpunit": ">=8.5.20", - "symfony/framework-bundle": " ^3.4 || ^4.2 || ^5.0 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - } - }, - "autoload": { - "psr-4": { - "Translation\\SymfonyStorage\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "description": "A translation file storage using Symfony translation component.", - "support": { - "issues": "https://github.com/php-translation/symfony-storage/issues", - "source": "https://github.com/php-translation/symfony-storage/tree/2.3.1" - }, - "time": "2022-02-14T11:36:15+00:00" + "time": "2024-03-15T13:55:21+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4656,28 +6325,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "version": "5.6.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" }, "type": "library", "extra": { @@ -4701,35 +6377,35 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2025-04-13T19:20:35+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.7.1", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "dfc078e8af9c99210337325ff5aa152872c98714" + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dfc078e8af9c99210337325ff5aa152872c98714", - "reference": "dfc078e8af9c99210337325ff5aa152872c98714", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", + "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { "ext-tokenizer": "*", @@ -4765,34 +6441,36 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.1" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" }, - "time": "2023-03-27T19:02:04+00:00" + "time": "2024-11-09T15:12:26+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.18.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", @@ -4810,26 +6488,26 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2023-04-07T11:51:11+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "psr/cache", - "version": "1.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { @@ -4849,7 +6527,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for caching libraries", @@ -4859,28 +6537,81 @@ "psr-6" ], "support": { - "source": "https://github.com/php-fig/cache/tree/master" + "source": "https://github.com/php-fig/cache/tree/3.0.0" }, - "time": "2016-08-06T20:24:11+00:00" + "time": "2021-02-03T23:26:27+00:00" }, { - "name": "psr/container", - "version": "1.1.2", + "name": "psr/clock", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -4907,9 +6638,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:50:12+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/event-dispatcher", @@ -4963,21 +6694,21 @@ }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -4997,7 +6728,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -5009,27 +6740,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -5049,10 +6780,10 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -5064,22 +6795,22 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", - "version": "1.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { @@ -5088,7 +6819,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -5103,7 +6834,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -5117,31 +6848,34 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/1.1" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2023-04-04T09:50:52+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/link", - "version": "1.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/link.git", - "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562" + "reference": "84b159194ecfd7eaa472280213976e96415433f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/link/zipball/eea8e8662d5cd3ae4517c9b864493f59fca95562", - "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562", + "url": "https://api.github.com/repos/php-fig/link/zipball/84b159194ecfd7eaa472280213976e96415433f7", + "reference": "84b159194ecfd7eaa472280213976e96415433f7", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" + }, + "suggest": { + "fig/link-util": "Provides some useful PSR-13 utilities" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -5160,6 +6894,7 @@ } ], "description": "Common interfaces for HTTP links", + "homepage": "https://github.com/php-fig/link", "keywords": [ "http", "http-link", @@ -5169,36 +6904,36 @@ "rest" ], "support": { - "source": "https://github.com/php-fig/link/tree/master" + "source": "https://github.com/php-fig/link/tree/2.0.1" }, - "time": "2016-10-28T16:06:13+00:00" + "time": "2021-03-11T23:00:27+00:00" }, { "name": "psr/log", - "version": "1.1.4", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -5219,31 +6954,31 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "psr/simple-cache", - "version": "1.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -5258,7 +6993,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interfaces for simple caching", @@ -5270,62 +7005,86 @@ "simple-cache" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/master" + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "time": "2017-10-23T01:57:42+00:00" + "time": "2021-10-29T13:26:27+00:00" }, { - "name": "ramsey/collection", - "version": "1.3.0", + "name": "ralouphie/getallheaders", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/ramsey/collection.git", - "reference": "ad7475d1c9e70b190ecffc58f2d989416af339b4" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/ad7475d1c9e70b190ecffc58f2d989416af339b4", - "reference": "ad7475d1c9e70b190ecffc58f2d989416af339b4", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "symfony/polyfill-php81": "^1.23" + "php": ">=5.6" }, "require-dev": { - "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.28.3", - "fakerphp/faker": "^1.21", - "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^1.0", - "mockery/mockery": "^1.5", - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcsstandards/phpcsutils": "^1.0.0-rc1", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18.4", - "ramsey/coding-standard": "^2.0.3", - "ramsey/conventional-commits": "^1.3", - "vimeo/psalm": "^5.4" + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" }, "type": "library", "extra": { - "captainhook": { - "force-install": true - }, - "ramsey/conventional-commits": { - "configFile": "conventional-commits.json" + "branch-alias": { + "dev-main": "1.x-dev" } }, "autoload": { "psr-4": { - "Ramsey\\Collection\\": "src/" + "Revolt\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -5334,146 +7093,95 @@ ], "authors": [ { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - } - ], - "description": "A PHP library for representing and manipulating collections.", - "keywords": [ - "array", - "collection", - "hash", - "map", - "queue", - "set" - ], - "support": { - "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/1.3.0" - }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "time": "2022-12-27T19:12:24+00:00" + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" }, { - "name": "ramsey/uuid", - "version": "4.2.3", + "name": "rhukster/dom-sanitizer", + "version": "1.0.7", "source": { "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df" + "url": "https://github.com/rhukster/dom-sanitizer.git", + "reference": "c2a98f27ad742668b254282ccc5581871d0fb601" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", - "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", + "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/c2a98f27ad742668b254282ccc5581871d0fb601", + "reference": "c2a98f27ad742668b254282ccc5581871d0fb601", "shasum": "" }, "require": { - "brick/math": "^0.8 || ^0.9", - "ext-json": "*", - "php": "^7.2 || ^8.0", - "ramsey/collection": "^1.0", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php80": "^1.14" - }, - "replace": { - "rhumsaa/uuid": "self.version" + "ext-dom": "*", + "ext-libxml": "*", + "php": ">=7.3" }, "require-dev": { - "captainhook/captainhook": "^5.10", - "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", - "moontoast/math": "^1.1", - "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-mockery": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^8.5 || ^9", - "slevomat/coding-standard": "^7.0", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" - }, - "suggest": { - "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", - "ext-ctype": "Enables faster processing of character classification using ctype functions.", - "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", - "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", - "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + "phpunit/phpunit": "^9" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.x-dev" - }, - "captainhook": { - "force-install": true - } - }, "autoload": { - "files": [ - "src/functions.php" - ], "psr-4": { - "Ramsey\\Uuid\\": "src/" + "Rhukster\\DomSanitizer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", - "keywords": [ - "guid", - "identifier", - "uuid" - ], - "support": { - "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.2.3" - }, - "funding": [ + "authors": [ { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" + "name": "Andy Miller", + "email": "rhuk@rhuk.net" } ], - "time": "2021-09-25T23:10:38+00:00" + "description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+", + "support": { + "issues": "https://github.com/rhukster/dom-sanitizer/issues", + "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.7" + }, + "time": "2023-11-06T16:46:48+00:00" }, { "name": "robrichards/xmlseclibs", - "version": "3.1.1", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df" + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df", - "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07", + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07", "shasum": "" }, "require": { @@ -5500,9 +7208,61 @@ ], "support": { "issues": "https://github.com/robrichards/xmlseclibs/issues", - "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1" + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.3" }, - "time": "2020-09-05T13:00:25+00:00" + "time": "2024-11-20T21:13:56+00:00" + }, + { + "name": "runtime/frankenphp-symfony", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-runtime/frankenphp-symfony.git", + "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-runtime/frankenphp-symfony/zipball/56822c3631d9522a3136a4c33082d006bdfe4bad", + "reference": "56822c3631d9522a3136a4c33082d006bdfe4bad", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/runtime": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Runtime\\FrankenPhpSymfony\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.dev" + } + ], + "description": "FrankenPHP runtime for Symfony", + "support": { + "issues": "https://github.com/php-runtime/frankenphp-symfony/issues", + "source": "https://github.com/php-runtime/frankenphp-symfony/tree/0.2.0" + }, + "funding": [ + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-12-12T12:06:11+00:00" }, { "name": "s9e/regexp-builder", @@ -5548,24 +7308,26 @@ }, { "name": "s9e/sweetdom", - "version": "2.1.0", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/s9e/SweetDOM.git", - "reference": "9e34ff8f353234daed102274012c840bda56aff2" + "reference": "ef3a7d2745b30b4ad0d1d3d60be391a3604c69dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/s9e/SweetDOM/zipball/9e34ff8f353234daed102274012c840bda56aff2", - "reference": "9e34ff8f353234daed102274012c840bda56aff2", + "url": "https://api.github.com/repos/s9e/SweetDOM/zipball/ef3a7d2745b30b4ad0d1d3d60be391a3604c69dd", + "reference": "ef3a7d2745b30b4ad0d1d3d60be391a3604c69dd", "shasum": "" }, "require": { "ext-dom": "*", - "php": ">=7.1" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "*" + "friendsofphp/php-cs-fixer": "^3.52", + "phpunit/phpunit": "^10.0", + "s9e/repdoc": "dev-wip" }, "type": "library", "autoload": { @@ -5586,34 +7348,35 @@ ], "support": { "issues": "https://github.com/s9e/SweetDOM/issues", - "source": "https://github.com/s9e/SweetDOM/tree/2.1.0" + "source": "https://github.com/s9e/SweetDOM/tree/3.4.1" }, - "time": "2021-05-24T21:06:33+00:00" + "time": "2024-03-23T14:03:01+00:00" }, { "name": "s9e/text-formatter", - "version": "2.13.1", + "version": "2.19.0", "source": { "type": "git", "url": "https://github.com/s9e/TextFormatter.git", - "reference": "bbd9e34e9c30d5daeb780f115fe69cd81dd9c352" + "reference": "d65a4f61cbe494937afb3150dc73b6e757d400d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/bbd9e34e9c30d5daeb780f115fe69cd81dd9c352", - "reference": "bbd9e34e9c30d5daeb780f115fe69cd81dd9c352", + "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/d65a4f61cbe494937afb3150dc73b6e757d400d3", + "reference": "d65a4f61cbe494937afb3150dc73b6e757d400d3", "shasum": "" }, "require": { "ext-dom": "*", "ext-filter": "*", "lib-pcre": ">=8.13", - "php": ">=7.4", + "php": "^8.1", "s9e/regexp-builder": "^1.4", - "s9e/sweetdom": "^2.0" + "s9e/sweetdom": "^3.4" }, "require-dev": { "code-lts/doctum": "*", + "friendsofphp/php-cs-fixer": "^3.52", "matthiasmullie/minify": "*", "phpunit/phpunit": "^9.5" }, @@ -5628,7 +7391,7 @@ }, "type": "library", "extra": { - "version": "2.13.1" + "version": "2.19.0" }, "autoload": { "psr-4": { @@ -5660,36 +7423,40 @@ ], "support": { "issues": "https://github.com/s9e/TextFormatter/issues", - "source": "https://github.com/s9e/TextFormatter/tree/2.13.1" + "source": "https://github.com/s9e/TextFormatter/tree/2.19.0" }, - "time": "2023-02-11T00:18:05+00:00" + "time": "2025-04-26T09:27:34+00:00" }, { "name": "sabberworm/php-css-parser", - "version": "8.4.0", + "version": "v8.8.0", "source": { "type": "git", - "url": "https://github.com/sabberworm/PHP-CSS-Parser.git", - "reference": "e41d2140031d533348b2192a83f02d8dd8a71d30" + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/e41d2140031d533348b2192a83f02d8dd8a71d30", - "reference": "e41d2140031d533348b2192a83f02d8dd8a71d30", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/3de493bdddfd1f051249af725c7e0d2c38fed740", + "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740", "shasum": "" }, "require": { "ext-iconv": "*", - "php": ">=5.6.20" + "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { - "codacy/coverage": "^1.4", - "phpunit/phpunit": "^4.8.36" + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, "autoload": { "psr-4": { "Sabberworm\\CSS\\": "src/" @@ -5702,6 +7469,14 @@ "authors": [ { "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" } ], "description": "Parser for CSS Files written in PHP", @@ -5712,26 +7487,27 @@ "stylesheet" ], "support": { - "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues", - "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0" + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.8.0" }, - "time": "2021-12-11T13:40:54+00:00" + "time": "2025-03-23T17:59:05+00:00" }, { "name": "scheb/2fa-backup-code", - "version": "v5.13.2", + "version": "v6.13.1", "source": { "type": "git", "url": "https://github.com/scheb/2fa-backup-code.git", - "reference": "5584eb7a2c3deb80635c7173ad77858e51129c35" + "reference": "6dceeb5be0f6339d76f8e380ee09631c8bbebc7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/5584eb7a2c3deb80635c7173ad77858e51129c35", - "reference": "5584eb7a2c3deb80635c7173ad77858e51129c35", + "url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/6dceeb5be0f6339d76f8e380ee09631c8bbebc7e", + "reference": "6dceeb5be0f6339d76f8e380ee09631c8bbebc7e", "shasum": "" }, "require": { + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "scheb/2fa-bundle": "self.version" }, "type": "library", @@ -5761,45 +7537,45 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-backup-code/tree/v5.13.2" + "source": "https://github.com/scheb/2fa-backup-code/tree/v6.13.1" }, - "time": "2022-01-03T10:21:24+00:00" + "time": "2024-11-29T19:22:48+00:00" }, { "name": "scheb/2fa-bundle", - "version": "v5.13.2", + "version": "v6.13.1", "source": { "type": "git", "url": "https://github.com/scheb/2fa-bundle.git", - "reference": "dc575cc7bc94fa3a52b547698086f2ef015d2e81" + "reference": "8eadd57ebc2078ef273dca72b1ac4bd283812346" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/dc575cc7bc94fa3a52b547698086f2ef015d2e81", - "reference": "dc575cc7bc94fa3a52b547698086f2ef015d2e81", + "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/8eadd57ebc2078ef273dca72b1ac4bd283812346", + "reference": "8eadd57ebc2078ef273dca72b1ac4bd283812346", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.2.5", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/framework-bundle": "^4.4|^5.0", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/http-kernel": "^4.4|^5.0", - "symfony/property-access": "^4.4|^5.0", - "symfony/security-bundle": "^4.4.1|^5.0", - "symfony/twig-bundle": "^4.4|^5.0" + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-foundation": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/property-access": "^5.4 || ^6.0", + "symfony/security-bundle": "^5.4 || ^6.0", + "symfony/twig-bundle": "^5.4 || ^6.0" }, "conflict": { - "scheb/two-factor-bundle": "*" + "scheb/two-factor-bundle": "*", + "symfony/security-core": "^7" }, "suggest": { "scheb/2fa-backup-code": "Emergency codes when you have no access to other methods", "scheb/2fa-email": "Send codes by email", "scheb/2fa-google-authenticator": "Google Authenticator support", - "scheb/2fa-qr-code": "Generate QR codes for Google Authenticator / TOTP", "scheb/2fa-totp": "Temporary one-time password (TOTP) support (Google Authenticator compatible)", "scheb/2fa-trusted-device": "Trusted devices support" }, @@ -5829,28 +7605,29 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-bundle/tree/v5.13.2" + "source": "https://github.com/scheb/2fa-bundle/tree/v6.13.1" }, - "time": "2022-04-16T10:18:34+00:00" + "time": "2024-11-29T19:29:49+00:00" }, { "name": "scheb/2fa-google-authenticator", - "version": "v5.13.2", + "version": "v6.13.1", "source": { "type": "git", "url": "https://github.com/scheb/2fa-google-authenticator.git", - "reference": "9477bfc47b5927fb165022dd75700aefdd45a8cc" + "reference": "2c960a5cb32edb4c37f719f10180df378a44fd6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/9477bfc47b5927fb165022dd75700aefdd45a8cc", - "reference": "9477bfc47b5927fb165022dd75700aefdd45a8cc", + "url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/2c960a5cb32edb4c37f719f10180df378a44fd6f", + "reference": "2c960a5cb32edb4c37f719f10180df378a44fd6f", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^2.2", + "paragonie/constant_time_encoding": "^2.4", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "scheb/2fa-bundle": "self.version", - "spomky-labs/otphp": "^9.1|^10.0" + "spomky-labs/otphp": "^10.0 || ^11.0" }, "type": "library", "autoload": { @@ -5879,26 +7656,28 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-google-authenticator/tree/v5.13.2" + "source": "https://github.com/scheb/2fa-google-authenticator/tree/v6.13.1" }, - "time": "2022-01-03T10:21:24+00:00" + "time": "2024-11-29T19:22:48+00:00" }, { "name": "scheb/2fa-trusted-device", - "version": "v5.13.2", + "version": "v6.13.1", "source": { "type": "git", "url": "https://github.com/scheb/2fa-trusted-device.git", - "reference": "acf5a1526eb2111fb7a82b9b52eb34b1ddfdc526" + "reference": "38e690325232a4037ff4aec8de926c938906942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scheb/2fa-trusted-device/zipball/acf5a1526eb2111fb7a82b9b52eb34b1ddfdc526", - "reference": "acf5a1526eb2111fb7a82b9b52eb34b1ddfdc526", + "url": "https://api.github.com/repos/scheb/2fa-trusted-device/zipball/38e690325232a4037ff4aec8de926c938906942c", + "reference": "38e690325232a4037ff4aec8de926c938906942c", "shasum": "" }, "require": { - "lcobucci/jwt": "^3.4|^4.0", + "lcobucci/clock": "^2.0 || ^3.0", + "lcobucci/jwt": "^4.1 || ^5.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "scheb/2fa-bundle": "self.version" }, "type": "library", @@ -5928,114 +7707,36 @@ "two-step" ], "support": { - "source": "https://github.com/scheb/2fa-trusted-device/tree/v5.13.2" + "source": "https://github.com/scheb/2fa-trusted-device/tree/v6.13.1" }, - "time": "2022-01-03T10:21:24+00:00" - }, - { - "name": "sensio/framework-extra-bundle", - "version": "v6.2.10", - "source": { - "type": "git", - "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", - "reference": "2f886f4b31f23c76496901acaedfedb6936ba61f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/2f886f4b31f23c76496901acaedfedb6936ba61f", - "reference": "2f886f4b31f23c76496901acaedfedb6936ba61f", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.0|^2.0", - "php": ">=7.2.5", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/framework-bundle": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0" - }, - "conflict": { - "doctrine/doctrine-cache-bundle": "<1.3.1", - "doctrine/persistence": "<1.3" - }, - "require-dev": { - "doctrine/dbal": "^2.10|^3.0", - "doctrine/doctrine-bundle": "^1.11|^2.0", - "doctrine/orm": "^2.5", - "symfony/browser-kit": "^4.4|^5.0|^6.0", - "symfony/doctrine-bridge": "^4.4|^5.0|^6.0", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/monolog-bridge": "^4.0|^5.0|^6.0", - "symfony/monolog-bundle": "^3.2", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", - "symfony/security-bundle": "^4.4|^5.0|^6.0", - "symfony/twig-bundle": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0", - "twig/twig": "^1.34|^2.4|^3.0" - }, - "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-master": "6.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Sensio\\Bundle\\FrameworkExtraBundle\\": "src/" - }, - "exclude-from-classmap": [ - "/tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "This bundle provides a way to configure your controllers with annotations", - "keywords": [ - "annotations", - "controllers" - ], - "support": { - "source": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/tree/v6.2.10" - }, - "abandoned": "Symfony", - "time": "2023-02-24T14:57:12+00:00" + "time": "2024-11-29T19:22:48+00:00" }, { "name": "shivas/versioning-bundle", - "version": "4.0.2", + "version": "4.1.1", "source": { "type": "git", "url": "https://github.com/shivas/versioning-bundle.git", - "reference": "427969bce2f139e1f7234d14efcd8cd0083d896c" + "reference": "fd89e3501ff1b0d3e6abe61eb7a878d1d4746868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shivas/versioning-bundle/zipball/427969bce2f139e1f7234d14efcd8cd0083d896c", - "reference": "427969bce2f139e1f7234d14efcd8cd0083d896c", + "url": "https://api.github.com/repos/shivas/versioning-bundle/zipball/fd89e3501ff1b0d3e6abe61eb7a878d1d4746868", + "reference": "fd89e3501ff1b0d3e6abe61eb7a878d1d4746868", "shasum": "" }, "require": { "nikolaposa/version": "^4", - "php": "^7.2 || ^8", - "symfony/console": "^3.4 || ^4 || ^5 || ^6", - "symfony/framework-bundle": "^3.4 || ^4 || ^5 || ^6", - "symfony/process": "^3.4 || ^4 || ^5 || ^6" + "php": "^7.2.5 || ^8", + "symfony/console": "^5.4 || ^6 || ^7", + "symfony/framework-bundle": "^5.4 || ^6 || ^7", + "symfony/process": "^5.4 || ^6 || ^7" }, "require-dev": { "mikey179/vfsstream": "^2", - "nyholm/symfony-bundle-test": "1.x-dev", + "nyholm/symfony-bundle-test": "^3.0", "phpunit/phpunit": "^8.5.27", - "symfony/phpunit-bridge": "^5 || ^6", + "symfony/phpunit-bridge": "^5.4 || ^6 || ^7", "twig/twig": "^2 || ^3" }, "type": "symfony-bundle", @@ -6065,31 +7766,31 @@ ], "support": { "issues": "https://github.com/shivas/versioning-bundle/issues", - "source": "https://github.com/shivas/versioning-bundle/tree/4.0.2", + "source": "https://github.com/shivas/versioning-bundle/tree/4.1.1", "wiki": "https://github.com/shivas/versioning-bundle/wiki" }, - "time": "2022-07-10T17:39:01+00:00" + "time": "2024-08-14T19:33:15+00:00" }, { "name": "spatie/db-dumper", - "version": "2.21.1", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/spatie/db-dumper.git", - "reference": "05e5955fb882008a8947c5a45146d86cfafa10d1" + "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/db-dumper/zipball/05e5955fb882008a8947c5a45146d86cfafa10d1", - "reference": "05e5955fb882008a8947c5a45146d86cfafa10d1", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/91e1fd4dc000aefc9753cda2da37069fc996baee", + "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "symfony/process": "^4.2|^5.0" + "php": "^8.0", + "symfony/process": "^5.0|^6.0|^7.0" }, "require-dev": { - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "pestphp/pest": "^1.22" }, "type": "library", "autoload": { @@ -6119,183 +7820,56 @@ "spatie" ], "support": { - "issues": "https://github.com/spatie/db-dumper/issues", - "source": "https://github.com/spatie/db-dumper/tree/2.21.1" + "source": "https://github.com/spatie/db-dumper/tree/3.8.0" }, "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, { "url": "https://github.com/spatie", "type": "github" } ], - "time": "2021-02-24T14:56:42+00:00" - }, - { - "name": "spomky-labs/base64url", - "version": "v2.0.4", - "source": { - "type": "git", - "url": "https://github.com/Spomky-Labs/base64url.git", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.11|^0.12", - "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", - "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", - "phpstan/phpstan-phpunit": "^0.11|^0.12", - "phpstan/phpstan-strict-rules": "^0.11|^0.12" - }, - "type": "library", - "autoload": { - "psr-4": { - "Base64Url\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky-Labs/base64url/contributors" - } - ], - "description": "Base 64 URL Safe Encoding/Decoding PHP Library", - "homepage": "https://github.com/Spomky-Labs/base64url", - "keywords": [ - "base64", - "rfc4648", - "safe", - "url" - ], - "support": { - "issues": "https://github.com/Spomky-Labs/base64url/issues", - "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" - }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - }, - { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" - } - ], - "time": "2020-11-03T09:10:25+00:00" - }, - { - "name": "spomky-labs/cbor-bundle", - "version": "v2.0.3", - "source": { - "type": "git", - "url": "https://github.com/Spomky-Labs/cbor-bundle.git", - "reference": "65a5a65e7fc20eca383a0be8f3ed287a4fe80b1f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/cbor-bundle/zipball/65a5a65e7fc20eca383a0be8f3ed287a4fe80b1f", - "reference": "65a5a65e7fc20eca383a0be8f3ed287a4fe80b1f", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "spomky-labs/cbor-php": "^1.0|^2.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/http-kernel": "^4.4|^5.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^9.0", - "symfony/framework-bundle": "^4.4|^5.0", - "symfony/phpunit-bridge": "^4.4|^5.0", - "thecodingmachine/phpstan-safe-rule": "^1.0@beta" - }, - "type": "symfony-bundle", - "autoload": { - "psr-4": { - "SpomkyLabs\\CborBundle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/spomky-labs/cbor-bundle/contributors" - } - ], - "description": "CBOR Encoder/Decoder Bundle for Symfony.", - "homepage": "https://github.com/spomky-labs", - "keywords": [ - "Concise Binary Object Representation", - "RFC7049", - "bundle", - "cbor", - "symfony" - ], - "support": { - "issues": "https://github.com/Spomky-Labs/cbor-bundle/issues", - "source": "https://github.com/Spomky-Labs/cbor-bundle/tree/v2.0.3" - }, - "time": "2020-07-12T22:47:45+00:00" + "time": "2025-02-14T15:04:22+00:00" }, { "name": "spomky-labs/cbor-php", - "version": "v2.1.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/cbor-php.git", - "reference": "28e2712cfc0b48fae661a48ffc6896d7abe83684" + "reference": "499d9bff0a6d59c4f1b813cc617fc3fd56d6dca4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/28e2712cfc0b48fae661a48ffc6896d7abe83684", - "reference": "28e2712cfc0b48fae661a48ffc6896d7abe83684", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/499d9bff0a6d59c4f1b813cc617fc3fd56d6dca4", + "reference": "499d9bff0a6d59c4f1b813cc617fc3fd56d6dca4", "shasum": "" }, "require": { - "brick/math": "^0.8.15|^0.9.0", + "brick/math": "^0.9|^0.10|^0.11|^0.12", "ext-mbstring": "*", - "php": ">=7.3" + "php": ">=8.0" }, "require-dev": { "ekino/phpstan-banned-code": "^1.0", "ext-json": "*", - "infection/infection": "^0.18|^0.25", + "infection/infection": "^0.29", + "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-beberlei-assert": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.12", + "phpunit/phpunit": "^10.1|^11.0", + "qossmic/deptrac": "^2.0", + "rector/rector": "^1.0", "roave/security-advisories": "dev-latest", - "symplify/easy-coding-standard": "^10.0" + "symfony/var-dumper": "^6.0|^7.0", + "symplify/easy-coding-standard": "^12.0" }, "suggest": { "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", @@ -6329,7 +7903,7 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/cbor-php/issues", - "source": "https://github.com/Spomky-Labs/cbor-php/tree/v2.1.0" + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.1.0" }, "funding": [ { @@ -6341,47 +7915,44 @@ "type": "patreon" } ], - "time": "2021-12-13T12:46:26+00:00" + "time": "2024-07-18T08:37:03+00:00" }, { "name": "spomky-labs/otphp", - "version": "v10.0.3", + "version": "11.3.0", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/otphp.git", - "reference": "9784d9f7c790eed26e102d6c78f12c754036c366" + "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/9784d9f7c790eed26e102d6c78f12c754036c366", - "reference": "9784d9f7c790eed26e102d6c78f12c754036c366", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", + "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", "shasum": "" }, "require": { - "beberlei/assert": "^3.0", "ext-mbstring": "*", - "paragonie/constant_time_encoding": "^2.0", - "php": "^7.2|^8.0", - "thecodingmachine/safe": "^0.1.14|^1.0|^2.0" + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^8.0", - "thecodingmachine/phpstan-safe-rule": "^1.0 || ^2.0" + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.26|^0.27|^0.28|^0.29", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5.26|^10.0|^11.0", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^1.0", + "symfony/phpunit-bridge": "^6.1|^7.0", + "symplify/easy-coding-standard": "^12.0" }, "type": "library", - "extra": { - "branch-alias": { - "v10.0": "10.0.x-dev", - "v9.0": "9.0.x-dev", - "v8.3": "8.3.x-dev" - } - }, "autoload": { "psr-4": { "OTPHP\\": "src/" @@ -6414,9 +7985,128 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/otphp/issues", - "source": "https://github.com/Spomky-Labs/otphp/tree/v10.0.3" + "source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0" }, - "time": "2022-03-17T08:00:35+00:00" + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-06-12T11:22:32+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "5ff1dcc21e961b60149a80e77f744fc047800b31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/5ff1dcc21e961b60149a80e77f744fc047800b31", + "reference": "5ff1dcc21e961b60149a80e77f744fc047800b31", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12|^0.13", + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0|^2.0|^3.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28|^0.29", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3|^2.0", + "phpstan/phpstan": "^1.8|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.1|^2.0", + "phpstan/phpstan-strict-rules": "^1.3|^2.0", + "phpunit/phpunit": "^10.1|^11.0|^12.0", + "rector/rector": "^1.0|^2.0", + "roave/security-advisories": "dev-latest", + "symfony/string": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + }, + { + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2025-04-25T15:57:13+00:00" }, { "name": "symfony/apache-pack", @@ -6446,33 +8136,28 @@ }, { "name": "symfony/asset", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/asset.git", - "reference": "1504b6773c6b90118f9871e90a67833b5d1dca3c" + "reference": "2466c17d61d14539cddf77e57ebb9cc971185302" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset/zipball/1504b6773c6b90118f9871e90a67833b5d1dca3c", - "reference": "1504b6773c6b90118f9871e90a67833b5d1dca3c", + "url": "https://api.github.com/repos/symfony/asset/zipball/2466c17d61d14539cddf77e57ebb9cc971185302", + "reference": "2466c17d61d14539cddf77e57ebb9cc971185302", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, "conflict": { - "symfony/http-foundation": "<5.3" + "symfony/http-foundation": "<5.4" }, "require-dev": { - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^5.3|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/http-foundation": "" + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -6500,7 +8185,7 @@ "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/asset/tree/v5.4.21" + "source": "https://github.com/symfony/asset/tree/v6.4.13" }, "funding": [ { @@ -6516,62 +8201,61 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-10-25T15:07:50+00:00" }, { "name": "symfony/cache", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "5ed986c4ef65f0dea5e9753630b5cb1f07f847d6" + "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/5ed986c4ef65f0dea5e9753630b5cb1f07f847d6", - "reference": "5ed986c4ef65f0dea5e9753630b5cb1f07f847d6", + "url": "https://api.github.com/repos/symfony/cache/zipball/d1abcf763a7414f2e572f676f22da7a06c8cd9ee", + "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0", + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", - "symfony/cache-contracts": "^1.1.7|^2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/var-exporter": "^4.4|^5.0|^6.0" + "symfony/cache-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.3.6|^7.0" }, "conflict": { "doctrine/dbal": "<2.13.1", - "symfony/dependency-injection": "<4.4", - "symfony/http-kernel": "<4.4", - "symfony/var-dumper": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" }, "provide": { - "psr/cache-implementation": "1.0|2.0", - "psr/simple-cache-implementation": "1.0|2.0", - "symfony/cache-implementation": "1.0|2.0" + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/cache": "^1.6|^2.0", - "doctrine/dbal": "^2.13.1|^3.0", - "predis/predis": "^1.1", - "psr/simple-cache": "^1.0|^2.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { "Symfony\\Component\\Cache\\": "" }, + "classmap": [ + "Traits/ValueWrapper.php" + ], "exclude-from-classmap": [ "/Tests/" ] @@ -6597,7 +8281,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v5.4.22" + "source": "https://github.com/symfony/cache/tree/v6.4.21" }, "funding": [ { @@ -6613,37 +8297,34 @@ "type": "tidelift" } ], - "time": "2023-03-29T20:01:08+00:00" + "time": "2025-04-08T08:21:20+00:00" }, { "name": "symfony/cache-contracts", - "version": "v2.5.2", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc" + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", - "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0|^3.0" - }, - "suggest": { - "symfony/cache-implementation": "" + "php": ">=8.1", + "psr/cache": "^3.0" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -6676,7 +8357,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" }, "funding": [ { @@ -6692,42 +8373,112 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/config", - "version": "v5.4.21", + "name": "symfony/clock", + "version": "v6.4.13", "source": { "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "2a6b1111d038adfa15d52c0871e540f3b352d1e4" + "url": "https://github.com/symfony/clock.git", + "reference": "b2bf55c4dd115003309eafa87ee7df9ed3dde81b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/2a6b1111d038adfa15d52c0871e540f3b352d1e4", - "reference": "2a6b1111d038adfa15d52c0871e540f3b352d1e4", + "url": "https://api.github.com/repos/symfony/clock/zipball/b2bf55c4dd115003309eafa87ee7df9ed3dde81b", + "reference": "b2bf55c4dd115003309eafa87ee7df9ed3dde81b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22" + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/config", + "version": "v6.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/4e55e7e4ffddd343671ea972216d4509f46c22ef", + "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<4.4" + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -6755,7 +8506,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v5.4.21" + "source": "https://github.com/symfony/config/tree/v6.4.14" }, "funding": [ { @@ -6771,56 +8522,51 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-11-04T11:33:53+00:00" }, { "name": "symfony/console", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8" + "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3cd51fd2e6c461ca678f84d419461281bd87a0a8", - "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8", + "url": "https://api.github.com/repos/symfony/console/zipball/a3011c7b7adb58d89f6c0d822abb641d7a5f9719", + "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" }, "provide": { - "psr/log-implementation": "1.0|2.0" + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -6854,7 +8600,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.22" + "source": "https://github.com/symfony/console/tree/v6.4.21" }, "funding": [ { @@ -6870,25 +8616,24 @@ "type": "tidelift" } ], - "time": "2023-03-25T09:27:28+00:00" + "time": "2025-04-07T15:42:41+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "95f3c7468db1da8cc360b24fa2a26e7cefcb355d" + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/95f3c7468db1da8cc360b24fa2a26e7cefcb355d", - "reference": "95f3c7468db1da8cc360b24fa2a26e7cefcb355d", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, "type": "library", "autoload": { @@ -6920,7 +8665,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.21" + "source": "https://github.com/symfony/css-selector/tree/v6.4.13" }, "funding": [ { @@ -6936,52 +8681,44 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.4.22", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "e1b7c1432efb4ad1dd89d62906187271e2601ed9" + "reference": "c49796a9184a532843e78e50df9e55708b92543a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e1b7c1432efb4ad1dd89d62906187271e2601ed9", - "reference": "e1b7c1432efb4ad1dd89d62906187271e2601ed9", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c49796a9184a532843e78e50df9e55708b92543a", + "reference": "c49796a9184a532843e78e50df9e55708b92543a", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1.1", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22", - "symfony/service-contracts": "^1.1.6|^2" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", - "symfony/config": "<5.3", - "symfony/finder": "<4.4", - "symfony/proxy-manager-bridge": "<4.4", - "symfony/yaml": "<4.4.26" + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.3", + "symfony/yaml": "<5.4" }, "provide": { - "psr/container-implementation": "1.0", - "symfony/service-implementation": "1.0|2.0" + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^5.3|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4.26|^5.0|^6.0" - }, - "suggest": { - "symfony/config": "", - "symfony/expression-language": "For using expressions in service container configuration", - "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", - "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", - "symfony/yaml": "" + "symfony/config": "^6.1|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7009,7 +8746,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.22" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.20" }, "funding": [ { @@ -7025,33 +8762,33 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:02:45+00:00" + "time": "2025-03-13T09:55:08+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.2", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -7076,7 +8813,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -7092,80 +8829,71 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "decc4801e2ecc4b72334ef472ed82e4d640492ec" + "reference": "fcce66ede41ca56100b91fd4a00131ba6cf89aba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/decc4801e2ecc4b72334ef472ed82e4d640492ec", - "reference": "decc4801e2ecc4b72334ef472ed82e4d640492ec", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/fcce66ede41ca56100b91fd4a00131ba6cf89aba", + "reference": "fcce66ede41ca56100b91fd4a00131ba6cf89aba", "shasum": "" }, "require": { - "doctrine/event-manager": "~1.0", - "doctrine/persistence": "^2|^3", - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "doctrine/event-manager": "^1.2|^2", + "doctrine/persistence": "^2.5|^3.1|^4", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3" + "symfony/service-contracts": "^2.5|^3" }, "conflict": { "doctrine/dbal": "<2.13.1", "doctrine/lexer": "<1.1", - "doctrine/orm": "<2.7.4", - "phpunit/phpunit": "<5.4.3", + "doctrine/orm": "<2.15", "symfony/cache": "<5.4", - "symfony/dependency-injection": "<4.4", - "symfony/form": "<5.4.21|>=6,<6.2.7", - "symfony/http-kernel": "<5", - "symfony/messenger": "<4.4", - "symfony/property-info": "<5", - "symfony/proxy-manager-bridge": "<4.4.19", - "symfony/security-bundle": "<5", - "symfony/security-core": "<5.3", - "symfony/validator": "<5.2" + "symfony/dependency-injection": "<6.2", + "symfony/form": "<5.4.38|>=6,<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.3", + "symfony/http-kernel": "<6.2", + "symfony/lock": "<6.3", + "symfony/messenger": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-bundle": "<5.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<6.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4|^2", "doctrine/collections": "^1.0|^2.0", - "doctrine/data-fixtures": "^1.1", - "doctrine/dbal": "^2.13.1|^3.0", - "doctrine/orm": "^2.7.4", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/orm": "^2.15|^3", "psr/log": "^1|^2|^3", - "symfony/cache": "^5.4|^6.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/doctrine-messenger": "^5.1|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/form": "^5.4.21|^6.2.7", - "symfony/http-kernel": "^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/property-access": "^4.4|^5.0|^6.0", - "symfony/property-info": "^5.0|^6.0", - "symfony/proxy-manager-bridge": "^4.4|^5.0|^6.0", - "symfony/security-core": "^5.3|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0", - "symfony/validator": "^5.2|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "doctrine/data-fixtures": "", - "doctrine/dbal": "", - "doctrine/orm": "", - "symfony/form": "", - "symfony/property-info": "", - "symfony/validator": "" + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.2|^7.0", + "symfony/doctrine-messenger": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4.38|^6.4.6|^7.0.6", + "symfony/http-kernel": "^6.3|^7.0", + "symfony/lock": "^6.3|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/proxy-manager-bridge": "^6.4", + "symfony/security-core": "^6.4|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "symfony-bridge", "autoload": { @@ -7193,7 +8921,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v5.4.22" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.21" }, "funding": [ { @@ -7209,29 +8937,99 @@ "type": "tidelift" } ], - "time": "2023-03-06T21:29:33+00:00" + "time": "2025-04-27T15:22:02+00:00" }, { - "name": "symfony/dotenv", - "version": "v5.4.22", + "name": "symfony/dom-crawler", + "version": "v6.4.19", "source": { "type": "git", - "url": "https://github.com/symfony/dotenv.git", - "reference": "77b7660bfcb85e8f28287d557d7af0046bcd2ca3" + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "19073e3e0bb50cbc1cb286077069b3107085206f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/77b7660bfcb85e8f28287d557d7af0046bcd2ca3", - "reference": "77b7660bfcb85e8f28287d557d7af0046bcd2ca3", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19073e3e0bb50cbc1cb286077069b3107085206f", + "reference": "19073e3e0bb50cbc1cb286077069b3107085206f", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3" + "masterminds/html5": "^2.6", + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0" + "symfony/css-selector": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T17:58:34+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v6.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/1ac5e7e7e862d4d574258daf08bd569ba926e4a5", + "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/process": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7264,7 +9062,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v5.4.22" + "source": "https://github.com/symfony/dotenv/tree/v6.4.16" }, "funding": [ { @@ -7280,31 +9078,35 @@ "type": "tidelift" } ], - "time": "2023-03-09T20:36:58+00:00" + "time": "2024-11-27T11:08:19+00:00" }, { "name": "symfony/error-handler", - "version": "v5.4.21", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "56a94aa8cb5a5fbc411551d8d014a296b5456549" + "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/56a94aa8cb5a5fbc411551d8d014a296b5456549", - "reference": "56a94aa8cb5a5fbc411551d8d014a296b5456549", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/aa3bcf4f7674719df078e61cc8062e5b7f752031", + "reference": "aa3bcf4f7674719df078e61cc8062e5b7f752031", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^4.4|^5.0|^6.0" + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/serializer": "^4.4|^5.0|^6.0" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -7335,7 +9137,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.4.21" + "source": "https://github.com/symfony/error-handler/tree/v6.4.20" }, "funding": [ { @@ -7351,48 +9153,43 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2025-03-01T13:00:38+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.22", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "1df20e45d56da29a4b1d8259dd6e950acbf1b13f" + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1df20e45d56da29a4b1d8259dd6e950acbf1b13f", - "reference": "1df20e45d56da29a4b1d8259dd6e950acbf1b13f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7420,7 +9217,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.22" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" }, "funding": [ { @@ -7436,37 +9233,34 @@ "type": "tidelift" } ], - "time": "2023-03-17T11:31:58+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.2", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { @@ -7499,7 +9293,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -7515,26 +9309,27 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/expression-language", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "501589522b844b8eecf012c133f0404f0eef77ac" + "reference": "3524904fb026356a5230cd197f9a4e6a61e0e7df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/501589522b844b8eecf012c133f0404f0eef77ac", - "reference": "501589522b844b8eecf012c133f0404f0eef77ac", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/3524904fb026356a5230cd197f9a4e6a61e0e7df", + "reference": "3524904fb026356a5230cd197f9a4e6a61e0e7df", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3" + "php": ">=8.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -7562,7 +9357,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v5.4.21" + "source": "https://github.com/symfony/expression-language/tree/v6.4.13" }, "funding": [ { @@ -7578,27 +9373,29 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-10-09T08:40:40+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e75960b1bbfd2b8c9e483e0d74811d555ca3de9f" + "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e75960b1bbfd2b8c9e483e0d74811d555ca3de9f", - "reference": "e75960b1bbfd2b8c9e483e0d74811d555ca3de9f", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", + "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" }, "type": "library", "autoload": { @@ -7626,7 +9423,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.21" + "source": "https://github.com/symfony/filesystem/tree/v6.4.13" }, "funding": [ { @@ -7642,26 +9439,27 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-10-25T15:07:50+00:00" }, { "name": "symfony/finder", - "version": "v5.4.21", + "version": "v6.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "078e9a5e1871fcfe6a5ce421b539344c21afef19" + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/078e9a5e1871fcfe6a5ce421b539344c21afef19", - "reference": "078e9a5e1871fcfe6a5ce421b539344c21afef19", + "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" }, "type": "library", "autoload": { @@ -7689,7 +9487,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.21" + "source": "https://github.com/symfony/finder/tree/v6.4.17" }, "funding": [ { @@ -7705,32 +9503,35 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:33:00+00:00" + "time": "2024-12-29T13:51:37+00:00" }, { "name": "symfony/flex", - "version": "v1.19.5", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "51077ed0f6dc2c94cd0b670167eee3747c31b2c1" + "reference": "62d5c38c7af6280d8605b725364680838b475641" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/51077ed0f6dc2c94cd0b670167eee3747c31b2c1", - "reference": "51077ed0f6dc2c94cd0b670167eee3747c31b2c1", + "url": "https://api.github.com/repos/symfony/flex/zipball/62d5c38c7af6280d8605b725364680838b475641", + "reference": "62d5c38c7af6280d8605b725364680838b475641", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0|^2.0", - "php": ">=7.1" + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "conflict": { + "composer/semver": "<1.7.2" }, "require-dev": { - "composer/composer": "^1.0.2|^2.0", - "symfony/dotenv": "^4.4|^5.0|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/phpunit-bridge": "^4.4.12|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0" + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" }, "type": "composer-plugin", "extra": { @@ -7754,7 +9555,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v1.19.5" + "source": "https://github.com/symfony/flex/tree/v2.5.1" }, "funding": [ { @@ -7770,66 +9571,60 @@ "type": "tidelift" } ], - "time": "2023-01-30T17:02:31+00:00" + "time": "2025-05-10T14:05:03+00:00" }, { "name": "symfony/form", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "e21648e71c579c9ccf5768e2484f79283c463535" + "reference": "44a0e253c16a3187299f07b8f80e23ecb000d360" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/e21648e71c579c9ccf5768e2484f79283c463535", - "reference": "e21648e71c579c9ccf5768e2484f79283c463535", + "url": "https://api.github.com/repos/symfony/form/zipball/44a0e253c16a3187299f07b8f80e23ecb000d360", + "reference": "44a0e253c16a3187299f07b8f80e23ecb000d360", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/options-resolver": "^5.1|^6.0", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/options-resolver": "^5.4|^6.0|^7.0", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-icu": "^1.21", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.23", - "symfony/property-access": "^5.0.8|^6.0", - "symfony/service-contracts": "^1.1|^2|^3" + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "phpunit/phpunit": "<5.4.3", - "symfony/console": "<4.4", - "symfony/dependency-injection": "<4.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", - "symfony/error-handler": "<4.4.5", - "symfony/framework-bundle": "<4.4", - "symfony/http-kernel": "<4.4", - "symfony/translation": "<4.4", - "symfony/translation-contracts": "<1.1.7", - "symfony/twig-bridge": "<5.4.21|>=6,<6.2.7" + "symfony/error-handler": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.3" }, "require-dev": { "doctrine/collections": "^1.0|^2.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", - "symfony/security-csrf": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0", - "symfony/validator": "^4.4.17|^5.1.9|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/security-csrf": "For protecting forms against CSRF attacks.", - "symfony/twig-bridge": "For templating with Twig.", - "symfony/validator": "For form validation." + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.2|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7857,7 +9652,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v5.4.22" + "source": "https://github.com/symfony/form/tree/v6.4.21" }, "funding": [ { @@ -7873,114 +9668,112 @@ "type": "tidelift" } ], - "time": "2023-03-06T21:29:33+00:00" + "time": "2025-04-27T15:22:02+00:00" }, { "name": "symfony/framework-bundle", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "6cb4f6aed4bd7fbf7b2ee74c231184a07f3d00c1" + "reference": "d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6cb4f6aed4bd7fbf7b2ee74c231184a07f3d00c1", - "reference": "6cb4f6aed4bd7fbf7b2ee74c231184a07f3d00c1", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a", + "reference": "d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a", "shasum": "" }, "require": { + "composer-runtime-api": ">=2.1", "ext-xml": "*", - "php": ">=7.2.5", - "symfony/cache": "^5.2|^6.0", - "symfony/config": "^5.3|^6.0", - "symfony/dependency-injection": "^5.4.5|^6.0.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/error-handler": "^4.4.1|^5.0.1|^6.0", - "symfony/event-dispatcher": "^5.1|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^5.3|^6.0", - "symfony/http-kernel": "^5.4|^6.0", + "php": ">=8.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/dependency-injection": "^6.4.12|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.1|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22", - "symfony/routing": "^5.3|^6.0" + "symfony/routing": "^6.4|^7.0" }, "conflict": { "doctrine/annotations": "<1.13.1", - "doctrine/cache": "<1.11", "doctrine/persistence": "<1.3", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "phpunit/phpunit": "<5.4.3", - "symfony/asset": "<5.3", - "symfony/console": "<5.2.5", - "symfony/dom-crawler": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/form": "<5.2", - "symfony/http-client": "<4.4", - "symfony/lock": "<4.4", - "symfony/mailer": "<5.2", - "symfony/messenger": "<5.4", - "symfony/mime": "<4.4", - "symfony/property-access": "<5.3", - "symfony/property-info": "<4.4", - "symfony/security-csrf": "<5.3", - "symfony/serializer": "<5.2", - "symfony/service-contracts": ">=3.0", - "symfony/stopwatch": "<4.4", - "symfony/translation": "<5.3", - "symfony/twig-bridge": "<4.4", - "symfony/twig-bundle": "<4.4", - "symfony/validator": "<5.2", - "symfony/web-profiler-bundle": "<4.4", - "symfony/workflow": "<5.2" + "symfony/asset": "<5.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.3", + "symfony/console": "<5.4|>=7.0", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<6.3", + "symfony/lock": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<6.3", + "symfony/mime": "<6.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4", + "symfony/runtime": "<5.4.45|>=6.0,<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<5.4", + "symfony/security-csrf": "<5.4", + "symfony/serializer": "<6.4", + "symfony/stopwatch": "<5.4", + "symfony/translation": "<6.4", + "symfony/twig-bridge": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/workflow": "<6.4" }, "require-dev": { "doctrine/annotations": "^1.13.1|^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/asset": "^5.3|^6.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/console": "^5.4.9|^6.0.9", - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/dom-crawler": "^4.4.30|^5.3.7|^6.0", - "symfony/dotenv": "^5.1|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/form": "^5.2|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/mailer": "^5.2|^6.0", - "symfony/messenger": "^5.4|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/notifier": "^5.4|^6.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/console": "^5.4.9|^6.0.9|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/dotenv": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-client": "^6.3|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/mailer": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.3|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/notifier": "^5.4|^6.0|^7.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/property-info": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0", - "symfony/security-bundle": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/string": "^5.0|^6.0", - "symfony/translation": "^5.3|^6.0", - "symfony/twig-bundle": "^4.4|^5.0|^6.0", - "symfony/validator": "^5.2|^6.0", - "symfony/web-link": "^4.4|^5.0|^6.0", - "symfony/workflow": "^5.2|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0", - "twig/twig": "^2.10|^3.0" - }, - "suggest": { - "ext-apcu": "For best performance of the system caches", - "symfony/console": "For using the console commands", - "symfony/form": "For using forms", - "symfony/property-info": "For using the property_info service", - "symfony/serializer": "For using the serializer service", - "symfony/validator": "For using validation", - "symfony/web-link": "For using web links, features such as preloading, prefetching or prerendering", - "symfony/yaml": "For using the debug:config and lint:yaml commands" + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0", + "symfony/scheduler": "^6.4.4|^7.0.4", + "symfony/security-bundle": "^5.4|^6.0|^7.0", + "symfony/semaphore": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/web-link": "^5.4|^6.0|^7.0", + "symfony/workflow": "^6.4|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0", + "twig/twig": "^2.10|^3.0.4" }, "type": "symfony-bundle", "autoload": { @@ -8008,7 +9801,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v5.4.22" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.21" }, "funding": [ { @@ -8024,50 +9817,53 @@ "type": "tidelift" } ], - "time": "2023-03-31T08:25:44+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/http-client", - "version": "v5.4.22", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4cd1b7e7ee846c8b22cb47cbc435344af9b2a8bf" + "reference": "3294a433fc9d12ae58128174896b5b1822c28dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4cd1b7e7ee846c8b22cb47cbc435344af9b2a8bf", - "reference": "4cd1b7e7ee846c8b22cb47cbc435344af9b2a8bf", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3294a433fc9d12ae58128174896b5b1822c28dad", + "reference": "3294a433fc9d12ae58128174896b5b1822c28dad", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-client-contracts": "^2.4", - "symfony/polyfill-php73": "^1.11", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.0|^2|^3" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.3" }, "provide": { "php-http/async-client-implementation": "*", "php-http/client-implementation": "*", "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "2.4" + "symfony/http-client-implementation": "3.0" }, "require-dev": { "amphp/amp": "^2.5", "amphp/http-client": "^4.2.1", "amphp/http-tunnel": "^1.0", "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4", + "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4.13|^5.1.5|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8098,7 +9894,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v5.4.22" + "source": "https://github.com/symfony/http-client/tree/v6.4.19" }, "funding": [ { @@ -8114,42 +9910,42 @@ "type": "tidelift" } ], - "time": "2023-03-24T15:16:26+00:00" + "time": "2025-02-13T09:55:13+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v2.5.2", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ba6a9f0e8f3edd190520ee3b9a958596b6ca2e70" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ba6a9f0e8f3edd190520ee3b9a958596b6ca2e70", - "reference": "ba6a9f0e8f3edd190520ee3b9a958596b6ca2e70", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/http-client-implementation": "" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\HttpClient\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8176,7 +9972,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -8192,39 +9988,40 @@ "type": "tidelift" } ], - "time": "2022-04-12T15:48:08+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "05cd1acdd0e3ce8473aaba1d86c188321d85f313" + "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/05cd1acdd0e3ce8473aaba1d86c188321d85f313", - "reference": "05cd1acdd0e3ce8473aaba1d86c188321d85f313", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3f0c7ea41db479383b81d436b836d37168fd5b99", + "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" }, "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8252,7 +10049,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.22" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.21" }, "funding": [ { @@ -8268,76 +10065,78 @@ "type": "tidelift" } ], - "time": "2023-03-28T07:28:17+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/http-kernel", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "2d3a8be2c756353627398827c409af6f126c096d" + "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2d3a8be2c756353627398827c409af6f126c096d", - "reference": "2d3a8be2c756353627398827c409af6f126c096d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/983ca05eec6623920d24ec0f1005f487d3734a0c", + "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^5.0|^6.0", - "symfony/http-foundation": "^5.4.21|^6.2.7", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.0", - "symfony/config": "<5.0", - "symfony/console": "<4.4", - "symfony/dependency-injection": "<5.3", - "symfony/doctrine-bridge": "<5.0", - "symfony/form": "<5.0", - "symfony/http-client": "<5.0", - "symfony/mailer": "<5.0", - "symfony/messenger": "<5.0", - "symfony/translation": "<5.0", - "symfony/twig-bridge": "<5.0", - "symfony/validator": "<5.0", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.3", "twig/twig": "<2.13" }, "provide": { - "psr/log-implementation": "1.0|2.0" + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/config": "^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.3|^6.0", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client-contracts": "^1.1|^2|^3", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/routing": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2|^3", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", "twig/twig": "^2.13|^3.0.4" }, - "suggest": { - "symfony/browser-kit": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "" - }, "type": "library", "autoload": { "psr-4": { @@ -8364,7 +10163,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.22" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.21" }, "funding": [ { @@ -8380,43 +10179,38 @@ "type": "tidelift" } ], - "time": "2023-03-31T11:54:37+00:00" + "time": "2025-05-02T08:46:38+00:00" }, { "name": "symfony/intl", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "8afe56b8472888d749ef8955acdc9d38578775d7" + "reference": "b248d227fa10fd6345efd4c1c74efaa1c1de6f76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/8afe56b8472888d749ef8955acdc9d38578775d7", - "reference": "8afe56b8472888d749ef8955acdc9d38578775d7", + "url": "https://api.github.com/repos/symfony/intl/zipball/b248d227fa10fd6345efd4c1c74efaa1c1de6f76", + "reference": "b248d227fa10fd6345efd4c1c74efaa1c1de6f76", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, "require-dev": { - "symfony/filesystem": "^4.4|^5.0|^6.0" + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { - "files": [ - "Resources/functions.php" - ], "psr-4": { "Symfony\\Component\\Intl\\": "" }, - "classmap": [ - "Resources/stubs" - ], "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/data/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -8441,7 +10235,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a PHP replacement layer for the C intl extension that includes additional data from the ICU library", + "description": "Provides access to the localization data of the ICU library", "homepage": "https://symfony.com", "keywords": [ "i18n", @@ -8452,7 +10246,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v5.4.22" + "source": "https://github.com/symfony/intl/tree/v6.4.21" }, "funding": [ { @@ -8468,118 +10262,43 @@ "type": "tidelift" } ], - "time": "2023-03-10T09:58:14+00:00" - }, - { - "name": "symfony/lock", - "version": "v5.4.22", - "source": { - "type": "git", - "url": "https://github.com/symfony/lock.git", - "reference": "cc0565235e16ef403097fbd30eba59690bee6b3c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/lock/zipball/cc0565235e16ef403097fbd30eba59690bee6b3c", - "reference": "cc0565235e16ef403097fbd30eba59690bee6b3c", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "doctrine/dbal": "<2.13" - }, - "require-dev": { - "doctrine/dbal": "^2.13|^3.0", - "predis/predis": "~1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Lock\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jérémy Derussé", - "email": "jeremy@derusse.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Creates and manages locks, a mechanism to provide exclusive access to a shared resource", - "homepage": "https://symfony.com", - "keywords": [ - "cas", - "flock", - "locking", - "mutex", - "redlock", - "semaphore" - ], - "support": { - "source": "https://github.com/symfony/lock/tree/v5.4.22" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-03-10T16:52:09+00:00" + "time": "2025-04-07T19:02:30+00:00" }, { "name": "symfony/mailer", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b" + "reference": "ada2809ccd4ec27aba9fc344e3efdaec624c6438" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/6330cd465dfd8b7a07515757a1c37069075f7b0b", - "reference": "6330cd465dfd8b7a07515757a1c37069075f7b0b", + "url": "https://api.github.com/repos/symfony/mailer/zipball/ada2809ccd4ec27aba9fc344e3efdaec624c6438", + "reference": "ada2809ccd4ec27aba9fc344e3efdaec624c6438", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/mime": "^5.2.6|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3" + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/http-kernel": "<4.4" + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" }, "require-dev": { - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" }, "type": "library", "autoload": { @@ -8607,7 +10326,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v5.4.22" + "source": "https://github.com/symfony/mailer/tree/v6.4.21" }, "funding": [ { @@ -8623,43 +10342,44 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:15:32+00:00" + "time": "2025-04-26T23:47:35+00:00" }, { "name": "symfony/mime", - "version": "v5.4.21", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "ef57d9fb9cdd5e6b2ffc567d109865d10b6920cd" + "reference": "fec8aa5231f3904754955fad33c2db50594d22d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/ef57d9fb9cdd5e6b2ffc567d109865d10b6920cd", - "reference": "ef57d9fb9cdd5e6b2ffc567d109865d10b6920cd", + "url": "https://api.github.com/repos/symfony/mime/zipball/fec8aa5231f3904754955fad33c2db50594d22d1", + "reference": "fec8aa5231f3904754955fad33c2db50594d22d1", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16" + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<4.4", - "symfony/serializer": "<5.4.14|>=6.0,<6.0.14|>=6.1,<6.1.6" + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/property-access": "^4.4|^5.1|^6.0", - "symfony/property-info": "^4.4|^5.1|^6.0", - "symfony/serializer": "^5.4.14|~6.0.14|^6.1.6" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, "type": "library", "autoload": { @@ -8691,7 +10411,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.21" + "source": "https://github.com/symfony/mime/tree/v6.4.21" }, "funding": [ { @@ -8707,47 +10427,42 @@ "type": "tidelift" } ], - "time": "2023-02-21T19:46:44+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v5.4.22", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "34be6f0695e4187dbb832a05905fb4c6581ac39a" + "reference": "9d14621e59f22c2b6d030d92d37ffe5ae1e60452" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/34be6f0695e4187dbb832a05905fb4c6581ac39a", - "reference": "34be6f0695e4187dbb832a05905fb4c6581ac39a", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/9d14621e59f22c2b6d030d92d37ffe5ae1e60452", + "reference": "9d14621e59f22c2b6d030d92d37ffe5ae1e60452", "shasum": "" }, "require": { - "monolog/monolog": "^1.25.1|^2", - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-kernel": "^5.3|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3" + "monolog/monolog": "^1.25.1|^2|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/console": "<4.4", - "symfony/http-foundation": "<5.3" + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<5.4" }, "require-dev": { - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/mailer": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/security-core": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.", - "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", - "symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler." + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/mailer": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "symfony-bridge", "autoload": { @@ -8775,7 +10490,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v5.4.22" + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.13" }, "funding": [ { @@ -8791,34 +10506,34 @@ "type": "tidelift" } ], - "time": "2023-03-06T21:29:33+00:00" + "time": "2024-10-14T08:49:08+00:00" }, { "name": "symfony/monolog-bundle", - "version": "v3.8.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bundle.git", - "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d" + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", - "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", "shasum": "" }, "require": { - "monolog/monolog": "^1.22 || ^2.0 || ^3.0", - "php": ">=7.1.3", - "symfony/config": "~4.4 || ^5.0 || ^6.0", - "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", - "symfony/http-kernel": "~4.4 || ^5.0 || ^6.0", - "symfony/monolog-bridge": "~4.4 || ^5.0 || ^6.0" + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "symfony/console": "~4.4 || ^5.0 || ^6.0", - "symfony/phpunit-bridge": "^5.2 || ^6.0", - "symfony/yaml": "~4.4 || ^5.0 || ^6.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "type": "symfony-bundle", "extra": { @@ -8856,7 +10571,7 @@ ], "support": { "issues": "https://github.com/symfony/monolog-bundle/issues", - "source": "https://github.com/symfony/monolog-bundle/tree/v3.8.0" + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" }, "funding": [ { @@ -8872,27 +10587,25 @@ "type": "tidelift" } ], - "time": "2022-05-10T14:24:36+00:00" + "time": "2023-11-06T17:08:13+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.4.21", + "version": "v6.4.16", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "4fe5cf6ede71096839f0e4b4444d65dd3a7c1eb9" + "reference": "368128ad168f20e22c32159b9f761e456cec0c78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4fe5cf6ede71096839f0e4b4444d65dd3a7c1eb9", - "reference": "4fe5cf6ede71096839f0e4b4444d65dd3a7c1eb9", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/368128ad168f20e22c32159b9f761e456cec0c78", + "reference": "368128ad168f20e22c32159b9f761e456cec0c78", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -8925,7 +10638,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.4.21" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.16" }, "funding": [ { @@ -8941,32 +10654,31 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-11-20T10:57:02+00:00" }, { "name": "symfony/password-hasher", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/password-hasher.git", - "reference": "7ce4529b2b2ea7de3b6f344a1a41f58201999180" + "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/password-hasher/zipball/7ce4529b2b2ea7de3b6f344a1a41f58201999180", - "reference": "7ce4529b2b2ea7de3b6f344a1a41f58201999180", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/e97a1b31f60b8bdfc1fdedab4398538da9441d47", + "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.1" }, "conflict": { - "symfony/security-core": "<5.3" + "symfony/security-core": "<5.4" }, "require-dev": { - "symfony/console": "^5.3|^6.0", - "symfony/security-core": "^5.3|^6.0" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -8998,7 +10710,7 @@ "password" ], "support": { - "source": "https://github.com/symfony/password-hasher/tree/v5.4.21" + "source": "https://github.com/symfony/password-hasher/tree/v6.4.13" }, "funding": [ { @@ -9014,24 +10726,24 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -9041,12 +10753,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9080,7 +10789,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -9096,36 +10805,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9161,7 +10867,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -9177,36 +10883,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "a3d9148e2c363588e05abbdd4ee4f971f0a5330c" + "reference": "763d2a91fea5681509ca01acbc1c5e450d127811" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/a3d9148e2c363588e05abbdd4ee4f971f0a5330c", - "reference": "a3d9148e2c363588e05abbdd4ee4f971f0a5330c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/763d2a91fea5681509ca01acbc1c5e450d127811", + "reference": "763d2a91fea5681509ca01acbc1c5e450d127811", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance and support of other locales than \"en\"" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9248,7 +10951,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.32.0" }, "funding": [ { @@ -9264,38 +10967,34 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-12-21T18:38:29+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", - "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9335,7 +11034,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" }, "funding": [ { @@ -9351,36 +11050,33 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9419,7 +11115,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -9435,24 +11131,25 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-iconv": "*", + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -9462,12 +11159,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9502,7 +11196,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -9518,33 +11212,30 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.27.0", + "name": "symfony/polyfill-php82", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9552,83 +11243,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php82\\": "" }, "classmap": [ "Resources/stubs" @@ -9648,7 +11263,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -9657,7 +11272,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.32.0" }, "funding": [ { @@ -9673,33 +11288,30 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "name": "symfony/polyfill-php83", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -9707,90 +11319,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-php81", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", - "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -9810,7 +11339,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -9819,7 +11348,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -9835,25 +11364,179 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-20T12:04:08+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v5.4.22", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4b850da0cc3a2a9181c1ed407adbca4733dc839b" + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4b850da0cc3a2a9181c1ed407adbca4733dc839b", - "reference": "4b850da0cc3a2a9181c1ed407adbca4733dc839b", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1" }, "type": "library", "autoload": { @@ -9881,7 +11564,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.22" + "source": "https://github.com/symfony/process/tree/v6.4.20" }, "funding": [ { @@ -9897,33 +11580,29 @@ "type": "tidelift" } ], - "time": "2023-03-06T21:29:33+00:00" + "time": "2025-03-10T17:11:00+00:00" }, { "name": "symfony/property-access", - "version": "v5.4.22", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "ffee082889586b5718347b291e04071f4d07b38f" + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/ffee082889586b5718347b291e04071f4d07b38f", - "reference": "ffee082889586b5718347b291e04071f4d07b38f", + "url": "https://api.github.com/repos/symfony/property-access/zipball/80e0378f2f058b60d87dedc3c760caec882e992c", + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16", - "symfony/property-info": "^5.2|^6.0" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/property-info": "^5.4|^6.0|^7.0" }, "require-dev": { - "symfony/cache": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/cache-implementation": "To cache access methods." + "symfony/cache": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -9962,7 +11641,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v5.4.22" + "source": "https://github.com/symfony/property-access/tree/v6.4.18" }, "funding": [ { @@ -9978,46 +11657,41 @@ "type": "tidelift" } ], - "time": "2023-03-14T14:59:20+00:00" + "time": "2024-12-16T14:42:05+00:00" }, { "name": "symfony/property-info", - "version": "v5.4.22", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "8454a441d117449abd0d61124e68f93f927ad5ba" + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/8454a441d117449abd0d61124e68f93f927ad5ba", - "reference": "8454a441d117449abd0d61124e68f93f927ad5ba", + "url": "https://api.github.com/repos/symfony/property-info/zipball/94d18e5cc11a37fd92856d38b61d9cdf72536a1e", + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16", - "symfony/string": "^5.1|^6.0" + "php": ">=8.1", + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/dependency-injection": "<4.4" + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/serializer": "<5.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4|^2", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "phpstan/phpdoc-parser": "^1.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/serializer": "^4.4|^5.0|^6.0" - }, - "suggest": { - "phpdocumentor/reflection-docblock": "To use the PHPDoc", - "psr/cache-implementation": "To cache results", - "symfony/doctrine-bridge": "To use Doctrine metadata", - "symfony/serializer": "To use Serializer metadata" + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.4|^7.0" }, "type": "library", "autoload": { @@ -10053,7 +11727,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v5.4.22" + "source": "https://github.com/symfony/property-info/tree/v6.4.18" }, "funding": [ { @@ -10069,113 +11743,42 @@ "type": "tidelift" } ], - "time": "2023-03-14T14:59:20+00:00" - }, - { - "name": "symfony/proxy-manager-bridge", - "version": "v5.4.21", - "source": { - "type": "git", - "url": "https://github.com/symfony/proxy-manager-bridge.git", - "reference": "a4cf96f3acfa252503a216bea877478f9621c7c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/proxy-manager-bridge/zipball/a4cf96f3acfa252503a216bea877478f9621c7c0", - "reference": "a4cf96f3acfa252503a216bea877478f9621c7c0", - "shasum": "" - }, - "require": { - "friendsofphp/proxy-manager-lts": "^1.0.2", - "php": ">=7.2.5", - "symfony/dependency-injection": "^5.0|^6.0", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "symfony/config": "^4.4|^5.0|^6.0" - }, - "type": "symfony-bridge", - "autoload": { - "psr-4": { - "Symfony\\Bridge\\ProxyManager\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides integration for ProxyManager with various Symfony components", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/proxy-manager-bridge/tree/v5.4.21" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-02-16T09:33:00+00:00" + "time": "2025-01-21T10:52:27+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v2.1.4", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "a125b93ef378c492e274f217874906fb9babdebb" + "reference": "c9cf83326a1074f83a738fc5320945abf7fb7fec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/a125b93ef378c492e274f217874906fb9babdebb", - "reference": "a125b93ef378c492e274f217874906fb9babdebb", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/c9cf83326a1074f83a738fc5320945abf7fb7fec", + "reference": "c9cf83326a1074f83a738fc5320945abf7fb7fec", "shasum": "" }, "require": { - "php": ">=7.1", - "psr/http-message": "^1.0", - "symfony/http-foundation": "^4.4 || ^5.0 || ^6.0" + "php": ">=8.1", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.2" }, "require-dev": { "nyholm/psr7": "^1.1", - "psr/log": "^1.1 || ^2 || ^3", - "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0", - "symfony/config": "^4.4 || ^5.0 || ^6.0", - "symfony/event-dispatcher": "^4.4 || ^5.0 || ^6.0", - "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", - "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", - "symfony/phpunit-bridge": "^5.4@dev || ^6.0" - }, - "suggest": { - "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.2|^7.0", + "symfony/http-kernel": "^6.2|^7.0" }, "type": "symfony-bridge", - "extra": { - "branch-alias": { - "dev-main": "2.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Bridge\\PsrHttpMessage\\": "" @@ -10195,11 +11798,11 @@ }, { "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" + "homepage": "https://symfony.com/contributors" } ], "description": "PSR HTTP message bridge", - "homepage": "http://symfony.com", + "homepage": "https://symfony.com", "keywords": [ "http", "http-message", @@ -10207,8 +11810,7 @@ "psr-7" ], "support": { - "issues": "https://github.com/symfony/psr-http-message-bridge/issues", - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.1.4" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.13" }, "funding": [ { @@ -10224,29 +11826,30 @@ "type": "tidelift" } ], - "time": "2022-11-28T22:46:34+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/rate-limiter", - "version": "v5.4.21", + "version": "v6.4.15", "source": { "type": "git", "url": "https://github.com/symfony/rate-limiter.git", - "reference": "342acb2d23f6012f6150e7a8b167bf9cd931c0f8" + "reference": "e250d82fc17b277b97cbce94efef5414aff29bf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/342acb2d23f6012f6150e7a8b167bf9cd931c0f8", - "reference": "342acb2d23f6012f6150e7a8b167bf9cd931c0f8", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/e250d82fc17b277b97cbce94efef5414aff29bf9", + "reference": "e250d82fc17b277b97cbce94efef5414aff29bf9", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/lock": "^5.2|^6.0", - "symfony/options-resolver": "^5.1|^6.0" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/options-resolver": "^5.4|^6.0|^7.0" }, "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0" + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/lock": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -10278,7 +11881,7 @@ "rate-limiter" ], "support": { - "source": "https://github.com/symfony/rate-limiter/tree/v5.4.21" + "source": "https://github.com/symfony/rate-limiter/tree/v6.4.15" }, "funding": [ { @@ -10294,47 +11897,40 @@ "type": "tidelift" } ], - "time": "2023-02-21T19:46:44+00:00" + "time": "2024-11-09T07:19:24+00:00" }, { "name": "symfony/routing", - "version": "v5.4.22", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "c2ac11eb34947999b7c38fb4c835a57306907e6d" + "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/c2ac11eb34947999b7c38fb4c835a57306907e6d", - "reference": "c2ac11eb34947999b7c38fb4c835a57306907e6d", + "url": "https://api.github.com/repos/symfony/routing/zipball/e9bfc94953019089acdfb9be51c1b9142c4afa68", + "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "doctrine/annotations": "<1.12", - "symfony/config": "<5.3", - "symfony/dependency-injection": "<4.4", - "symfony/yaml": "<4.4" + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^5.3|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/config": "For using the all-in-one router or any loader", - "symfony/expression-language": "For using expression matching", - "symfony/http-foundation": "For using a Symfony Request object", - "symfony/yaml": "For using the YAML loader" + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -10368,7 +11964,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v5.4.22" + "source": "https://github.com/symfony/routing/tree/v6.4.18" }, "funding": [ { @@ -10384,36 +11980,35 @@ "type": "tidelift" } ], - "time": "2023-03-14T14:59:20+00:00" + "time": "2025-01-09T08:51:02+00:00" }, { "name": "symfony/runtime", - "version": "v5.4.22", + "version": "v6.4.14", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "4a78e519d40a3845437e29dc514959631badfed4" + "reference": "4facd4174f45cd37c65860403412b67c7381136a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/4a78e519d40a3845437e29dc514959631badfed4", - "reference": "4a78e519d40a3845437e29dc514959631badfed4", + "url": "https://api.github.com/repos/symfony/runtime/zipball/4facd4174f45cd37c65860403412b67c7381136a", + "reference": "4facd4174f45cd37c65860403412b67c7381136a", "shasum": "" }, "require": { "composer-plugin-api": "^1.0|^2.0", - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.1" }, "conflict": { - "symfony/dotenv": "<5.1" + "symfony/dotenv": "<5.4" }, "require-dev": { "composer/composer": "^1.0.2|^2.0", - "symfony/console": "^4.4.30|^5.3.7|^6.0", - "symfony/dotenv": "^5.1|^6.0", - "symfony/http-foundation": "^4.4.30|^5.3.7|^6.0", - "symfony/http-kernel": "^4.4.30|^5.3.7|^6.0" + "symfony/console": "^5.4.9|^6.0.9|^7.0", + "symfony/dotenv": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0" }, "type": "composer-plugin", "extra": { @@ -10448,7 +12043,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v5.4.22" + "source": "https://github.com/symfony/runtime/tree/v6.4.14" }, "funding": [ { @@ -10464,65 +12059,75 @@ "type": "tidelift" } ], - "time": "2023-03-14T15:48:23+00:00" + "time": "2024-11-05T16:39:55+00:00" }, { "name": "symfony/security-bundle", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "36eddff8266126de032ab528417ad13eb43f6cb5" + "reference": "99b656ff6046ef217d4e3f852940de7e22489849" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/36eddff8266126de032ab528417ad13eb43f6cb5", - "reference": "36eddff8266126de032ab528417ad13eb43f6cb5", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/99b656ff6046ef217d4e3f852940de7e22489849", + "reference": "99b656ff6046ef217d4e3f852940de7e22489849", "shasum": "" }, "require": { + "composer-runtime-api": ">=2.1", "ext-xml": "*", - "php": ">=7.2.5", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.3|^6.0", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher": "^5.1|^6.0", - "symfony/http-foundation": "^5.3|^6.0", - "symfony/http-kernel": "^5.3|^6.0", - "symfony/password-hasher": "^5.3|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/security-core": "^5.4|^6.0", - "symfony/security-csrf": "^4.4|^5.0|^6.0", - "symfony/security-guard": "^5.3", - "symfony/security-http": "^5.4.20|~6.0.20|~6.1.12|^6.2.6" + "php": ">=8.1", + "symfony/clock": "^6.3|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.2|^7.0", + "symfony/http-kernel": "^6.2", + "symfony/password-hasher": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.2|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/security-http": "^6.3.6|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/browser-kit": "<4.4", - "symfony/console": "<4.4", - "symfony/framework-bundle": "<4.4", - "symfony/ldap": "<5.1", - "symfony/twig-bundle": "<4.4" + "symfony/browser-kit": "<5.4", + "symfony/console": "<5.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-client": "<5.4", + "symfony/ldap": "<5.4", + "symfony/serializer": "<6.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<6.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4|^2", - "symfony/asset": "^4.4|^5.0|^6.0", - "symfony/browser-kit": "^4.4|^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/form": "^4.4|^5.0|^6.0", - "symfony/framework-bundle": "^5.3|^6.0", - "symfony/ldap": "^5.3|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0", - "symfony/serializer": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/twig-bridge": "^4.4|^5.0|^6.0", - "symfony/twig-bundle": "^4.4|^5.0|^6.0", - "symfony/validator": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/twig-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4", + "web-token/jwt-checker": "^3.1", + "web-token/jwt-signature-algorithm-ecdsa": "^3.1", + "web-token/jwt-signature-algorithm-eddsa": "^3.1", + "web-token/jwt-signature-algorithm-hmac": "^3.1", + "web-token/jwt-signature-algorithm-none": "^3.1", + "web-token/jwt-signature-algorithm-rsa": "^3.1" }, "type": "symfony-bundle", "autoload": { @@ -10550,7 +12155,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v5.4.22" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.21" }, "funding": [ { @@ -10566,56 +12171,49 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:02:45+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/security-core", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "a801d525c7545332e2ddf7f52c163959354b1650" + "reference": "c6e70da38436a9a49ed39d9cbead1ecf760f0fbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/a801d525c7545332e2ddf7f52c163959354b1650", - "reference": "a801d525c7545332e2ddf7f52c163959354b1650", + "url": "https://api.github.com/repos/symfony/security-core/zipball/c6e70da38436a9a49ed39d9cbead1ecf760f0fbd", + "reference": "c6e70da38436a9a49ed39d9cbead1ecf760f0fbd", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^1.1|^2|^3", - "symfony/password-hasher": "^5.3|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1.6|^2|^3" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/event-dispatcher": "<4.4", - "symfony/http-foundation": "<5.3", - "symfony/ldap": "<4.4", - "symfony/security-guard": "<4.4", - "symfony/validator": "<5.2" + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<5.4" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "psr/container": "^1.0|^2.0", + "psr/container": "^1.1|^2.0", "psr/log": "^1|^2|^3", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^5.3|^6.0", - "symfony/ldap": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/validator": "^5.2|^6.0" - }, - "suggest": { - "psr/container-implementation": "To instantiate the Security class", - "symfony/event-dispatcher": "", - "symfony/expression-language": "For using the expression voter", - "symfony/http-foundation": "", - "symfony/ldap": "For using LDAP integration", - "symfony/validator": "For using the user password constraint" + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10643,7 +12241,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v5.4.22" + "source": "https://github.com/symfony/security-core/tree/v6.4.21" }, "funding": [ { @@ -10659,35 +12257,31 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:02:45+00:00" + "time": "2025-04-17T07:43:34+00:00" }, { "name": "symfony/security-csrf", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/security-csrf.git", - "reference": "776a538e5f20fb560a182f790979c71455694203" + "reference": "c34421b7d34efbaef5d611ab2e646a0ec464ffe3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-csrf/zipball/776a538e5f20fb560a182f790979c71455694203", - "reference": "776a538e5f20fb560a182f790979c71455694203", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/c34421b7d34efbaef5d611ab2e646a0ec464ffe3", + "reference": "c34421b7d34efbaef5d611ab2e646a0ec464ffe3", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16", - "symfony/security-core": "^4.4|^5.0|^6.0" + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0|^7.0" }, "conflict": { - "symfony/http-foundation": "<5.3" + "symfony/http-foundation": "<5.4" }, "require-dev": { - "symfony/http-foundation": "^5.3|^6.0" - }, - "suggest": { - "symfony/http-foundation": "For using the class SessionTokenStorage." + "symfony/http-foundation": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -10715,7 +12309,7 @@ "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-csrf/tree/v5.4.21" + "source": "https://github.com/symfony/security-csrf/tree/v6.4.13" }, "funding": [ { @@ -10731,115 +12325,51 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:33:00+00:00" - }, - { - "name": "symfony/security-guard", - "version": "v5.4.22", - "source": { - "type": "git", - "url": "https://github.com/symfony/security-guard.git", - "reference": "62d064b1ee682e4617f4c5ddc0d31f73e1a7ecaa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/security-guard/zipball/62d064b1ee682e4617f4c5ddc0d31f73e1a7ecaa", - "reference": "62d064b1ee682e4617f4c5ddc0d31f73e1a7ecaa", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15", - "symfony/security-core": "^5.0", - "symfony/security-http": "^5.3" - }, - "require-dev": { - "psr/log": "^1|^2|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Security\\Guard\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Security Component - Guard", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/security-guard/tree/v5.4.22" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-03-06T21:29:33+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/security-http", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "428c84954e95f54383d031693fa649f1e466461f" + "reference": "67d0edaf6702c3192f27ad483df9a875c9a1f1a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/428c84954e95f54383d031693fa649f1e466461f", - "reference": "428c84954e95f54383d031693fa649f1e466461f", + "url": "https://api.github.com/repos/symfony/security-http/zipball/67d0edaf6702c3192f27ad483df9a875c9a1f1a2", + "reference": "67d0edaf6702c3192f27ad483df9a875c9a1f1a2", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-foundation": "^5.3|^6.0", - "symfony/http-kernel": "^5.3|^6.0", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.2|^7.0", + "symfony/http-kernel": "^6.3|^7.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/property-access": "^4.4|^5.0|^6.0", - "symfony/security-core": "^5.4.19|~6.0.19|~6.1.11|^6.2.5" + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/event-dispatcher": "<4.3", - "symfony/security-bundle": "<5.3", - "symfony/security-csrf": "<4.4" + "symfony/clock": "<6.3", + "symfony/event-dispatcher": "<5.4.9|>=6,<6.0.9", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<5.4", + "symfony/security-csrf": "<5.4" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0", - "symfony/routing": "^4.4|^5.0|^6.0", - "symfony/security-csrf": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs", - "symfony/security-csrf": "For using tokens to protect authentication/logout attempts" + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.3|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "web-token/jwt-checker": "^3.1", + "web-token/jwt-signature-algorithm-ecdsa": "^3.1" }, "type": "library", "autoload": { @@ -10867,7 +12397,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v5.4.22" + "source": "https://github.com/symfony/security-http/tree/v6.4.21" }, "funding": [ { @@ -10883,66 +12413,61 @@ "type": "tidelift" } ], - "time": "2023-03-10T10:02:45+00:00" + "time": "2025-04-27T13:58:34+00:00" }, { "name": "symfony/serializer", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "5b1e0234bb801e6e565771c0cec64551137ea3ef" + "reference": "c45f8f7763afb11e85772c0c1debb8f272c17f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/5b1e0234bb801e6e565771c0cec64551137ea3ef", - "reference": "5b1e0234bb801e6e565771c0cec64551137ea3ef", + "url": "https://api.github.com/repos/symfony/serializer/zipball/c45f8f7763afb11e85772c0c1debb8f272c17f51", + "reference": "c45f8f7763afb11e85772c0c1debb8f272c17f51", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { "doctrine/annotations": "<1.12", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/dependency-injection": "<4.4", + "symfony/dependency-injection": "<5.4", "symfony/property-access": "<5.4", - "symfony/property-info": "<5.3.13", - "symfony/uid": "<5.3", - "symfony/yaml": "<4.4" + "symfony/property-info": "<5.4.24|>=6,<6.2.11", + "symfony/uid": "<5.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.12|^2", "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/form": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/property-access": "^5.4|^6.0", - "symfony/property-info": "^5.3.13|^6.0", - "symfony/uid": "^5.3|^6.0", - "symfony/validator": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0", - "symfony/var-exporter": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/cache-implementation": "For using the metadata cache.", - "symfony/config": "For using the XML mapping loader.", - "symfony/mime": "For using a MIME type guesser within the DataUriNormalizer.", - "symfony/property-access": "For using the ObjectNormalizer.", - "symfony/property-info": "To deserialize relations.", - "symfony/var-exporter": "For using the metadata compiler.", - "symfony/yaml": "For using the default YAML mapping loader." + "seld/jsonlint": "^1.10", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.26|^6.3|^7.0", + "symfony/property-info": "^5.4.24|^6.2.11|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -10970,7 +12495,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v5.4.22" + "source": "https://github.com/symfony/serializer/tree/v6.4.21" }, "funding": [ { @@ -10986,47 +12511,47 @@ "type": "tidelift" } ], - "time": "2023-03-31T09:21:17+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.2", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -11053,7 +12578,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -11069,25 +12594,94 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:29+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/stopwatch", - "version": "v5.4.21", + "name": "symfony/stimulus-bundle", + "version": "v2.24.0", "source": { "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "f83692cd869a6f2391691d40a01e8acb89e76fee" + "url": "https://github.com/symfony/stimulus-bundle.git", + "reference": "e09840304467cda3324cc116c7f4ee23c8ff227c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/f83692cd869a6f2391691d40a01e8acb89e76fee", - "reference": "f83692cd869a6f2391691d40a01e8acb89e76fee", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/e09840304467cda3324cc116c7f4ee23c8ff227c", + "reference": "e09840304467cda3324cc116c7f4ee23c8ff227c", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1|^2|^3" + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.0|^3.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "twig/twig": "^2.15.3|^3.8" + }, + "require-dev": { + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "zenstruck/browser": "^1.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\UX\\StimulusBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Stimulus!", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/stimulus-bundle/tree/v2.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-09T21:10:04+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -11115,7 +12709,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.21" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.19" }, "funding": [ { @@ -11131,38 +12725,38 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2025-02-21T10:06:30+00:00" }, { "name": "symfony/string", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62" + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", - "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", + "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": ">=3.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -11201,7 +12795,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.22" + "source": "https://github.com/symfony/string/tree/v6.4.21" }, "funding": [ { @@ -11217,57 +12811,55 @@ "type": "tidelift" } ], - "time": "2023-03-14T06:11:53+00:00" + "time": "2025-04-18T15:23:29+00:00" }, { "name": "symfony/translation", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "9a401392f01bc385aa42760eff481d213a0cc2ba" + "reference": "bb92ea5588396b319ba43283a5a3087a034cb29c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/9a401392f01bc385aa42760eff481d213a0cc2ba", - "reference": "9a401392f01bc385aa42760eff481d213a0cc2ba", + "url": "https://api.github.com/repos/symfony/translation/zipball/bb92ea5588396b319ba43283a5a3087a034cb29c", + "reference": "bb92ea5588396b319ba43283a5a3087a034cb29c", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^2.3" + "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { - "symfony/config": "<4.4", - "symfony/console": "<5.3", - "symfony/dependency-injection": "<5.0", - "symfony/http-kernel": "<5.0", - "symfony/twig-bundle": "<5.0", - "symfony/yaml": "<4.4" + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" }, "provide": { - "symfony/translation-implementation": "2.3" + "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { + "nikic/php-parser": "^4.18|^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client-contracts": "^1.1|^2.0|^3.0", - "symfony/http-kernel": "^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/service-contracts": "^1.1.2|^2|^3", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log-implementation": "To use logging capability in translator", - "symfony/config": "", - "symfony/yaml": "" + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -11298,7 +12890,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.22" + "source": "https://github.com/symfony/translation/tree/v6.4.21" }, "funding": [ { @@ -11314,42 +12906,42 @@ "type": "tidelift" } ], - "time": "2023-03-27T16:07:23+00:00" + "time": "2025-04-07T19:02:30+00:00" }, { "name": "symfony/translation-contracts", - "version": "v2.5.2", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe" + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/136b19dd05cdf0709db6537d058bcab6dd6e2dbe", - "reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", "shasum": "" }, "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/translation-implementation": "" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\Translation\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -11376,7 +12968,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" }, "funding": [ { @@ -11392,85 +12984,73 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/twig-bridge", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "e5b174464f68be6876046db3ad6e217d9a7dbbac" + "reference": "0457b7944bf1cc9c846c98d4923b5379ec6afc09" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/e5b174464f68be6876046db3ad6e217d9a7dbbac", - "reference": "e5b174464f68be6876046db3ad6e217d9a7dbbac", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/0457b7944bf1cc9c846c98d4923b5379ec6afc09", + "reference": "0457b7944bf1cc9c846c98d4923b5379ec6afc09", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^1.1|^2|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/translation-contracts": "^2.5|^3", "twig/twig": "^2.13|^3.0.4" }, "conflict": { "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/console": "<5.3", - "symfony/form": "<5.4.21|>=6,<6.2.7", - "symfony/http-foundation": "<5.3", - "symfony/http-kernel": "<4.4", - "symfony/translation": "<5.2", - "symfony/workflow": "<5.2" + "symfony/console": "<5.4", + "symfony/form": "<6.3", + "symfony/http-foundation": "<5.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.2", + "symfony/serializer": "<6.4", + "symfony/translation": "<5.4", + "symfony/workflow": "<5.4" }, "require-dev": { - "doctrine/annotations": "^1.12|^2", "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/asset": "^4.4|^5.0|^6.0", - "symfony/console": "^5.3|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/form": "^5.4.21|^6.2.7", - "symfony/http-foundation": "^5.3|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", - "symfony/mime": "^5.2|^6.0", + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/form": "^6.4.20|^7.2.5", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/property-info": "^4.4|^5.1|^6.0", - "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", "symfony/security-acl": "^2.8|^3.0", - "symfony/security-core": "^4.4|^5.0|^6.0", - "symfony/security-csrf": "^4.4|^5.0|^6.0", - "symfony/security-http": "^4.4|^5.0|^6.0", - "symfony/serializer": "^5.2|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/translation": "^5.2|^6.0", - "symfony/web-link": "^4.4|^5.0|^6.0", - "symfony/workflow": "^5.2|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/security-http": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^6.1|^7.0", + "symfony/web-link": "^5.4|^6.0|^7.0", + "symfony/workflow": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0", "twig/cssinliner-extra": "^2.12|^3", "twig/inky-extra": "^2.12|^3", "twig/markdown-extra": "^2.12|^3" }, - "suggest": { - "symfony/asset": "For using the AssetExtension", - "symfony/expression-language": "For using the ExpressionExtension", - "symfony/finder": "", - "symfony/form": "For using the FormExtension", - "symfony/http-kernel": "For using the HttpKernelExtension", - "symfony/routing": "For using the RoutingExtension", - "symfony/security-core": "For using the SecurityExtension", - "symfony/security-csrf": "For using the CsrfExtension", - "symfony/security-http": "For using the LogoutUrlExtension", - "symfony/stopwatch": "For using the StopwatchExtension", - "symfony/translation": "For using the TranslationExtension", - "symfony/var-dumper": "For using the DumpExtension", - "symfony/web-link": "For using the WebLinkExtension", - "symfony/yaml": "For using the YamlExtension" - }, "type": "symfony-bridge", "autoload": { "psr-4": { @@ -11497,7 +13077,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v5.4.22" + "source": "https://github.com/symfony/twig-bridge/tree/v6.4.21" }, "funding": [ { @@ -11513,52 +13093,47 @@ "type": "tidelift" } ], - "time": "2023-03-31T08:28:44+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/twig-bundle", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "875d0edfc8df7505c1993419882c4071fc28c477" + "reference": "c3beeb5336aba1ea03c37e526968c2fde3ef25c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/875d0edfc8df7505c1993419882c4071fc28c477", - "reference": "875d0edfc8df7505c1993419882c4071fc28c477", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/c3beeb5336aba1ea03c37e526968c2fde3ef25c4", + "reference": "c3beeb5336aba1ea03c37e526968c2fde3ef25c4", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^5.0|^6.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php80": "^1.16", - "symfony/twig-bridge": "^5.3|^6.0", + "composer-runtime-api": ">=2.1", + "php": ">=8.1", + "symfony/config": "^6.1|^7.0", + "symfony/dependency-injection": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^6.2", + "symfony/twig-bridge": "^6.4", "twig/twig": "^2.13|^3.0.4" }, "conflict": { - "symfony/dependency-injection": "<5.3", - "symfony/framework-bundle": "<5.0", - "symfony/service-contracts": ">=3.0", - "symfony/translation": "<5.0" + "symfony/framework-bundle": "<5.4", + "symfony/translation": "<5.4" }, "require-dev": { - "doctrine/annotations": "^1.10.4|^2", - "doctrine/cache": "^1.0|^2.0", - "symfony/asset": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.3|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/form": "^4.4|^5.0|^6.0", - "symfony/framework-bundle": "^5.0|^6.0", - "symfony/routing": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/translation": "^5.0|^6.0", - "symfony/web-link": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0" + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/web-link": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "symfony-bundle", "autoload": { @@ -11586,7 +13161,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v5.4.21" + "source": "https://github.com/symfony/twig-bundle/tree/v6.4.13" }, "funding": [ { @@ -11602,53 +13177,206 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { - "name": "symfony/ux-turbo", - "version": "v2.7.1", + "name": "symfony/uid", + "version": "v6.4.13", "source": { "type": "git", - "url": "https://github.com/symfony/ux-turbo.git", - "reference": "2c82aad06d7c1493615d09188b70a3a86f6cc6ab" + "url": "https://github.com/symfony/uid.git", + "reference": "18eb207f0436a993fffbdd811b5b8fa35fa5e007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/2c82aad06d7c1493615d09188b70a3a86f6cc6ab", - "reference": "2c82aad06d7c1493615d09188b70a3a86f6cc6ab", + "url": "https://api.github.com/repos/symfony/uid/zipball/18eb207f0436a993fffbdd811b5b8fa35fa5e007", + "reference": "18eb207f0436a993fffbdd811b5b8fa35fa5e007", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/webpack-encore-bundle": "^1.11" + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/ux-translator", + "version": "v2.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-translator.git", + "reference": "a829b5c83ed676a8e848dce90dd6d42f12e90be6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-translator/zipball/a829b5c83ed676a8e848dce90dd6d42f12e90be6", + "reference": "a829b5c83ed676a8e848dce90dd6d42f12e90be6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ux", + "name": "symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\Translator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hugo Alliaume", + "email": "hugo@alliau.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Exposes Symfony Translations directly to JavaScript.", + "homepage": "https://symfony.com", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/ux-translator/tree/v2.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-09T21:10:04+00:00" + }, + { + "name": "symfony/ux-turbo", + "version": "v2.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-turbo.git", + "reference": "22954300bd0b01ca46f17c7890ea15138d9cf67f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/22954300bd0b01ca46f17c7890ea15138d9cf67f", + "reference": "22954300bd0b01ca46f17c7890ea15138d9cf67f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/stimulus-bundle": "^2.9.1" }, "conflict": { "symfony/flex": "<1.13" }, "require-dev": { - "doctrine/annotations": "^1.12", - "doctrine/doctrine-bundle": "^2.2", + "dbrekelmans/bdi": "dev-main", + "doctrine/doctrine-bundle": "^2.4.3", "doctrine/orm": "^2.8 | 3.0", - "phpstan/phpstan": "^0.12", - "symfony/debug-bundle": "^5.2|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/form": "^5.2|^6.0", - "symfony/framework-bundle": "^5.2|^6.0", - "symfony/mercure-bundle": "^0.3", - "symfony/messenger": "^5.2|^6.0", - "symfony/panther": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.2.1|^6.0", - "symfony/property-access": "^5.2|^6.0", - "symfony/security-core": "^5.2|^6.0", - "symfony/stopwatch": "^5.2|^6.0", - "symfony/twig-bundle": "^5.2|^6.0", - "symfony/web-profiler-bundle": "^5.2|^6.0" + "phpstan/phpstan": "^1.10", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/debug-bundle": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/mercure-bundle": "^0.3.7", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/panther": "^2.1", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|6.3.*|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/ux-twig-component": "^2.21", + "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0" }, "type": "symfony-bundle", "extra": { "thanks": { - "name": "symfony/ux", - "url": "https://github.com/symfony/ux" + "url": "https://github.com/symfony/ux", + "name": "symfony/ux" } }, "autoload": { @@ -11681,7 +13409,7 @@ "turbo-stream" ], "support": { - "source": "https://github.com/symfony/ux-turbo/tree/v2.7.1" + "source": "https://github.com/symfony/ux-turbo/tree/v2.24.0" }, "funding": [ { @@ -11697,76 +13425,59 @@ "type": "tidelift" } ], - "time": "2023-01-24T15:40:19+00:00" + "time": "2025-04-04T17:29:20+00:00" }, { "name": "symfony/validator", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "7c953a48f436bb180d8f5ae471f2665d7dd245da" + "reference": "47610116f476595b90c368ff2a22514050712785" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/7c953a48f436bb180d8f5ae471f2665d7dd245da", - "reference": "7c953a48f436bb180d8f5ae471f2665d7dd245da", + "url": "https://api.github.com/repos/symfony/validator/zipball/47610116f476595b90c368ff2a22514050712785", + "reference": "47610116f476595b90c368ff2a22514050712785", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22", - "symfony/translation-contracts": "^1.1|^2|^3" + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" }, "conflict": { "doctrine/annotations": "<1.13", - "doctrine/cache": "<1.11", "doctrine/lexer": "<1.1", - "phpunit/phpunit": "<5.4.3", - "symfony/dependency-injection": "<4.4", - "symfony/expression-language": "<5.1", - "symfony/http-kernel": "<4.4", - "symfony/intl": "<4.4", - "symfony/property-info": "<5.3", - "symfony/translation": "<4.4", - "symfony/yaml": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.13|^2", - "doctrine/cache": "^1.11|^2.0", "egulias/email-validator": "^2.1.10|^3|^4", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^5.1|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/intl": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/property-access": "^4.4|^5.0|^6.0", - "symfony/property-info": "^5.3|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "egulias/email-validator": "Strict (RFC compliant) email validation", - "psr/cache-implementation": "For using the mapping cache.", - "symfony/config": "", - "symfony/expression-language": "For using the Expression validator and the ExpressionLanguageSyntax constraints", - "symfony/http-foundation": "", - "symfony/intl": "", - "symfony/property-access": "For accessing properties within comparison constraints", - "symfony/property-info": "To automatically add NotNull and Type constraints", - "symfony/translation": "For translating validation errors.", - "symfony/yaml": "" + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -11774,7 +13485,8 @@ "Symfony\\Component\\Validator\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -11794,7 +13506,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v5.4.22" + "source": "https://github.com/symfony/validator/tree/v6.4.21" }, "funding": [ { @@ -11810,43 +13522,39 @@ "type": "tidelift" } ], - "time": "2023-03-17T15:39:32+00:00" + "time": "2025-04-30T18:50:04+00:00" }, { "name": "symfony/var-dumper", - "version": "v5.4.22", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "e2edac9ce47e6df07e38143c7cfa6bdbc1a6dcc4" + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e2edac9ce47e6df07e38143c7cfa6bdbc1a6dcc4", - "reference": "e2edac9ce47e6df07e38143c7cfa6bdbc1a6dcc4", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3", - "symfony/console": "<4.4" + "symfony/console": "<5.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", "twig/twig": "^2.13|^3.0.4" }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" - }, "bin": [ "Resources/bin/var-dump-server" ], @@ -11883,7 +13591,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.22" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" }, "funding": [ { @@ -11899,28 +13607,30 @@ "type": "tidelift" } ], - "time": "2023-03-25T09:27:28+00:00" + "time": "2025-04-09T07:34:50+00:00" }, { "name": "symfony/var-exporter", - "version": "v5.4.21", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "be74908a6942fdd331554b3cec27ff41b45ccad4" + "reference": "717e7544aa99752c54ecba5c0e17459c48317472" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/be74908a6942fdd331554b3cec27ff41b45ccad4", - "reference": "be74908a6942fdd331554b3cec27ff41b45ccad4", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/717e7544aa99752c54ecba5c0e17459c48317472", + "reference": "717e7544aa99752c54ecba5c0e17459c48317472", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -11953,10 +13663,12 @@ "export", "hydrate", "instantiate", + "lazy-loading", + "proxy", "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v5.4.21" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.21" }, "funding": [ { @@ -11972,38 +13684,34 @@ "type": "tidelift" } ], - "time": "2023-02-21T19:46:44+00:00" + "time": "2025-04-27T21:06:26+00:00" }, { "name": "symfony/web-link", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/web-link.git", - "reference": "57c03a5e89ed7c2d7a1a09258dfec12f95f95adb" + "reference": "4d188b64bb9a9c5e2e4d20c8d5fdce6bbbb32c94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-link/zipball/57c03a5e89ed7c2d7a1a09258dfec12f95f95adb", - "reference": "57c03a5e89ed7c2d7a1a09258dfec12f95f95adb", + "url": "https://api.github.com/repos/symfony/web-link/zipball/4d188b64bb9a9c5e2e4d20c8d5fdce6bbbb32c94", + "reference": "4d188b64bb9a9c5e2e4d20c8d5fdce6bbbb32c94", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/link": "^1.0", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "psr/link": "^1.1|^2.0" }, "conflict": { - "symfony/http-kernel": "<5.3" + "symfony/http-kernel": "<5.4" }, "provide": { - "psr/link-implementation": "1.0" + "psr/link-implementation": "1.0|2.0" }, "require-dev": { - "symfony/http-kernel": "^5.3|^6.0" - }, - "suggest": { - "symfony/http-kernel": "" + "symfony/http-kernel": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -12043,7 +13751,7 @@ "push" ], "support": { - "source": "https://github.com/symfony/web-link/tree/v5.4.21" + "source": "https://github.com/symfony/web-link/tree/v6.4.13" }, "funding": [ { @@ -12059,43 +13767,42 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/webpack-encore-bundle", - "version": "v1.16.1", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/webpack-encore-bundle.git", - "reference": "1862d71e483769b40278548a30e756ce13ef9d4c" + "reference": "e335394b68a775a9b2bd173a8ba4fd2001f3870c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/1862d71e483769b40278548a30e756ce13ef9d4c", - "reference": "1862d71e483769b40278548a30e756ce13ef9d4c", + "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/e335394b68a775a9b2bd173a8ba4fd2001f3870c", + "reference": "e335394b68a775a9b2bd173a8ba4fd2001f3870c", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/asset": "^4.4 || ^5.0 || ^6.0", - "symfony/config": "^4.4 || ^5.0 || ^6.0", - "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", - "symfony/deprecation-contracts": "^2.1 || ^3.0", - "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", - "symfony/polyfill-php80": "^1.25.0", - "symfony/service-contracts": "^1.0 || ^2.0 || ^3.0" + "php": ">=8.1.0", + "symfony/asset": "^5.4 || ^6.2 || ^7.0", + "symfony/config": "^5.4 || ^6.2 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.2 || ^7.0", + "symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0" }, "require-dev": { - "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", - "symfony/phpunit-bridge": "^5.3 || ^6.0", - "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0", - "symfony/web-link": "^4.4 || ^5.0 || ^6.0" + "symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0", + "symfony/http-client": "^5.4 || ^6.2 || ^7.0", + "symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0", + "symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0", + "symfony/web-link": "^5.4 || ^6.2 || ^7.0" }, "type": "symfony-bundle", "extra": { "thanks": { - "name": "symfony/webpack-encore", - "url": "https://github.com/symfony/webpack-encore" + "url": "https://github.com/symfony/webpack-encore", + "name": "symfony/webpack-encore" } }, "autoload": { @@ -12113,10 +13820,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Integration with your Symfony app & Webpack Encore!", + "description": "Integration of your Symfony app with Webpack Encore", "support": { "issues": "https://github.com/symfony/webpack-encore-bundle/issues", - "source": "https://github.com/symfony/webpack-encore-bundle/tree/v1.16.1" + "source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.2.0" }, "funding": [ { @@ -12132,35 +13839,32 @@ "type": "tidelift" } ], - "time": "2023-01-18T19:37:55+00:00" + "time": "2024-10-02T07:27:19+00:00" }, { "name": "symfony/yaml", - "version": "v5.4.21", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "3713e20d93e46e681e51605d213027e48dab3469" + "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/3713e20d93e46e681e51605d213027e48dab3469", - "reference": "3713e20d93e46e681e51605d213027e48dab3469", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f01987f45676778b474468aa266fe2eda1f2bc7e", + "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.3" + "symfony/console": "<5.4" }, "require-dev": { - "symfony/console": "^5.3|^6.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "symfony/console": "^5.4|^6.0|^7.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -12191,7 +13895,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.21" + "source": "https://github.com/symfony/yaml/tree/v6.4.21" }, "funding": [ { @@ -12207,20 +13911,20 @@ "type": "tidelift" } ], - "time": "2023-02-21T19:46:44+00:00" + "time": "2025-04-04T09:48:44+00:00" }, { "name": "tecnickcom/tc-lib-barcode", - "version": "1.17.19", + "version": "2.4.6", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-barcode.git", - "reference": "8dbed267c44cb95a903d1149b81752ec4401dab1" + "reference": "c6130222fdc02a2d7c90682b5c2ca24a059c16f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/8dbed267c44cb95a903d1149b81752ec4401dab1", - "reference": "8dbed267c44cb95a903d1149b81752ec4401dab1", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/c6130222fdc02a2d7c90682b5c2ca24a059c16f4", + "reference": "c6130222fdc02a2d7c90682b5c2ca24a059c16f4", "shasum": "" }, "require": { @@ -12228,16 +13932,14 @@ "ext-date": "*", "ext-gd": "*", "ext-pcre": "*", - "php": ">=5.4", - "tecnickcom/tc-lib-color": "^1.14" + "php": ">=8.1", + "tecnickcom/tc-lib-color": "^2.2" }, "require-dev": { - "pdepend/pdepend": "2.12.1", - "phploc/phploc": "7.0.2 || 6.0.2 || 5.0.0 || 4.0.1 || 3.0.1 || 2.1.5", - "phpmd/phpmd": "2.13.0", - "phpunit/phpunit": "9.5.27 || 8.5.31 || 7.5.20 || 6.5.14 || 5.7.27 || 4.8.36", - "sebastian/phpcpd": "6.0.3 || 5.0.2 || 4.1.0 || 3.0.1 || 2.0.4", - "squizlabs/php_codesniffer": "3.7.1 || 2.9.2" + "pdepend/pdepend": "2.16.2", + "phpmd/phpmd": "2.15.0", + "phpunit/phpunit": "12.1.3 || 11.5.7 || 10.5.40", + "squizlabs/php_codesniffer": "3.12.2" }, "type": "library", "autoload": { @@ -12247,7 +13949,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "LGPL-3.0-or-later" ], "authors": [ { @@ -12270,6 +13972,9 @@ "EAN 13", "EAN 8", "ECC200", + "ISO IEC 15438 2006", + "ISO IEC 16022", + "ISO IEC 24778 2008", "Intelligent Mail Barcode", "Interleaved 2 of 5", "KIX", @@ -12286,6 +13991,7 @@ "USD-3", "USPS-B-3200", "USS-93", + "aztec", "barcode", "datamatrix", "pdf417", @@ -12297,41 +14003,39 @@ ], "support": { "issues": "https://github.com/tecnickcom/tc-lib-barcode/issues", - "source": "https://github.com/tecnickcom/tc-lib-barcode/tree/1.17.19" + "source": "https://github.com/tecnickcom/tc-lib-barcode/tree/2.4.6" }, "funding": [ { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tc-lib-barcode%20project", + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", "type": "custom" } ], - "time": "2022-12-21T16:26:37+00:00" + "time": "2025-05-13T05:49:21+00:00" }, { "name": "tecnickcom/tc-lib-color", - "version": "1.14.18", + "version": "2.2.11", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-color.git", - "reference": "c430e0b8a8847935a72bc5fcc334d1e4d029e23b" + "reference": "ead45d76932d936e065062194032bf87f7ea45f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/c430e0b8a8847935a72bc5fcc334d1e4d029e23b", - "reference": "c430e0b8a8847935a72bc5fcc334d1e4d029e23b", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/ead45d76932d936e065062194032bf87f7ea45f8", + "reference": "ead45d76932d936e065062194032bf87f7ea45f8", "shasum": "" }, "require": { "ext-pcre": "*", - "php": ">=5.3" + "php": ">=8.1" }, "require-dev": { - "pdepend/pdepend": "2.12.1", - "phploc/phploc": "7.0.2 || 6.0.2 || 5.0.0 || 4.0.1 || 3.0.1 || 2.1.5", - "phpmd/phpmd": "2.13.0", - "phpunit/phpunit": "9.5.27 || 8.5.31 || 7.5.20 || 6.5.14 || 5.7.27 || 4.8.36", - "sebastian/phpcpd": "6.0.3 || 5.0.2 || 4.1.0 || 3.0.1 || 2.0.4", - "squizlabs/php_codesniffer": "3.7.1 || 2.9.2" + "pdepend/pdepend": "2.16.2", + "phpmd/phpmd": "2.15.0", + "phpunit/phpunit": "12.1.3 || 11.5.7 || 10.5.40", + "squizlabs/php_codesniffer": "3.12.2" }, "type": "library", "autoload": { @@ -12341,7 +14045,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "LGPL-3.0-or-later" ], "authors": [ { @@ -12368,182 +14072,45 @@ ], "support": { "issues": "https://github.com/tecnickcom/tc-lib-color/issues", - "source": "https://github.com/tecnickcom/tc-lib-color/tree/1.14.18" + "source": "https://github.com/tecnickcom/tc-lib-color/tree/2.2.11" }, "funding": [ { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tc-lib-color%20project", + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", "type": "custom" } ], - "time": "2022-12-21T16:25:34+00:00" - }, - { - "name": "thecodingmachine/safe", - "version": "v1.3.3", - "source": { - "type": "git", - "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "phpstan/phpstan": "^0.12", - "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1-dev" - } - }, - "autoload": { - "files": [ - "deprecated/apc.php", - "deprecated/libevent.php", - "deprecated/mssql.php", - "deprecated/stats.php", - "lib/special_cases.php", - "generated/apache.php", - "generated/apcu.php", - "generated/array.php", - "generated/bzip2.php", - "generated/calendar.php", - "generated/classobj.php", - "generated/com.php", - "generated/cubrid.php", - "generated/curl.php", - "generated/datetime.php", - "generated/dir.php", - "generated/eio.php", - "generated/errorfunc.php", - "generated/exec.php", - "generated/fileinfo.php", - "generated/filesystem.php", - "generated/filter.php", - "generated/fpm.php", - "generated/ftp.php", - "generated/funchand.php", - "generated/gmp.php", - "generated/gnupg.php", - "generated/hash.php", - "generated/ibase.php", - "generated/ibmDb2.php", - "generated/iconv.php", - "generated/image.php", - "generated/imap.php", - "generated/info.php", - "generated/ingres-ii.php", - "generated/inotify.php", - "generated/json.php", - "generated/ldap.php", - "generated/libxml.php", - "generated/lzf.php", - "generated/mailparse.php", - "generated/mbstring.php", - "generated/misc.php", - "generated/msql.php", - "generated/mysql.php", - "generated/mysqli.php", - "generated/mysqlndMs.php", - "generated/mysqlndQc.php", - "generated/network.php", - "generated/oci8.php", - "generated/opcache.php", - "generated/openssl.php", - "generated/outcontrol.php", - "generated/password.php", - "generated/pcntl.php", - "generated/pcre.php", - "generated/pdf.php", - "generated/pgsql.php", - "generated/posix.php", - "generated/ps.php", - "generated/pspell.php", - "generated/readline.php", - "generated/rpminfo.php", - "generated/rrd.php", - "generated/sem.php", - "generated/session.php", - "generated/shmop.php", - "generated/simplexml.php", - "generated/sockets.php", - "generated/sodium.php", - "generated/solr.php", - "generated/spl.php", - "generated/sqlsrv.php", - "generated/ssdeep.php", - "generated/ssh2.php", - "generated/stream.php", - "generated/strings.php", - "generated/swoole.php", - "generated/uodbc.php", - "generated/uopz.php", - "generated/url.php", - "generated/var.php", - "generated/xdiff.php", - "generated/xml.php", - "generated/xmlrpc.php", - "generated/yaml.php", - "generated/yaz.php", - "generated/zip.php", - "generated/zlib.php" - ], - "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "support": { - "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" - }, - "time": "2020-10-28T17:51:34+00:00" + "time": "2025-05-13T05:47:44+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.6", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c" + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c", - "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", - "php": "^5.5 || ^7.0 || ^8.0", - "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -12566,39 +14133,38 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" }, - "time": "2023-01-03T09:29:04+00:00" + "time": "2024-12-21T16:25:41+00:00" }, { "name": "twig/cssinliner-extra", - "version": "v3.5.1", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/cssinliner-extra.git", - "reference": "381877765d17b0178322d68b818e0c67f9c93187" + "reference": "378d29b61d6406c456e3a4afbd15bbeea0b72ea8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/381877765d17b0178322d68b818e0c67f9c93187", - "reference": "381877765d17b0178322d68b818e0c67f9c93187", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/378d29b61d6406c456e3a4afbd15bbeea0b72ea8", + "reference": "378d29b61d6406c456e3a4afbd15bbeea0b72ea8", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", "tijsverkoyen/css-to-inline-styles": "^2.0", - "twig/twig": "^2.7|^3.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Twig\\Extra\\CssInliner\\": "" }, @@ -12626,7 +14192,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.5.1" + "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.21.0" }, "funding": [ { @@ -12638,45 +14204,40 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:44:55+00:00" + "time": "2025-01-31T20:45:36+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.5.1", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "a961e553a624eebdbd423ad5ab931497ca6d87cd" + "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/a961e553a624eebdbd423ad5ab931497ca6d87cd", - "reference": "a961e553a624eebdbd423ad5ab931497ca6d87cd", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/62d1cf47a1aa009cbd07b21045b97d3d5cb79896", + "reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/framework-bundle": "^4.4|^5.0|^6.0", - "symfony/twig-bundle": "^4.4|^5.0|^6.0", - "twig/twig": "^2.7|^3.0" + "php": ">=8.1.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "twig/twig": "^3.2|^4.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", - "twig/cssinliner-extra": "^2.12|^3.0", - "twig/html-extra": "^2.12|^3.0", - "twig/inky-extra": "^2.12|^3.0", - "twig/intl-extra": "^2.12|^3.0", - "twig/markdown-extra": "^2.12|^3.0", - "twig/string-extra": "^2.12|^3.0" + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" }, "type": "symfony-bundle", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { "psr-4": { "Twig\\Extra\\TwigExtraBundle\\": "" @@ -12705,7 +14266,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.5.1" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.21.0" }, "funding": [ { @@ -12717,37 +14278,36 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:44:55+00:00" + "time": "2025-02-19T14:29:33+00:00" }, { "name": "twig/html-extra", - "version": "v3.5.1", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/html-extra.git", - "reference": "fc65dafbf8e3e319a8666e5d5437cbd404ecc496" + "reference": "5442dd707601c83b8cd4233e37bb10ab8489a90f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/html-extra/zipball/fc65dafbf8e3e319a8666e5d5437cbd404ecc496", - "reference": "fc65dafbf8e3e319a8666e5d5437cbd404ecc496", + "url": "https://api.github.com/repos/twigphp/html-extra/zipball/5442dd707601c83b8cd4233e37bb10ab8489a90f", + "reference": "5442dd707601c83b8cd4233e37bb10ab8489a90f", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/mime": "^4.4|^5.0|^6.0", - "twig/twig": "^2.7|^3.0" + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/mime": "^5.4|^6.4|^7.0", + "twig/twig": "^3.13|^4.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Twig\\Extra\\Html\\": "" }, @@ -12774,7 +14334,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/html-extra/tree/v3.5.1" + "source": "https://github.com/twigphp/html-extra/tree/v3.21.0" }, "funding": [ { @@ -12786,37 +14346,36 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:44:55+00:00" + "time": "2025-02-19T14:29:33+00:00" }, { "name": "twig/inky-extra", - "version": "v3.5.1", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/inky-extra.git", - "reference": "4efde499b99942a27e206898e55208a8692c16ca" + "reference": "aacd79d94534b4a7fd6533cb5c33c4ee97239a0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/4efde499b99942a27e206898e55208a8692c16ca", - "reference": "4efde499b99942a27e206898e55208a8692c16ca", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/aacd79d94534b4a7fd6533cb5c33c4ee97239a0d", + "reference": "aacd79d94534b4a7fd6533cb5c33c4ee97239a0d", "shasum": "" }, "require": { "lorenzo/pinky": "^1.0.5", - "php": ">=7.1.3", - "twig/twig": "^2.7|^3.0" + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "twig/twig": "^3.13|^4.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Twig\\Extra\\Inky\\": "" }, @@ -12845,7 +14404,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/inky-extra/tree/v3.5.1" + "source": "https://github.com/twigphp/inky-extra/tree/v3.21.0" }, "funding": [ { @@ -12857,36 +14416,31 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:44:55+00:00" + "time": "2025-01-31T20:45:36+00:00" }, { "name": "twig/intl-extra", - "version": "v3.5.1", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", - "reference": "c3ebfac1624228c0556de57a34af6b7d83a1a408" + "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/c3ebfac1624228c0556de57a34af6b7d83a1a408", - "reference": "c3ebfac1624228c0556de57a34af6b7d83a1a408", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/05bc5d46b9df9e62399eae53e7c0b0633298b146", + "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/intl": "^4.4|^5.0|^6.0", - "twig/twig": "^2.7|^3.0" + "php": ">=8.1.0", + "symfony/intl": "^5.4|^6.4|^7.0", + "twig/twig": "^3.13|^4.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { "psr-4": { "Twig\\Extra\\Intl\\": "" @@ -12914,7 +14468,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/intl-extra/tree/v3.5.1" + "source": "https://github.com/twigphp/intl-extra/tree/v3.21.0" }, "funding": [ { @@ -12926,40 +14480,39 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:44:55+00:00" + "time": "2025-01-31T20:45:36+00:00" }, { "name": "twig/markdown-extra", - "version": "v3.5.1", + "version": "v3.21.0", "source": { "type": "git", "url": "https://github.com/twigphp/markdown-extra.git", - "reference": "9474c89fd2787187a3809e5e964dfce2395ae5f0" + "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/9474c89fd2787187a3809e5e964dfce2395ae5f0", - "reference": "9474c89fd2787187a3809e5e964dfce2395ae5f0", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/f4616e1dd375209dacf6026f846e6b537d036ce4", + "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4", "shasum": "" }, "require": { - "php": ">=7.1.3", - "twig/twig": "^2.7|^3.0" + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "twig/twig": "^3.13|^4.0" }, "require-dev": { - "erusev/parsedown": "^1.7", + "erusev/parsedown": "dev-master as 1.x-dev", "league/commonmark": "^1.0|^2.0", "league/html-to-markdown": "^4.8|^5.0", "michelf/php-markdown": "^1.8|^2.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Twig\\Extra\\Markdown\\": "" }, @@ -12987,7 +14540,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/markdown-extra/tree/v3.5.1" + "source": "https://github.com/twigphp/markdown-extra/tree/v3.21.0" }, "funding": [ { @@ -12999,38 +14552,108 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:44:55+00:00" + "time": "2025-01-31T20:45:36+00:00" }, { - "name": "twig/twig", - "version": "v3.5.1", + "name": "twig/string-extra", + "version": "v3.21.0", "source": { "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15" + "url": "https://github.com/twigphp/string-extra.git", + "reference": "4b3337544ac8f76c280def94e32b53acfaec0589" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6e0510cc793912b451fd40ab983a1d28f611c15", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/4b3337544ac8f76c280def94e32b53acfaec0589", + "reference": "4b3337544ac8f76c280def94e32b53acfaec0589", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1.0", + "symfony/string": "^5.4|^6.4|^7.0", + "symfony/translation-contracts": "^1.1|^2|^3", + "twig/twig": "^3.13|^4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for Symfony String", + "homepage": "https://twig.symfony.com", + "keywords": [ + "html", + "string", + "twig", + "unicode" + ], + "support": { + "source": "https://github.com/twigphp/string-extra/tree/v3.21.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-01-31T20:45:36+00:00" + }, + { + "name": "twig/twig", + "version": "v3.21.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -13063,7 +14686,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.5.1" + "source": "https://github.com/twigphp/Twig/tree/v3.21.1" }, "funding": [ { @@ -13075,7 +14698,7 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:49:20+00:00" + "time": "2025-05-03T07:21:55+00:00" }, { "name": "ua-parser/uap-php", @@ -13142,25 +14765,43 @@ }, { "name": "web-auth/cose-lib", - "version": "v3.3.12", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/web-auth/cose-lib.git", - "reference": "efa6ec2ba4e840bc1316a493973c9916028afeeb" + "reference": "2166016e48e0214f4f63320a7758a9386d14c92a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/efa6ec2ba4e840bc1316a493973c9916028afeeb", - "reference": "efa6ec2ba4e840bc1316a493973c9916028afeeb", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/2166016e48e0214f4f63320a7758a9386d14c92a", + "reference": "2166016e48e0214f4f63320a7758a9386d14c92a", "shasum": "" }, "require": { - "beberlei/assert": "^3.2", + "brick/math": "^0.9|^0.10|^0.11|^0.12", "ext-json": "*", - "ext-mbstring": "*", "ext-openssl": "*", - "fgrosse/phpasn1": "^2.1", - "php": ">=7.2" + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.29", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^10.1|^11.0", + "qossmic/deptrac": "^2.0", + "rector/rector": "^1.0", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension" }, "type": "library", "autoload": { @@ -13189,7 +14830,8 @@ "RFC8152" ], "support": { - "source": "https://github.com/web-auth/cose-lib/tree/v3.3.12" + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.4.0" }, "funding": [ { @@ -13201,118 +14843,57 @@ "type": "patreon" } ], - "time": "2021-12-04T12:13:35+00:00" - }, - { - "name": "web-auth/metadata-service", - "version": "v3.3.12", - "source": { - "type": "git", - "url": "https://github.com/web-auth/webauthn-metadata-service.git", - "reference": "ef40d2b7b68c4964247d13fab52e2fa8dbd65246" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-metadata-service/zipball/ef40d2b7b68c4964247d13fab52e2fa8dbd65246", - "reference": "ef40d2b7b68c4964247d13fab52e2fa8dbd65246", - "shasum": "" - }, - "require": { - "beberlei/assert": "^3.2", - "ext-json": "*", - "league/uri": "^6.0", - "php": ">=7.2", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/log": "^1.1" - }, - "suggest": { - "web-token/jwt-key-mgmt": "Mandatory for fetching Metadata Statement from distant sources", - "web-token/jwt-signature-algorithm-ecdsa": "Mandatory for fetching Metadata Statement from distant sources" - }, - "type": "library", - "autoload": { - "psr-4": { - "Webauthn\\MetadataService\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-auth/metadata-service/contributors" - } - ], - "description": "Metadata Service for FIDO2/Webauthn", - "homepage": "https://github.com/web-auth", - "keywords": [ - "FIDO2", - "fido", - "webauthn" - ], - "support": { - "source": "https://github.com/web-auth/webauthn-metadata-service/tree/v3.3.12" - }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - }, - { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" - } - ], - "time": "2021-11-21T11:14:31+00:00" + "time": "2024-07-18T08:47:32+00:00" }, { "name": "web-auth/webauthn-lib", - "version": "v3.3.12", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "5ef9b21c8e9f8a817e524ac93290d08a9f065b33" + "reference": "008b25171c27cf4813420d0de31cc059bcc71f1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/5ef9b21c8e9f8a817e524ac93290d08a9f065b33", - "reference": "5ef9b21c8e9f8a817e524ac93290d08a9f065b33", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/008b25171c27cf4813420d0de31cc059bcc71f1a", + "reference": "008b25171c27cf4813420d0de31cc059bcc71f1a", "shasum": "" }, "require": { - "beberlei/assert": "^3.2", "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", - "fgrosse/phpasn1": "^2.1", - "php": ">=7.2", + "lcobucci/clock": "^2.2|^3.0", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "psr/log": "^1.1", - "ramsey/uuid": "^3.8|^4.0", - "spomky-labs/base64url": "^2.0", - "spomky-labs/cbor-php": "^1.0|^2.0", - "symfony/process": "^3.0|^4.0|^5.0", - "thecodingmachine/safe": "^1.1", - "web-auth/cose-lib": "self.version", - "web-auth/metadata-service": "self.version" + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "spomky-labs/pki-framework": "^1.0", + "symfony/deprecation-contracts": "^3.2", + "symfony/uid": "^6.1|^7.0", + "web-auth/cose-lib": "^4.2.3" }, "suggest": { + "phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement", + "psr/clock-implementation": "As of 4.5.x, the PSR Clock implementation will replace lcobucci/clock", "psr/log-implementation": "Recommended to receive logs from the library", - "web-token/jwt-key-mgmt": "Mandatory for the AndroidSafetyNet Attestation Statement support", - "web-token/jwt-signature-algorithm-ecdsa": "Recommended for the AndroidSafetyNet Attestation Statement support", - "web-token/jwt-signature-algorithm-eddsa": "Recommended for the AndroidSafetyNet Attestation Statement support", - "web-token/jwt-signature-algorithm-rsa": "Mandatory for the AndroidSafetyNet Attestation Statement support" + "symfony/event-dispatcher": "Recommended to use dispatched events", + "symfony/property-access": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/property-info": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/web-auth/webauthn-framework", + "name": "web-auth/webauthn-framework" + } + }, "autoload": { "psr-4": { "Webauthn\\": "src/" @@ -13340,7 +14921,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/v3.3.12" + "source": "https://github.com/web-auth/webauthn-lib/tree/4.9.2" }, "funding": [ { @@ -13352,31 +14933,49 @@ "type": "patreon" } ], - "time": "2022-02-18T07:13:44+00:00" + "time": "2025-01-04T09:47:58+00:00" }, { "name": "web-auth/webauthn-symfony-bundle", - "version": "v3.3.12", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-symfony-bundle.git", - "reference": "15f2091dc351f190d27a377a0dbbc117e6be5329" + "reference": "80aa16fa6f16ab8f017a4108ffcd2ecc12264c07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/15f2091dc351f190d27a377a0dbbc117e6be5329", - "reference": "15f2091dc351f190d27a377a0dbbc117e6be5329", + "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/80aa16fa6f16ab8f017a4108ffcd2ecc12264c07", + "reference": "80aa16fa6f16ab8f017a4108ffcd2ecc12264c07", "shasum": "" }, "require": { - "spomky-labs/cbor-bundle": "^2.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/framework-bundle": "^4.4|^5.0", + "nyholm/psr7": "^1.5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^5.3", + "psr/event-dispatcher": "^1.0", + "symfony/config": "^6.1|^7.0", + "symfony/dependency-injection": "^6.1|^7.0", + "symfony/framework-bundle": "^6.1|^7.0", + "symfony/http-client": "^6.1|^7.0", + "symfony/property-access": "^6.1|^7.0", + "symfony/property-info": "^6.1|^7.0", + "symfony/psr-http-message-bridge": "^2.1|^6.1|^7.0", + "symfony/security-bundle": "^6.1|^7.0", + "symfony/security-core": "^6.1|^7.0", + "symfony/security-http": "^6.1|^7.0", + "symfony/serializer": "^6.1|^7.0", + "symfony/validator": "^6.1|^7.0", "web-auth/webauthn-lib": "self.version", - "web-token/jwt-signature": "^2.0.9" + "web-token/jwt-library": "^3.3|^4.0" }, "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/web-auth/webauthn-framework", + "name": "web-auth/webauthn-framework" + } + }, "autoload": { "psr-4": { "Webauthn\\Bundle\\": "src/" @@ -13400,11 +14999,14 @@ "homepage": "https://github.com/web-auth", "keywords": [ "FIDO2", + "bundle", "fido", + "symfony", + "symfony-bundle", "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/v3.3.12" + "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/4.9.2" }, "funding": [ { @@ -13416,37 +15018,54 @@ "type": "patreon" } ], - "time": "2021-11-21T11:14:31+00:00" + "time": "2025-01-04T09:38:56+00:00" }, { - "name": "web-token/jwt-core", - "version": "v2.2.11", + "name": "web-token/jwt-library", + "version": "3.4.8", "source": { "type": "git", - "url": "https://github.com/web-token/jwt-core.git", - "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678" + "url": "https://github.com/web-token/jwt-library.git", + "reference": "92445671cc788fa5f639898a67c06f9fd0bf491f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-token/jwt-core/zipball/53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", - "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", + "url": "https://api.github.com/repos/web-token/jwt-library/zipball/92445671cc788fa5f639898a67c06f9fd0bf491f", + "reference": "92445671cc788fa5f639898a67c06f9fd0bf491f", "shasum": "" }, "require": { - "brick/math": "^0.8.17|^0.9", + "brick/math": "^0.9|^0.10|^0.11|^0.12", "ext-json": "*", "ext-mbstring": "*", - "fgrosse/phpasn1": "^2.0", - "php": ">=7.2", - "spomky-labs/base64url": "^1.0|^2.0" + "paragonie/constant_time_encoding": "^2.6|^3.0", + "paragonie/sodium_compat": "^1.20|^2.0", + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", + "psr/clock": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "spomky-labs/pki-framework": "^1.2.1", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.12" }, "conflict": { "spomky-labs/jose": "*" }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance", + "ext-openssl": "For key management (creation, optimization, etc.) and some algorithms (AES, RSA, ECDSA, etc.)", + "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", + "paragonie/sodium_compat": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", + "spomky-labs/aes-key-wrap": "For all Key Wrapping algorithms (A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW, PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW...)", + "symfony/http-client": "To enable JKU/X5U support." + }, "type": "library", "autoload": { "psr-4": { - "Jose\\Component\\Core\\": "" + "Jose\\Component\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -13463,7 +15082,7 @@ "homepage": "https://github.com/web-token/jwt-framework/contributors" } ], - "description": "Core component of the JWT Framework.", + "description": "JWT library", "homepage": "https://github.com/web-token", "keywords": [ "JOSE", @@ -13484,91 +15103,20 @@ "symfony" ], "support": { - "source": "https://github.com/web-token/jwt-core/tree/v2.2.11" + "issues": "https://github.com/web-token/jwt-library/issues", + "source": "https://github.com/web-token/jwt-library/tree/3.4.8" }, "funding": [ { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" - } - ], - "time": "2021-03-17T14:55:52+00:00" - }, - { - "name": "web-token/jwt-signature", - "version": "v2.2.11", - "source": { - "type": "git", - "url": "https://github.com/web-token/jwt-signature.git", - "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", - "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", - "shasum": "" - }, - "require": { - "web-token/jwt-core": "^2.1" - }, - "suggest": { - "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", - "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", - "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms", - "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", - "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", - "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms" - }, - "type": "library", - "autoload": { - "psr-4": { - "Jose\\Component\\Signature\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" + "url": "https://github.com/Spomky", + "type": "github" }, - { - "name": "All contributors", - "homepage": "https://github.com/web-token/jwt-signature/contributors" - } - ], - "description": "Signature component of the JWT Framework.", - "homepage": "https://github.com/web-token", - "keywords": [ - "JOSE", - "JWE", - "JWK", - "JWKSet", - "JWS", - "Jot", - "RFC7515", - "RFC7516", - "RFC7517", - "RFC7518", - "RFC7519", - "RFC7520", - "bundle", - "jwa", - "jwt", - "symfony" - ], - "support": { - "source": "https://github.com/web-token/jwt-signature/tree/v2.2.11" - }, - "funding": [ { "url": "https://www.patreon.com/FlorentMorselli", "type": "patreon" } ], - "time": "2021-03-01T19:55:28+00:00" + "time": "2025-05-07T09:11:18+00:00" }, { "name": "webmozart/assert", @@ -13627,429 +15175,100 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "willdurand/negotiation", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Negotiation.git", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Negotiation\\": "src/Negotiation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + } + ], + "description": "Content Negotiation tools for PHP provided as a standalone library.", + "homepage": "http://williamdurand.fr/Negotiation/", + "keywords": [ + "accept", + "content", + "format", + "header", + "negotiation" + ], + "support": { + "issues": "https://github.com/willdurand/Negotiation/issues", + "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" + }, + "time": "2022-01-30T20:08:53+00:00" } ], "packages-dev": [ - { - "name": "amphp/amp", - "version": "v2.6.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^7 | ^8 | ^9", - "psalm/phar": "^3.11@dev", - "react/promise": "^2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "files": [ - "lib/functions.php", - "lib/Internal/functions.php" - ], - "psr-4": { - "Amp\\": "lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", - "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" - ], - "support": { - "irc": "irc://irc.freenode.org/amphp", - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2022-02-20T17:52:18+00:00" - }, - { - "name": "amphp/byte-stream", - "version": "v1.8.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", - "shasum": "" - }, - "require": { - "amphp/amp": "^2", - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1.4", - "friendsofphp/php-cs-fixer": "^2.3", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6 || ^7 || ^8", - "psalm/phar": "^3.11.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "files": [ - "lib/functions.php" - ], - "psr-4": { - "Amp\\ByteStream\\": "lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "http://amphp.org/byte-stream", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" - ], - "support": { - "irc": "irc://irc.freenode.org/amphp", - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2021-03-30T17:13:30+00:00" - }, - { - "name": "composer/pcre", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-11-17T09:50:14+00:00" - }, - { - "name": "composer/semver", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-04-01T19:23:25+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", - "shasum": "" - }, - "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" - }, - "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Composer\\XdebugHandler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", - "keywords": [ - "Xdebug", - "performance" - ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-02-25T21:32:43+00:00" - }, { "name": "dama/doctrine-test-bundle", - "version": "v7.2.1", + "version": "v8.2.2", "source": { "type": "git", "url": "https://github.com/dmaicher/doctrine-test-bundle.git", - "reference": "175b47153609a369117d97d36049b8a8c3b69dc1" + "reference": "eefe54fdf00d910f808efea9cfce9cc261064a0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/175b47153609a369117d97d36049b8a8c3b69dc1", - "reference": "175b47153609a369117d97d36049b8a8c3b69dc1", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/eefe54fdf00d910f808efea9cfce9cc261064a0a", + "reference": "eefe54fdf00d910f808efea9cfce9cc261064a0a", "shasum": "" }, "require": { - "doctrine/dbal": "^3.3", - "doctrine/doctrine-bundle": "^2.2.2", - "ext-json": "*", - "php": "^7.3 || ^8.0", + "doctrine/dbal": "^3.3 || ^4.0", + "doctrine/doctrine-bundle": "^2.11.0", + "php": "^7.4 || ^8.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^5.4 || ^6.0", - "symfony/framework-bundle": "^5.4 || ^6.0" + "symfony/cache": "^5.4 || ^6.3 || ^7.0", + "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0" }, "require-dev": { "behat/behat": "^3.0", - "doctrine/cache": "^1.12", - "phpstan/phpstan": "^1.2", - "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0", - "symfony/phpunit-bridge": "^6.0", - "symfony/process": "^5.4 || ^6.0", - "symfony/yaml": "^5.4 || ^6.0" + "friendsofphp/php-cs-fixer": "^3.27", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "symfony/phpunit-bridge": "^7.2", + "symfony/process": "^5.4 || ^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.3 || ^7.0" }, "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "8.x-dev" } }, "autoload": { @@ -14073,170 +15292,55 @@ "isolation", "performance", "symfony", + "testing", "tests" ], "support": { "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", - "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v7.2.1" + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.2.2" }, - "time": "2023-02-07T10:02:27+00:00" - }, - { - "name": "dnoegel/php-xdg-base-dir", - "version": "v0.1.1", - "source": { - "type": "git", - "url": "https://github.com/dnoegel/php-xdg-base-dir.git", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", - "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "XdgBaseDir\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "implementation of xdg base directory specification for php", - "support": { - "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", - "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" - }, - "time": "2019-12-04T15:06:13+00:00" - }, - { - "name": "doctrine/data-fixtures", - "version": "1.6.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/data-fixtures.git", - "reference": "e6b97f557942ea17564bbc30ae3ebc9bd2209363" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/e6b97f557942ea17564bbc30ae3ebc9bd2209363", - "reference": "e6b97f557942ea17564bbc30ae3ebc9bd2209363", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^0.5.3 || ^1.0", - "doctrine/persistence": "^1.3.3 || ^2.0 || ^3.0", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "doctrine/dbal": "<2.13", - "doctrine/orm": "<2.12", - "doctrine/phpcr-odm": "<1.3.0" - }, - "require-dev": { - "doctrine/coding-standard": "^11.0", - "doctrine/dbal": "^2.13 || ^3.0", - "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", - "doctrine/orm": "^2.12", - "ext-sqlite3": "*", - "phpstan/phpstan": "^1.5", - "phpunit/phpunit": "^8.5 || ^9.5 || ^10.0", - "symfony/cache": "^5.0 || ^6.0", - "vimeo/psalm": "^4.10 || ^5.9" - }, - "suggest": { - "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", - "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", - "doctrine/orm": "For loading ORM fixtures", - "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\DataFixtures\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - } - ], - "description": "Data Fixtures for all Doctrine Object Managers", - "homepage": "https://www.doctrine-project.org", - "keywords": [ - "database" - ], - "support": { - "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/1.6.5" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", - "type": "tidelift" - } - ], - "time": "2023-04-03T14:58:58+00:00" + "time": "2025-02-04T14:37:36+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.4.2", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5" + "reference": "a06db6b81ff20a2980bf92063d80c013bb8b4b7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/601988c5b46dbd20a0f886f967210aba378a6fd5", - "reference": "601988c5b46dbd20a0f886f967210aba378a6fd5", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/a06db6b81ff20a2980bf92063d80c013bb8b4b7c", + "reference": "a06db6b81ff20a2980bf92063d80c013bb8b4b7c", "shasum": "" }, "require": { - "doctrine/data-fixtures": "^1.3", - "doctrine/doctrine-bundle": "^1.11|^2.0", - "doctrine/orm": "^2.6.0", - "doctrine/persistence": "^1.3.7|^2.0|^3.0", - "php": "^7.1 || ^8.0", - "symfony/config": "^3.4|^4.3|^5.0|^6.0", - "symfony/console": "^3.4|^4.3|^5.0|^6.0", - "symfony/dependency-injection": "^3.4.47|^4.3|^5.0|^6.0", - "symfony/doctrine-bridge": "^3.4|^4.1|^5.0|^6.0", - "symfony/http-kernel": "^3.4|^4.3|^5.0|^6.0" + "doctrine/data-fixtures": "^2.0", + "doctrine/doctrine-bundle": "^2.2", + "doctrine/orm": "^2.14.0 || ^3.0", + "doctrine/persistence": "^2.4 || ^3.0 || ^4.0", + "php": "^8.1", + "psr/log": "^2 || ^3", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^6.4.16 || ^7.1.9", + "symfony/http-kernel": "^6.4 || ^7.0" + }, + "conflict": { + "doctrine/dbal": "< 3" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "^1.4.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", - "symfony/phpunit-bridge": "^6.0.8", - "vimeo/psalm": "^4.22" + "doctrine/coding-standard": "13.0.0", + "phpstan/phpstan": "2.1.11", + "phpunit/phpunit": "^10.5.38 || 11.4.14" }, "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\FixturesBundle\\": "" + "Doctrine\\Bundle\\FixturesBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -14265,7 +15369,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.4.2" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.1.0" }, "funding": [ { @@ -14281,43 +15385,43 @@ "type": "tidelift" } ], - "time": "2022-04-28T17:58:29+00:00" + "time": "2025-03-26T10:56:26+00:00" }, { "name": "ekino/phpstan-banned-code", - "version": "v1.0.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/ekino/phpstan-banned-code.git", - "reference": "4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3" + "reference": "27122aa1783d6521e500c0c397c53244cfbde26f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ekino/phpstan-banned-code/zipball/4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3", - "reference": "4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3", + "url": "https://api.github.com/repos/ekino/phpstan-banned-code/zipball/27122aa1783d6521e500c0c397c53244cfbde26f", + "reference": "27122aa1783d6521e500c0c397c53244cfbde26f", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^8.1", + "phpstan/phpstan": "^2.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.6", "friendsofphp/php-cs-fixer": "^3.0", "nikic/php-parser": "^4.3", - "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "symfony/var-dumper": "^5.0" }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-master": "1.0-dev" } }, "autoload": { @@ -14340,147 +15444,50 @@ "homepage": "https://github.com/ekino/phpstan-banned-code", "keywords": [ "PHPStan", - "code quality" + "code quality", + "static analysis" ], "support": { "issues": "https://github.com/ekino/phpstan-banned-code/issues", - "source": "https://github.com/ekino/phpstan-banned-code/tree/v1.0.0" + "source": "https://github.com/ekino/phpstan-banned-code/tree/v3.0.0" }, - "time": "2021-11-02T08:37:34+00:00" + "time": "2024-11-13T09:57:22+00:00" }, { - "name": "felixfbecker/advanced-json-rpc", - "version": "v3.2.1", + "name": "jbtronics/translation-editor-bundle", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + "url": "https://github.com/jbtronics/translation-editor-bundle.git", + "reference": "fa003c38f3f61060a368734b91aeb40d788b0825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "url": "https://api.github.com/repos/jbtronics/translation-editor-bundle/zipball/fa003c38f3f61060a368734b91aeb40d788b0825", + "reference": "fa003c38f3f61060a368734b91aeb40d788b0825", "shasum": "" }, "require": { - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "php": "^7.1 || ^8.0", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + "ext-json": "*", + "php": "^8.1", + "symfony/deprecation-contracts": "^3.4", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/translation": "^7.0|^6.4", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/twig-bundle": "^7.0|^6.4", + "symfony/web-profiler-bundle": "^7.0|^6.4" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^8.0" + "ekino/phpstan-banned-code": "^1.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-strict-rules": "^1.5", + "roave/security-advisories": "dev-latest" }, - "type": "library", + "type": "symfony-bundle", "autoload": { "psr-4": { - "AdvancedJsonRpc\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - } - ], - "description": "A more advanced JSONRPC implementation", - "support": { - "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", - "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" - }, - "time": "2021-06-11T22:34:44+00:00" - }, - { - "name": "felixfbecker/language-server-protocol", - "version": "v1.5.2", - "source": { - "type": "git", - "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpstan/phpstan": "*", - "squizlabs/php_codesniffer": "^3.1", - "vimeo/psalm": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "LanguageServerProtocol\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - } - ], - "description": "PHP classes for the Language Server Protocol", - "keywords": [ - "language", - "microsoft", - "php", - "server" - ], - "support": { - "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" - }, - "time": "2022-03-02T22:36:06+00:00" - }, - { - "name": "fidry/cpu-core-counter", - "version": "0.5.1", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623", - "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.26 || ^8.5.31", - "theofidry/php-cs-fixer-config": "^1.0", - "webmozarts/strict-phpunit": "^7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" + "Jbtronics\\TranslationEditorBundle\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -14489,96 +15496,287 @@ ], "authors": [ { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" + "name": "Jan Böhmer", + "email": "mail@jan-boehmer.de" } ], - "description": "Tiny utility to get the number of CPU cores.", + "description": "A symfony bundle to allow editing translations in the symfony profiler", "keywords": [ - "CPU", - "core" + "profiler", + "symfony", + "symfony-bundle", + "translations" ], "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1" + "issues": "https://github.com/jbtronics/translation-editor-bundle/issues", + "source": "https://github.com/jbtronics/translation-editor-bundle/tree/v1.1.1" }, "funding": [ { - "url": "https://github.com/theofidry", + "url": "https://www.paypal.me/do9jhb", + "type": "custom" + }, + { + "url": "https://github.com/jbtronics", "type": "github" } ], - "time": "2022-12-24T12:35:10+00:00" + "time": "2025-03-29T15:14:31+00:00" }, { - "name": "netresearch/jsonmapper", - "version": "v4.1.0", + "name": "myclabs/deep-copy", + "version": "1.13.1", "source": { "type": "git", - "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "cfa81ea1d35294d64adb9c68aa4cb9e92400e53f" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/cfa81ea1d35294d64adb9c68aa4cb9e92400e53f", - "reference": "cfa81ea1d35294d64adb9c68aa4cb9e92400e53f", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=7.1" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", - "squizlabs/php_codesniffer": "~3.5" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { - "psr-0": { - "JsonMapper": "src/" + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "OSL-3.0" + "MIT" ], - "authors": [ + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ { - "name": "Christian Weiske", - "email": "cweiske@cweiske.de", - "homepage": "http://github.com/cweiske/jsonmapper/", - "role": "Developer" + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" } ], - "description": "Map nested JSON structures onto PHP classes", - "support": { - "email": "cweiske@cweiske.de", - "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.1.0" - }, - "time": "2022-12-08T20:46:14+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { - "name": "phpstan/extension-installer", - "version": "1.2.0", + "name": "nikic/php-parser", + "version": "v5.4.0", "source": { "type": "git", - "url": "https://github.com/phpstan/extension-installer.git", - "reference": "f06dbb052ddc394e7896fcd1cfcd533f9f6ace40" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/f06dbb052ddc394e7896fcd1cfcd533f9f6ace40", - "reference": "f06dbb052ddc394e7896fcd1cfcd533f9f6ace40", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", "shasum": "" }, "require": { "composer-plugin-api": "^2.0", "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.0" + "phpstan/phpstan": "^1.9.0 || ^2.0" }, "require-dev": { "composer/composer": "^2.0", @@ -14599,28 +15797,32 @@ "MIT" ], "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.2.0" + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" }, - "time": "2022-10-17T12:59:16+00:00" + "time": "2024-09-04T20:21:43+00:00" }, { "name": "phpstan/phpstan", - "version": "1.10.11", + "version": "2.1.16", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21" + "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", - "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", + "reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -14659,31 +15861,27 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2023-04-04T19:17:42+00:00" + "time": "2025-05-16T09:40:10+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.37", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "62bd362b432fe29e175168689510ddd927b698f8" + "reference": "4497663eb17b9d29211830df5aceaa3a4d256a35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/62bd362b432fe29e175168689510ddd927b698f8", - "reference": "62bd362b432fe29e175168689510ddd927b698f8", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4497663eb17b9d29211830df5aceaa3a4d256a35", + "reference": "4497663eb17b9d29211830df5aceaa3a4d256a35", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" }, "conflict": { "doctrine/collections": "<1.0", @@ -14693,24 +15891,26 @@ "doctrine/persistence": "<1.3" }, "require-dev": { + "cache/array-adapter": "^1.1", "composer/semver": "^3.3.2", - "doctrine/annotations": "^1.11.0", - "doctrine/collections": "^1.6", + "cweagans/composer-patches": "^1.7.3", + "doctrine/annotations": "^2.0", + "doctrine/collections": "^1.6 || ^2.1", "doctrine/common": "^2.7 || ^3.0", - "doctrine/dbal": "^2.13.8 || ^3.3.3", - "doctrine/lexer": "^1.2.1", - "doctrine/mongodb-odm": "^1.3 || ^2.1", - "doctrine/orm": "^2.11.0", - "doctrine/persistence": "^1.3.8 || ^2.2.1", + "doctrine/dbal": "^3.3.8", + "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/mongodb-odm": "^2.4.3", + "doctrine/orm": "^2.16.0", + "doctrine/persistence": "^2.2.1 || ^3.2", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", - "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5.10", - "ramsey/uuid-doctrine": "^1.5.0", - "symfony/cache": "^4.4.35" + "phpstan/phpstan-deprecation-rules": "^2.0.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6.20", + "ramsey/uuid": "^4.2", + "symfony/cache": "^5.4" }, "type": "phpstan-extension", "extra": { @@ -14733,39 +15933,86 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.37" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.3" }, - "time": "2023-03-17T14:57:03+00:00" + "time": "2025-05-05T15:28:52+00:00" }, { - "name": "phpstan/phpstan-symfony", - "version": "1.2.25", + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3" + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/1da7bf450c6b351fec08ca0aa97298473d4f6ab3", - "reference": "1da7bf450c6b351fec08ca0aa97298473d4f6ab3", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.4" + }, + "time": "2025-03-18T11:42:40+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "5005288e07583546ea00b52de4a9ac412eb869d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/5005288e07583546ea00b52de4a9ac412eb869d7", + "reference": "5005288e07583546ea00b52de4a9ac412eb869d7", "shasum": "" }, "require": { "ext-simplexml": "*", - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.18" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" }, "conflict": { "symfony/framework-bundle": "<3.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^8.5.29 || ^9.5", - "psr/container": "1.0 || 1.1.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.1.2", "symfony/config": "^5.4 || ^6.1", "symfony/console": "^5.4 || ^6.1", "symfony/dependency-injection": "^5.4 || ^6.1", @@ -14774,7 +16021,8 @@ "symfony/http-foundation": "^5.4 || ^6.1", "symfony/messenger": "^5.4", "symfony/polyfill-php80": "^1.24", - "symfony/serializer": "^5.4" + "symfony/serializer": "^5.4", + "symfony/service-contracts": "^2.2.0" }, "type": "phpstan-extension", "extra": { @@ -14804,74 +16052,498 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.2.25" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.6" }, - "time": "2023-04-05T12:16:20+00:00" + "time": "2025-05-14T07:00:05+00:00" }, { - "name": "psalm/plugin-symfony", - "version": "v5.0.2", + "name": "phpunit/php-code-coverage", + "version": "9.2.32", "source": { "type": "git", - "url": "https://github.com/psalm/psalm-plugin-symfony.git", - "reference": "f017563fa40728a5a40628915774e7d3469a0cbd" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/psalm/psalm-plugin-symfony/zipball/f017563fa40728a5a40628915774e7d3469a0cbd", - "reference": "f017563fa40728a5a40628915774e7d3469a0cbd", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { - "ext-simplexml": "*", - "php": "^7.4 || ^8.0", - "symfony/framework-bundle": "^5.0 || ^6.0", - "vimeo/psalm": "^5.1" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "doctrine/annotations": "^1.8|^2", - "doctrine/orm": "^2.9", - "phpunit/phpunit": "~7.5 || ~9.5", - "symfony/cache-contracts": "^1.0 || ^2.0", - "symfony/console": "*", - "symfony/form": "^5.0 || ^6.0", - "symfony/messenger": "^5.0 || ^6.0", - "symfony/security-guard": "*", - "symfony/serializer": "^5.0 || ^6.0", - "symfony/validator": "*", - "twig/twig": "^2.10 || ^3.0", - "weirdan/codeception-psalm-module": "dev-master" + "phpunit/phpunit": "^9.6" }, "suggest": { - "weirdan/doctrine-psalm-plugin": "If Doctrine is used, it is recommended install this plugin" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, - "type": "psalm-plugin", + "type": "library", "extra": { - "psalm": { - "pluginClass": "Psalm\\SymfonyPsalmPlugin\\Plugin" + "branch-alias": { + "dev-main": "9.2.x-dev" } }, "autoload": { - "psr-4": { - "Psalm\\SymfonyPsalmPlugin\\": "src" + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.23", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-05-02T06:40:34+00:00" + }, + { + "name": "rector/rector", + "version": "2.0.16", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2", + "reference": "f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.14" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.0.16" + }, + "funding": [ { - "name": "Farhad Safarov", - "email": "farhad.safarov@gmail.com" + "url": "https://github.com/tomasvotruba", + "type": "github" } ], - "description": "Psalm Plugin for Symfony", - "support": { - "issues": "https://github.com/psalm/psalm-plugin-symfony/issues", - "source": "https://github.com/psalm/psalm-plugin-symfony/tree/v5.0.2" - }, - "time": "2023-03-22T20:58:14+00:00" + "time": "2025-05-12T16:37:16+00:00" }, { "name": "roave/security-advisories", @@ -14879,429 +16551,696 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "6efa800243b92a3601e0101b0333d45df35832a4" + "reference": "0cc529f6cf08a858fcb7a2c5617780fcdc20d1fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/6efa800243b92a3601e0101b0333d45df35832a4", - "reference": "6efa800243b92a3601e0101b0333d45df35832a4", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0cc529f6cf08a858fcb7a2c5617780fcdc20d1fe", + "reference": "0cc529f6cf08a858fcb7a2c5617780fcdc20d1fe", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", - "admidio/admidio": "<4.1.9", - "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", - "aheinze/cockpit": "<=2.2.1", + "adaptcms/adaptcms": "<=1.3", + "admidio/admidio": "<4.3.12", + "adodb/adodb-php": "<=5.22.8", + "aheinze/cockpit": "<2.2", + "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", + "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1", + "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7", + "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1", + "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", + "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "airesvsg/acf-to-rest-api": "<=3.1", "akaunting/akaunting": "<2.1.13", "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", - "alextselegidis/easyappointments": "<1.5", + "alextselegidis/easyappointments": "<=1.5.1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "amazing/media2click": ">=1,<1.3.3", + "ameos/ameos_tarteaucitron": "<1.2.23", "amphp/artax": "<1.0.6|>=2,<2.0.6", - "amphp/http": "<1.0.1", + "amphp/http": "<=1.7.2|>=2,<=2.1", "amphp/http-client": ">=4,<4.4", "anchorcms/anchor-cms": "<=0.12.7", "andreapollastri/cipi": "<=3.1.15", - "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<=1.0.1|>=2,<=2.2.4", + "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", + "aoe/restler": "<1.7.1", + "apache-solr-for-typo3/solr": "<2.8.3", "apereo/phpcas": "<1.6", - "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6|>=2.6,<2.7.10|>=3,<3.0.12|>=3.1,<3.1.3", - "appwrite/server-ce": "<0.11.1|>=0.12,<0.12.2", + "api-platform/core": "<3.4.17|>=4.0.0.0-alpha1,<4.0.22", + "api-platform/graphql": "<3.4.17|>=4.0.0.0-alpha1,<4.0.22", + "appwrite/server-ce": "<=1.2.1", "arc/web": "<3", "area17/twill": "<1.2.5|>=2,<2.5.3", - "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99", - "automad/automad": "<1.8", + "artesaos/seotools": "<0.17.2", + "asymmetricrypt/asymmetricrypt": "<9.9.99", + "athlon1600/php-proxy": "<=5.1", + "athlon1600/php-proxy-app": "<=3", + "athlon1600/youtube-downloader": "<=4", + "austintoddj/canvas": "<=3.4.2", + "auth0/auth0-php": ">=8.0.0.0-beta1,<8.14", + "auth0/login": "<7.17", + "auth0/symfony": "<5.4", + "auth0/wordpress": "<5.3", + "automad/automad": "<2.0.0.0-alpha5", + "automattic/jetpack": "<9.8", "awesome-support/awesome-support": "<=6.0.7", - "aws/aws-sdk-php": ">=3,<3.2.1", - "backdrop/backdrop": "<=1.23", + "aws/aws-sdk-php": "<3.288.1", + "azuracast/azuracast": "<0.18.3", + "b13/seo_basics": "<0.8.2", + "backdrop/backdrop": "<1.27.3|>=1.28,<1.28.2", + "backpack/crud": "<3.4.9", + "backpack/filemanager": "<2.0.2|>=3,<3.0.9", + "bacula-web/bacula-web": "<8.0.0.0-RC2-dev", "badaso/core": "<2.7", - "bagisto/bagisto": "<0.1.5", + "bagisto/bagisto": "<2.1", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", "barryvdh/laravel-translation-manager": "<0.6.2", "barzahlen/barzahlen-php": "<2.0.1", - "baserproject/basercms": "<4.7.5", + "baserproject/basercms": "<=5.1.1", "bassjobsen/bootstrap-3-typeahead": ">4.0.2", - "bigfork/silverstripe-form-capture": ">=3,<=3.1", - "billz/raspap-webgui": "<=2.6.6", + "bbpress/bbpress": "<2.6.5", + "bcosca/fatfree": "<3.7.2", + "bedita/bedita": "<4", + "bednee/cooluri": "<1.0.30", + "bigfork/silverstripe-form-capture": ">=3,<3.1.1", + "billz/raspap-webgui": "<=3.1.4", "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3", + "blueimp/jquery-file-upload": "==6.4.4", "bmarshall511/wordpress_zero_spam": "<5.2.13", "bolt/bolt": "<3.7.2", "bolt/core": "<=4.2", + "born05/craft-twofactorauthentication": "<3.3.4", "bottelet/flarepoint": "<2.2.1", + "bref/bref": "<2.1.17", "brightlocal/phpwhois": "<=4.2.5", "brotkrueml/codehighlight": "<2.7", "brotkrueml/schema": "<1.13.1|>=2,<2.5.1", "brotkrueml/typo3-matomo-integration": "<1.3.2", "buddypress/buddypress": "<7.2.1", "bugsnag/bugsnag-laravel": ">=2,<2.0.2", + "bvbmedia/multishop": "<2.0.39", "bytefury/crater": "<6.0.2", "cachethq/cachet": "<2.5.1", - "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10|= 1.3.7|>=4.1,<4.1.4", + "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", "cardgate/magento2": "<2.0.33", + "cardgate/woocommerce": "<=3.1.15", "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4", "cartalyst/sentry": "<=2.1.6", "catfan/medoo": "<1.7.5", - "centreon/centreon": "<22.10-beta.1", + "causal/oidc": "<4", + "cecil/cecil": "<7.47.1", + "centreon/centreon": "<22.10.15", "cesnet/simplesamlphp-module-proxystatistics": "<3.1", - "cockpit-hq/cockpit": "<2.4.1", + "chriskacerguis/codeigniter-restserver": "<=2.7.1", + "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", + "ckeditor/ckeditor": "<4.25", + "clickstorm/cs-seo": ">=6,<6.7|>=7,<7.4|>=8,<8.3|>=9,<9.2", + "co-stack/fal_sftp": "<0.2.6", + "cockpit-hq/cockpit": "<2.7|==2.7", "codeception/codeception": "<3.1.3|>=4,<4.1.22", - "codeigniter/framework": "<=3.0.6", - "codeigniter4/framework": "<4.2.11", - "codeigniter4/shield": "<1-beta.4|= 1.0.0-beta", + "codeigniter/framework": "<3.1.9", + "codeigniter4/framework": "<4.5.8", + "codeigniter4/shield": "<1.0.0.0-beta8", "codiad/codiad": "<=2.8.4", - "composer/composer": "<1.10.26|>=2-alpha.1,<2.2.12|>=2.3,<2.3.5", - "concrete5/concrete5": "<=9.1.3|>= 9.0.0RC1, < 9.1.3", + "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9", + "commerceteam/commerce": ">=0.9.6,<0.9.9", + "components/jquery": ">=1.0.3,<3.5", + "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7", + "concrete5/concrete5": "<9.4.0.0-RC2-dev", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", - "contao/contao": ">=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3", - "contao/core": ">=2,<3.5.39", - "contao/core-bundle": "<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3|= 4.10.0", - "contao/listing-bundle": ">=4,<4.4.8", + "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", + "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.9.40|>=4.10,<4.11.7|>=4.13,<4.13.21|>=5.1,<5.1.4", + "contao/core": "<3.5.39", + "contao/core-bundle": "<4.13.54|>=5,<5.3.30|>=5.4,<5.5.6", + "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", "contao/managed-edition": "<=1.5", - "craftcms/cms": "<3.7.64|>= 4.0.0-RC1, < 4.3.7|>= 4.0.0-RC1, < 4.2.1", - "croogo/croogo": "<3.0.7", + "corveda/phpsandbox": "<1.3.5", + "cosenary/instagram": "<=2.3", + "craftcms/cms": "<4.15.3|>=5,<5.7.5", + "croogo/croogo": "<4", "cuyz/valinor": "<0.12", + "czim/file-handling": "<1.5|>=2,<2.3", "czproject/git-php": "<4.0.3", + "damienharper/auditor-bundle": "<5.2.6", + "dapphp/securimage": "<3.6.6", "darylldoyle/safe-svg": "<1.9.10", "datadog/dd-trace": ">=0.30,<0.30.2", + "datatables/datatables": "<1.10.10", "david-garcia/phpwhois": "<=4.3.1", "dbrisinajumi/d2files": "<1", + "dcat/laravel-admin": "<=2.1.3|==2.2.0.0-beta|==2.2.2.0-beta", "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", - "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1", - "directmailteam/direct-mail": "<5.2.4", - "doctrine/annotations": ">=1,<1.2.7", + "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", + "desperado/xml-bundle": "<=0.1.7", + "dev-lancer/minecraft-motd-parser": "<=1.0.5", + "devgroup/dotplant": "<2020.09.14-dev", + "digimix/wp-svg-upload": "<=1", + "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "dl/yag": "<3.0.1", + "dmk/webkitpdf": "<1.1.4", + "dnadesign/silverstripe-elemental": "<5.3.12", + "doctrine/annotations": "<1.2.7", "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", - "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1", + "doctrine/common": "<2.4.3|>=2.5,<2.5.1", "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4", "doctrine/doctrine-bundle": "<1.5.2", - "doctrine/doctrine-module": "<=0.7.1", - "doctrine/mongodb-odm": ">=1,<1.0.2", - "doctrine/mongodb-odm-bundle": ">=2,<3.0.1", - "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", - "dolibarr/dolibarr": "<16|>=16.0.1,<16.0.3|= 12.0.5|>= 3.3.beta1, < 13.0.2", - "dompdf/dompdf": "<2.0.2|= 2.0.2", - "drupal/core": ">=7,<7.91|>=8,<9.3.19|>=9.4,<9.4.3", - "drupal/drupal": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4", + "doctrine/doctrine-module": "<0.7.2", + "doctrine/mongodb-odm": "<1.0.2", + "doctrine/mongodb-odm-bundle": "<3.0.1", + "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", + "dolibarr/dolibarr": "<19.0.2|==21.0.0.0-beta", + "dompdf/dompdf": "<2.0.4", + "doublethreedigital/guest-entries": "<3.1.2", + "drupal/ai": "<1.0.5", + "drupal/alogin": "<2.0.6", + "drupal/cache_utility": "<1.2.1", + "drupal/config_split": "<1.10|>=2,<2.0.2", + "drupal/core": ">=6,<6.38|>=7,<7.102|>=8,<10.3.14|>=10.4,<10.4.5|>=11,<11.0.13|>=11.1,<11.1.5", + "drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/formatter_suite": "<2.1", + "drupal/gdpr": "<3.0.1|>=3.1,<3.1.2", + "drupal/google_tag": "<1.8|>=2,<2.0.8", + "drupal/ignition": "<1.0.4", + "drupal/link_field_display_mode_formatter": "<1.6", + "drupal/matomo": "<1.24", + "drupal/oauth2_client": "<4.1.3", + "drupal/oauth2_server": "<2.1", + "drupal/obfuscate": "<2.0.1", + "drupal/rapidoc_elements_field_formatter": "<1.0.1", + "drupal/spamspan": "<3.2.1", + "drupal/tfa": "<1.10", + "duncanmcclean/guest-entries": "<3.1.2", "dweeves/magmi": "<=0.7.24", + "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", "ecodev/newsletter": "<=4", "ectouch/ectouch": "<=2.7.2", - "elefant/cms": "<1.3.13", + "egroupware/egroupware": "<23.1.20240624", + "elefant/cms": "<2.0.7", "elgg/elgg": "<3.3.24|>=4,<4.0.5", + "elijaa/phpmemcacheadmin": "<=1.3", "encore/laravel-admin": "<=1.8.19", "endroid/qr-code-bundle": "<3.4.2", + "enhavo/enhavo-app": "<=0.13.1", "enshrined/svg-sanitize": "<0.15", "erusev/parsedown": "<1.7.2", "ether/logs": "<3.0.4", + "evolutioncms/evolution": "<=3.2.3", "exceedone/exment": "<4.4.3|>=5,<5.0.3", - "exceedone/laravel-admin": "= 3.0.0|<2.2.3", - "ezsystems/demobundle": ">=5.4,<5.4.6.1", + "exceedone/laravel-admin": "<2.2.3|==3", + "ezsystems/demobundle": ">=5.4,<5.4.6.1-dev", "ezsystems/ez-support-tools": ">=2.2,<2.2.3", - "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1", - "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1", + "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1-dev", + "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1-dev|>=5.4,<5.4.11.1-dev|>=2017.12,<2017.12.0.1-dev", "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24", - "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.26", + "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.26|>=3.3,<3.3.39", "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1", - "ezsystems/ezplatform-graphql": ">=1-rc.1,<1.0.13|>=2-beta.1,<2.3.12", - "ezsystems/ezplatform-kernel": "<1.2.5.1|>=1.3,<1.3.26", + "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", + "ezsystems/ezplatform-http-cache": "<2.3.16", + "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35", "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", - "ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1", + "ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40", + "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", "ezsystems/ezplatform-user": ">=1,<1.0.1", - "ezsystems/ezpublish-kernel": "<6.13.8.2|>=7,<7.5.30", - "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.3.5.1", + "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", + "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", - "ezsystems/repository-forms": ">=2.3,<2.3.2.1|>=2.5,<2.5.15", - "ezyang/htmlpurifier": "<4.1.1", + "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", + "ezyang/htmlpurifier": "<=4.2", "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", - "facturascripts/facturascripts": "<=2022.8", + "facturascripts/facturascripts": "<=2022.08", + "fastly/magento2": "<1.2.26", "feehi/cms": "<=2.1.1", "feehi/feehicms": "<=2.1.1", "fenom/fenom": "<=2.12.1", + "filament/actions": ">=3.2,<3.2.123", + "filament/infolists": ">=3,<3.2.115", + "filament/tables": ">=3,<3.2.115", "filegator/filegator": "<7.8", + "filp/whoops": "<2.1.13", + "fineuploader/php-traditional-server": "<=1.2.2", "firebase/php-jwt": "<6", + "fisharebest/webtrees": "<=2.1.18", "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", - "fixpunkt/fp-newsletter": "<1.1.1|>=2,<2.1.2|>=2.2,<3.2.6", - "flarum/core": "<1.7", + "fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6", + "flarum/core": "<1.8.10", + "flarum/flarum": "<0.1.0.0-beta8", + "flarum/framework": "<1.8.10", "flarum/mentions": "<1.6.3", - "flarum/sticky": ">=0.1-beta.14,<=0.1-beta.15", - "flarum/tags": "<=0.1-beta.13", + "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", + "flarum/tags": "<=0.1.0.0-beta13", + "floriangaerber/magnesium": "<0.3.1", "fluidtypo3/vhs": "<5.1.1", - "fof/byobu": ">=0.3-beta.2,<1.1.7", + "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", "fof/upload": "<1.2.3", + "foodcoopshop/foodcoopshop": ">=3.2,<3.6.1", "fooman/tcpdf": "<6.2.22", "forkcms/forkcms": "<5.11.1", "fossar/tcpdf-parser": "<6.2.22", - "francoisjacquet/rosariosis": "<10.8.2", + "francoisjacquet/rosariosis": "<=11.5.1", "frappant/frp-form-answers": "<3.1.2|>=4,<4.0.2", "friendsofsymfony/oauth2-php": "<1.3", "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", - "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", + "friendsofsymfony/user-bundle": ">=1,<1.3.5", + "friendsofsymfony1/swiftmailer": ">=4,<5.4.13|>=6,<6.2.5", + "friendsofsymfony1/symfony1": ">=1.1,<1.5.19", "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", - "froala/wysiwyg-editor": "<3.2.7", - "froxlor/froxlor": "<2.0.13", + "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", + "froala/wysiwyg-editor": "<=4.3", + "froxlor/froxlor": "<=2.2.5", + "frozennode/administrator": "<=5.0.12", "fuel/core": "<1.8.1", - "funadmin/funadmin": "<=3.2", + "funadmin/funadmin": "<=5.0.2", "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", - "getgrav/grav": "<1.7.34", - "getkirby/cms": "= 3.8.0|<3.5.8.2|>=3.6,<3.6.6.2|>=3.7,<3.7.5.1", + "georgringer/news": "<1.3.3", + "geshi/geshi": "<1.0.8.11-dev", + "getformwork/formwork": "<1.13.1|>=2.0.0.0-beta1,<2.0.0.0-beta4", + "getgrav/grav": "<1.7.46", + "getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", + "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", - "gilacms/gila": "<=1.11.4", + "gilacms/gila": "<=1.15.4", + "gleez/cms": "<=1.3|==2", "globalpayments/php-sdk": "<2", + "goalgorilla/open_social": "<12.3.11|>=12.4,<12.4.10|>=13.0.0.0-alpha1,<13.0.0.0-alpha11", + "gogentooss/samlbase": "<1.2.7", "google/protobuf": "<3.15", "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", - "grumpydictator/firefly-iii": "<6", + "grumpydictator/firefly-iii": "<6.1.17", + "gugoan/economizzer": "<=0.9.0.0-beta1", "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", - "guzzlehttp/psr7": "<1.8.4|>=2,<2.1.1", + "guzzlehttp/oauth-subscriber": "<0.8.1", + "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", + "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", "harvesthq/chosen": "<1.8.7", - "helloxz/imgurl": "= 2.31|<=2.31", + "helloxz/imgurl": "<=2.31", + "hhxsv5/laravel-s": "<3.7.36", "hillelcoren/invoice-ninja": "<5.3.35", "himiklab/yii2-jqgrid-widget": "<1.0.8", "hjue/justwriting": "<=1", "hov/jobfair": "<1.0.13|>=2,<2.0.2", + "httpsoft/http-message": "<1.0.12", "hyn/multi-tenant": ">=5.6,<5.7.2", - "ibexa/admin-ui": ">=4.2,<4.2.3", - "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3", + "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6,<4.6.14", + "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.6|>=4.6,<4.6.2", + "ibexa/fieldtype-richtext": ">=4.6,<4.6.19", "ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3", - "ibexa/post-install": "<=1.0.4", + "ibexa/http-cache": ">=4.6,<4.6.14", + "ibexa/post-install": "<1.0.16|>=4.6,<4.6.14", + "ibexa/solr": ">=4.5,<4.5.4", + "ibexa/user": ">=4,<4.4.3", "icecoder/icecoder": "<=8.1", "idno/known": "<=1.3.1", - "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10", - "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4", + "ilicmiljan/secure-props": ">=1.2,<1.2.2", + "illuminate/auth": "<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4", "illuminate/database": "<6.20.26|>=7,<7.30.5|>=8,<8.40", "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", "illuminate/view": "<6.20.42|>=7,<7.30.6|>=8,<8.75", - "impresscms/impresscms": "<=1.4.3", - "in2code/femanager": "<5.5.3|>=6,<6.3.4|>=7,<7.1", + "imdbphp/imdbphp": "<=5.1.1", + "impresscms/impresscms": "<=1.4.5", + "impresspages/impresspages": "<1.0.13", + "in2code/femanager": "<5.5.3|>=6,<6.3.4|>=7,<7.2.3", + "in2code/ipandlanguageredirect": "<5.1.2", "in2code/lux": "<17.6.1|>=18,<24.0.2", + "in2code/powermail": "<7.5.1|>=8,<8.5.1|>=9,<10.9.1|>=11,<12.4.1", "innologi/typo3-appointments": "<2.0.6", - "intelliants/subrion": "<=4.2.1", + "intelliants/subrion": "<4.2.2", + "inter-mediator/inter-mediator": "==5.5", + "ipl/web": "<0.10.1", + "islandora/crayfish": "<4.1", "islandora/islandora": ">=2,<2.4.1", "ivankristianto/phpwhois": "<=4.3", "jackalope/jackalope-doctrine-dbal": "<1.7.4", + "jambagecom/div2007": "<0.10.2", "james-heinrich/getid3": "<1.9.21", + "james-heinrich/phpthumb": "<1.7.12", "jasig/phpcas": "<1.3.3", + "jbartels/wec-map": "<3.0.3", + "jcbrand/converse.js": "<3.3.3", + "joelbutcher/socialstream": "<5.6|>=6,<6.2", + "johnbillion/wp-crontrol": "<1.16.2", + "joomla/application": "<1.0.13", "joomla/archive": "<1.1.12|>=2,<2.0.1", + "joomla/database": ">=1,<2.2|>=3,<3.4", "joomla/filesystem": "<1.6.2|>=2,<2.0.1", "joomla/filter": "<1.4.4|>=2,<2.0.1", + "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", "joomla/input": ">=2,<2.0.2", + "joomla/joomla-cms": "<3.9.12|>=4,<4.4.13|>=5,<5.2.6", + "joomla/joomla-platform": "<1.5.4", "joomla/session": "<1.3.1", "joyqi/hyper-down": "<=2.4.27", "jsdecena/laracom": "<2.0.9", "jsmitty12/phpwhois": "<5.1", + "juzaweb/cms": "<=3.4", + "jweiland/events2": "<8.3.8|>=9,<9.0.6", + "jweiland/kk-downloader": "<1.2.2", "kazist/phpwhois": "<=4.2.6", "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", - "kimai/kimai": "<1.1", - "kitodo/presentation": "<3.1.2", + "khodakhah/nodcms": "<=3", + "kimai/kimai": "<=2.20.1", + "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", - "knplabs/knp-snappy": "<1.4.2", - "krayin/laravel-crm": "<1.2.2", + "knplabs/knp-snappy": "<=1.4.2", + "kohana/core": "<3.3.3", + "koillection/koillection": "<1.6.12", + "krayin/laravel-crm": "<=1.3", "kreait/firebase-php": ">=3.2,<3.8.1", + "kumbiaphp/kumbiapp": "<=1.1.1", "la-haute-societe/tcpdf": "<6.2.22", - "laminas/laminas-diactoros": "<2.11.1", + "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", "laminas/laminas-http": "<2.14.2", + "lara-zeus/artemis": ">=1,<=1.0.6", + "lara-zeus/dynamic-dashboard": ">=3,<=3.0.1", "laravel/fortify": "<1.11.1", - "laravel/framework": "<6.20.42|>=7,<7.30.6|>=8,<8.75", - "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", + "laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1", + "laravel/laravel": ">=5.4,<5.4.22", + "laravel/pulse": "<1.3.1", + "laravel/reverb": "<1.4", + "laravel/socialite": ">=1,<2.0.10", "latte/latte": "<2.10.8", - "lavalite/cms": "<=5.8", + "lavalite/cms": "<=9|==10.1", "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", - "league/commonmark": "<0.18.3", + "league/commonmark": "<2.7", "league/flysystem": "<1.1.4|>=2,<2.1.1", + "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "leantime/leantime": "<3.3", "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", - "librenms/librenms": "<22.10", + "libreform/libreform": ">=2,<=2.0.8", + "librenms/librenms": "<2017.08.18", "liftkit/database": "<2.13.2", - "limesurvey/limesurvey": "<3.27.19", + "lightsaml/lightsaml": "<1.3.5", + "limesurvey/limesurvey": "<6.5.12", "livehelperchat/livehelperchat": "<=3.91", - "livewire/livewire": ">2.2.4,<2.2.6", + "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.5.2", + "livewire/volt": "<1.7", "lms/routes": "<2.1.1", "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", + "luracast/restler": "<3.1", "luyadev/yii-helpers": "<1.2.1", - "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3", - "magento/magento1ce": "<1.9.4.3", - "magento/magento1ee": ">=1,<1.14.4.3", - "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", + "macropay-solutions/laravel-crud-wizard-free": "<3.4.17", + "maestroerror/php-heic-to-jpg": "<1.0.5", + "magento/community-edition": "<2.4.5|==2.4.5|>=2.4.5.0-patch1,<2.4.5.0-patch12|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch10|>=2.4.7.0-beta1,<2.4.7.0-patch5|>=2.4.8.0-beta1,<2.4.8.0-beta2", + "magento/core": "<=1.9.4.5", + "magento/magento1ce": "<1.9.4.3-dev", + "magento/magento1ee": ">=1,<1.14.4.3-dev", + "magento/product-community-edition": "<2.4.4.0-patch9|>=2.4.5,<2.4.5.0-patch8|>=2.4.6,<2.4.6.0-patch6|>=2.4.7,<2.4.7.0-patch1", + "magento/project-community-edition": "<=2.0.2", + "magneto/core": "<1.9.4.4-dev", "maikuolan/phpmussel": ">=1,<1.6", - "mantisbt/mantisbt": "<=2.25.5", + "mainwp/mainwp": "<=4.4.3.3", + "mantisbt/mantisbt": "<=2.26.3", "marcwillmann/turn": "<0.3.3", + "matomo/matomo": "<1.11", "matyhtf/framework": "<3.0.6", - "mautic/core": "<4.3|= 2.13.1", - "mediawiki/core": ">=1.27,<1.27.6|>=1.29,<1.29.3|>=1.30,<1.30.2|>=1.31,<1.31.9|>=1.32,<1.32.6|>=1.32.99,<1.33.3|>=1.33.99,<1.34.3|>=1.34.99,<1.35", + "mautic/core": "<5.2.3", + "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", + "maximebf/debugbar": "<1.19", + "mdanter/ecc": "<2", + "mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2", + "mediawiki/cargo": "<3.6.1", + "mediawiki/core": "<1.39.5|==1.40", + "mediawiki/data-transfer": ">=1.39,<1.39.11|>=1.41,<1.41.3|>=1.42,<1.42.2", "mediawiki/matomo": "<2.4.3", + "mediawiki/semantic-media-wiki": "<4.0.2", + "mehrwert/phpmyadmin": "<3.2", "melisplatform/melis-asset-manager": "<5.0.1", "melisplatform/melis-cms": "<5.0.1", "melisplatform/melis-front": "<5.0.1", "mezzio/mezzio-swoole": "<3.7|>=4,<4.3", "mgallegos/laravel-jqgrid": "<=1.3", - "microweber/microweber": "<1.3.3", + "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", + "microsoft/microsoft-graph-beta": "<2.0.1", + "microsoft/microsoft-graph-core": "<2.0.2", + "microweber/microweber": "<=2.0.16", + "mikehaertl/php-shellcommand": "<1.6.1", "miniorange/miniorange-saml": "<1.4.3", "mittwald/typo3_forum": "<1.2.1", "mobiledetect/mobiledetectlib": "<2.8.32", - "modx/revolution": "<= 2.8.3-pl|<2.8", + "modx/revolution": "<=3.1", "mojo42/jirafeau": "<4.4", + "mongodb/mongodb": ">=1,<1.9.2", "monolog/monolog": ">=1.8,<1.12", - "moodle/moodle": "<4.0.7|>=4.1-beta,<4.1.2|= 3.11", + "moodle/moodle": "<4.3.12|>=4.4,<4.4.8|>=4.5.0.0-beta,<4.5.4", + "mos/cimage": "<0.7.19", + "movim/moxl": ">=0.8,<=0.10", + "movingbytes/social-network": "<=1.2.1", + "mpdf/mpdf": "<=7.1.7", + "munkireport/comment": "<4.1", + "munkireport/managedinstalls": "<2.6", + "munkireport/munki_facts": "<1.5", + "munkireport/munkireport": ">=2.5.3,<5.6.3", + "munkireport/reportdata": "<3.5", + "munkireport/softwareupdate": "<1.6", "mustache/mustache": ">=2,<2.14.1", + "mwdelaney/wp-enable-svg": "<=0.2", "namshi/jose": "<2.2", + "nasirkhan/laravel-starter": "<11.11", + "nategood/httpful": "<1", "neoan3-apps/template": "<1.1.1", - "neorazorx/facturascripts": "<2022.4", + "neorazorx/facturascripts": "<2022.04", "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3", - "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.9.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", - "neos/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "neos/media-browser": "<7.3.19|>=8,<8.0.16|>=8.1,<8.1.11|>=8.2,<8.2.11|>=8.3,<8.3.9", + "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", + "neos/swiftmailer": "<5.4.5", + "nesbot/carbon": "<2.72.6|>=3,<3.8.4", + "netcarver/textile": "<=4.1.2", "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15", "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", - "nilsteampassnet/teampass": "<3.0.0.23", + "nilsteampassnet/teampass": "<3.1.3.1-dev", + "nonfiction/nterchange": "<4.1.1", "notrinos/notrinos-erp": "<=0.7", "noumo/easyii": "<=0.9", - "nukeviet/nukeviet": "<4.5.2", + "novaksolutions/infusionsoft-php-sdk": "<1", + "nukeviet/nukeviet": "<4.5.02", + "nyholm/psr7": "<1.6.1", "nystudio107/craft-seomatic": "<3.4.12", + "nzedb/nzedb": "<0.8", "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", "october/backend": "<1.1.2", - "october/cms": "= 1.1.1|= 1.0.471|= 1.0.469|>=1.0.319,<1.0.469", - "october/october": ">=1.0.319,<1.0.466|>=2.1,<2.1.12", + "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", + "october/october": "<3.7.5", "october/rain": "<1.0.472|>=1.1,<1.1.2", - "october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.34|>=3,<3.0.66", + "october/system": "<3.7.5", + "oliverklee/phpunit": "<3.5.15", + "omeka/omeka-s": "<4.0.3", "onelogin/php-saml": "<2.10.4", - "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5", + "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", "open-web-analytics/open-web-analytics": "<1.7.4", - "opencart/opencart": "<=3.0.3.7", + "opencart/opencart": ">=0", "openid/php-openid": "<2.3", - "openmage/magento-lts": "<19.4.22|>=20,<20.0.19", - "orchid/platform": ">=9,<9.4.4", - "oro/commerce": ">=4.1,<5.0.6", + "openmage/magento-lts": "<20.12.3", + "opensolutions/vimbadmin": "<=3.0.15", + "opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7", + "orchid/platform": ">=8,<14.43", + "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", + "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7", - "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<4.2.8", + "oro/crm-call-bundle": ">=4.2,<=4.2.5|>=5,<5.0.4|>=5.1,<5.1.1", + "oro/customer-portal": ">=4.1,<=4.1.13|>=4.2,<=4.2.10|>=5,<=5.0.11|>=5.1,<=5.1.3", + "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<=5.0.12|>=5.1,<=5.1.3", + "oveleon/contao-cookiebar": "<1.16.3|>=2,<2.1.3", + "oxid-esales/oxideshop-ce": "<=7.0.5", + "oxid-esales/paymorrow-module": ">=1,<1.0.2|>=2,<2.0.1", "packbackbooks/lti-1-3-php-library": "<5", "padraic/humbug_get_contents": "<1.1.2", - "pagarme/pagarme-php": ">=0,<3", + "pagarme/pagarme-php": "<3", "pagekit/pagekit": "<=1.0.18", + "paragonie/ecc": "<2.0.1", "paragonie/random_compat": "<2", - "passbolt/passbolt_api": "<2.11", + "passbolt/passbolt_api": "<4.6.2", + "paypal/adaptivepayments-sdk-php": "<=3.9.2", + "paypal/invoice-sdk-php": "<=3.9", "paypal/merchant-sdk-php": "<3.12", + "paypal/permissions-sdk-php": "<=3.9.1", "pear/archive_tar": "<1.4.14", + "pear/auth": "<1.2.4", "pear/crypt_gpg": "<1.6.7", + "pear/http_request2": "<2.7", + "pear/pear": "<=1.10.1", "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", "personnummer/personnummer": "<3.0.2", "phanan/koel": "<5.1.4", + "phenx/php-svg-lib": "<0.5.2", + "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", "php-mod/curl": "<2.3.2", + "phpbb/phpbb": "<3.3.11", + "phpems/phpems": ">=6,<=6.1.3", "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", "phpmailer/phpmailer": "<6.5", "phpmussel/phpmussel": ">=1,<1.6", - "phpmyadmin/phpmyadmin": "<5.2.1", - "phpmyfaq/phpmyfaq": "<=3.1.7", - "phpoffice/phpexcel": "<1.8", - "phpoffice/phpspreadsheet": "<1.16", - "phpseclib/phpseclib": "<2.0.31|>=3,<3.0.19", - "phpservermon/phpservermon": "<=3.5.2", - "phpsysinfo/phpsysinfo": "<3.2.5", - "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5,<5.6.3", + "phpmyadmin/phpmyadmin": "<5.2.2", + "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5|>=3.2.10,<=4.0.1", + "phpoffice/common": "<0.2.9", + "phpoffice/phpexcel": "<=1.8.2", + "phpoffice/phpspreadsheet": "<1.29.9|>=2,<2.1.8|>=2.2,<2.3.7|>=3,<3.9", + "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", + "phpservermon/phpservermon": "<3.6", + "phpsysinfo/phpsysinfo": "<3.4.3", + "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", + "pi/pi": "<=2.5", + "pimcore/admin-ui-classic-bundle": "<1.7.6", + "pimcore/customer-management-framework-bundle": "<4.2.1", "pimcore/data-hub": "<1.2.4", + "pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3", + "pimcore/demo": "<10.3", + "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", - "pimcore/pimcore": "<10.5.20", - "pixelfed/pixelfed": "<=0.11.4", + "pimcore/pimcore": "<11.5.4", + "piwik/piwik": "<1.11", + "pixelfed/pixelfed": "<0.12.5", + "plotly/plotly.js": "<2.25.2", "pocketmine/bedrock-protocol": "<8.0.2", - "pocketmine/pocketmine-mp": "<4.12.5|>= 4.0.0-BETA5, < 4.4.2", + "pocketmine/pocketmine-mp": "<5.25.2", + "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", + "prestashop/blockreassurance": "<=5.1.3", "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.0.1", + "prestashop/prestashop": "<8.1.6", "prestashop/productcomments": "<5.0.2", + "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", "prestashop/ps_linklist": "<3.1", - "privatebin/privatebin": "<1.4", - "processwire/processwire": "<=3.0.200", - "propel/propel": ">=2-alpha.1,<=2-alpha.7", + "privatebin/privatebin": "<1.4|>=1.5,<1.7.4", + "processwire/processwire": "<=3.0.229", + "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", "propel/propel1": ">=1,<=1.7.1", - "pterodactyl/panel": "<1.7", + "pterodactyl/panel": "<1.11.8", + "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", "ptrofimov/beanstalk_console": "<1.7.14", + "pubnub/pubnub": "<6.1", + "punktde/pt_extbase": "<1.5.1", "pusher/pusher-php-server": "<2.2.1", - "pwweb/laravel-core": "<=0.3.6-beta", + "pwweb/laravel-core": "<=0.3.6.0-beta", + "pxlrbt/filament-excel": "<1.1.14|>=2.0.0.0-alpha,<2.3.3", "pyrocms/pyrocms": "<=3.9.1", + "qcubed/qcubed": "<=3.1.1", + "quickapps/cms": "<=2.0.0.0-beta2", + "rainlab/blog-plugin": "<1.4.1", "rainlab/debugbar-plugin": "<3.1", + "rainlab/user-plugin": "<=1.4.5", "rankmath/seo-by-rank-math": "<=1.0.95", - "react/http": ">=0.7,<1.7", + "rap2hpoutre/laravel-log-viewer": "<0.13", + "react/http": ">=0.7,<1.9", "really-simple-plugins/complianz-gdpr": "<6.4.2", - "remdex/livehelperchat": "<3.99", + "redaxo/source": "<5.18.3", + "remdex/livehelperchat": "<4.29", + "reportico-web/reportico": "<=8.1", + "rhukster/dom-sanitizer": "<1.0.7", "rmccue/requests": ">=1.6,<1.8", - "robrichards/xmlseclibs": "<3.0.4", + "robrichards/xmlseclibs": ">=1,<3.0.4", "roots/soil": "<4.1", "rudloff/alltube": "<3.0.3", + "rudloff/rtmpdump-bin": "<=2.3.1", "s-cart/core": "<6.9", "s-cart/s-cart": "<6.9", "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", - "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9", - "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", + "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "samwilson/unlinked-wikibase": "<1.42", + "scheb/two-factor-bundle": "<3.26|>=4,<4.11", "sensiolabs/connect": "<4.2.3", "serluck/phpwhois": "<=4.2.6", - "shopware/core": "<=6.4.18", - "shopware/platform": "<=6.4.18", + "sfroemken/url_redirect": "<=1.2.1", + "sheng/yiicms": "<1.2.1", + "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", + "shopware/platform": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", "shopware/production": "<=6.3.5.2", - "shopware/shopware": "<=5.7.14", - "shopware/storefront": "<=6.4.8.1", - "shopxo/shopxo": "<2.2.6", + "shopware/shopware": "<=5.7.17", + "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev", + "shopxo/shopxo": "<=6.4", "showdoc/showdoc": "<2.10.4", - "silverstripe/admin": ">=1,<1.11.3", + "shuchkin/simplexlsx": ">=1.0.12,<1.1.13", + "silverstripe-australia/advancedreports": ">=1,<=2", + "silverstripe/admin": "<1.13.19|>=2,<2.1.8", "silverstripe/assets": ">=1,<1.11.1", "silverstripe/cms": "<4.11.3", - "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1", + "silverstripe/comments": ">=1.3,<3.1.1", "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", - "silverstripe/framework": "<4.11.14", - "silverstripe/graphql": "<3.5.2|>=4-alpha.1,<4-alpha.2|>=4.1.1,<4.1.2|>=4.2.2,<4.2.3|= 4.0.0-alpha1", + "silverstripe/framework": "<5.3.23", + "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", + "silverstripe/recipe-cms": ">=4.5,<4.5.3", "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", - "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", + "silverstripe/reports": "<5.2.3", + "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4|>=2.1,<2.1.2", "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", "silverstripe/subsites": ">=2,<2.6.1", "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", - "silverstripe/userforms": "<3", + "silverstripe/userforms": "<3|>=5,<5.4.2", "silverstripe/versioned-admin": ">=1,<1.11.1", "simple-updates/phpwhois": "<=1", - "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4", + "simplesamlphp/saml2": "<=4.16.15|>=5.0.0.0-alpha1,<=5.0.0.0-alpha19", + "simplesamlphp/saml2-legacy": "<=4.16.15", "simplesamlphp/simplesamlphp": "<1.18.6", "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", "simplesamlphp/simplesamlphp-module-openid": "<1", "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", + "simplesamlphp/xml-common": "<1.20", + "simplesamlphp/xml-security": "==1.6.11", "simplito/elliptic-php": "<1.0.6", "sitegeist/fluid-components": "<3.5", + "sjbr/sr-feuser-register": "<2.6.2", + "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", + "sjbr/static-info-tables": "<2.3.1", + "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", "slim/slim": "<2.6", - "smarty/smarty": "<3.1.48|>=4,<4.3.1", - "snipe/snipe-it": "<=6.0.14|>= 6.0.0-RC-1, <= 6.0.0-RC-5", + "slub/slub-events": "<3.0.3", + "smarty/smarty": "<4.5.3|>=5,<5.1.1", + "snipe/snipe-it": "<8.1", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", - "spatie/browsershot": "<3.57.4", - "spipu/html2pdf": "<5.2.4", + "spatie/browsershot": "<5.0.5", + "spatie/image-optimizer": "<1.7.3", + "spencer14420/sp-php-email-handler": "<1", + "spipu/html2pdf": "<5.2.8", + "spoon/library": "<1.4.1", "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", - "ssddanbrown/bookstack": "<22.2.3", - "statamic/cms": "<3.2.39|>=3.3,<3.3.2", - "stormpath/sdk": ">=0,<9.9.99", - "studio-42/elfinder": "<2.1.59", - "subrion/cms": "<=4.2.1", + "ssddanbrown/bookstack": "<24.05.1", + "starcitizentools/citizen-skin": ">=2.6.3,<2.31", + "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2", + "statamic/cms": "<=5.16", + "stormpath/sdk": "<9.9.99", + "studio-42/elfinder": "<=2.1.64", + "studiomitte/friendlycaptcha": "<0.1.4", + "subhh/libconnect": "<7.0.8|>=8,<8.1", "sukohi/surpass": "<1", - "sulu/sulu": "= 2.4.0-RC1|<1.6.44|>=2,<2.2.18|>=2.3,<2.3.8", + "sulu/form-bundle": ">=2,<2.5.3", + "sulu/sulu": "<1.6.44|>=2,<2.5.25|>=2.6,<2.6.9|>=3.0.0.0-alpha1,<3.0.0.0-alpha3", "sumocoders/framework-user-bundle": "<1.4", + "superbig/craft-audit": "<3.0.2", + "svewap/a21glossary": "<=0.4.10", "swag/paypal": "<5.4.4", - "swiftmailer/swiftmailer": ">=4,<5.4.5", + "swiftmailer/swiftmailer": "<6.2.5", + "swiftyedit/swiftyedit": "<1.2", "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", "sylius/grid-bundle": "<1.10.1", - "sylius/paypal-plugin": ">=1,<1.2.4|>=1.3,<1.3.1", - "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", - "sylius/sylius": "<1.9.10|>=1.10,<1.10.11|>=1.11,<1.11.2", - "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", + "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", + "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", + "sylius/sylius": "<1.12.19|>=1.13.0.0-alpha1,<1.13.4", + "symbiote/silverstripe-multivaluefield": ">=3,<3.1", "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", "symbiote/silverstripe-seed": "<6.0.3", "symbiote/silverstripe-versionedfiles": "<=2.0.3", @@ -15310,8 +17249,9 @@ "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", - "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<=5.3.14|>=5.4.3,<=5.4.3|>=6.0.3,<=6.0.3|= 6.0.3|= 5.4.3|= 5.3.14", - "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4", + "symfony/http-client": ">=4.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", + "symfony/http-foundation": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1", @@ -15319,87 +17259,142 @@ "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/polyfill": ">=1,<1.10", "symfony/polyfill-php55": ">=1,<1.10", + "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/routing": ">=2,<2.0.19", + "symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", - "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.4.10|>=7,<7.0.10|>=7.1,<7.1.3", "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", - "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.3.2", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", - "symfony/symfony": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/symfony": "<5.4.47|>=6,<6.4.15|>=7,<7.1.8", "symfony/translation": ">=2,<2.0.17", - "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", + "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/ux-autocomplete": "<2.11.2", + "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", - "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7", - "t3/dce": ">=2.2,<2.6.2", + "symfony/webhook": ">=6.3,<6.3.8", + "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7|>=2.2.0.0-beta1,<2.2.0.0-beta2", + "symphonycms/symphony-2": "<2.6.4", + "t3/dce": "<0.11.5|>=2.2,<2.6.2", "t3g/svg-sanitizer": "<1.0.3", - "tastyigniter/tastyigniter": "<3.3", - "tecnickcom/tcpdf": "<6.2.22", + "t3s/content-consent": "<1.0.3|>=2,<2.0.2", + "tastyigniter/tastyigniter": "<4", + "tcg/voyager": "<=1.8", + "tecnickcom/tc-lib-pdf-font": "<2.6.4", + "tecnickcom/tcpdf": "<6.8", "terminal42/contao-tablelookupwizard": "<3.3.5", "thelia/backoffice-default-template": ">=2.1,<2.1.2", - "thelia/thelia": ">=2.1-beta.1,<2.1.3", + "thelia/thelia": ">=2.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", - "thinkcmf/thinkcmf": "<=5.1.7", - "thorsten/phpmyfaq": "<3.1.12", - "tinymce/tinymce": "<5.10.7|>=6,<6.3.1", + "thinkcmf/thinkcmf": "<6.0.8", + "thorsten/phpmyfaq": "<=4.0.1", + "tikiwiki/tiki-manager": "<=17.1", + "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", + "tinymce/tinymce": "<7.2", "tinymighty/wiki-seo": "<1.2.2", - "titon/framework": ">=0,<9.9.99", - "tobiasbg/tablepress": "<= 2.0-RC1", - "topthink/framework": "<6.0.14", + "titon/framework": "<9.9.99", + "tltneon/lgsl": "<7", + "tobiasbg/tablepress": "<=2.0.0.0-RC1", + "topthink/framework": "<6.0.17|>=6.1,<=8.0.4", "topthink/think": "<=6.1.1", - "topthink/thinkphp": "<=3.2.3", - "tribalsystems/zenario": "<=9.3.57595", + "topthink/thinkphp": "<=3.2.3|>=6.1.3,<=8.0.4", + "torrentpier/torrentpier": "<=2.4.3", + "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", + "tribalsystems/zenario": "<=9.7.61188", "truckersmp/phpwhois": "<=4.3.1", "ttskch/pagination-service-provider": "<1", - "twig/twig": "<1.44.7|>=2,<2.15.3|>=3,<3.4.3", - "typo3/cms": "<2.0.5|>=3,<3.0.3|>=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.38|>=9,<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": ">=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", - "typo3/cms-core": "<8.7.51|>=9,<9.5.40|>=10,<10.4.36|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", + "twbs/bootstrap": "<=3.4.1|>=4,<=4.6.2", + "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", + "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<10.4.46|>=11,<11.5.40|>=12,<12.4.21|>=13,<13.3.1", + "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-beuser": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-core": "<=8.7.56|>=9,<=9.5.48|>=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-dashboard": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", + "typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-felogin": ">=4.2,<4.2.3", + "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", + "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", + "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2", + "typo3/cms-lowlevel": ">=11,<=11.5.41", + "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", + "typo3/cms-scheduler": ">=11,<=11.5.41", "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", - "typo3/html-sanitizer": ">=1,<1.5|>=2,<2.1.1", + "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10", "ua-parser/uap-php": "<3.8", - "unisharp/laravel-filemanager": "<=2.5.1", + "uasoft-indonesia/badaso": "<=2.9.7", + "unisharp/laravel-filemanager": "<2.9.1", + "unopim/unopim": "<0.1.5", "userfrosting/userfrosting": ">=0.3.1,<4.6.3", "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", - "uvdesk/community-skeleton": "<1.1", + "uvdesk/community-skeleton": "<=1.1.1", + "uvdesk/core-framework": "<=1.1.1", "vanilla/safecurl": "<0.9.2", - "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4", + "verbb/comments": "<1.5.5", + "verbb/formie": "<=2.1.43", + "verbb/image-resizer": "<2.0.9", + "verbb/knock-knock": "<1.2.8", + "verot/class.upload.php": "<=2.1.6", + "vertexvaar/falsftp": "<0.2.6", + "villagedefrance/opencart-overclocked": "<=1.11.1", "vova07/yii2-fileapi-widget": "<0.1.9", "vrana/adminer": "<4.8.1", + "vufind/vufind": ">=2,<9.1.1", + "waldhacker/hcaptcha": "<2.1.2", "wallabag/tcpdf": "<6.2.22", - "wallabag/wallabag": "<2.5.4", + "wallabag/wallabag": "<2.6.11", "wanglelecc/laracms": "<=1.0.3", - "web-auth/webauthn-framework": ">=3.3,<3.3.4", + "wapplersystems/a21glossary": "<=0.4.10", + "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9", + "web-auth/webauthn-lib": ">=4.5,<4.9", + "web-feet/coastercms": "==5.5", + "web-tp3/wec_map": "<3.0.3", "webbuilders-group/silverstripe-kapost-bridge": "<0.4", "webcoast/deferred-image-processing": "<1.0.2", + "webklex/laravel-imap": "<5.3", + "webklex/php-imap": "<5.3", "webpa/webpa": "<3.1.2", + "wikibase/wikibase": "<=1.39.3", "wikimedia/parsoid": "<0.12.2", "willdurand/js-translation-bundle": "<2.1.1", - "wintercms/winter": "<1.0.475|>=1.1,<1.1.10|>=1.2,<1.2.1", - "woocommerce/woocommerce": "<6.6", - "wp-cli/wp-cli": "<2.5", - "wp-graphql/wp-graphql": "<0.3.5", + "winter/wn-backend-module": "<1.2.4", + "winter/wn-cms-module": "<1.0.476|>=1.1,<1.1.11|>=1.2,<1.2.7", + "winter/wn-dusk-plugin": "<2.1", + "winter/wn-system-module": "<1.2.4", + "wintercms/winter": "<=1.2.3", + "wireui/wireui": "<1.19.3|>=2,<2.1.3", + "woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3", + "wp-cli/wp-cli": ">=0.12,<2.5", + "wp-graphql/wp-graphql": "<=1.14.5", + "wp-premium/gravityforms": "<2.4.21", "wpanel/wpanel4-cms": "<=4.3.1", "wpcloud/wp-stateless": "<3.2", - "wwbn/avideo": "<12.4", + "wpglobus/wpglobus": "<=1.9.6", + "wwbn/avideo": "<14.3", "xataface/xataface": "<3", "xpressengine/xpressengine": "<3.0.15", - "yeswiki/yeswiki": "<4.1", - "yetiforce/yetiforce-crm": "<=6.4", + "yab/quarx": "<2.4.5", + "yeswiki/yeswiki": "<4.5.4", + "yetiforce/yetiforce-crm": "<6.5", "yidashi/yii2cmf": "<=2", "yii2mod/yii2-cms": "<1.9.2", - "yiisoft/yii": "<1.1.27", - "yiisoft/yii2": "<2.0.38", + "yiisoft/yii": "<1.1.31", + "yiisoft/yii2": "<2.0.52", + "yiisoft/yii2-authclient": "<2.2.15", "yiisoft/yii2-bootstrap": "<2.0.4", - "yiisoft/yii2-dev": "<2.0.43", + "yiisoft/yii2-dev": "<=2.0.45", "yiisoft/yii2-elasticsearch": "<2.0.5", "yiisoft/yii2-gii": "<=2.2.4", "yiisoft/yii2-jui": "<2.0.4", @@ -15407,11 +17402,13 @@ "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", "yoast-seo-for-typo3/yoast_seo": "<7.2.3", "yourls/yourls": "<=1.8.2", + "yuan1994/tpadmin": "<=1.3.12", + "zencart/zencart": "<=1.5.7.0-beta", "zendesk/zendesk_api_client_php": "<2.2.11", "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", - "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5", + "zendframework/zend-db": "<2.2.10|>=2.3,<2.3.5", "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3", "zendframework/zend-diactoros": "<1.8.4", "zendframework/zend-feed": "<2.10.3", @@ -15419,21 +17416,30 @@ "zendframework/zend-http": "<2.8.1", "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", - "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2", + "zendframework/zend-mail": "<2.4.11|>=2.5,<2.7.2", "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1", - "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4", + "zendframework/zend-session": ">=2,<2.2.9|>=2.3,<2.3.4", "zendframework/zend-validator": ">=2.3,<2.3.6", "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", "zendframework/zendframework": "<=3", "zendframework/zendframework1": "<1.12.20", - "zendframework/zendopenid": ">=2,<2.0.2", + "zendframework/zendopenid": "<2.0.2", + "zendframework/zendrest": "<2.0.2", + "zendframework/zendservice-amazon": "<2.0.3", + "zendframework/zendservice-api": "<1", + "zendframework/zendservice-audioscrobbler": "<2.0.2", + "zendframework/zendservice-nirvanix": "<2.0.2", + "zendframework/zendservice-slideshare": "<2.0.2", + "zendframework/zendservice-technorati": "<2.0.2", + "zendframework/zendservice-windowsazure": "<2.0.2", "zendframework/zendxml": ">=1,<1.0.1", + "zenstruck/collection": "<0.2.1", "zetacomponents/mail": "<1.8.2", "zf-commons/zfc-user": "<1.2.2", "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", "zfr/zfr-oauth2-server-module": "<0.1.2", - "zoujingli/thinkadmin": "<6.0.22" + "zoujingli/thinkadmin": "<=6.1.53" }, "default-branch": true, "type": "metapackage", @@ -15471,20 +17477,318 @@ "type": "tidelift" } ], - "time": "2023-04-06T17:04:19+00:00" + "time": "2025-05-17T16:05:20+00:00" }, { - "name": "sebastian/diff", - "version": "4.0.4", + "name": "sebastian/cli-parser", + "version": "1.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -15529,7 +17833,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -15537,99 +17841,630 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { - "name": "spatie/array-to-xml", - "version": "2.17.1", + "name": "sebastian/environment", + "version": "5.1.5", "source": { "type": "git", - "url": "https://github.com/spatie/array-to-xml.git", - "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", - "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { - "ext-dom": "*", - "php": "^7.4|^8.0" + "php": ">=7.3" }, "require-dev": { - "mockery/mockery": "^1.2", - "pestphp/pest": "^1.21", - "phpunit/phpunit": "^9.0", - "spatie/pest-plugin-snapshots": "^1.1" + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", - "autoload": { - "psr-4": { - "Spatie\\ArrayToXml\\": "src" + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://freek.dev", - "role": "Developer" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Convert an array to xml", - "homepage": "https://github.com/spatie/array-to-xml", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", "keywords": [ - "array", - "convert", - "xml" + "Xdebug", + "environment", + "hhvm" ], "support": { - "source": "https://github.com/spatie/array-to-xml/tree/2.17.1" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", + "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2022-12-26T08:22:07+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { - "name": "symfony/browser-kit", - "version": "v5.4.21", + "name": "sebastian/exporter", + "version": "4.0.6", "source": { "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a866ca7e396f15d7efb6d74a8a7d364d4e05b704", - "reference": "a866ca7e396f15d7efb6d74a8a7d364d4e05b704", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/polyfill-php80": "^1.16" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0" + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" }, "suggest": { - "symfony/process": "" + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "ce95f3e3239159e7fa3be7690c6ce95a4714637f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/ce95f3e3239159e7fa3be7690c6ce95a4714637f", + "reference": "ce95f3e3239159e7fa3be7690c6ce95a4714637f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/dom-crawler": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -15657,7 +18492,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.4.21" + "source": "https://github.com/symfony/browser-kit/tree/v6.4.19" }, "funding": [ { @@ -15673,42 +18508,37 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" + "time": "2025-02-14T11:23:16+00:00" }, { "name": "symfony/debug-bundle", - "version": "v5.4.21", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", - "reference": "8b4360bf8ce9a917ef8796c5e6065a185d8722bd" + "reference": "7bcfaff39e094cc09455201916d016d9b2ae08ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/8b4360bf8ce9a917ef8796c5e6065a185d8722bd", - "reference": "8b4360bf8ce9a917ef8796c5e6065a185d8722bd", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/7bcfaff39e094cc09455201916d016d9b2ae08ff", + "reference": "7bcfaff39e094cc09455201916d016d9b2ae08ff", "shasum": "" }, "require": { "ext-xml": "*", - "php": ">=7.2.5", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/twig-bridge": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" + "php": ">=8.1", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/twig-bridge": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "conflict": { - "symfony/config": "<4.4", - "symfony/dependency-injection": "<5.2" + "symfony/config": "<5.4", + "symfony/dependency-injection": "<5.4" }, "require-dev": { - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/web-profiler-bundle": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/config": "For service container configuration", - "symfony/dependency-injection": "For using as a service from the container" + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0" }, "type": "symfony-bundle", "autoload": { @@ -15736,7 +18566,7 @@ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug-bundle/tree/v5.4.21" + "source": "https://github.com/symfony/debug-bundle/tree/v6.4.13" }, "funding": [ { @@ -15752,129 +18582,54 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:03:56+00:00" - }, - { - "name": "symfony/dom-crawler", - "version": "v5.4.22", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "4c633facee8da59998e0c90e337a586cf07a21e7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4c633facee8da59998e0c90e337a586cf07a21e7", - "reference": "4c633facee8da59998e0c90e337a586cf07a21e7", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "masterminds/html5": "<2.6" - }, - "require-dev": { - "masterminds/html5": "^2.6", - "symfony/css-selector": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/css-selector": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases DOM navigation for HTML and XML documents", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.4.22" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-03-06T21:29:33+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/maker-bundle", - "version": "v1.43.0", + "version": "v1.63.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "e3f9a1d9e0f4968f68454403e820dffc7db38a59" + "reference": "69478ab39bc303abfbe3293006a78b09a8512425" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/e3f9a1d9e0f4968f68454403e820dffc7db38a59", - "reference": "e3f9a1d9e0f4968f68454403e820dffc7db38a59", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", - "nikic/php-parser": "^4.11", - "php": ">=7.2.5", - "symfony/config": "^5.4.7|^6.0", - "symfony/console": "^5.4.7|^6.0", - "symfony/dependency-injection": "^5.4.7|^6.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^5.4.7|^6.0", - "symfony/finder": "^5.4.3|^6.0", - "symfony/framework-bundle": "^5.4.7|^6.0", - "symfony/http-kernel": "^5.4.7|^6.0" + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" }, "conflict": { - "doctrine/orm": "<2.10" + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" }, "require-dev": { "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.4", - "doctrine/orm": "^2.10.0", - "symfony/http-client": "^5.4.7|^6.0", - "symfony/phpunit-bridge": "^5.4.7|^6.0", - "symfony/polyfill-php80": "^1.16.0", - "symfony/process": "^5.4.7|^6.0", - "symfony/security-core": "^5.4.7|^6.0", - "symfony/yaml": "^5.4.3|^6.0", - "twig/twig": "^2.0|^3.0" + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" }, "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-main": "1.0-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -15896,13 +18651,14 @@ "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", "keywords": [ "code generator", + "dev", "generator", "scaffold", "scaffolding" ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.43.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" }, "funding": [ { @@ -15918,34 +18674,32 @@ "type": "tidelift" } ], - "time": "2022-05-17T15:46:50+00:00" + "time": "2025-04-26T01:41:37+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v5.4.21", + "version": "v6.4.16", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "28d8a15a0b4c7186042fa4e0ddea94d561e7ea9e" + "reference": "cebafe2f1ad2d1e745c1015b7c2519592341e4e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/28d8a15a0b4c7186042fa4e0ddea94d561e7ea9e", - "reference": "28d8a15a0b4c7186042fa4e0ddea94d561e7ea9e", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/cebafe2f1ad2d1e745c1015b7c2519592341e4e6", + "reference": "cebafe2f1ad2d1e745c1015b7c2519592341e4e6", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/deprecation-contracts": "^2.1|^3" + "php": ">=7.1.3" }, "conflict": { "phpunit/phpunit": "<7.5|9.1.2" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/polyfill-php81": "^1.27" }, "bin": [ "bin/simple-phpunit" @@ -15953,8 +18707,8 @@ "type": "symfony-bridge", "extra": { "thanks": { - "name": "phpunit/phpunit", - "url": "https://github.com/sebastianbergmann/phpunit" + "url": "https://github.com/sebastianbergmann/phpunit", + "name": "phpunit/phpunit" } }, "autoload": { @@ -15965,7 +18719,8 @@ "Symfony\\Bridge\\PhpUnit\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -15985,7 +18740,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v5.4.21" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.4.16" }, "funding": [ { @@ -16001,43 +18756,42 @@ "type": "tidelift" } ], - "time": "2023-02-16T09:33:00+00:00" + "time": "2024-11-13T15:06:22+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v5.4.21", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "a83337a813d1398789bf4190a56dc3c4d8cc4d93" + "reference": "7d1026a8e950d416cb5148ae88ac23db5d264839" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/a83337a813d1398789bf4190a56dc3c4d8cc4d93", - "reference": "a83337a813d1398789bf4190a56dc3c4d8cc4d93", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/7d1026a8e950d416cb5148ae88ac23db5d264839", + "reference": "7d1026a8e950d416cb5148ae88ac23db5d264839", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/framework-bundle": "^5.3|^6.0", - "symfony/http-kernel": "^5.3|^6.0", - "symfony/polyfill-php80": "^1.16", - "symfony/routing": "^4.4|^5.0|^6.0", - "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0", "twig/twig": "^2.13|^3.0.4" }, "conflict": { - "symfony/dependency-injection": "<5.2", - "symfony/form": "<4.4", + "symfony/form": "<5.4", "symfony/mailer": "<5.4", - "symfony/messenger": "<4.4" + "symfony/messenger": "<5.4", + "symfony/twig-bundle": ">=7.0" }, "require-dev": { - "symfony/browser-kit": "^4.4|^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0" + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "symfony-bundle", "autoload": { @@ -16064,8 +18818,11 @@ ], "description": "Provides a development tool that gives detailed information about the execution of any request", "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v5.4.21" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.19" }, "funding": [ { @@ -16081,29 +18838,32 @@ "type": "tidelift" } ], - "time": "2023-02-17T16:59:45+00:00" + "time": "2025-02-14T12:21:59+00:00" }, { "name": "symplify/easy-coding-standard", - "version": "11.3.2", + "version": "12.5.18", "source": { "type": "git", "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", - "reference": "d4159e06c0970e71a2a58d350160241e7cb3994b" + "reference": "451dfeba3770f2d7476468b891a789c451ae4b34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/d4159e06c0970e71a2a58d350160241e7cb3994b", - "reference": "d4159e06c0970e71a2a58d350160241e7cb3994b", + "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/451dfeba3770f2d7476468b891a789c451ae4b34", + "reference": "451dfeba3770f2d7476468b891a789c451ae4b34", "shasum": "" }, "require": { "php": ">=7.2" }, "conflict": { - "friendsofphp/php-cs-fixer": "<3.0", - "squizlabs/php_codesniffer": "<3.6", - "symplify/coding-standard": "<11.3" + "friendsofphp/php-cs-fixer": "<3.46", + "phpcsstandards/php_codesniffer": "<3.8", + "symplify/coding-standard": "<12.1" + }, + "suggest": { + "ext-dom": "Needed to support checkstyle output format in class CheckstyleOutputFormatter" }, "bin": [ "bin/ecs" @@ -16127,7 +18887,7 @@ ], "support": { "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", - "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/11.3.2" + "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.5.18" }, "funding": [ { @@ -16139,116 +18899,67 @@ "type": "github" } ], - "time": "2023-03-22T15:28:51+00:00" + "time": "2025-05-14T09:38:08+00:00" }, { - "name": "vimeo/psalm", - "version": "5.9.0", + "name": "theseer/tokenizer", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/vimeo/psalm.git", - "reference": "8b9ad1eb9e8b7d3101f949291da2b9f7767cd163" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/8b9ad1eb9e8b7d3101f949291da2b9f7767cd163", - "reference": "8b9ad1eb9e8b7d3101f949291da2b9f7767cd163", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { - "amphp/amp": "^2.4.2", - "amphp/byte-stream": "^1.5", - "composer-runtime-api": "^2", - "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^2.0 || ^3.0", - "dnoegel/php-xdg-base-dir": "^0.1.1", - "ext-ctype": "*", "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", "ext-tokenizer": "*", - "felixfbecker/advanced-json-rpc": "^3.1", - "felixfbecker/language-server-protocol": "^1.5.2", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1", - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.14", - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0", - "sebastian/diff": "^4.0 || ^5.0", - "spatie/array-to-xml": "^2.17.0 || ^3.0", - "symfony/console": "^4.1.6 || ^5.0 || ^6.0", - "symfony/filesystem": "^5.4 || ^6.0" + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, - "provide": { - "psalm/psalm": "self.version" - }, - "require-dev": { - "amphp/phpunit-util": "^2.0", - "bamarni/composer-bin-plugin": "^1.4", - "brianium/paratest": "^6.9", - "ext-curl": "*", - "mockery/mockery": "^1.5", - "nunomaduro/mock-final-classes": "^1.1", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpdoc-parser": "^1.6", - "phpunit/phpunit": "^9.6", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^4.4 || ^5.0 || ^6.0" - }, - "suggest": { - "ext-curl": "In order to send data to shepherd", - "ext-igbinary": "^2.0.5 is required, used to serialize caching data" - }, - "bin": [ - "psalm", - "psalm-language-server", - "psalm-plugin", - "psalm-refactor", - "psalter" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev", - "dev-4.x": "4.x-dev", - "dev-3.x": "3.x-dev", - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" - } - }, "autoload": { - "psr-4": { - "Psalm\\": "src/Psalm/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Matthew Brown" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "A static analysis tool for finding errors in PHP applications", - "keywords": [ - "code", - "inspection", - "php", - "static analysis" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/5.9.0" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, - "time": "2023-03-29T21:38:21+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [ + { + "package": "brick/math", + "version": "0.12.1.0", + "alias": "0.11.0", + "alias_normalized": "0.11.0.0" } ], - "aliases": [], "minimum-stability": "stable", "stability-flags": { "florianv/swap-bundle": 20, @@ -16257,18 +18968,18 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.4 || ^8.0", + "php": "^8.1", "ext-ctype": "*", + "ext-dom": "*", "ext-gd": "*", "ext-iconv": "*", "ext-intl": "*", "ext-json": "*", - "ext-mbstring": "*", - "ext-dom": "*" + "ext-mbstring": "*" }, "platform-dev": [], "platform-overrides": { - "php": "7.4.0" + "php": "8.1.0" }, "plugin-api-version": "2.3.0" } diff --git a/config/bootstrap.php b/config/bootstrap.php deleted file mode 100644 index 55560fb8..00000000 --- a/config/bootstrap.php +++ /dev/null @@ -1,23 +0,0 @@ -=1.2) -if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { - (new Dotenv(false))->populate($env); -} else { - // load all the .env files - (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); -} - -$_SERVER += $_ENV; -$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; -$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; -$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/config/bundles.php b/config/bundles.php index 91ddea04..ea066084 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -2,7 +2,6 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], - Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], @@ -19,13 +18,18 @@ return [ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true], - Translation\Bundle\TranslationBundle::class => ['all' => true], Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true], Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true], Symfony\UX\Turbo\TurboBundle::class => ['all' => true], Jbtronics\TFAWebauthn\TFAWebauthnBundle::class => ['all' => true], Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], - SpomkyLabs\CborBundle\SpomkyLabsCborBundle::class => ['all' => true], Webauthn\Bundle\WebauthnBundle::class => ['all' => true], - Hslavich\OneloginSamlBundle\HslavichOneloginSamlBundle::class => ['all' => true], + Nbgrp\OneloginSamlBundle\NbgrpOneloginSamlBundle::class => ['all' => true], + Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], + Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], + Jbtronics\DompdfFontLoaderBundle\DompdfFontLoaderBundle::class => ['all' => true], + KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], + ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], + Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true], ]; diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml new file mode 100644 index 00000000..d55f91ea --- /dev/null +++ b/config/packages/api_platform.yaml @@ -0,0 +1,41 @@ +api_platform: + title: 'Part-DB API' + description: 'API of Part-DB' + version: '0.1.0' + + formats: + jsonld: ['application/ld+json'] + json: ['application/json'] + jsonapi: ['application/vnd.api+json'] + + docs_formats: + jsonld: ['application/ld+json'] + jsonopenapi: ['application/vnd.openapi+json'] + html: ['text/html'] + json: ['application/vnd.openapi+json'] + + swagger: + api_keys: + # overridden in OpenApiFactoryDecorator + access_token: + name: Authorization + type: header + + defaults: + # TODO: Change this to true later. In the moment it is false, because we use the session in somewhere + stateless: false + cache_headers: + vary: ['Content-Type', 'Authorization', 'Origin'] + extra_properties: + standard_put: true + rfc_7807_compliant_errors: true + + pagination_client_items_per_page: true # Allow clients to override the default items per page + + keep_legacy_inflector: false + # Need to be true, or some tests will fail + use_symfony_listeners: true + + serializer: + # Change this to false later, to remove the hydra prefix on the API + hydra_prefix: true \ No newline at end of file diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml index 07ecf18a..6adea442 100644 --- a/config/packages/cache.yaml +++ b/config/packages/cache.yaml @@ -20,3 +20,6 @@ framework: tree.cache: adapter: cache.app tags: true + + info_provider.cache: + adapter: cache.app diff --git a/config/packages/dama_doctrine_test_bundle.yaml b/config/packages/dama_doctrine_test_bundle.yaml new file mode 100644 index 00000000..3482cbae --- /dev/null +++ b/config/packages/dama_doctrine_test_bundle.yaml @@ -0,0 +1,5 @@ +when@test: + dama_doctrine_test: + enable_static_connection: true + enable_static_meta_data_cache: true + enable_static_query_cache: true diff --git a/config/packages/datatables.yaml b/config/packages/datatables.yaml index 0ccd6434..6076a6c7 100644 --- a/config/packages/datatables.yaml +++ b/config/packages/datatables.yaml @@ -8,15 +8,14 @@ datatables: # Set options, as documented at https://datatables.net/reference/option/ options: - lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]] - pageLength: 50 - #dom: "<'row' <'col-sm-12' tr>><'row' <'col-sm-6'l><'col-sm-6 text-right'pif>>" - dom: " <'row'<'col mb-2 input-group' B l> <'col mb-2' <'pull-end' p>>> - <'card' - rt - <'card-footer card-footer-table text-muted' i > - > - <'row'<'col mt-2 input-group' B l> <'col mt-2' <'pull-right' p>>>" + lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated + pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default + dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>> + <'card' + rt + <'card-footer card-footer-table text-muted' i > + > + <'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>" pagingType: 'simple_numbers' searching: true stateSave: true diff --git a/config/packages/dev/php_translation.yaml b/config/packages/dev/php_translation.yaml deleted file mode 100644 index 53398fbc..00000000 --- a/config/packages/dev/php_translation.yaml +++ /dev/null @@ -1,5 +0,0 @@ -translation: - symfony_profiler: - enabled: true - webui: - enabled: true diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 5cdb115a..3211fbbe 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -2,18 +2,32 @@ doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' + # Required for DAMA doctrine test bundle + use_savepoints: true + # IMPORTANT: You MUST configure your server version, # either here or in the DATABASE_URL env var (see .env file) types: + # UTC datetimes datetime: class: App\Doctrine\Types\UTCDateTimeType date: class: App\Doctrine\Types\UTCDateTimeType + + datetime_immutable: + class: App\Doctrine\Types\UTCDateTimeImmutableType + date_immutable: + class: App\Doctrine\Types\UTCDateTimeImmutableType + big_decimal: class: App\Doctrine\Types\BigDecimalType tinyint: class: App\Doctrine\Types\TinyIntType + + # This was removed in doctrine/orm 4.0 but we need it for the WebauthnKey entity + array: + class: App\Doctrine\Types\ArrayType schema_filter: ~^(?!internal)~ # Only enable this when needed @@ -21,20 +35,29 @@ doctrine: orm: auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + report_fields_where_declared: true + validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true + controller_resolver: + auto_mapping: true mappings: App: + type: attribute is_bundle: false - type: annotation dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App dql: string_functions: - regexp: DoctrineExtensions\Query\Mysql\Regexp - ifnull: DoctrineExtensions\Query\Mysql\IfNull + regexp: App\Doctrine\Functions\Regexp + field: DoctrineExtensions\Query\Mysql\Field + field2: App\Doctrine\Functions\Field2 + natsort: App\Doctrine\Functions\Natsort + array_position: App\Doctrine\Functions\ArrayPosition + ilike: App\Doctrine\Functions\ILike when@test: doctrine: diff --git a/config/packages/dompdf_font_loader.yaml b/config/packages/dompdf_font_loader.yaml new file mode 100644 index 00000000..8e35c37b --- /dev/null +++ b/config/packages/dompdf_font_loader.yaml @@ -0,0 +1,11 @@ +dompdf_font_loader: + auto_install: true + + fonts: + unifont: + normal: "%kernel.project_dir%/vendor/part-db/label-fonts/fonts/unifont.ttf" + + # Enable autodiscovery of fonts, so that font installation is much easier + autodiscovery: + paths: + - "%kernel.project_dir%/assets/fonts/dompdf" \ No newline at end of file diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 6adac2bb..279c51f5 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -2,8 +2,13 @@ framework: secret: '%env(APP_SECRET)%' csrf_protection: true + annotations: false + handle_all_throwables: true - # Must be set to true, to enable the change of HTTP methhod via _method parameter, otherwise our delete routines does not work anymore + # We set this header by ourselves, so we can disable it here + disallow_search_engine_index: false + + # Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore # TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278) http_method_override: true @@ -22,16 +27,12 @@ framework: handler_id: null cookie_secure: auto cookie_samesite: lax - storage_factory_id: session.storage.factory.native #esi: true #fragments: true php_errors: log: true - form: - legacy_error_messages: false # Enable to use the new Form component validation messages - when@test: framework: test: true diff --git a/config/packages/hslavich_onelogin_saml.yaml b/config/packages/hslavich_onelogin_saml.yaml deleted file mode 100644 index cae3c539..00000000 --- a/config/packages/hslavich_onelogin_saml.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings - -hslavich_onelogin_saml: - # Basic settings - idp: - entityId: '%env(string:SAML_IDP_ENTITY_ID)%' - singleSignOnService: - url: '%env(string:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%' - binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - singleLogoutService: - url: '%env(string:SAML_IDP_SINGLE_LOGOUT_SERVICE)%' - binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - x509cert: '%env(string:SAML_IDP_X509_CERT)%' - sp: - entityId: '%env(string:SAML_SP_ENTITY_ID)%' - assertionConsumerService: - url: '%partdb.default_uri%saml/acs' - binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' - singleLogoutService: - url: '%partdb.default_uri%logout' - binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - x509cert: '%env(string:SAML_SP_X509_CERT)%' - privateKey: '%env(string:SAMLP_SP_PRIVATE_KEY)%' - - # Optional settings - #baseurl: 'http://myapp.com' - strict: true - debug: false - security: - allowRepeatAttributeName: true - # nameIdEncrypted: false - authnRequestsSigned: true - logoutRequestSigned: true - logoutResponseSigned: true - # wantMessagesSigned: false - # wantAssertionsSigned: true - # wantNameIdEncrypted: false - # requestedAuthnContext: true - # signMetadata: false - # wantXMLValidation: true - # relaxDestinationValidation: false - # destinationStrictlyMatches: true - # rejectUnsolicitedResponsesWithInResponseTo: false - # signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' - # digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' - #contactPerson: - # technical: - # givenName: 'Tech User' - # emailAddress: 'techuser@example.com' - # support: - # givenName: 'Support User' - # emailAddress: 'supportuser@example.com' - # administrative: - # givenName: 'Administrative User' - # emailAddress: 'administrativeuser@example.com' - #organization: - # en: - # name: 'Part-DB-name' - # displayname: 'Displayname' - # url: 'http://example.com' \ No newline at end of file diff --git a/config/packages/http_client.yaml b/config/packages/http_client.yaml new file mode 100644 index 00000000..2e693f7f --- /dev/null +++ b/config/packages/http_client.yaml @@ -0,0 +1,5 @@ +framework: + http_client: + default_options: + headers: + 'User-Agent': 'Part-DB' \ No newline at end of file diff --git a/config/packages/http_discovery.yaml b/config/packages/http_discovery.yaml new file mode 100644 index 00000000..2a789e73 --- /dev/null +++ b/config/packages/http_discovery.yaml @@ -0,0 +1,10 @@ +services: + Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory' + + http_discovery.psr17_factory: + class: Http\Discovery\Psr17Factory diff --git a/config/packages/knpu_oauth2_client.yaml b/config/packages/knpu_oauth2_client.yaml new file mode 100644 index 00000000..7d296a8b --- /dev/null +++ b/config/packages/knpu_oauth2_client.yaml @@ -0,0 +1,38 @@ +knpu_oauth2_client: + clients: + # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration + + ip_digikey_oauth: + type: generic + provider_class: '\League\OAuth2\Client\Provider\GenericProvider' + + client_id: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%' + client_secret: '%env(PROVIDER_DIGIKEY_SECRET)%' + + redirect_route: 'oauth_client_check' + redirect_params: {name: 'ip_digikey_oauth'} + + provider_options: + urlAuthorize: 'https://api.digikey.com/v1/oauth2/authorize' + urlAccessToken: 'https://api.digikey.com/v1/oauth2/token' + urlResourceOwnerDetails: '' + + # Sandbox + #urlAuthorize: 'https://sandbox-api.digikey.com/v1/oauth2/authorize' + #urlAccessToken: 'https://sandbox-api.digikey.com/v1/oauth2/token' + #urlResourceOwnerDetails: '' + + ip_octopart_oauth: + type: generic + provider_class: '\League\OAuth2\Client\Provider\GenericProvider' + + client_id: '%env(PROVIDER_OCTOPART_CLIENT_ID)%' + client_secret: '%env(PROVIDER_OCTOPART_SECRET)%' + + redirect_route: 'oauth_client_check' + redirect_params: { name: 'ip_octopart_oauth' } + + provider_options: + urlAuthorize: 'https://identity.nexar.com/connect/authorize' + urlAccessToken: 'https://identity.nexar.com/connect/token' + urlResourceOwnerDetails: '' \ No newline at end of file diff --git a/config/packages/liip_imagine.yaml b/config/packages/liip_imagine.yaml index 18cba87c..51686b58 100644 --- a/config/packages/liip_imagine.yaml +++ b/config/packages/liip_imagine.yaml @@ -3,6 +3,9 @@ liip_imagine: # valid drivers options include "gd" or "gmagick" or "imagick" driver: "gd" + twig: + mode: lazy + default_filter_set_settings: format: webp diff --git a/config/packages/lock.yaml b/config/packages/lock.yaml deleted file mode 100644 index 574879f8..00000000 --- a/config/packages/lock.yaml +++ /dev/null @@ -1,2 +0,0 @@ -framework: - lock: '%env(LOCK_DSN)%' diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 8938cc13..44a078b8 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -50,7 +50,6 @@ when@prod: type: stream path: "%kernel.logs_dir%/%kernel.environment%.log" level: debug - formatter: monolog.formatter.json console: type: console process_psr_3_messages: false @@ -74,7 +73,6 @@ when@docker: type: stream path: "php://stderr" level: debug - formatter: monolog.formatter.json console: type: console process_psr_3_messages: false diff --git a/config/packages/nbgrp_onelogin_saml.yaml b/config/packages/nbgrp_onelogin_saml.yaml new file mode 100644 index 00000000..2b1974f3 --- /dev/null +++ b/config/packages/nbgrp_onelogin_saml.yaml @@ -0,0 +1,69 @@ +# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings + +# Define a parameter here, so we can access it later in the default fallback +parameters: + saml.sp.privateKey: '%env(string:SAML_SP_PRIVATE_KEY)%' + +nbgrp_onelogin_saml: + use_proxy_vars: '%env(bool:SAML_BEHIND_PROXY)%' + onelogin_settings: + default: + # Basic settings + idp: + entityId: '%env(string:SAML_IDP_ENTITY_ID)%' + singleSignOnService: + url: '%env(string:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + singleLogoutService: + url: '%env(string:SAML_IDP_SINGLE_LOGOUT_SERVICE)%' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + x509cert: '%env(string:SAML_IDP_X509_CERT)%' + sp: + entityId: '%env(string:SAML_SP_ENTITY_ID)%' + assertionConsumerService: + url: '%partdb.default_uri%saml/acs' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + singleLogoutService: + url: '%partdb.default_uri%logout' + binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + x509cert: '%env(string:SAML_SP_X509_CERT)%' + # Before the env variable was wrongly named "SAMLP_SP_PRIVATE_KEY". + # For compatibility reasons we keep it and only fallback to the new name if the old one is not set. This may be removed in the future. + privateKey: '%env(string:default:saml.sp.privateKey:string:SAMLP_SP_PRIVATE_KEY)%' + + # Optional settings + baseurl: '%partdb.default_uri%saml/' + strict: true + debug: false + security: + allowRepeatAttributeName: true + # nameIdEncrypted: false + authnRequestsSigned: true + logoutRequestSigned: true + logoutResponseSigned: true + # wantMessagesSigned: false + # wantAssertionsSigned: true + # wantNameIdEncrypted: false + # requestedAuthnContext: true + # signMetadata: false + # wantXMLValidation: true + # relaxDestinationValidation: false + # destinationStrictlyMatches: true + # rejectUnsolicitedResponsesWithInResponseTo: false + # signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + # digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' + #contactPerson: + # technical: + # givenName: 'Tech User' + # emailAddress: 'techuser@example.com' + # support: + # givenName: 'Support User' + # emailAddress: 'supportuser@example.com' + # administrative: + # givenName: 'Administrative User' + # emailAddress: 'administrativeuser@example.com' + #organization: + # en: + # name: 'Part-DB-name' + # displayname: 'Displayname' + # url: 'http://example.com' \ No newline at end of file diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml new file mode 100644 index 00000000..c7665081 --- /dev/null +++ b/config/packages/nelmio_cors.yaml @@ -0,0 +1,10 @@ +nelmio_cors: + defaults: + origin_regex: true + allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] + allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + allow_headers: ['Content-Type', 'Authorization'] + expose_headers: ['Link'] + max_age: 3600 + paths: + '^/': null diff --git a/config/packages/nelmio_security.yaml b/config/packages/nelmio_security.yaml index d97b3983..1cb74da7 100644 --- a/config/packages/nelmio_security.yaml +++ b/config/packages/nelmio_security.yaml @@ -12,6 +12,13 @@ nelmio_security: external_redirects: abort: true log: true + allow_list: + # Whitelist the domain of the SAML IDP, so we can redirect to it during the SAML login process + - '%env(string:key:host:url:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%' + + # Whitelist the info provider APIs (OAuth redirects) + - 'digikey.com' + - 'nexar.com' # forces Microsoft's XSS-Protection with # its block mode @@ -44,12 +51,16 @@ nelmio_security: img-src: - '*' - 'data:' + # Required for be able to load pictures in the QR code scanner + - 'blob:' style-src: - 'self' - 'unsafe-inline' - 'data:' script-src: - 'self' + # Required for loading the Wasm for the barcode scanner: + - 'wasm-unsafe-eval' object-src: - 'self' - 'data:' diff --git a/config/packages/php_translation.yaml b/config/packages/php_translation.yaml deleted file mode 100644 index 7c4f6ad9..00000000 --- a/config/packages/php_translation.yaml +++ /dev/null @@ -1,11 +0,0 @@ -translation: - locales: ["en", "de"] - edit_in_place: - enabled: false - config_name: app - configs: - app: - dirs: ["%kernel.project_dir%/templates", "%kernel.project_dir%/src"] - output_dir: "%kernel.project_dir%/translations" - excluded_names: ["*TestCase.php", "*Test.php"] - excluded_dirs: [cache, data, logs] diff --git a/config/packages/scheb_2fa.yaml b/config/packages/scheb_2fa.yaml index f58bdacc..ad0e7a5a 100644 --- a/config/packages/scheb_2fa.yaml +++ b/config/packages/scheb_2fa.yaml @@ -1,12 +1,12 @@ -# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/5.x/configuration.html +# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html scheb_two_factor: google: enabled: true # If Google Authenticator should be enabled, default false - server_name: '%partdb.title%' # Server name used in QR code - issuer: 'Part-DB' # Issuer name used in QR code + server_name: '$$DOMAIN$$' # This field is replaced by the domain name of the server in DecoratedGoogleAuthenticator + issuer: '%partdb.title%' # Issuer name used in QR code digits: 6 # Number of digits in authentication code - window: 1 # How many codes before/after the current one would be accepted as valid + leeway: 5 # Acceptable time drift in seconds template: security/2fa_form.html.twig backup_codes: @@ -23,6 +23,6 @@ scheb_two_factor: security_tokens: - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken # If you're using guard-based authentication, you have to use this one: - # - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken + # - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken # If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one: - # - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken \ No newline at end of file + - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 7e0ded3c..95f5c6b1 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,6 +1,5 @@ security: - enable_authenticator_manager: true - + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' @@ -20,7 +19,13 @@ security: provider: app_user_provider lazy: true user_checker: App\Security\UserChecker - entry_point: form_login + entry_point: App\Security\AuthenticationEntryPoint + + # Enable user impersonation + switch_user: { role: CAN_SWITCH_USER } + + custom_authenticators: + - App\Security\ApiTokenAuthenticator two_factor: auth_form_path: 2fa_login @@ -64,3 +69,7 @@ security: # We get into trouble with the U2F authentication, if the calls to the trees trigger an 2FA login # This settings should not do much harm, because a read only access to show available data structures is not really critical - { path: "^/\\w{2}/tree", role: PUBLIC_ACCESS } + # Restrict access to API to users, which has the API access permission + - { path: "^/api", allow_if: 'is_granted("@api.access_api") and is_authenticated()' } + # Restrict access to KICAD to users, which has API access permission + - { path: "^/kicad-api", allow_if: 'is_granted("@api.access_api") and is_authenticated()' } diff --git a/config/packages/sensio_framework_extra.yaml b/config/packages/sensio_framework_extra.yaml deleted file mode 100644 index 1821ccc0..00000000 --- a/config/packages/sensio_framework_extra.yaml +++ /dev/null @@ -1,3 +0,0 @@ -sensio_framework_extra: - router: - annotations: false diff --git a/config/packages/test/framework.yaml b/config/packages/test/framework.yaml index d051c840..f76cc2ef 100644 --- a/config/packages/test/framework.yaml +++ b/config/packages/test/framework.yaml @@ -1,4 +1,2 @@ framework: test: true - session: - storage_id: session.storage.mock_file diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 05dec32c..5b2d64e5 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -20,6 +20,7 @@ twig: avatar_helper: '@App\Services\UserSystem\UserAvatarHelper' available_themes: '%partdb.available_themes%' saml_enabled: '%partdb.saml.enabled%' + part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator' when@test: twig: diff --git a/config/packages/uid.yaml b/config/packages/uid.yaml new file mode 100644 index 00000000..01520944 --- /dev/null +++ b/config/packages/uid.yaml @@ -0,0 +1,4 @@ +framework: + uid: + default_uuid_version: 7 + time_based_uuid_version: 7 diff --git a/config/packages/ux_translator.yaml b/config/packages/ux_translator.yaml new file mode 100644 index 00000000..1c1c7060 --- /dev/null +++ b/config/packages/ux_translator.yaml @@ -0,0 +1,3 @@ +ux_translator: + # The directory where the JavaScript translations are dumped + dump_directory: '%kernel.project_dir%/var/translations' diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml index 17893da1..b9461110 100644 --- a/config/packages/web_profiler.yaml +++ b/config/packages/web_profiler.yaml @@ -4,7 +4,9 @@ when@dev: intercept_redirects: false framework: - profiler: { only_exceptions: false } + profiler: + only_exceptions: false + collect_serializer_data: true when@test: web_profiler: diff --git a/config/parameters.yaml b/config/parameters.yaml index a7a23db3..b2c10893 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -11,18 +11,22 @@ parameters: partdb.banner: '%env(trim:string:BANNER)%' # The info text shown in the homepage, if empty config/banner.md is used partdb.default_currency: '%env(string:BASE_CURRENCY)%' # 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 partdb.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme - partdb.locale_menu: ['en', 'de', 'fr', 'ru', 'ja'] # The languages that are shown in user drop down menu + partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all. partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails + partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets. + ###################################################################################################################### # Users and Privacy ###################################################################################################################### - partdb.gpdr_compliance: true # If this option is activated, IP addresses are anonymized to be GPDR compliant + partdb.gdpr_compliance: true # If this option is activated, IP addresses are anonymized to be GDPR compliant partdb.users.use_gravatar: '%env(bool:USE_GRAVATAR)%' # Set to false, if no Gravatar images should be used for user profiles. partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured. + partdb.check_for_updates: '%env(bool:CHECK_FOR_UPDATES)' # Set to false, if Part-DB should not contact the GitHub API to check for updates + ###################################################################################################################### # Mail settings ###################################################################################################################### @@ -33,6 +37,7 @@ parameters: # Attachments and files ###################################################################################################################### partdb.attachments.allow_downloads: '%env(bool:ALLOW_ATTACHMENT_DOWNLOADS)%' # Allow users to download attachments to server. Warning: This can be dangerous, because via that feature attackers maybe can access ressources on your intranet! + partdb.attachments.download_by_default: '%env(bool:ATTACHMENT_DOWNLOAD_BY_DEFAULT)%' # If this is set the 'download external files' checkbox is set by default for new attachments (only if allow_downloads is set to true) partdb.attachments.dir.media: 'public/media/' # The folder where uploaded attachment files are saved (must be in public folder) partdb.attachments.dir.secure: 'uploads/' # The folder where secured attachment files are saved (must not be in public/) partdb.attachments.max_file_size: '%env(string:MAX_ATTACHMENT_FILE_SIZE)%' # The maximum size of an attachment file (in bytes, you can use M for megabytes and G for gigabytes) @@ -48,6 +53,12 @@ parameters: ###################################################################################################################### partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled + ###################################################################################################################### + # Table settings + ###################################################################################################################### + partdb.table.default_page_size: '%env(int:TABLE_DEFAULT_PAGE_SIZE)%' # The default number of entries shown per page in tables + partdb.table.parts.default_columns: '%env(trim:string:TABLE_PARTS_DEFAULT_COLUMNS)%' # The default columns in part tables and their order + ###################################################################################################################### # Sidebar ###################################################################################################################### @@ -106,6 +117,8 @@ parameters: env(USE_GRAVATAR): '0' env(MAX_ATTACHMENT_FILE_SIZE): '100M' + env(REDIRECT_TO_HTTPS): 0 + env(ENFORCE_CHANGE_COMMENTS_FOR): '' env(ERROR_PAGE_ADMIN_EMAIL): '' @@ -119,9 +132,20 @@ parameters: env(EMAIL_SENDER_NAME): 'Part-DB Mailer' env(ALLOW_EMAIL_PW_RESET): 0 + env(TABLE_DEFAULT_PAGE_SIZE): 50 + env(TRUSTED_PROXIES): '127.0.0.1' #By default trust only our own server env(TRUSTED_HOSTS): '' # Trust all host names by default env(DEFAULT_URI): 'https://partdb.changeme.invalid/' env(SAML_ROLE_MAPPING): '{}' + + env(HISTORY_SAVE_CHANGED_DATA): 1 + env(HISTORY_SAVE_CHANGED_FIELDS): 1 + env(HISTORY_SAVE_REMOVED_DATA): 1 + env(HISTORY_SAVE_NEW_DATA): 1 + + env(EDA_KICAD_CATEGORY_DEPTH): 0 + + env(DATABASE_EMULATE_NATURAL_SORT): 0 diff --git a/config/permissions.yaml b/config/permissions.yaml index bcd3d79c..b8970556 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -25,27 +25,35 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co # If a part can be read by a user, he can also see all the datastructures (except devices) alsoSet: ['storelocations.read', 'footprints.read', 'categories.read', 'suppliers.read', 'manufacturers.read', 'currencies.read', 'attachment_types.read', 'measurement_units.read'] + apiTokenRole: ROLE_API_READ_ONLY edit: label: "perm.edit" alsoSet: ['read', 'parts_stock.withdraw', 'parts_stock.add', 'parts_stock.move'] + apiTokenRole: ROLE_API_EDIT create: label: "perm.create" alsoSet: ['read', 'edit'] + apiTokenRole: ROLE_API_EDIT delete: label: "perm.delete" alsoSet: ['read', 'edit'] + apiTokenRole: ROLE_API_EDIT change_favorite: label: "perm.part.change_favorite" alsoSet: ['edit'] + apiTokenRole: ROLE_API_EDIT show_history: label: "perm.part.show_history" alsoSet: ['read'] + apiTokenRole: ROLE_API_READ_ONLY revert_element: label: "perm.revert_elements" alsoSet: ["read", "edit", "create", "delete", "show_history"] + apiTokenRole: ROLE_API_EDIT import: label: "perm.import" alsoSet: ["read", "edit", "create"] + apiTokenRole: ROLE_API_EDIT parts_stock: group: "data" @@ -53,10 +61,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co operations: withdraw: label: "perm.parts_stock.withdraw" + apiTokenRole: ROLE_API_EDIT add: label: "perm.parts_stock.add" + apiTokenRole: ROLE_API_EDIT move: label: "perm.parts_stock.move" + apiTokenRole: ROLE_API_EDIT storelocations: &PART_CONTAINING @@ -65,23 +76,30 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co operations: read: label: "perm.read" + apiTokenRole: ROLE_API_READ_ONLY edit: label: "perm.edit" alsoSet: 'read' + apiTokenRole: ROLE_API_EDIT create: label: "perm.create" alsoSet: ['read', 'edit'] + apiTokenRole: ROLE_API_EDIT delete: label: "perm.delete" alsoSet: ['read', 'edit'] + apiTokenRole: ROLE_API_EDIT show_history: label: "perm.show_history" + apiTokenRole: ROLE_API_READ_ONLY revert_element: label: "perm.revert_elements" alsoSet: ["read", "edit", "create", "delete", "show_history"] + apiTokenRole: ROLE_API_EDIT import: label: "perm.import" alsoSet: [ "read", "edit", "create" ] + apiTokenRole: ROLE_API_EDIT footprints: <<: *PART_CONTAINING @@ -139,32 +157,48 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co ic_logos: label: "perm.tools.ic_logos" + info_providers: + label: "perm.part.info_providers" + operations: + create_parts: + label: "perm.part.info_providers.create_parts" + alsoSet: ['parts.create'] + apiTokenRole: ROLE_API_EDIT + groups: label: "perm.groups" group: "system" operations: read: label: "perm.read" + apiTokenRole: ROLE_API_ADMIN edit: label: "perm.edit" alsoSet: 'read' + apiTokenRole: ROLE_API_ADMIN create: label: "perm.create" alsoSet: ['read', 'edit'] + apiTokenRole: ROLE_API_ADMIN delete: label: "perm.delete" alsoSet: ['read', 'delete'] + apiTokenRole: ROLE_API_ADMIN edit_permissions: label: "perm.edit_permissions" alsoSet: ['read', 'edit'] + apiTokenRole: ROLE_API_ADMIN show_history: label: "perm.show_history" + apiTokenRole: ROLE_API_ADMIN revert_element: label: "perm.revert_elements" alsoSet: ["read", "edit", "create", "delete", "edit_permissions", "show_history"] + apiTokenRole: ROLE_API_ADMIN import: label: "perm.import" alsoSet: [ "read", "edit", "create" ] + apiTokenRole: ROLE_API_ADMIN users: label: "perm.users" @@ -172,34 +206,49 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co operations: read: label: "perm.read" + apiTokenRole: ROLE_API_ADMIN create: label: "perm.create" alsoSet: ['read', 'edit_username', 'edit_infos'] + apiTokenRole: ROLE_API_ADMIN delete: label: "perm.delete" alsoSet: ['read', 'edit_username', 'edit_infos'] + apiTokenRole: ROLE_API_ADMIN edit_username: label: "perm.users.edit_user_name" alsoSet: ['read'] + apiTokenRole: ROLE_API_ADMIN edit_infos: label: "perm.users.edit_infos" alsoSet: 'read' + apiTokenRole: ROLE_API_ADMIN edit_permissions: label: "perm.users.edit_permissions" alsoSet: 'read' + apiTokenRole: ROLE_API_ADMIN set_password: label: "perm.users.set_password" alsoSet: 'read' + apiTokenRole: ROLE_API_FULL + impersonate: + label: "perm.users.impersonate" + alsoSet: ['set_password'] + apiTokenRole: ROLE_API_FULL change_user_settings: label: "perm.users.change_user_settings" + apiTokenRole: ROLE_API_ADMIN show_history: label: "perm.show_history" + apiTokenRole: ROLE_API_ADMIN revert_element: label: "perm.revert_elements" alsoSet: ["read", "create", "delete", "edit_permissions", "show_history", "edit_infos", "edit_username"] + apiTokenRole: ROLE_API_ADMIN import: label: "perm.import" alsoSet: [ "read", "create" ] + apiTokenRole: ROLE_API_ADMIN #database: # label: "perm.database" @@ -234,60 +283,94 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co operations: show_logs: label: "perm.show_logs" + apiTokenRole: ROLE_API_ADMIN delete_logs: label: "perm.delete_logs" alsoSet: 'show_logs' + apiTokenRole: ROLE_API_ADMIN server_infos: label: "perm.server_infos" + apiTokenRole: ROLE_API_ADMIN + manage_oauth_tokens: + label: "Manage OAuth tokens" + apiTokenRole: ROLE_API_ADMIN + show_updates: + label: "perm.system.show_available_updates" + apiTokenRole: ROLE_API_ADMIN + attachments: label: "perm.part.attachments" operations: show_private: label: "perm.attachments.show_private" + apiTokenRole: ROLE_API_READ_ONLY list_attachments: label: "perm.attachments.list_attachments" alsoSet: ['attachment_types.read'] + apiTokenRole: ROLE_API_READ_ONLY self: label: "perm.self" operations: edit_infos: label: "perm.self.edit_infos" + apiTokenRole: ROLE_API_FULL edit_username: label: "perm.self.edit_username" + apiTokenRole: ROLE_API_FULL show_permissions: label: "perm.self.show_permissions" + apiTokenRole: ROLE_API_READ_ONLY show_logs: label: "perm.self.show_logs" + apiTokenRole: ROLE_API_FULL labels: label: "perm.labels" operations: create_labels: label: "perm.self.create_labels" + apiTokenRole: ROLE_API_READ_ONLY edit_options: label: "perm.self.edit_options" alsoSet: ['create_labels'] + apiTokenRole: ROLE_API_READ_ONLY read_profiles: label: "perm.self.read_profiles" + apiTokenRole: ROLE_API_READ_ONLY edit_profiles: label: "perm.self.edit_profiles" alsoSet: ['read_profiles'] + apiTokenRole: ROLE_API_EDIT create_profiles: label: "perm.self.create_profiles" alsoSet: ['read_profiles', 'edit_profiles'] + apiTokenRole: ROLE_API_EDIT delete_profiles: label: "perm.self.delete_profiles" alsoSet: ['read_profiles', 'edit_profiles', 'create_profiles'] + apiTokenRole: ROLE_API_EDIT use_twig: label: "perm.labels.use_twig" alsoSet: ['create_labels', 'edit_options'] + apiTokenRole: ROLE_API_ADMIN show_history: label: "perm.show_history" alsoSet: ['read_profiles'] + apiTokenRole: ROLE_API_READ_ONLY revert_element: label: "perm.revert_elements" alsoSet: ['read_profiles', 'edit_profiles', 'create_profiles', 'delete_profiles'] + apiTokenRole: ROLE_API_EDIT - + api: + label: "perm.api" + operations: + access_api: + label: "perm.api.access_api" + apiTokenRole: ROLE_API_READ_ONLY + manage_tokens: + label: "perm.api.manage_tokens" + alsoSet: ['access_api'] + apiTokenRole: ROLE_API_FULL \ No newline at end of file diff --git a/config/routes.yaml b/config/routes.yaml index 7d495d6d..8b38fa71 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -1,12 +1,8 @@ -#index: -# path: / -# controller: App\Controller\DefaultController::index - # Redirect every url without an locale to the locale of the user/the global base locale scan_qr: path: /scan/{type}/{id} - controller: App\Controller\ScanController:scanQRCode + controller: App\Controller\ScanController::scanQRCode csp_report: path: /csp/report @@ -19,5 +15,5 @@ redirector: requirements: url: ".*" controller: App\Controller\RedirectController::addLocalePart - # Dont match localized routes (no redirection loop, if no root with that name exists) - condition: "not (request.getPathInfo() matches '/^\\\\/[a-z]{2}(_[A-Z]{2})?\\\\//')" \ No newline at end of file + # Dont match localized routes (no redirection loop, if no root with that name exists) or API prefixed routes + condition: "not (request.getPathInfo() matches '/^\\\\/([a-z]{2}(_[A-Z]{2})?|api)\\\\//')" \ No newline at end of file diff --git a/config/routes/api_platform.yaml b/config/routes/api_platform.yaml new file mode 100644 index 00000000..38f11cba --- /dev/null +++ b/config/routes/api_platform.yaml @@ -0,0 +1,4 @@ +api_platform: + resource: . + type: api_platform + prefix: /api diff --git a/config/routes/annotations.yaml b/config/routes/attributes.yaml similarity index 66% rename from config/routes/annotations.yaml rename to config/routes/attributes.yaml index bd9ea273..72d7c9bc 100644 --- a/config/routes/annotations.yaml +++ b/config/routes/attributes.yaml @@ -1,6 +1,8 @@ controllers: - resource: ../../src/Controller/ - type: annotation + resource: + path: ../../src/Controller/ + namespace: App\Controller + type: attribute prefix: '{_locale}' defaults: @@ -11,4 +13,4 @@ controllers: kernel: resource: ../../src/Kernel.php - type: annotation + type: attribute diff --git a/config/routes/dev/php_translation.yaml b/config/routes/dev/php_translation.yaml deleted file mode 100644 index 903b23fb..00000000 --- a/config/routes/dev/php_translation.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_translation_webui: - resource: '@TranslationBundle/Resources/config/routing_webui.yaml' - prefix: /admin - -_translation_profiler: - resource: '@TranslationBundle/Resources/config/routing_symfony_profiler.yaml' diff --git a/config/routes/jbtronics_translation_editor.yaml b/config/routes/jbtronics_translation_editor.yaml new file mode 100644 index 00000000..31409a61 --- /dev/null +++ b/config/routes/jbtronics_translation_editor.yaml @@ -0,0 +1,3 @@ +when@dev: + translation_editor: + resource: '@JbtronicsTranslationEditorBundle/config/routes.php' \ No newline at end of file diff --git a/config/routes/hslavich_saml.yaml b/config/routes/nbgrp_saml.yaml similarity index 58% rename from config/routes/hslavich_saml.yaml rename to config/routes/nbgrp_saml.yaml index a902a93e..6bf45795 100644 --- a/config/routes/hslavich_saml.yaml +++ b/config/routes/nbgrp_saml.yaml @@ -1,4 +1,4 @@ -hslavich_saml_sp: - resource: "@HslavichOneloginSamlBundle/Resources/config/routing.yml" +nbgrp_saml: + resource: "@NbgrpOneloginSamlBundle/Resources/config/routes.php" # Only load the SAML routes if SAML is enabled condition: "env('SAML_ENABLED') == '1' or env('SAML_ENABLED') == 'true'" diff --git a/config/routes/php_translation.yaml b/config/routes/php_translation.yaml deleted file mode 100644 index 96ffd76f..00000000 --- a/config/routes/php_translation.yaml +++ /dev/null @@ -1,3 +0,0 @@ -_translation_edit_in_place: - resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yaml' - prefix: /admin diff --git a/config/routes/security.yaml b/config/routes/security.yaml new file mode 100644 index 00000000..f853be15 --- /dev/null +++ b/config/routes/security.yaml @@ -0,0 +1,3 @@ +_security_logout: + resource: security.route_loader.logout + type: service diff --git a/config/services.yaml b/config/services.yaml index b075684a..b2342edd 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,16 +14,19 @@ services: autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. bind: bool $demo_mode: '%partdb.demo_mode%' - bool $gpdr_compliance : '%partdb.gpdr_compliance%' - bool $kernel_debug: '%kernel.debug%' + bool $gdpr_compliance: '%partdb.gdpr_compliance%' + bool $kernel_debug_enabled: '%kernel.debug%' string $kernel_cache_dir: '%kernel.cache_dir%' string $partdb_title: '%partdb.title%' - string $default_currency: '%partdb.default_currency%' + string $base_currency: '%partdb.default_currency%' _instanceof: App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface: tags: ['app.label_placeholder_provider'] + App\Services\InfoProviderSystem\Providers\InfoProviderInterface: + tags: ['app.info_provider'] + # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: @@ -73,22 +76,18 @@ services: # Only the event classes specified here are saved to DB (set to []) to log all events $whitelist: [] - App\EventSubscriber\LogSystem\EventLoggerSubscriber: + App\EventListener\LogSystem\EventLoggerListener: arguments: $save_changed_fields: '%env(bool:HISTORY_SAVE_CHANGED_FIELDS)%' $save_changed_data: '%env(bool:HISTORY_SAVE_CHANGED_DATA)%' $save_removed_data: '%env(bool:HISTORY_SAVE_REMOVED_DATA)%' - tags: - - { name: 'doctrine.event_subscriber' } - - App\EventSubscriber\LogSystem\LogDBMigrationSubscriber: - tags: - - { name: 'doctrine.event_subscriber' } + $save_new_data: '%env(bool:HISTORY_SAVE_NEW_DATA)%' App\Form\AttachmentFormType: arguments: - $allow_attachments_downloads: '%partdb.attachments.allow_downloads%' + $allow_attachments_download: '%partdb.attachments.allow_downloads%' $max_file_size: '%partdb.attachments.max_file_size%' + $download_by_default: '%partdb.attachments.download_by_default%' App\Services\Attachments\AttachmentSubmitHandler: arguments: @@ -96,12 +95,6 @@ services: $mimeTypes: '@mime_types' $max_upload_size: '%partdb.attachments.max_file_size%' - App\EventSubscriber\LogSystem\LogoutLoggerListener: - tags: - - name: 'kernel.event_listener' - event: 'Symfony\Component\Security\Http\Event\LogoutEvent' - dispatcher: security.event_dispatcher.main - App\Services\LogSystem\EventCommentNeededHelper: arguments: $enforce_change_comments_for: '%partdb.enforce_change_comments_for%' @@ -142,6 +135,19 @@ services: $saml_role_mapping: '%env(json:SAML_ROLE_MAPPING)%' $update_group_on_login: '%env(bool:SAML_UPDATE_GROUP_ON_LOGIN)%' + + security.access_token_extractor.header.token: + class: Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor + arguments: + $tokenType: 'Token' + + security.access_token_extractor.main: + class: Symfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor + arguments: + $accessTokenExtractors: + - '@security.access_token_extractor.header' + - '@security.access_token_extractor.header.token' + #################################################################################################################### # Cache #################################################################################################################### @@ -182,7 +188,7 @@ services: App\EventSubscriber\UserSystem\SetUserTimezoneSubscriber: arguments: - $timezone: '%partdb.timezone%' + $default_timezone: '%partdb.timezone%' App\Controller\SecurityController: arguments: @@ -213,6 +219,15 @@ services: arguments: $saml_enabled: '%partdb.saml.enabled%' + #################################################################################################################### + # Table settings + #################################################################################################################### + App\DataTables\PartsDataTable: + arguments: + $visible_columns: '%partdb.table.parts.default_columns%' + + App\DataTables\Helpers\ColumnSortHelper: + shared: false # Service has a state so not share it between different tables #################################################################################################################### # Label system @@ -226,6 +241,11 @@ services: tags: - { name: 'app.label_placeholder_provider', priority: 10} + App\Services\LabelSystem\DompdfFactory: + arguments: + $fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/' + $tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/' + #################################################################################################################### # Trees #################################################################################################################### @@ -234,6 +254,84 @@ services: $rootNodeExpandedByDefault: '%partdb.sidebar.root_expanded%' $rootNodeEnabled: '%partdb.sidebar.root_node_enable%' + #################################################################################################################### + # Part info provider system + #################################################################################################################### + App\Services\InfoProviderSystem\ProviderRegistry: + arguments: + $providers: !tagged_iterator 'app.info_provider' + + App\Services\InfoProviderSystem\Providers\Element14Provider: + arguments: + $api_key: '%env(string:PROVIDER_ELEMENT14_KEY)%' + $store_id: '%env(string:PROVIDER_ELEMENT14_STORE_ID)%' + + App\Services\InfoProviderSystem\Providers\DigikeyProvider: + arguments: + $clientId: '%env(string:PROVIDER_DIGIKEY_CLIENT_ID)%' + $currency: '%env(string:PROVIDER_DIGIKEY_CURRENCY)%' + $language: '%env(string:PROVIDER_DIGIKEY_LANGUAGE)%' + $country: '%env(string:PROVIDER_DIGIKEY_COUNTRY)%' + + App\Services\InfoProviderSystem\Providers\TMEClient: + arguments: + $secret: '%env(string:PROVIDER_TME_SECRET)%' + $token: '%env(string:PROVIDER_TME_KEY)%' + + App\Services\InfoProviderSystem\Providers\TMEProvider: + arguments: + $currency: '%env(string:PROVIDER_TME_CURRENCY)%' + $country: '%env(string:PROVIDER_TME_COUNTRY)%' + $language: '%env(string:PROVIDER_TME_LANGUAGE)%' + $get_gross_prices: '%env(bool:PROVIDER_TME_GET_GROSS_PRICES)%' + + App\Services\InfoProviderSystem\Providers\OctopartProvider: + arguments: + $clientId: '&env(string:PROVIDER_OCTOPART_CLIENT_ID)%' + $secret: '%env(string:PROVIDER_OCTOPART_SECRET)%' + $country: '%env(string:PROVIDER_OCTOPART_COUNTRY)%' + $currency: '%env(string:PROVIDER_OCTOPART_CURRENCY)%' + $search_limit: '%env(int:PROVIDER_OCTOPART_SEARCH_LIMIT)%' + $onlyAuthorizedSellers: '%env(bool:PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS)%' + + App\Services\InfoProviderSystem\Providers\MouserProvider: + arguments: + $api_key: '%env(string:PROVIDER_MOUSER_KEY)%' + $language: '%env(string:PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE)%' + $options: '%env(string:PROVIDER_MOUSER_SEARCH_OPTION)%' + $search_limit: '%env(int:PROVIDER_MOUSER_SEARCH_LIMIT)%' + + App\Services\InfoProviderSystem\Providers\LCSCProvider: + arguments: + $enabled: '%env(bool:PROVIDER_LCSC_ENABLED)%' + $currency: '%env(string:PROVIDER_LCSC_CURRENCY)%' + + App\Services\InfoProviderSystem\Providers\OEMSecretsProvider: + arguments: + $api_key: '%env(string:PROVIDER_OEMSECRETS_KEY)%' + $country_code: '%env(string:PROVIDER_OEMSECRETS_COUNTRY_CODE)%' + $currency: '%env(PROVIDER_OEMSECRETS_CURRENCY)%' + $zero_price: '%env(PROVIDER_OEMSECRETS_ZERO_PRICE)%' + $set_param: '%env(PROVIDER_OEMSECRETS_SET_PARAM)%' + $sort_criteria: '%env(PROVIDER_OEMSECRETS_SORT_CRITERIA)%' + + + #################################################################################################################### + # API system + #################################################################################################################### + App\State\PartDBInfoProvider: + arguments: + $default_uri: '%partdb.default_uri%' + $global_locale: '%partdb.locale%' + $global_timezone: '%partdb.timezone%' + + #################################################################################################################### + # EDA system + #################################################################################################################### + App\Services\EDA\KiCadHelper: + arguments: + $category_depth: '%env(int:EDA_KICAD_CATEGORY_DEPTH)%' + #################################################################################################################### # Symfony overrides #################################################################################################################### @@ -244,6 +342,13 @@ services: tags: - {name: serializer.normalizer, priority: -9000} + # Disable igbinary serialization for cache even when igbinary is available, as it causes issues with the doctrine + # proxy objects (see https://github.com/igbinary/igbinary/issues/377 and https://github.com/igbinary/igbinary/issues/273) + cache.default_marshaller: + class: Symfony\Component\Cache\Marshaller\DefaultMarshaller + arguments: + $useIgbinarySerialize: false + #################################################################################################################### # Miscellaneous @@ -257,7 +362,7 @@ services: tags: - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } - # We are needing this service inside of a migration, where only the container is injected. So we need to define it as public, to access it from the container. + # We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container. App\Services\UserSystem\PermissionPresetsHelper: public: true @@ -265,6 +370,20 @@ services: arguments: $project_dir: '%kernel.project_dir%' + App\Services\System\UpdateAvailableManager: + arguments: + $check_for_updates: '%partdb.check_for_updates%' + + App\Services\System\BannerHelper: + arguments: + $partdb_banner: '%partdb.banner%' + $project_dir: '%kernel.project_dir%' + + App\Doctrine\Middleware\MySQLSSLConnectionMiddlewareWrapper: + arguments: + $enabled: '%env(bool:DATABASE_MYSQL_USE_SSL_CA)%' + $verify: '%env(bool:DATABASE_MYSQL_SSL_VERIFY_CERT)%' + #################################################################################################################### # Monolog #################################################################################################################### @@ -281,3 +400,14 @@ services: autowire: true tags: - { name: monolog.processor } + +when@test: + services: + # Decorate the doctrine fixtures load command to use our custom purger by default + doctrine.fixtures_load_command.custom: + decorates: doctrine.fixtures_load_command + class: Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand + arguments: + - '@doctrine.fixtures.loader' + - '@doctrine' + - { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' } diff --git a/docs/api/authentication.md b/docs/api/authentication.md new file mode 100644 index 00000000..b386c0cd --- /dev/null +++ b/docs/api/authentication.md @@ -0,0 +1,77 @@ +--- +title: Authentication +layout: default +parent: API +nav_order: 2 +--- + +# Authentication + +To use API endpoints, the external application has to authenticate itself, so that Part-DB knows which user is accessing +the data and which permissions +the application should have during the access. Authentication is always bound to a specific user, so the external +applications is acting on behalf of a +specific user. This user limits the permissions of the application so that it can only access data, which the user is +allowed to access. + +The only method currently available for authentication is to use API tokens: + +## API tokens + +An API token is a long alphanumeric string, which is bound to a specific user and can be used to authenticate as this user when accessing the API. +The API token is passed via the `Authorization` HTTP header during the API request, like the +following: `Authorization: Bearer tcp_sdjfks....`. + +{: .important } +> Everybody who knows the API token can access the API as the user, which is bound to the token. So you should treat the +> API token like a password +> and keep it secret. Only share it with trusted applications. + +API tokens can be created and managed on the user settings page in the API token section. You can create as many API +tokens as you want and also delete them again. +When deleting a token, it is immediately invalidated and can not be used anymore, which means that the application can +not access the API anymore with this token. + +### Token permissions and scopes + +API tokens are ultimately limited by the permissions of the user, which belongs to the token. That means that the token +can only access data, that the user is allowed to access, no matter the token permissions. + +But you can further limit the permissions of a token by choosing a specific scope for the token. The scope defines which +subset of permissions the token has, which can be less than the permissions of the user. For example, you can have a +user +with full read and write permissions, but create a token with only read permissions, which can only read data, but not +change anything in the database. + +{: .warning } +> In general, you should always use the least possible permissions for a token, to limit the possible damage, which can +> be done with a stolen token or a bug in the application. +> Only use the full or admin scope, if you really need it, as they could potentially be used to do a lot of damage to +> your Part-DB instance. + +The following token scopes are available: + +* **Read-Only**: The token can only read non-sensitive data (like parts, but no users or groups) from the API and can + not change anything. +* **Edit**: The token can read and write non-sensitive data via the API. This includes creating, updating and deleting + data. This should be enough for most applications. +* **Admin**: The token can read and write all data via the API, including sensitive data like users and groups. This + should only be used for trusted applications, which need to access sensitive data and perform administrative actions. +* **Full**: The token can do anything the user can do, including changing the user's password and creating new tokens. This + should only be used for highly trusted applications!! + +Please note, that in early versions of the API, there might be no endpoints yet, to really perform the actions, which +would be allowed by the token scope. + +### Expiration date + +API tokens can have an expiration date, which means that the token is only valid until the expiration date. After that +the token is automatically invalidated and can not be used anymore. The token is still listed on the user settings page, +and can be deleted there, but the code can not be used to access Part-DB anymore after the expiration date. + +### Get token information + +When authenticating with an API token, you can get information about the currently used token by accessing +the `/api/tokens/current` endpoint. +It gives you information about the token scope, expiration date and the user, which is bound to the token and the last +time the token was used. \ No newline at end of file diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 00000000..441ede9a --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,11 @@ +--- +layout: default +title: API +nav_order: 7 +has_children: true +--- + +# API + +Part-DB provides a REST API to access the data stored in the database. +In this section you can find information about the API and how to use it. diff --git a/docs/api/intro.md b/docs/api/intro.md new file mode 100644 index 00000000..78a8d2c1 --- /dev/null +++ b/docs/api/intro.md @@ -0,0 +1,229 @@ +--- +title: Introduction +layout: default +parent: API +nav_order: 1 +--- + +# Introduction + +Part-DB provides a [REST API](https://en.wikipedia.org/wiki/REST) to programmatically access the data stored in the +database. +This allows external applications to interact with Part-DB, extend it or integrate it into other applications. + +{: .warning } +> This feature is currently in beta. Please report any bugs you find. +> The API should not be considered stable yet and could change in future versions, without prior notice. +> Some features might be missing or not working yet. +> Also be aware, that there might be security issues in the API, which could allow attackers to access or edit data via +> the API, which +> they normally should be able to access. So currently you should only use the API with trusted users and trusted +> applications. + +Part-DB uses [API Platform](https://api-platform.com/) to provide the API, which allows for easy creation of REST APIs +with Symfony and gives you a lot of features out of the box. +See the [API Platform documentation](https://api-platform.com/docs/core/) for more details about the API Platform +features and how to use them. + +## Enable the API + +The API is available under the `/api` path, but not reachable without proper permissions. +You have to give the users, which should be able to access the API the proper permissions (Miscellaneous -> API). +Please note that there are two relevant permissions, the first one allows users to access the `/api/` path at all and show the documentation, +and the second one allows them to create API tokens which are needed for the authentication of external applications. + +## Authentication + +To use API endpoints, the external application has to authenticate itself, so that Part-DB knows which user is accessing +the data and +which permissions the application should have. Basically, this is done by creating an API token for a user and then +passing it on every request +with the `Authorization` header as bearer token, so you add a header `Authorization: Bearer `. + +See [Authentication chapter]({% link api/authentication.md %}) for more details. + +## API endpoints + +The API is split into different endpoints, which are reachable under the `/api/` path of your Part-DB instance ( +e.g. `https://your-part-db.local/api/`). +There are various endpoints for each entity type (like `part`, `manufacturer`, etc.), which allow you to read and write data, and some special endpoints like `search` or `statistics`. + +For example, all API endpoints for managing categories are available under `/api/categories/`. Depending on the exact +path and the HTTP method used, you can read, create, update or delete categories. +For most entities, there are endpoints like this: + +* **GET**: `/api/categories/` - List all categories in the database (with pagination of the results) +* **POST**: `/api/categories/` - Create a new category +* **GET**: `/api/categories/{id}` - Get a specific category by its ID +* **DELETE**: `/api/categories/{id}` - Delete a specific category by its ID +* **UPDATE**: `/api/categories/{id}` - Update a specific category by its ID. Only the fields which are sent in the + request are updated, all other fields are left unchanged. + Be aware that you have to set the [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) content type + header (`Content-Type: application/merge-patch+json`) for this to work. + +A full (interactive) list of endpoints can be displayed when visiting the `/api/` path in your browser, when you are +logged in with a user, which is allowed to access the API. +There is also a link to this page, on the user settings page in the API token section. +This documentation also lists all available fields for each entity type and the allowed operations. + +## Formats + +The API supports different formats for the request and response data, which you can control via the `Accept` +and `Content-Type` headers. +You should use [JSON-LD](https://json-ld.org/) as format, which is basically JSON with some additional metadata, which +allows you to describe the data in a more structured way and also allows to link between different entities. You can achieve this +by setting `Accept: application/ld+json` header to the API requests. + +To get plain JSON without any metadata or links, use the `Accept: application/json` header. + +Without an `Accept` header (e.g. when you call the endpoint in a browser), the API will return an HTML page with the +documentation, so be sure to include the desired `Accept` header in your API requests. +If you can not control the `Accept` header, you can add a `.json` or `.jsonld` suffix to the URL to enforce a JSON or +JSON-LD response (e.g. `/api/parts.jsonld`). + +## OpenAPI schema + +Part-DB provides a [OpenAPI](https://swagger.io/specification/) (formally Swagger) schema for the API +under `/api/docs.json` (so `https://your-part-db.local/api/docs.json`). +This schema is a machine-readable description of the API, which can be imported into software to test the API or even +automatically generate client libraries for the API. + +API generators which can generate a client library for the API from the schema are available for many programming +languages, like [OpenAPI Generator](https://openapi-generator.tech/). + +An JSONLD/Hydra version of the schema is also available under `/api/docs.jsonld` ( +so `https://your-part-db.local/api/docs.jsonld`). + +## Interactive documentation + +Part-DB provides an interactive documentation for the API, which is available under `/api/docs` ( +so `https://your-part-db.local/api/docs`). +You can pass your API token in the form on the top of the page, to authenticate yourself, and then you can try out the +API directly in the browser. +This is a great way to test the API and see how it works, without having to write any code. + +## Pagination + +By default, all list endpoints are paginated, which means only a certain number of results is returned per request. +To get another page of the results, you have to use the `page` query parameter, which contains the page number you want +to get (e.g. `/api/categoues/?page=2`). +When using JSONLD, the links to the next page are also included in the `hydra:view` property of the response. + +To change the size of the pages (the number of items in a single page) use the `itemsPerPage` query parameter ( +e.g. `/api/categoues/?itemsPerPage=50`). + +See [API Platform docs](https://api-platform.com/docs/core/pagination) for more infos. + +## Filtering results / Searching + +When retrieving a list of entities, you can restrict the results by various filters. Almost all entities have a search +filter, which allows you to only include entities, which (text) fields match the given search term: For example, if you only want +to get parts, with the Name "BC547", you can use `/api/parts.jsonld?name=BC547`. You can use `%` as a wildcard for multiple +characters in the search term (Be sure to properly encode the search term, if you use special characters). For example, if you want +to get all parts, whose name starts with "BC", you can use `/api/parts.jsonld?name=BC%25` (the `%25` is the url encoded version of `%`). + +There are other filters available for some entities, allowing you to search on other fields, or restricting the results +by numeric values or dates. See the endpoint documentation for the available filters. + +## Filter by associated entities + +To get all parts with a certain category, manufacturer, etc. you can use the `category`, `manufacturer`, etc. query +parameters of the `/api/parts` endpoint. +They are so-called entity filters and accept a comma-separated list of IDs of the entities you want to filter by. +For example, if you want to get all parts with the category "Resistor" (Category ID 1) and "Capacitor" (Category ID 2), +you can use `/api/parts.jsonld?category=1,2`. + +Suffix an id with `+` to suffix, to include all direct children categories of the given category. Use the `++` suffix to +include all children categories recursively. +To get all parts with the category "Resistor" (Category ID 1) and all children categories of "Capacitor" (Category ID +2), you can use `/api/parts.jsonld?category=1,2++`. + +See the endpoint documentation for the available entity filters. + +## Ordering results + +When retrieving a list of entities, you can order the results by various fields using the `order` query parameter. +For example, if you want to get all parts ordered by their name, you can use `/api/parts/?order[name]=asc`. You can use +this parameter multiple times to order by multiple fields. + +See the endpoint documentation for the available fields to order by. + +## Property filter + +Sometimes you only want to get a subset of the properties of an entity, for example when you only need the name of a +part, but not all the other properties. +You can achieve this using the `properties[]` query parameter with the name of the field you want to get. You can use +this parameter multiple times to get multiple fields. +For example, if you only want to get the name and the description of a part, you can +use `/api/parts/123?properties[]=name&properties[]=description`. +It is also possible to use these filters on list endpoints (get collection), to only get a subset of the properties of +all entities in the collection. + +See [API Platform docs](https://api-platform.com/docs/core/filters/#property-filter) for more info. + +## Change comment + +Similar to the changes using Part-DB web interface, you can add a change comment to every change you make via the API, +which will be +visible in the log of the entity. + +You can pass the text for this via the `_comment` query parameter (beware of the proper encoding). For +example `/api/parts/123?_comment=This%20is%20a%20change%20comment`. + +## Creating attachments and parameters + +To create attachments and parameters, use the POST endpoint. Internally there are different types of attachments and +parameters, for each entity type, where the attachments or parameters are used (e.g. PartAttachment for parts, etc.). +The type of the attachment or parameter is automatically determined by the `element` property of the request data if a +IRI is passed. You can use the `_type` property to explicitly set the type of the attachment or parameter (the value must +be the value of the `@type` property of the owning entity. e.g. `Part` for parts). + +For example, to create an attachment on a part, you can use the following request: + +``` +POST /api/attachments + +{ + "name": "front68", + "attachment_type": "/api/attachment_types/1", + "url": "https://invalid.invalid/test.url", + "element": "/api/parts/123" +} +``` + +## Uploading files to attachments + +To upload files to the attachments you can use the special `upload` property of the attachment entity during write operations (POST, PUT, PATCH). +Under `data` you can pass a base64 encoded string of the file content, and under `filename` the name of the file. +Using the `private` property you can control if the file is the attachment should be stored privately or public. + +For example, to upload a file to an attachment, you can use the following request: + +``` +PATCH /api/attachments/123 + +{ + "upload": { + "data": "data:@file/octet-stream;base64,LS0gcGhwTXlB[...]", + "filename": "test.csv", + "private": false + }, + "name": "Rename attachment" +} +``` + +This also works for creating new attachments, by including the `upload` property in the request data along with the other properties. + +Using the `downloadUrl` property of `upload` you can say Part-DB to upload the file specified at the URL set on the attachment. + +``` +PATCH /api/attachments/123 + +{ + "upload": { + "downloadUrl": true + }, + "url": "https://host.invalid/myfile.pdf" +} + +``` \ No newline at end of file diff --git a/docs/assets/usage/information_provider_system/animation.gif b/docs/assets/usage/information_provider_system/animation.gif new file mode 100644 index 00000000..d16c9747 Binary files /dev/null and b/docs/assets/usage/information_provider_system/animation.gif differ diff --git a/docs/concepts.md b/docs/concepts.md index 844c50ee..ddf38633 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -5,50 +5,85 @@ nav_order: 2 --- # Concepts + This page explains the different concepts of Part-DB and what their intended use is: 1. TOC {:toc} -## Part managment +## Part management ### Part -A part is the central concept of Part-DB. A part represents a single kind (or type) of a thing, like an electronic component, an device, an book or similar (depending on what you use Part-DB for). A part entity just represents a certain type of a thing, so if you have 1000 times an BC547 transistor you would create ONE part with the name BC547 and set its quantity to 1000. The individual quantities (so a single BC547 transistor) of a part, should be indistinguishable from each other, so that it does not matter which one of your 1000 things of Part you use. -A part entity have many fields, which can be used to describe it better. Most of the fields are optional: -* **Name** (Required): The name of the part or how you wanna call it. This could be an manufacturer provided name, or a name you thought of your self. The name have to be unique in a single category. -* **Description**: A short (single-line) description of what this part is/does. For longer informations you should use the comment field or the specifications + +A part is the central concept of Part-DB. A part represents a single kind (or type) of a thing, like an electronic +component, a device, a book or similar (depending on what you use Part-DB for). A part entity just represents a certain +type of thing, so if you have 1000 times a BC547 transistor you would create ONE part with the name BC547 and set its +quantity to 1000. The individual quantities (so a single BC547 transistor) of a part, should be indistinguishable from +each other so that it does not matter which one of your 1000 things of Part you use. +A part entity has many fields, which can be used to describe it better. Most of the fields are optional: + +* **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a + name you thought of yourself. Each name needs to be unique and must exist in a single category. +* **Description**: A short (single-line) description of what this part is/does. For longer information, you should use + the comment field or the specifications * **Category** (Required): The category (see there) to which this part belongs to. -* **Tags**: The list of tags this part belong to. Tags can be used to group parts logically (similar to the category), but tags are much less strict and formal (they dont have to be defined forehands) and you can assign multiple tags to a part. When clicking on a tag, a list with all parts which have the same tag, is shown. -* **Min Instock**: *Not really implemented yet*. Parts where the total instock is below this value, will show up for ordering. -* **Footprint**: See there. Useful especially for electronic parts, which have one of the common electronic footprints (like DIP8, SMD0805 or similar). If a part has no explicit defined preview picture, the preview picture of its footprint will be shown instead in tables. +* **Tags**: The list of tags this part belongs to. Tags can be used to group parts logically (similar to the category), + but tags are much less strict and formal (they don't have to be defined forehands) and you can assign multiple tags to + a part. When clicking on a tag, a list with all parts which have the same tag, is shown. +* **Min Instock**: *Not really implemented yet*. Parts where the total instock is below this value, will show up for + ordering. +* **Footprint**: See there. Useful especially for electronic parts, which have one of the common electronic footprints ( + like DIP8, SMD0805 or similar). If a part has no explicitly defined preview picture, the preview picture of its + footprint will be shown instead in tables. * **Manufacturer**: The manufacturer which has manufactured (not sold) this part. See Manufacturer entity for more info. -* **Manufacturer part number** (MPN): If you have used your own name for a part, you can put the part number the manufacturer uses in this field, so that you can find a part also under its manufacturer number. -* **Link to product page**: If you want to link to the manufacturer website of a part, and it is not possible to determine it automatically from the part name, set in the manufacturer entity (or no manfacturer is set), you can set the link here for each part individually. -* **Manufacturing Status**: The manufacturing status of this part, meaning the information about where the part is in its manufacturing lifecycle. -* **Needs review**: If you think parts informations maybe are inaccurate or incomplete and needs some later review/checking, you can set this flag. A part with this flag is marked, so that users know the informations are not completly trustworthy. +* **Manufacturer part number** (MPN): If you have used your own name for a part, you can put the part number the + manufacturer uses in this field so that you can find a part also under its manufacturer number. +* **Link to product page**: If you want to link to the manufacturer website of a part, and it is not possible to + determine it automatically from the part name, set in the manufacturer entity (or no manufacturer is set), you can set + the link here for each part individually. +* **Manufacturing Status**: The manufacturing status of this part, meaning the information about where the part is in + its manufacturing lifecycle. +* **Needs review**: If you think parts information may be inaccurate or incomplete and needs some later + review/checking, you can set this flag. A part with this flag is marked, so that users know the information is not + completely trustworthy. * **Favorite**: Parts with this flag are highlighted in parts lists * **Mass**: The mass of a single piece of this part (so of a single transistor). Given in grams. -* **Internal Part number** (IPN): Each part is automatically assigned an numerical ID which identifies a part in the database. This ID depends on when a part was created and can not be changed. If you want to assign your own unique identifiers, or sync parts identifiers with the identifiers of another database you can use this field. +* **Internal Part number** (IPN): Each part is automatically assigned a numerical ID that identifies a part in the + database. This ID depends on when a part was created and can not be changed. If you want to assign your own unique + identifiers, or sync parts identifiers with the identifiers of another database you can use this field. ### Stock / Part lot -A part can have many stock at multiple different locations. This is represented by part lots / stocks, which consists basically of a storelocation (so where are the parts of this lot are stored) and an amount (how many parts are there). -### Purchase Informations -The purchase informations describe where the part can be bought (at which vendors) and to which prices. -The first part (the order information) describes at which supplier the part can be bought and which is the name of the part under which you can order the part there. -An order information can contain multiple price informations, which describes the prices for the part at the supplier including bulk discount, etc. +A part can have many stocks at multiple different locations. This is represented by part lots/stocks, which consists +basically of a storage location (so where the parts of this lot are stored) and an amount (how many parts are there). + +### Purchase Information + +The purchase information describes where the part can be bought (at which vendors) and at which prices. +The first part (the order information) describes at which supplier the part can be bought and which is the name of the +part under which you can order the part there. +An order information can contain multiple price information, which describes the prices for the part at the supplier +including bulk discount, etc. ### Parameters -Parameters represents various specifications / parameters of a part, like the the maximum current of a diode, etc. The advantage of using parameters instead of just putting the data in the comment field or so, is that you can filter for parameters values (including ranges and more) later on. -Parameters describe can describe numeric values and/or text values for which they can be filtered. This basically allows you to define custom fields on a part. -Using the group field a parameter allows you to group parameters together in the info page later (all parameters with the same group value will be shown under the same group title). +Parameters represent various specifications/parameters of a part, like the maximum current of a diode, etc. The +advantage of using parameters instead of just putting the data in the comment field or so, is that you can filter for +parameter's values (including ranges and more) later on. +Parameters can describe numeric values and/or text values for which they can be filtered. This allows +you to define custom fields on a part. + +Using the group field as a parameter allows you to group parameters together on the info page later (all parameters with +the same group value will be shown under the same group title). ## Core data + ### Category -A category is used to group parts logically by their function (e.g. all NPN transistors would be put in a "NPN-Transistors" category). -Categories are hierarchical structures meaning that you can create logical trees to group categories together. A possible category tree could look like this: +A category is used to group parts logically by their function (e.g. all NPN transistors would be put in a " +NPN-Transistors" category). +Categories are hierarchical structures meaning that you can create logical trees to group categories together. A +possible category tree could look like this: * Active Components * Transistors @@ -60,97 +95,148 @@ Categories are hierarchical structures meaning that you can create logical trees * MCUs * Passive Components * Capacitors - * Resitors + * Resistors ### Supplier -A Supplier is a vendor / distributor where you can buy/order parts. Price informations of parts are associated with a supplier. + +A Supplier is a vendor/distributor where you can buy/order parts. Price information of parts is associated with a +supplier. ### Manufacturer -A manufacturer represents the company that manufacturer / build various parts (not necessary sell them). If the manufacturer also sell the parts, you have to create a supplier for that. -### Storelocation -A storelocation represents a place where parts can be stored. This could be a box, a shelf or other things (like the SMD feeder of a machine or so). +A manufacturer represents the company that manufacturers/builds various parts (not necessarily sell them). If the +manufacturer also sells the parts, you have to create a supplier for that. -Storelocations are hierarchical to represent storelocations contained in each other. +### Storage location + +A storage location represents a place where parts can be stored. This could be a box, a shelf, or other things (like the +SMD feeder of a machine or so). + +Storage locations are hierarchical to represent storage locations contained in each other. An example tree could look like this: + * Shelf 1 - * Box 1 - * Box 2 - * Box shelf A1 - * Box shelf A2 - * Box shelf B1 - * Box shelf B2 + * Box 1 + * Box 2 + * Box shelf A1 + * Box shelf A2 + * Box shelf B1 + * Box shelf B2 * Shelf 2 * Cupboard -Storelocations should be defined down to the smallest possible location, to make finding the part again easy. +Storage locations should be defined down to the smallest possible location, to make finding the part again easy. ### Footprint -In electronics many components have one of the common components cases / footprints. The footprint entity describes such common footprints, which can be assigned to parts. -You can assign an image (and an 3D model) as an attachment to a footprint, which will be used as preview for parts with this footprint, even if the parts do not have an explicitly assigned preview image. -Footprints are a hierachically which allows you to build logical sorted trees. An example tree could look like this: +In electronics, many components have one of the common components cases/footprints. The footprint entity describes such +common footprints, which can be assigned to parts. +You can assign an image (and a 3D model) as an attachment to a footprint, which will be used as preview for parts with +this footprint, even if the parts do not have an explicitly assigned preview image. + +Footprints are hierarchically which allows you to build logically sorted trees. An example tree could look like this: * Through-Hole components * DIP - * DIP-8 - * DIP-28 - * DIP-28W + * DIP-8 + * DIP-28 + * DIP-28W * TO - * TO-92 + * TO-92 * SMD components * SOIC - * SO-8 + * SO-8 * Resistors - * 0805 - * 0603 + * 0805 + * 0603 ### Measurement Unit -By default part instock is counted in number of individual parts, which is fine for things like electronic components, which exists only in integer quantities. However if you have things with fractional units like the length of a wire or the volume of a liquid, you have to define a measurement unit. -The measurement unit represents a physical quantity like mass, volume or length. -You can define a short unit for it (like m for Meters, or g for gramms) which will be shown, when a quantity of a part with this unit is shown. +By default, part in stock is counted in number of individual parts, which is fine for things like electronic components, +which exist only in integer quantities. However, if you have things with fractional units like the length of a wire or +the volume of a liquid, you have to define a measurement unit. +The measurement unit represents a physical quantity like mass, volume, or length. + +You can define a short unit for it (like m for Meters, or g for grams) which will be shown when a quantity of a part +with this unit is shown. + +In order to cover wider use cases and allow you to define measurement units further, it is possible to define parameters +associated to a measurement unit. These parameters are distinct from a part's parameters and are not inherited. ### Currency -By default all prices are set in the base currency configured for the instance (by default euros). If you want to use multiple currencies together (as e.g. vendors use foreign currencies for their price and you do not want to update the prices for every exchange rate change), you have to define these currencies here. -You can set an exchange rate here in terms of the base currency (or fetch it from the internet if configured). The exchange rate will be used to show users the prices in their preferred currency. +By default, all prices are set in the base currency configured for the instance (by default euros). If you want to use +multiple currencies together (e.g. vendors use foreign currencies for their price, and you do not want to update the +prices for every exchange rate change), you have to define these currencies here. + +You can set an exchange rate here in terms of the base currency (or fetch it from the internet if configured). The +exchange rate will be used to show users the prices in their preferred currency. ## Attachments + ### Attachment -An attachment is an file that can be associated with another entity (like a Part, Storelocation, User, etc.). This could for example be a datasheet in a Part, the logo of a vendor or some CAD drawing of a footprint. -An attachment has an attachment type (see below), which groups the attachments logically (and optionally restricts the allowed file types), a name describing the attachment and a file. The file can either be uploaded to the server and stored there, or given as a link to a file on another webpath. If configured in the settings, it is also possible that the webserver downloads the file from the supplied website and stores it locally on the server. +An attachment is a file that can be associated with another entity (like a Part, location, User, etc.). This could +for example be a datasheet in a Part, the logo of a vendor or some CAD drawing of a footprint. -By default all uploaded files, are accessible for everyone (even non logged in users), if the link is known. If your Part-DB instance is publicly available and you want to store private/sensitve files on it, you should mark the attachment as "Private attachment". Private attachments are only accessible to users, which has the permission to access private attachments. -Please not, that no thumbnails are generated for private attachments, which can have an performance impact. +An attachment has an attachment type (see below), which groups the attachments logically (and optionally restricts the +allowed file types), a name describing the attachment and a file. The file can either be uploaded to the server and +stored there, or given as a link to a file on another web path. If configured in the settings, it is also possible that +the web server downloads the file from the supplied website and stores it locally on the server. -Part-DB ships some preview images for various common footprints like DIP-8 and others, as internal ressources. These can be accessed/searched by typing the keyword in the URL field of a part and choosing one of the choices from the dropdown. +By default, all uploaded files, are accessible for everyone (even non-logged-in users), if the link is known. If your +Part-DB instance is publicly available, and you want to store private/sensitive files on it, you should mark the +attachment as "Private attachment". Private attachments are only accessible to users, which has permission to access +private attachments. +Please note, that no thumbnails are generated for private attachments, which can have a performance impact. -### Preview image / attachment -Most entities with attachments allow you to select one of the defined attachments as "Preview image". You can select an image attachment here, that previews the entity, this could be a picture of a Part, the logo of a manufacturer or supplier, the schematic symbol of a category or the image of an footprint. -The preview image will be shown in various locations together with the entities name. +Part-DB ships some preview images for various common footprints like DIP-8 and others, as internal resources. These can +be accessed/searched by typing the keyword in the URL field of a part and choosing one of the choices from the dropdown. -Please note that as long as the picture is not secret, it should be stored on the Part-DB instance (by upload, or letting Part-DB download the file) and *not* be marked as a private attachments, so that thumbnails can be generated for the picture (which improves performance). +### Preview image/attachment +Most entities with attachments allow you to select one of the defined attachments as "Preview image". You can select an +image attachment here, that previews the entity, this could be a picture of a Part, the logo of a manufacturer or +supplier, the schematic symbol of a category or the image of a footprint. +The preview image will be shown in various locations together with the entity's name. + +Please note that as long as the picture is not secret, it should be stored on the Part-DB instance (by uploading, or +letting Part-DB download the file) and *not* be marked as a private attachment, so that thumbnails can be generated for +the picture (which improves performance). ### Attachment types -Attachment types define logical groups of attachments. For example you could define an attachment group "Datasheets" where all datasheets of Parts, Footprints, etc. belong in, "Pictures" for preview images and more. -You can define file type restrictions, which file types and extensions are allowed for files with that attachment type. + +Attachment types define logical groups of attachments. For example, you could define an attachment group "Datasheets" +where all datasheets of Parts, Footprints, etc. belong in, "Pictures" for preview images and more. +You can define file type restrictions, and which file types and extensions are allowed for files with that attachment type. ## User System -### User -Each person which should be able to use Part-DB (by logging in) is represented by an user entity, which defines things like access rights, the password, and other things. For security reasons, every person which will use Part-DB should use its own personal account with an secret password. This allows to track activity of the users via the log. -There is a special user called `anonymous`, whose access rights are used to determine what an non-logged in user can do. Normally the anonymous user should be the most restricted user. +### User + +Each person who should be able to use Part-DB (by logging in) is represented by a user entity, which defines things +like access rights, the password, and other things. For security reasons, every person who will use Part-DB should use +their own personal account with a secret password. This allows to track activity of the users via the log. + +There is a special user called `anonymous`, whose access rights are used to determine what a non-logged-in user can do. +Normally the anonymous user should be the most restricted user. For simplification of access management users can be assigned to groups. ### Group -A group is entity, to which users can be assigned to. This can be used to logically group users by for example organisational structures and to simplify permissions managment, as you can define groups with access rights for common use cases and then just assign users to them, without the need to change every permission on the users individually. + +A group is an entity, to which users can be assigned to. This can be used to logically group users by for example +organizational structures and to simplify permissions management, as you can define groups with access rights for common +use cases and then just assign users to them, without the need to change every permission on the users individually. ## Labels -### Label profiles -A label profile represents an template for a label (for a storelocation, a part or part lot). It consists of a size, an barcode type and the content. There are various placeholders which can be inserted in the text content and which will be used replaced with data for the actual thing. -You do not have to define a label profile to generate labels (you can just set the settings on the fly in the label dialog), however if you want to generate many labels, it is recommended to save the settings as label profile, to save it for later usage. This ensures that all generated labels look the same. \ No newline at end of file +### Label profiles + +A label profile represents a template for a label (for a storage location, a part or part lot). It consists of a size, a +barcode type and the content. There are various placeholders that can be inserted in the text content and which will be +replaced with data for the actual thing. + +You do not have to define a label profile to generate labels (you can just set the settings on the fly in the label +dialog), however, if you want to generate many labels, it is recommended to save the settings as a label profile, to save +it for later usage. This ensures that all generated labels look the same. diff --git a/docs/configuration.md b/docs/configuration.md index 1132f6a7..0ad30a00 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,99 +6,250 @@ nav_order: 5 # Configuration -Part-DBs behavior can be configured to your needs. There are different kind of configuration options: Options which are user changable (changable dynamically via frontend), options which can be configured by environment variables, and options which are only configurable via symfony config files. +Part-DBs behavior can be configured to your needs. There are different kinds of configuration options: Options, which are +user-changeable (changeable dynamically via frontend), options that can be configured by environment variables, and +options that are only configurable via Symfony config files. -## User changable -Following things can be changed for every user and a user can change it for himself (if he has the correct permission for it). Configuration is either possible via the users own setting page (where you can also change the password) or via the user admin page: -* **Language**: The language that the users prefers, and which will be used when no language is explicitly specified. Language can still always be changed via the language selector. By default the global configured language is used. -* **Timezone**: The timezone which the user resides in and in which all dates and times should be shown. By default the globally configured language. -* **Theme**: The theme to use for the frontend. Allows the user to choose the frontend design, he prefers. -* **Prefered currency**: One of the defined currencies, in which all prices should be shown, if possible. Prices with other currencies will be converted to the price selected here +## User changeable + +The following things can be changed for every user and a user can change it for himself (if he has the correct permission +for it). Configuration is either possible via the user's own settings page (where you can also change the password) or via +the user admin page: + +* **Language**: The language that the users prefer, and which will be used when no language is explicitly specified. + Language can still always be changed via the language selector. By default, the globally configured language is used. +* **Timezone**: The timezone in which the user resides and in which all dates and times should be shown. By default, the + globally configured language. +* **Theme**: The theme to use for the front end. Allows the user to choose the front end design, he prefers. +* **Preferred currency**: One of the defined currencies, in which all prices should be shown, if possible. Prices with + other currencies will be converted to the price selected here ## Environment variables (.env.local) -The following configuration options can only be changed by the server administrator, by either changing the server variables, changing the `.env.local` file or setting env for your docker container. Here are just the most important options listed, see `.env` file for full list of possible env variables. + +The following configuration options can only be changed by the server administrator, by either changing the server +variables, changing the `.env.local` file or setting env for your docker container. Here are just the most important +options listed, see `.env` file for the full list of possible env variables. ### General options -* `DATABASE_URL`: Configures the database which Part-DB uses. For mysql use a string in the form of `mysql://:@:/` here (e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`. For sqlite use the following format to specify the absolute path where it should be located `sqlite:///path/part/app.db`. You can use `%kernel.project_dir%` as placeholder for the Part-DB root folder (e.g. `sqlite:///%kernel.project_dir%/var/app.db`) -* `DEFAULT_LANG`: The default language to use serverwide (when no language is explictly specified by a user or via language chooser). Must be something like `en`, `de`, `fr`, etc. -* `DEFAULT_TIMEZONE`: The default timezone to use globally, when a user has not timezone specified. Must be something like `Europe/Berlin`. See [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) under TZ Database name for a list of available options. -* `BASE_CURRENCY`: The currency to use internally for monetary values and when no currency is explictly specified. When migrating from a legacy Part-DB version, this should be the same as the currency in the old Part-DB instance (normally euro). This should be the currency you use the most. **Please note that you can not really change this setting after you have created data**. The value has to be a valid [ISO4217](https://en.wikipedia.org/wiki/ISO_4217) code, like `EUR` or `USD`. -* `INSTANCE_NAME`: The name of your installation. It will be shown as a title in the navbar and other places. By default `Part-DB`, but you can customize it to something likes `ExampleCorp. Inventory`. -* `ALLOW_ATTACHMENT_DOWNLOADS` (allowed values `0` or `1`): By setting this option to 1, users can make Part-DB directly download a file specified as an URL and create it as local file. Please not that this allows users access to all ressources publicly available to the server (so full access to other servers in the same local network), which could be a security risk. -* `USE_GRAVATAR`: Set to `1` to use [gravatar.com](gravatar.com) images for user avatars (as long as they have not set their own picture). The users browsers have to download the pictures from a third-party (gravatars) server, so this might be a privacy risk. -* `MAX_ATTACHMENT_FILE_SIZE`: The maximum file size (in bytes) for attachments. You can use the suffix `K`, `M` or `G` to specify the size in kilobytes, megabytes or gigabytes. By default `100M` (100 megabytes). Please note that this only the limit of Part-DB. You still need to configure the php.ini `upload_max_filesize` and `post_max_size` to allow bigger files to be uploaded. -* `DEFAULT_URI`: The default URI base to use for the Part-DB, when no URL can be determined from the browser request. This should be the primary URL/Domain, which is used to access Part-DB. This value is used to create correct links in emails and other places, where the URL is needed. It is also used, when SAML is enabled.s If you are using a reverse proxy, you should set this to the URL of the reverse proxy (e.g. `https://part-db.example.com`). **This value must end with a slash**. -* `ENFORCE_CHANGE_COMMENTS_FOR`: With this option you can configure, where users are enforced to give a change reason, which will be written to the log. This is a comma separated list of values (e.g. `part_edit,part_delete`). Leave empty to make change comments optional everywhere. Possible values are: - * `part_edit`: Edit operation of a existing part - * `part_delete`: Delete operation of a existing part - * `part_create`: Creation of a new part - * `part_stock_operation`: Stock operation on a part (therefore withdraw, add or move stock) - * `datastructure_edit`: Edit operation of a existing datastructure (e.g. category, manufacturer, ...) - * `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...) - * `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...) + +* `DATABASE_URL`: Configures the database which Part-DB uses: + * For MySQL (or MariaDB) use a string in the form of `mysql://:@:/` here + (e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`). + * For SQLite use the following format to specify the + absolute path where it should be located `sqlite:///path/part/app.db`. You can use `%kernel.project_dir%` as + placeholder for the Part-DB root folder (e.g. `sqlite:///%kernel.project_dir%/var/app.db`) + * For Postgresql use a string in the form of `DATABASE_URL=postgresql://user:password@127.0.0.1:5432/part-db?serverVersion=x.y`. + + Please note that **`serverVersion=x.y`** variable is required due to dependency of Symfony framework. + +* `DATABASE_MYSQL_USE_SSL_CA`: If this value is set to `1` or `true` and a MySQL connection is used, then the connection + is encrypted by SSL/TLS and the server certificate is verified against the system CA certificates or the CA certificate +bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept all certificates. +* `DATABASE_EMULATE_NATURAL_SORT` (default 0): If set to 1, Part-DB will emulate natural sorting, even if the database + does not support it natively. However this is much slower than the native sorting, and contain bugs or quirks, so use + it only, if you have to. +* `DEFAULT_LANG`: The default language to use server-wide (when no language is explicitly specified by a user or via + language chooser). Must be something like `en`, `de`, `fr`, etc. +* `DEFAULT_TIMEZONE`: The default timezone to use globally, when a user has no timezone specified. Must be something + like `Europe/Berlin`. See [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) under TZ Database name + for a list of available options. +* `BASE_CURRENCY`: The currency to use internally for monetary values and when no currency is explicitly specified. When + migrating from a legacy Part-DB version, this should be the same as the currency in the old Part-DB instance (normally + euro). This should be the currency you use the most. **Please note that you can not really change this setting after + you have created data**. The value has to be a valid [ISO4217](https://en.wikipedia.org/wiki/ISO_4217) code, + like `EUR` or `USD`. +* `INSTANCE_NAME`: The name of your installation. It will be shown as a title in the navbar and other places. By + default `Part-DB`, but you can customize it to something likes `ExampleCorp. Inventory`. +* `ALLOW_ATTACHMENT_DOWNLOADS` (allowed values `0` or `1`): By setting this option to 1, users can make Part-DB directly + download a file specified as a URL and create it as a local file. Please note that this allows users access to all + resources publicly available to the server (so full access to other servers in the same local network), which could + be a security risk. +* `ATTACHMENT_DOWNLOAD_BY_DEFAULT`: When this is set to 1, the "download external file" checkbox is checked by default + when adding a new attachment. Otherwise, it is unchecked by default. Use this if you wanna download all attachments + locally by default. Attachment download is only possible, when `ALLOW_ATTACHMENT_DOWNLOADS` is set to 1. +* `USE_GRAVATAR`: Set to `1` to use [gravatar.com](https://gravatar.com/) images for user avatars (as long as they have + not set their own picture). The users browsers have to download the pictures from a third-party (gravatar) server, so + this might be a privacy risk. +* `MAX_ATTACHMENT_FILE_SIZE`: The maximum file size (in bytes) for attachments. You can use the suffix `K`, `M` or `G` + to specify the size in kilobytes, megabytes or gigabytes. By default `100M` (100 megabytes). Please note that this is + only the limit of Part-DB. You still need to configure the php.ini `upload_max_filesize` and `post_max_size` to allow + bigger files to be uploaded. +* `DEFAULT_URI`: The default URI base to use for the Part-DB, when no URL can be determined from the browser request. + This should be the primary URL/Domain, which is used to access Part-DB. This value is used to create correct links in + emails and other places, where the URL is needed. It is also used, when SAML is enabled.s If you are using a reverse + proxy, you should set this to the URL of the reverse proxy (e.g. `https://part-db.example.com`). **This value must end + with a slash**. +* `ENFORCE_CHANGE_COMMENTS_FOR`: With this option, you can configure, where users are enforced to give a change reason, + which will be written to the log. This is a comma-separated list of values (e.g. `part_edit,part_delete`). Leave empty + to make change comments optional everywhere. Possible values are: + * `part_edit`: Edit operation of an existing part + * `part_delete`: Delete operation of an existing part + * `part_create`: Creation of a new part + * `part_stock_operation`: Stock operation on a part (therefore withdraw, add or move stock) + * `datastructure_edit`: Edit operation of an existing datastructure (e.g. category, manufacturer, ...) + * `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...) + * `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...) +* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new + versions, or if your server can not connect to the internet. +* `APP_SECRET`: This variable is a configuration parameter used for various security-related purposes, + particularly for securing and protecting various aspects of your application. It's a secret key that is used for + cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this + value should be handled as confidential data and not shared publicly. ### E-Mail settings -* `MAILER_DSN`: You can configure the mail provider which should be used for email delivery (see https://symfony.com/doc/current/components/mailer.html for full documentation). If you just want to use an SMTP mail account, you can use the following syntax `MAILER_DSN=smtp://user:password@smtp.mailserver.invalid:587` -* `EMAIL_SENDER_EMAIL`: The email address from which emails should be sent from (in most cases this has to be the same as the email address used for SMTP access) -* `EMAIL_SENDER_NAME`: Similar to `EMAIL_SENDER_EMAIL` but this allows you to specify the name from which the mails are sent from. -* `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you wan to allow users to reset their password via an email notification. You have to configure the mailprovider first before via the MAILER_DSN setting. -### History/Eventlog related settings +* `MAILER_DSN`: You can configure the mail provider which should be used for email delivery ( + see https://symfony.com/doc/current/components/mailer.html for full documentation). If you just want to use an SMTP + mail account, you can use the following syntax `MAILER_DSN=smtp://user:password@smtp.mailserver.invalid:587` +* `EMAIL_SENDER_EMAIL`: The email address from which emails should be sent from (in most cases this has to be the same + as the email address used for SMTP access) +* `EMAIL_SENDER_NAME`: Similar to `EMAIL_SENDER_EMAIL`, but this allows you to specify the name from which the mails are + sent from. +* `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email + notification. You have to configure the mail provider first before via the MAILER_DSN setting. + +### Table related settings + +* `TABLE_DEFAULT_PAGE_SIZE`: The default page size for tables. This is the number of rows which are shown per page. Set + to `-1` to disable pagination and show all rows at once. +* `TABLE_PARTS_DEFAULT_COLUMNS`: The columns in parts tables, which are visible by default (when loading table for first + time). + Also specify the default order of the columns. This is a comma separated list of column names. Available columns + are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`. + +### History/Eventlog-related settings + The following options are used to configure, which (and how much) data is written to the system log: -* `HISTORY_SAVE_CHANGED_FIELDS`: When this option is set to true, the name of the fields which are changed, are saved to the DB (so for example it is logged that a user has changed, that the user has changed the name and description of the field, but not the data/content of these changes) -* `HISTORY_SAVE_CHANGED_DATA`: When this option is set to true, the changed data is saved to log (so it is logged, that a user has changed the name of a part and what the name was before). This can increase database size, when you have a lot of changes to enties. -* `HISTORY_SAVE_REMOVED_DATA`: When this option is set to true, removed data is saved to log, meaning that you can easily undelete an entity, when it was removed accidentally. -If you wanna use want to revert changes or view older revisions of entities, then `HISTORY_SAVE_CHANGED_FIELDS`, `HISTORY_SAVE_CHANGED_DATA` and `HISTORY_SAVE_REMOVED_DATA` all have to be true. +* `HISTORY_SAVE_CHANGED_FIELDS`: When this option is set to true, the name of the fields that are changed, are saved to + the DB (so for example it is logged that a user has changed, that the user has changed the name and description of the + field, but not the data/content of these changes) +* `HISTORY_SAVE_CHANGED_DATA`: When this option is set to true, the changed data is saved to log (so it is logged, that + a user has changed the name of a part and what the name was before). This can increase database size when you have a + lot of changes to entities. +* `HISTORY_SAVE_REMOVED_DATA`: When this option is set to true, removed data is saved to log, meaning that you can + easily undelete an entity, when it was removed accidentally. +* `HISTORY_SAVE_NEW_DATA`: When this option is set to true, the new data (the data after a change) is saved to element + changed log entries. This allows you to easily see the changes between two revisions of an entity. This can increase + database size, when you have a lot of changes to entities. + +If you want to use want to revert changes or view older revisions of entities, +then `HISTORY_SAVE_CHANGED_FIELDS`, `HISTORY_SAVE_CHANGED_DATA` and `HISTORY_SAVE_REMOVED_DATA` all have to be true. ### Error pages settings -* `ERROR_PAGE_ADMIN_EMAIL`: You can set an email-address here, which is shown on the error page, who should be contacted about the issue (e.g. an IT support email of your company) -* `ERROR_PAGE_SHOW_HELP`: Set this 0, to disable the solution hints shown on an error page. These hints should not contain senstive informations, but could confuse end-users. + +* `ERROR_PAGE_ADMIN_EMAIL`: You can set an email address here, which is shown on the error page, who should be contacted + about the issue (e.g. an IT support email of your company) +* `ERROR_PAGE_SHOW_HELP`: Set this 0, to disable the solution hints shown on an error page. These hints should not + contain sensitive information but could confuse end-users. + +### EDA related settings + +* `EDA_KICAD_CATEGORY_DEPTH`: A number, which determines how many levels of Part-DB categories should be shown inside KiCad. + All parts in the selected category and all subcategories are shown in KiCad. + For performance reason this value should not be too high. The default is 0, which means that only the top level categories are shown in KiCad. + All parts in the selected category and all subcategories are shown in KiCad. Set this to a higher value, if you want to show more categories in KiCad. + When you set this value to -1, all parts are shown inside a single category in KiCad. ### SAML SSO settings -The following settings can be used to enable and configure Single-Sign on via SAML. This allows users to login to Part-DB without entering a username and password, but instead they are redirected to a SAML Identity Provider (IdP) and are logged in automatically. This is especially useful, when you want to use Part-DB in a company, where all users have a SAML account (e.g. via Active Directory or LDAP). -You can find more advanced settings in the `config/packages/hslavich_onelogin_saml.yaml` file. Please note that this file is not backuped by the backup script, so you have to backup it manually, if you want to keep your changes. If you want to edit it on docker, you have to map the file to a volume. -* `SAML_ENABLED`: When this is set to 1, SAML SSO is enabled and the SSO Login button is shown in the login form. You have to configure the SAML settings below, before you can use this feature. -* `SAML_ROLE_MAPPING`: A [JSON](https://en.wikipedia.org/wiki/JSON) encoded map which specifies how Part-DB should convert the user roles given by SAML attribute `group` should be converted to a Part-DB group (specified by ID). You can use a wildcard `*` to map all otherwise unmapped roles to a certain group. Example: `{"*": 1, "admin": 2, "editor": 3}`. This would map all roles to the group with ID 1, except the role `admin`, which is mapped to the group with ID 2 and the role `editor`, which is mapped to the group with ID 3. -* `SAML_UPDATE_GROUP_ON_LOGIN`: When this is enabled the group of the user is updated on every login of the user based on the SAML role attributes. When this is disabled, the group is only assigned on the first login of the user, and a Part-DB administrator can change the group afterwards by editing the user. -* `SAML_IDP_ENTITY_ID`: The entity ID of your SAML Identity Provider (IdP). You can find this value in the metadata XML file or configuration UI of your IdP. -* `SAML_IDP_SINGLE_SIGN_ON_SERVICE`: The URL of the SAML IdP Single Sign-On Service (SSO). You can find this value in the metadata XML file or configuration UI of your IdP. -* `SAML_IDP_SINGLE_LOGOUT_SERVICE`: The URL of the SAML IdP Single Logout Service (SLO). You can find this value in the metadata XML file or configuration UI of your IdP. -* `SAML_IDP_X509_CERT`: The base64 encoded X.509 public certificate of your SAML IdP. You can find this value in the metadata XML file or configuration UI of your IdP. It should start with `MIIC` and end with `=`. -* `SAML_SP_ENTITY_ID`: The entity ID of your SAML Service Provider (SP). This is the value you have configured for the Part-DB client in your IdP. -* `SAML_SP_X509_CERT`: The public X.509 certificate of your SAML SP (here Part-DB). This is the value you have configured for the Part-DB client in your IdP. It should start with `MIIC` and end with `=`. IdPs like keycloak allows you to generate a public/private key pair for the client which you can setup here and in the `SAML_SP_PRIVATE_KEY` setting. -* `SAML_SP_PRIVATE_KEY`: The private key of your SAML SP (here Part-DB), corresponding the public key specified in `SAML_SP_X509_CERT`. This is the value you have configured for the Part-DB client in your IdP. It should start with `MIIE` and end with `=`. IdPs like keycloak allows you to generate a public/private key pair for the client which you can setup here and in the `SAML_SP_X509_CERT` setting. +The following settings can be used to enable and configure Single-Sign on via SAML. This allows users to log in to +Part-DB without entering a username and password, but instead they are redirected to a SAML Identity Provider (IdP) and +are logged in automatically. This is especially useful when you want to use Part-DB in a company, where all users have +a SAML account (e.g. via Active Directory or LDAP). +You can find more advanced settings in the `config/packages/hslavich_onelogin_saml.yaml` file. Please note that this +file is not backed up by the backup script, so you have to back up it manually, if you want to keep your changes. If you +want to edit it on docker, you have to map the file to a volume. +* `SAML_ENABLED`: When this is set to 1, SAML SSO is enabled and the SSO Login button is shown in the login form. You + have to configure the SAML settings below before you can use this feature. +* `SAML_BEHIND_PROXY`: Set this to 1, if Part-DB is behind a reverse proxy. See [here]({% link installation/reverse-proxy.md %}) + for more information. Otherwise, leave it to 0 (default.) +* `SAML_ROLE_MAPPING`: A [JSON](https://en.wikipedia.org/wiki/JSON)-encoded map which specifies how Part-DB should + convert the user roles given by SAML attribute `group` should be converted to a Part-DB group (specified by ID). You + can use a wildcard `*` to map all otherwise unmapped roles to a certain group. + Example: `{"*": 1, "admin": 2, "editor": 3}`. This would map all roles to the group with ID 1, except the + role `admin`, which is mapped to the group with ID 2, and the role `editor`, which is mapped to the group with ID 3. +* `SAML_UPDATE_GROUP_ON_LOGIN`: When this is enabled the group of the user is updated on every login of the user based + on the SAML role attributes. When this is disabled, the group is only assigned on the first login of the user, and a + Part-DB administrator can change the group afterward by editing the user. +* `SAML_IDP_ENTITY_ID`: The entity ID of your SAML Identity Provider (IdP). You can find this value in the metadata XML + file or configuration UI of your IdP. +* `SAML_IDP_SINGLE_SIGN_ON_SERVICE`: The URL of the SAML IdP Single Sign-On Service (SSO). You can find this value in + the metadata XML file or configuration UI of your IdP. +* `SAML_IDP_SINGLE_LOGOUT_SERVICE`: The URL of the SAML IdP Single Logout Service (SLO). You can find this value in the + metadata XML file or configuration UI of your IdP. +* `SAML_IDP_X509_CERT`: The base64 encoded X.509 public certificate of your SAML IdP. You can find this value in the + metadata XML file or configuration UI of your IdP. It should start with `MIIC` and end with `=`. +* `SAML_SP_ENTITY_ID`: The entity ID of your SAML Service Provider (SP). This is the value you have configured for the + Part-DB client in your IdP. +* `SAML_SP_X509_CERT`: The public X.509 certificate of your SAML SP (here Part-DB). This is the value you have + configured for the Part-DB client in your IdP. It should start with `MIIC` and end with `=`. IdPs like keycloak allows + you to generate a public/private key pair for the client which you can set up here and in the `SAML_SP_PRIVATE_KEY` + setting. +* `SAML_SP_PRIVATE_KEY`: The private key of your SAML SP (here Part-DB), corresponding the public key specified + in `SAML_SP_X509_CERT`. This is the value you have configured for the Part-DB client in your IdP. It should start + with `MIIE` and end with `=`. IdPs like keycloak allows you to generate a public/private key pair for the client which + you can set up here and in the `SAML_SP_X509_CERT` setting. -### Other / less used options -* `TRUSTED_PROXIES`: Set the IP addresses (or IP blocks) of trusted reverse proxies here. This is needed to get correct IP informations (see [here](https://symfony.com/doc/current/deployment/proxies.html) for more info). -* `TRUSTED_HOSTS`: To prevent `HTTP Host header attacks` you can set a regex containing all host names via which Part-DB should be accessible. If accessed via the wrong hostname, an error will be shown. -* `DEMO_MODE`: Set Part-DB into demo mode, which forbids users to change their passwords and settings. Used for the demo instance, should not be needed for normal installations. -* `NO_URL_REWRITE_AVAILABLE` (allowed values `true` or `false`): Set this value to true, if your webserver does not support rewrite. In this case, all URL pathes will contain index.php/, which is needed then. Normally this setting do not need to be changed. -* `FIXER_API_KEY`: If you want to automatically retrieve exchange rates for base currencies other than euros, you have configure an exchange rate provider API. [Fixer.io](https://fixer.io/) is preconfigured, and you just have to register there and set the retrieved API key in this environment variable. -* `APP_ENV`: This value should always be set to `prod` in normal use. Set it to `dev` to enable debug/development mode. (**You should not do this on a publicly accessible server, as it will leak sensitive informations!**) -* `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker container. In all other applications you can just change the `config/banner.md` file. +### Information provider settings + +The settings prefixes with `PROVIDER_*` are used to configure the information providers. +See the [information providers]({% link usage/information_provider_system.md %}) page for more information. + +### Other / less-used options + +* `TRUSTED_PROXIES`: Set the IP addresses (or IP blocks) of trusted reverse proxies here. This is needed to get correct + IP information (see [here](https://symfony.com/doc/current/deployment/proxies.html) for more info). +* `TRUSTED_HOSTS`: To prevent `HTTP Host header attacks` you can set a regex containing all host names via which Part-DB + should be accessible. If accessed via the wrong hostname, an error will be shown. +* `DEMO_MODE`: Set Part-DB into demo mode, which forbids users to change their passwords and settings. Used for the demo + instance. This should not be needed for normal installations. +* `NO_URL_REWRITE_AVAILABLE` (allowed values `true` or `false`): Set this value to true, if your webserver does not + support rewrite. In this case, all URL paths will contain index.php/, which is needed then. Normally this setting does + not need to be changed. +* `REDIRECT_TO_HTTPS`: If this is set to true, all requests to http will be redirected to https. This is useful if your + web server does not already do this (like the one used in the demo instance). If your web server already redirects to + https, you don't need to set this. Ensure that Part-DB is accessible via HTTPS before you enable this setting. +* `FIXER_API_KEY`: If you want to automatically retrieve exchange rates for base currencies other than euros, you have to + configure an exchange rate provider API. [Fixer.io](https://fixer.io/) is preconfigured, and you just have to register + there and set the retrieved API key in this environment variable. +* `APP_ENV`: This value should always be set to `prod` in normal use. Set it to `dev` to enable debug/development + mode. (**You should not do this on a publicly accessible server, as it will leak sensitive information!**) +* `BANNER`: You can configure the text that should be shown as the banner on the homepage. Useful especially for docker + containers. In all other applications you can just change the `config/banner.md` file. +* `DISABLE_YEAR2038_BUG_CHECK`: If set to `1`, the year 2038 bug check is disabled on 32-bit systems, and dates after +2038 are no longer forbidden. However this will lead to 500 error messages when rendering dates after 2038 as all current +32-bit PHP versions can not format these dates correctly. This setting is for the case that future PHP versions will +handle this correctly on 32-bit systems. 64-bit systems are not affected by this bug, and the check is always disabled. ## Banner + To change the banner you can find on the homepage, you can either set the `BANNER` environment variable to the text you want to show, or you can edit the `config/banner.md` file. The banner is written in markdown, so you can use all markdown (and even some subset of HTML) syntax to format the text. ## parameters.yaml -You can also configure some options via the `config/parameters.yaml` file. This should normally not needed, -and you should know what you are doing, when you change something here. You should expect, that you will have to do some -manual merge, when you have changed something here and update to a newer version of Part-DB. It is possible that configuration -options here will change or completely removed in future versions of Part-DB. -If you change something here, you have to clear the cache, before the changes will take effect with the command `bin/console cache:clear`. +You can also configure some options via the `config/parameters.yaml` file. This should normally not need, +and you should know what you are doing, when you change something here. You should expect, that you will have to do some +manual merge, when you have changed something here and update to a newer version of Part-DB. It is possible that +configuration options here will change or be completely removed in future versions of Part-DB. + +If you change something here, you have to clear the cache, before the changes will take effect with the +command `bin/console cache:clear`. The following options are available: -* `partdb.global_theme`: The default theme to use, when no user specific theme is set. Should be one of the themes from the `partdb.available_themes` config option. -* `partdb.locale_menu`: The codes of the languages, which should be shown in the language chooser menu (the one with the user icon in the navbar). The first language in the list will be the default language. -* `partdb.gpdr_compliance`: When set to true (default value), IP addresses which are saved in the database will be anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in the EU. -* `partdb.sidebar.items`: The panel contents which should be shown in the sidebar by default. You can also change the number of sidebar panels by changing the number of items in this list. +* `partdb.global_theme`: The default theme to use, when no user specific theme is set. Should be one of the themes from + the `partdb.available_themes` config option. +* `partdb.locale_menu`: The codes of the languages, which should be shown in the language chooser menu (the one with the + user icon in the navbar). The first language in the list will be the default language. +* `partdb.gdpr_compliance`: When set to true (default value), IP addresses which are saved in the database will be + anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in + the EU. +* `partdb.sidebar.items`: The panel contents which should be shown in the sidebar by default. You can also change the + number of sidebar panels by changing the number of items in this list. * `partdb.sidebar.root_node_enable`: Show a root node in the sidebar trees, of which all nodes are children of * `partdb.sidebar.root_expanded`: Expand the root node in the sidebar trees by default -* `partdb.available_themes`: The list of available themes a user can choose from. \ No newline at end of file +* `partdb.available_themes`: The list of available themes a user can choose from. diff --git a/docs/index.md b/docs/index.md index 42d5c516..d732f31d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,38 +5,52 @@ nav_order: 0 --- # Part-DB + 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. {: .important-title } > Demo -> -> If you want to test Part-DB without installing it, you can use [this](https://part-db.herokuapp.com) Heroku instance. -> (Or this link for the [German Version](https://part-db.herokuapp.com/de/)). +> +> 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://demo.part-db.de/de/)). > > You can log in with username: **user** and password: **user**, to change/create data. > -> Every change to the master branch gets automatically deployed, so it represents the currenct development progress and is -> maybe not completly stable. Please mind, that the free Heroku instance is used, so it can take some time when loading the page +> Every change to the master branch gets automatically deployed, so it represents the current development progress and +> is +> maybe not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading +> the page > for the first time. ## Features -* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer - and multiple store locations and price information. Parts can be grouped using tags. You can associate various files like datasheets or pictures with the parts. -* Multi-Language support (currently German, English, Russian, Japanese and French (experimental)) + +* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer, + and multiple store locations and price information. Parts can be grouped using tags. You can associate various files + like datasheets or pictures with the parts. +* Multi-language support (currently German, English, Russian, Japanese and French (experimental)) * 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. - Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. Password reset via email can be setuped. -* Optional support for single sign-on (SSO) via SAML (using an intermediate service like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) + Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups. + Password reset via email can be setup. +* Optional support for single sign-on (SSO) via SAML (using an intermediate service + like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server) * Import/Export system -* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build this project and directly withdraw all components needed from DB -* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older versions. +* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build + this project and directly withdraw all components needed from DB +* Event log: Track what changes happens 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 and SQLite supported as database backends +* MySQL, SQLite and PostgreSQL are supported as database backends * Support for rich text descriptions and comments in parts * 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) * Easy migration from an existing PartKeepr instance (see [here]({%link partkeepr_migration.md %})) +* Use cloud providers (like Octopart, Digikey, Farnell or TME) to automatically get part information, datasheets and + prices for parts (see [here]({% link usage/information_provider_system.md %})) +* API to access Part-DB from other applications/scripts +* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as 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 makerspaces, where many users have should have (controlled) access to the shared inventory. @@ -44,25 +58,31 @@ or makerspaces, where many users have should have (controlled) access to the sha Part-DB is also used by small companies and universities for managing their inventory. ## License + 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) 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-symfony/blob/master/LICENSE) for more informations. +See [LICENSE](https://github.com/Part-DB/Part-DB-symfony/blob/master/LICENSE) for more information. ## 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). There you will find various methods to support development on a monthly or a one time base. ## Built with + * [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 * [Fontawesome](https://fontawesome.com/): Used as icon set -* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend Javascript +* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend + Javascript ## Authors -* **Jan Böhmer** - *Inital work and Maintainer* - [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. +* **Jan Böhmer** - *Initial work and Maintainer* - [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. Based on the original Part-DB by Christoph Lechner and K. Jacobs diff --git a/docs/installation/choosing_database.md b/docs/installation/choosing_database.md index b3a7a3a8..cd9657d4 100644 --- a/docs/installation/choosing_database.md +++ b/docs/installation/choosing_database.md @@ -7,24 +7,176 @@ nav_order: 1 # Choosing database: SQLite or MySQL -Part-DB saves its data in a [relational (SQL) database](https://en.wikipedia.org/wiki/Relational_database). Part-DB supports either the use of [SQLite](https://www.sqlite.org/index.html) or [MySQL](https://www.mysql.com/) / [MariaDB](https://mariadb.org/) (which are mostly the same, except for some minor differences). +Part-DB saves its data in a [relational (SQL) database](https://en.wikipedia.org/wiki/Relational_database). + +For this multiple database types are supported, currently these are: + +* [SQLite](https://www.sqlite.org/index.html) +* [MySQL](https://www.mysql.com/) / [MariaDB](https://mariadb.org/) (which are mostly the same, except for some minor + differences) +* [PostgreSQL](https://www.postgresql.org/) + +All these database types allow for the same basic functionality and allow Part-DB to run. However, there are some minor +differences between them, which might be important for you. Therefore the pros and cons of the different database types +are listed here. {: .important } -You have to choose between the database types before you start using Part-DB and **you can not change it (easily) after you have started creating data**. So you should choose the database type for your usecase (and possible future uses). +You have to choose between the database types before you start using Part-DB and **you can not change it (easily) after +you have started creating data**. So you should choose the database type for your use case (and possible future uses). ## Comparison -**SQLite** is the default database type which is configured out of the box. All data is saved in a single file (normally `var/app.db` in the Part-DB folder) and no additional installation or configuration besides Part-DB is needed. -To use **MySQL/MariaDB** as database, you have to install and configure the MySQL server, configure it and create a database and user for Part-DB, which needs some additional work. When using docker you need an additional docker container, and volume for the data +### SQLite -When using **SQLite** The database can be backuped easily by just copying the SQLite file to a safe place. Ideally the **MySQL** database has to be dumped to a SQL file (using `mysqldump`). The `console partdb:backup` command can do this automatically - -However SQLite does not support certain operations like regex search, which has to be emulated by PHP and therefore are pretty slow compared to the same operation at MySQL. In future there might be features that may only be available, when using MySQL. +#### Pros -In general MySQL might perform better for big Part-DB instances with many entries, lots of users and high activity, than SQLite. +* **Easy to use**: No additional installation or configuration is needed, just start Part-DB and it will work out of the box +* **Easy backup**: Just copy the SQLite file to a safe place, and you have a backup, which you can restore by copying it + back. No need to work with SQL dumps -## Conclusion and Suggestion +#### Cons -When you are a hobbyist and use Part-DB for your own small inventory managment with only you as user (or maybe sometimes a few other people), then the easy to use SQLite database will be fine. +* **Performance**: SQLite is not as fast as MySQL or PostgreSQL, especially when using complex queries or many users. +* **Emulated RegEx search**: SQLite does not support RegEx search natively. Part-DB can emulate it, however that is pretty slow. +* **Emualted natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow. +* **Limitations with Unicode**: SQLite has limitations in comparisons and sorting of Unicode characters, which might lead to + unexpected behavior when using non-ASCII characters in your data. For example `µ` (micro sign) is not seen as equal to + `μ` (greek minuscule mu), therefore searching for `µ` (micro sign) will not find parts containing `μ` (mu) and vice versa. + The other databases behave more intuitive in this case. +* **No advanced features**: SQLite do no support many of the advanced features of MySQL or PostgreSQL, which might be utilized + in future versions of Part-DB -When you are planning to have a very big database, with a lot of entries and many users which regulary (and concurrently) using Part-DB you should maybe use MySQL as this will scale better. \ No newline at end of file + +### MySQL/MariaDB + +**If possible, it is recommended to use MariaDB 10.7+ (instead of MySQL), as it supports natural sorting of columns natively.** + +#### Pros + +* **Performance**: Compared to SQLite, MySQL/MariaDB will probably perform better, especially in large databases with many + users and high activity. +* **Natural Sorting**: MariaDB 10.7+ supports natural sorting of columns. On other databases it has to be emulated, which is pretty + slow. +* **Native RegEx search**: MySQL supports RegEx search natively, which is faster than emulating it in PHP. +* **Advanced features**: MySQL/MariaDB supports many advanced features, which might be utilized in future versions of Part-DB. +* **Full Unicode support**: MySQL/MariaDB has better support for Unicode characters, which makes it more intuitive to use + non-ASCII characters in your data. + +#### Cons + +* **Additional installation and configuration**: You have to install and configure the MySQL server, create a database and + user for Part-DB, which needs some additional work compared to SQLite. +* **Backup**: The MySQL database has to be dumped to a SQL file (using `mysqldump`). The `console partdb:backup` command can automate this. + + +### PostgreSQL + +#### Pros +* **Performance**: PostgreSQL is known for its performance, especially in large databases with many users and high activity. +* **Advanced features**: PostgreSQL supports many advanced features, which might be utilized in future versions of Part-DB. +* **Full Unicode support**: PostgreSQL has better support for Unicode characters, which makes it more intuitive to use + non-ASCII characters in your data. +* **Native RegEx search**: PostgreSQL supports RegEx search natively, which is faster than emulating it in PHP. +* **Native Natural Sorting**: PostgreSQL supports natural sorting of columns natively in all versions and in general the support for it + is better than on MariaDB. +* **Support of transactional DDL**: PostgreSQL supports transactional DDL, which means that if you encounter a problem during a schema change, +the database will automatically rollback the changes. On MySQL/MariaDB you have to manually rollback the changes, by restoring from a database backup. + +#### Cons +* **New backend**: The support of postgresql is new, and it was not tested as much as the other backends. There might be some bugs caused by this. +* **Additional installation and configuration**: You have to install and configure the PostgreSQL server, create a database and + user for Part-DB, which needs some additional work compared to SQLite. +* **Backup**: The PostgreSQL database has to be dumped to a SQL file (using `pg_dump`). The `console partdb:backup` command can automate this. + + +## Recommendation + +When you are a hobbyist and use Part-DB for your own small inventory management with only you as user (or maybe sometimes +a few other people), then the easy-to-use SQLite database will be fine, as long as you can live with the limitations, stated above. +However using MariaDB (or PostgreSQL), has no disadvantages in that situation (besides the initial setup requirements), so you might +want to use it, to be prepared for future use cases. + +When you are planning to have a very big database, with a lot of entries and many users which regularly using Part-DB, then you should +use MariaDB or PostgreSQL, as they will perform better in that situation and allow for more advanced features. +If you should use MariaDB or PostgreSQL depends on your personal preference and what you already have installed on your servers and +what you are familiar with. + +## Using the different databases + +The only difference in using the different databases, is a different value in the `DATABASE_URL` environment variable in the `.env.local` file +or in the `DATABASE_URL` environment variable in your server or container configuration. It has the shape of a URL, where the scheme (the part before `://`) +is the database type, and the rest is connection information. + +**The env var format below is for the `env.local` file. It might work differently for other env configuration. E.g. in a docker-compose file you have to remove the quotes!** + +### SQLite + +```shell +DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db" +``` + +Here you just need to configure the path to the SQLite file, which is created by Part-DB when performing the database migrations. +The `%kernel.project_dir%` is a placeholder for the path to the project directory, which is replaced by the actual path by Symfony, so that you do not +need to specify the path manually. In the example the database will be created as `app.db` in the `var` directory of your Part-DB installation folder. + +### MySQL/MariaDB + +```shell +DATABASE_URL="mysql://user:password@127.0.0.1:3306/database?serverVersion=8.0.37" +``` + +Here you have to replace `user`, `password` and `database` with the credentials of the MySQL/MariaDB user and the database name you want to use. +The host (here 127.0.0.1) and port should also be specified according to your MySQL/MariaDB server configuration. + +In the `serverVersion` parameter you can specify the version of the MySQL/MariaDB server you are using, in the way the server returns it +(e.g. `8.0.37` for MySQL and `10.4.14-MariaDB`). If you do not know it, you can leave the default value. + +If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `unix_socket` parameter. +```shell +DATABASE_URL="mysql://user:password@localhost/database?serverVersion=8.0.37&unix_socket=/var/run/mysqld/mysqld.sock" +``` + +### PostgreSQL + +```shell +DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=12.19&charset=utf8" +``` + +Here you have to replace `db_user`, `db_password` and `db_name` with the credentials of the PostgreSQL user and the database name you want to use. +The host (here 127.0.0.1) and port should also be specified according to your PostgreSQL server configuration. + +In the `serverVersion` parameter you can specify the version of the PostgreSQL server you are using, in the way the server returns it +(e.g. `12.19 (Debian 12.19-1.pgdg120+1)`). If you do not know it, you can leave the default value. + +The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly. + +If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `host` parameter. +```shell +DATABASE_URL="postgresql://db_user@localhost/db_name?serverVersion=16.6&charset=utf8&host=/var/run/postgresql" +``` + + +## Natural Sorting + +Natural sorting is the sorting of strings in a way that numbers are sorted by their numerical value, not by their ASCII value. + +For example in the classical binary sorting the string `DIP-4`, `DIP-8`, `DIP-16`, `DIP-28` would be sorted as following: + +* `DIP-16` +* `DIP-28` +* `DIP-4` +* `DIP-8` + +In natural sorting, it would be sorted as: + +* `DIP-4` +* `DIP-8` +* `DIP-16` +* `DIP-28` + +Part-DB can sort names in part tables and tree views naturally. PostgreSQL and MariaDB 10.7+ support natural sorting natively, +and it is automatically used if available. + +For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicity enabled by setting the +`DATABASE_EMULATE_NATURAL_SORT` environment variable to `1`. If it is 0 the classical binary sorting is used, on these databases. The emulations +might have some quirks and issues, so it is recommended to use a database which supports natural sorting natively, if you want to use it. diff --git a/docs/installation/email.md b/docs/installation/email.md index 08211616..c9feaba6 100644 --- a/docs/installation/email.md +++ b/docs/installation/email.md @@ -7,31 +7,34 @@ nav_order: 12 # Email -Part-DB can communicate with its users via email. +Part-DB can communicate with its users via email. At the moment this is only used to send password reset links, but in future this will be used for other things too. To make emails work you have to properly configure a mail provider in Part-DB. ## Configuration -Part-DB uses [Symfony Mailer](https://symfony.com/doc/current/mailer.html) to send emails, which supports multiple -automatic mail providers (like MailChimp or SendGrid). If you want to use one of these providers, check the Symfony Mailer documentation for more information. -We will only cover the configuration of a SMTP provider here, which is sufficient for most usecases. -You will need an email account, which you can use send emails from via password-bases SMTP authentication, this account +Part-DB uses [Symfony Mailer](https://symfony.com/doc/current/mailer.html) to send emails, which supports multiple +automatic mail providers (like MailChimp or SendGrid). If you want to use one of these providers, check the Symfony +Mailer documentation for more information. + +We will only cover the configuration of an SMTP provider here, which is sufficient for most use-cases. +You will need an email account, which you can use send emails from via password-bases SMTP authentication, this account should be dedicated to Part-DB. To configure the SMTP provider, you have to set the following environment variables: -`MAILER_DSN`: You have to provide the SMTP server address and the credentials for the email account here. The format is the following: -`smtp://:@:`. In most cases the username is the email address of the account, and the port is 587. +`MAILER_DSN`: You have to provide the SMTP server address and the credentials for the email account here. The format is +the following: +`smtp://:@:`. In most cases the username is the email address of the +account, and the port is 587. So the resulting DSN could look like this: `smtp://j.doe@mail.invalid:SUPER_SECRET_PA$$WORD@smtp.mail.invalid:587`. `EMAIL_SENDER_EMAIL`: This is the email address which will be used as sender address for all emails sent by Part-DB. -This should be the same email address as the one used in the `MAILER_DSN` (the email adress of your email account): +This should be the same email address as the one used in the `MAILER_DSN` (the email address of your email account): e.g. `j.doe@mail.invalid`. -`EMAIL_SENDER_NAME`: This is the name which will be used as sender name for all emails sent by Part-DB. +`EMAIL_SENDER_NAME`: This is the name which will be used as sender name for all emails sent by Part-DB. This can be anything you want, e.g. `My Part-DB Mailer`. - Now you can enable the possibility to reset password by setting the `ALLOW_EMAIL_PW_RESET` env to `1` (or `true`). \ No newline at end of file diff --git a/docs/installation/index.md b/docs/installation/index.md index e7b59104..217f702a 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -6,4 +6,6 @@ has_children: true --- # Installation -Below you can find some guides to install Part-DB. \ No newline at end of file +Below you can find some guides to install Part-DB. + +For the hobbyists without much experience, we recommend the docker installation or direct installation on debian. \ No newline at end of file diff --git a/docs/installation/installation_docker.md b/docs/installation/installation_docker.md index ddac7e42..c9b46fdb 100644 --- a/docs/installation/installation_docker.md +++ b/docs/installation/installation_docker.md @@ -7,19 +7,23 @@ nav_order: 2 # Installation of Part-DB via docker -Part-DB can be installed containerized via docker. This is the easiest way to get Part-DB up and running and works on all platforms, -where docker is available (especially recommended for Windows and MacOS). - +Part-DB can be installed containerized via docker. This is the easiest way to get Part-DB up and running and works on +all platforms, +where docker is available (especially recommended for Windows and macOS). {: .warning } -> The methods described here, configure PHP without HTTPS and therefore should only be used locally in a trusted network. +> The methods described here, configure PHP without HTTPS and therefore should only be used locally in a trusted +> network. > If you want to expose Part-DB to the internet, you have to configure a reverse proxy with an SSL certificate! +It is recommended to install Part-DB on a 64-bit system, as the 32-bit version of PHP is affected by the +[Year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) and can not handle dates after 2038 correctly. + ## Docker-compose + Docker-compose configures the needed images and automatically creates the needed containers and volumes. - -1. Install docker and docker-compose like described under https://docs.docker.com/compose/install/ +1. Install docker and docker-compose as described under https://docs.docker.com/compose/install/ 2. Create a folder where the Part-DB data should live 3. Create a file named docker-compose.yaml with the following content: @@ -43,11 +47,18 @@ services: - DATABASE_URL=sqlite:///%kernel.project_dir%/var/db/app.db # In docker env logs will be redirected to stderr - APP_ENV=docker + + # Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to + # run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/ + # folder (under .automigration-backup), so you can restore it, if the migration fails. + # This feature is currently experimental, so use it at your own risk! + # - DB_AUTOMIGRATE=true # You can configure Part-DB using environment variables # Below you can find the most essential ones predefined - # However you can add add any other environment configuration you want here + # However you can add any other environment configuration you want here # See .env file for all available options or https://docs.part-db.de/configuration.html + # !!! Do not use quotes around the values, as they will be interpreted as part of the value and this will lead to errors !!! # The language to use serverwide as default (en, de, ru, etc.) - DEFAULT_LANG=en @@ -64,23 +75,35 @@ services: # Use gravatars for user avatars, when user has no own avatar defined - USE_GRAVATAR=0 - # Override value if you want to show to show a given text on homepage. + # Override value if you want to show a given text on homepage. # When this is empty the content of config/banner.md is used as banner #- BANNER=This is a test banner
with a line break + + # If you use a reverse proxy in front of Part-DB, you must configure the trusted proxies IP addresses here (see reverse proxy documentation for more information): + # - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ``` -4. Customize the settings by changing the environment variables (or add new ones). See [Configuration]({% link configuration.md %}) for more information. + +4. Customize the settings by changing the environment variables (or adding new ones). See [Configuration]({% link + configuration.md %}) for more information. 5. Inside the folder, run + ```bash docker-compose up -d ``` -6. Create the inital database with + +6. Create the initial database with + ```bash docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate ``` -and watch for the password output -6. Part-DB is available under `http://localhost:8080` and you can log in with username `admin` and the password shown before -The docker image uses a SQLite database and all data (database, uploads and other media) is put into folders relative to the docker-compose.yml. +and watch for the password output + +6. Part-DB is available under `http://localhost:8080` and you can log in with the username `admin` and the password shown + before + +The docker image uses a SQLite database and all data (database, uploads, and other media) is put into folders relative to +the docker-compose.yml. ### MySQL @@ -113,6 +136,12 @@ services: # In docker env logs will be redirected to stderr - APP_ENV=docker + # Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to + # run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/ + # folder (under .automigration-backup), so you can restore it, if the migration fails. + # This feature is currently experimental, so use it at your own risk! + # - DB_AUTOMIGRATE=true + # You can configure Part-DB using environment variables # Below you can find the most essential ones predefined # However you can add add any other environment configuration you want here @@ -140,7 +169,8 @@ services: database: container_name: partdb_database image: mysql:8.0 - command: --default-authentication-plugin=mysql_native_password + restart: unless-stopped + command: --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=1 environment: # Change this Password MYSQL_ROOT_PASSWORD: SECRET_ROOT_PASSWORD @@ -156,8 +186,10 @@ services: ``` ### Update Part-DB + You can update Part-DB by pulling the latest image and restarting the container. Then you have to run the database migrations again + ```bash docker-compose pull docker-compose up -d @@ -165,19 +197,30 @@ docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate ``` ## Direct use of docker image -You can use the `jbtronics/part-db1:master` image directly. You have to expose the port 80 to a host port and configure volumes for `/var/www/html/uploads` and `/var/www/html/public/media`. -If you want to use SQLite database (which is default), you have to configure Part-DB to put the database file in a mapped volume via the `DATABASE_URL` environment variable. -For example if you set `DATABASE_URL=sqlite:///%kernel.project_dir%/var/db/app.db` then you will have to map the `/var/www/html/var/db/` folder to the docker container (see docker-compose.yaml for example). +You can use the `jbtronics/part-db1:master` image directly. You have to expose port 80 to a host port and configure +volumes for `/var/www/html/uploads` and `/var/www/html/public/media`. -You also have to create the database like described above in step 4. +If you want to use SQLite database (which is default), you have to configure Part-DB to put the database file in a +mapped volume via the `DATABASE_URL` environment variable. +For example, if you set `DATABASE_URL=sqlite:///%kernel.project_dir%/var/db/app.db` then you will have to map +the `/var/www/html/var/db/` folder to the docker container (see docker-compose.yaml for example). + +You also have to create the database as described above in step 4. ## Running console commands -You can run the console commands described in README by executing `docker exec --user=www-data -it partdb bin/console [command]` + +You can run the console commands described in README by +executing `docker exec --user=www-data -it partdb bin/console [command]` + +{: .warning } +> If you run a root console inside the container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell. +> Otherwise Part-DB console might use the wrong configuration to execute commands. ## Troubleshooting -*Login not possible. Login page is just reloading and no error message is shown or something like "CSFR token invalid"*: +*Login is not possible. Login page is just reloading and no error message is shown or something like "CSFR token invalid"*: -Clear all cookies in your browser or use a inkognito tab for Part-DB. -This related to the fact that Part-DB can not set cookies via HTTP, after some webpage has set cookies before under localhost via https. This is a security mechanism of the browser and can not be bypassed by Part-DB. +Clear all cookies in your browser or use an incognito tab for Part-DB. +This is related to the fact that Part-DB can not set cookies via HTTP after some webpages have set cookies before under +localhost via HTTPS. This is a security mechanism of the browser and can not be bypassed by Part-DB. diff --git a/docs/installation/installation_guide-debian.md b/docs/installation/installation_guide-debian.md index 68a53b03..885eea90 100644 --- a/docs/installation/installation_guide-debian.md +++ b/docs/installation/installation_guide-debian.md @@ -6,25 +6,41 @@ nav_order: 4 --- # Part-DB installation guide for Debian 11 (Bullseye) -This guide shows you how to install Part-DB directly on Debian 11 using apache2 and SQLite. This guide should work with recent Ubuntu and other Debian based distributions with little to no changes. -Depending on what you want to do, using the prebuilt docker images may be a better choice, as you dont need to install this much dependencies. See **TODO** for more information of the docker installation. + +This guide shows you how to install Part-DB directly on Debian 11 using apache2 and SQLite. This guide should work with +recent Ubuntu and other Debian-based distributions with little to no changes. +Depending on what you want to do, using the prebuilt docker images may be a better choice, as you don't need to install +this many dependencies. See [here]({% link installation/installation_docker.md %}) for more information on the docker +installation. {: .warning } -> The methods described here, configure PHP without HTTPS and therefore should only be used locally in a trusted network. +> The methods described here, configure PHP without HTTPS and therefore should only be used locally in a trusted +> network. > If you want to expose Part-DB to the internet, you HAVE to configure an SSL connection! +It is recommended to install Part-DB on a 64-bit system, as the 32-bit version of PHP is affected by the +[Year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) and can not handle dates after 2038 correctly. + ## Installation with SQLite database ### Install prerequisites + For the installation of Part-DB, we need some prerequisites. They can be installed by running the following command: + ```bash sudo apt install git curl zip ca-certificates software-properties-common apt-transport-https lsb-release nano wget ``` ### Install PHP and apache2 -Part-DB is written in [PHP](https://php.net) and therefore needs an PHP interpreter to run. Part-DB needs PHP 7.3 or higher, however it is recommended to use the most recent version of PHP for performance reasons and future compatibility. -As Debian 11 does not ship PHP 8.1 in it's default repositories, we have to add a repository for it. You can skip this step if your distribution is shipping a recent version of PHP or you want to use the built-in PHP version. +Part-DB is written in [PHP](https://php.net) and therefore needs a PHP interpreter to run. Part-DB needs PHP 8.1 or +higher. However, it is recommended to use the most recent version of PHP for performance reasons and future +compatibility. + +As Debian 11 does not ship PHP 8.1 in its default repositories, we have to add a repository for it. You can skip this +step if your distribution is shipping a recent version of PHP or you want to use the built-in PHP version. If you are +using Debian 12, you can skip this step, as PHP 8.1 is already included in the default repositories. + ```bash # Add sury repository for PHP 8.1 sudo curl -sSL https://packages.sury.org/php/README.txt | sudo bash -x @@ -32,14 +48,21 @@ sudo curl -sSL https://packages.sury.org/php/README.txt | sudo bash -x # Update package list sudo apt update && sudo apt upgrade ``` -Now you can install PHP 8.1 and required packages (change the 8.1 in the package version according to the version you want to use): + +Now you can install PHP 8.1 and the required packages (change the 8.1 in the package version according to the version you +want to use): + ```bash sudo apt install 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 ``` + The apache2 webserver should be already installed with this command and configured basically. ### Install composer -Part-DB uses [composer](https://getcomposer.org/) to install required PHP libraries. As the versions shipped in the repositories is pretty old we install it manually: + +Part-DB uses [composer](https://getcomposer.org/) to install required PHP libraries. As the version shipped in the +repositories is pretty old, we will install it manually: + ```bash # Download composer installer script wget -O /tmp/composer-setup.php https://getcomposer.org/installer @@ -50,7 +73,10 @@ chmod +x /usr/local/bin/composer ``` ### Install yarn and nodejs -To build the frontend (the user interface) Part-DB uses [yarn](https://yarnpkg.com/). As it dependens on nodejs and the shipped versions are pretty old, we install new versions from offical nodejs repository: + +To build the front end (the user interface) Part-DB uses [yarn](https://yarnpkg.com/). As it depends on Node.js and the +shipped versions are pretty old, we install new versions from the official Node.js repository: + ```bash # Add recent node repository (nodejs 18 is supported until 2025) curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash - @@ -59,6 +85,7 @@ sudo apt install nodejs ``` We can install yarn with the following commands: + ```bash # Add yarn repository curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarnkey.gpg >/dev/null @@ -68,35 +95,64 @@ sudo apt update && sudo apt install yarn ``` ### Create a folder for Part-DB and download it -We now have all prerequisites installed and can start to install Part-DB. We will create a folder for Part-DB in a webfolder of apache2 and download it to this folder. The downloading is done via git, which allows you to update easily later. + +We now have all prerequisites installed and can start to install Part-DB. We will create a folder for Part-DB in the +webroot of apache2 and download it to this folder. The downloading is done via git, which allows you to update easily +later. + ```bash # Download Part-DB into the new folder /var/www/partdb git clone https://github.com/Part-DB/Part-DB-symfony.git /var/www/partdb ``` +By default, you are now on the latest development version. In most cases, you want to use the latest stable version. You +can switch to the latest stable version (tagged) by running the following command: + +```bash +# This finds the latest release/tag and checks it out +git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) +``` + +Alternatively, you can check out a specific version by running ( +see [GitHub Releases page](https://github.com/Part-DB/Part-DB-server/releases) for a list of available versions): + +```bash +# This checks out the version 1.5.2 +git checkout v1.5.2 +``` + Change ownership of the files to the apache user: + ```bash chown -R www-data:www-data /var/www/partdb ``` For the next steps we should be in the Part-DB folder, so move into it: + ```bash cd /var/www/partdb ``` ### Create configuration for Part-DB -The basic configuration of Part-DB is done by a `.env.local` file in the main directory. Create on by from the default configuration: + +The basic configuration of Part-DB is done by a `.env.local` file in the main directory. Create on by from the default +configuration: + ```bash cp .env .env.local ``` -In your `.env.local` you can configure Part-DB according to your wishes. A full list of configuration options can be found [here]({% link configuration.md %}. +In your `.env.local` you can configure Part-DB according to your wishes. A full list of configuration options can be +found [here](../configuration.md). Other configuration options like the default language or default currency can be found in `config/parameters.yaml`. -Please check that the `partdb.default_currency` value in `config/parameters.yaml` matches your mainly used currency, as this can not be changed after creating price informations. +Please check that the `partdb.default_currency` value in `config/parameters.yaml` matches your mainly used currency, as +this can not be changed after creating price information. ### Install dependencies for Part-DB and build frontend + Part-DB depends on several other libraries and components. Install them by running the following commands: + ```bash # Install composer dependencies (please note the sudo command, to run it under the web server user) sudo -u www-data composer install --no-dev -o @@ -110,32 +166,47 @@ sudo yarn build ### Clear cache To ensure everything is working, clear the cache: + ```bash sudo -u www-data php bin/console cache:clear ``` ### Check if everything is installed + To check if everything is installed, run the following command: + ```bash sudo -u www-data php bin/console partdb:check-requirements ``` -The most things should be green, and no red ones. Yellow messages means optional dependencies which are not important but can improve performance and functionality. + +Most things should be green, and no red ones. Yellow messages mean optional dependencies which are not important +but can improve performance and functionality. ### Create a database for Part-DB -Part-DB by default uses a file based sqlite database to store the data. Use the following command to create the database. The database will normally created at `/var/www/partdb/var/app.db`. + +Part-DB by default uses a file-based SQLite database to store the data. Use the following command to create the +database. The database will normally be created at `/var/www/partdb/var/app.db`. + ```bash sudo -u www-data php bin/console doctrine:migrations:migrate ``` + The command will warn you about schema changes and potential data loss. Continue with typing `yes`. -The command will output several lines of informations. Somewhere should be a a yellow background message like `The initial password for the "admin" user is: f502481134`. Write down this password as you will need it later for inital login. +The command will output several lines of information. Somewhere should be a yellow background message +like `The initial password for the "admin" user is: f502481134`. Write down this password as you will need it later for the initial login. ### Configure apache2 to show Part-DB -Part-DB is now configured, but we have to say apache2 to serve Part-DB as web application. This is done by creating a new apache site: + +Part-DB is now configured, but we have to say apache2 to serve Part-DB as web application. This is done by creating a +new apache site: + ```bash sudo nano /etc/apache2/sites-available/partdb.conf ``` + and add the following content (change ServerName and ServerAlias to your needs): + ``` ServerName partdb.lan @@ -152,38 +223,53 @@ and add the following content (change ServerName and ServerAlias to your needs): CustomLog /var/log/apache2/partdb_access.log combined ``` + Activate the new site by: + ```bash sudo ln -s /etc/apache2/sites-available/partdb.conf /etc/apache2/sites-enabled/partdb.conf ``` -Configure apache to show pretty URL pathes for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`): +Configure apache to show pretty URL paths for Part-DB (`/label/dialog` instead of `/index.php/label/dialog`): + ```bash sudo a2enmod rewrite ``` -If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the apache2 default configuration with: +If you want to access Part-DB via the IP-Address of the server, instead of the domain name, you have to remove the +apache2 default configuration with: + ```bash sudo rm /etc/apache2/sites-enabled/000-default.conf ``` Restart the apache2 webserver with: + ```bash sudo service apache2 restart ``` -and Part-DB should now be available under `http://YourServerIP` (or `http://partdb.lan` if you configured DNS in your network to point on the server). +and Part-DB should now be available under `http://YourServerIP` (or `http://partdb.lan` if you configured DNS in your +network to point to the server). ### Login to Part-DB -Navigate to the Part-DB web interface and login via the user icon in the top right corner. You can login using the username `admin` and the password you have written down earlier. + +Navigate to the Part-DB web interface and login via the user icon in the top right corner. You can log in using the +username `admin` and the password you have written down earlier. ## Update Part-DB + If you want to update your existing Part-DB installation, you just have to run the following commands: + ```bash # Move into Part-DB folder cd /var/www/partdb # Pull latest Part-DB version from GitHub git pull + +# Checkout the latest version (or use a specific version, like described above) +git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) + # Apply correct permission chown -R www-data:www-data . # Install new composer dependencies @@ -203,7 +289,8 @@ sudo -u www-data php bin/console cache:clear ``` ## MySQL/MariaDB database -To use a MySQL database, follow the steps from above (except the creation of database, we will do this later). + +To use a MySQL database, follow the steps from above (except the creation of the database, we will do this later). Debian 11 does not ship MySQL in its repositories anymore, so we use the compatible MariaDB instead: 1. Install maria-db with: @@ -213,9 +300,11 @@ sudo apt update && sudo apt install mariadb-server ``` 2. Configure maria-db with: + ```bash sudo mysql_secure_installation ``` + When asked for the root password, just press enter, as we have not set a root password yet. In the next steps you are asked if you want to switch to unix_socket authentication, answer with `n` and press enter. Then you are asked if you want to remove anonymous users, answer with `y` and press enter. @@ -224,33 +313,42 @@ Then you are asked if you want to remove the test database and access to it, ans Then you are asked if you want to reload the privilege tables now, answer with `y` and press enter. 3. Create a new database and user for Part-DB: Run the following commands: + ```bash sudo mariadb ``` + A SQL shell will open, in which you can run the following commands to create a new database and user for Part-DB. Replace 'YOUR_SECRET_PASSWORD' with a secure password. + ```sql CREATE DATABASE partdb; GRANT ALL PRIVILEGES ON partdb.* TO 'partdb'@'localhost' IDENTIFIED BY 'YOUR_SECRET_PASSWORD'; ``` -Finally save the changes with: + +Finally, save the changes with: + ```sql FLUSH PRIVILEGES; ``` + and exit the SQL shell with: + ```sql exit ``` 4. Configure Part-DB to use the new database. Open your `.env.local` file and search the line `DATABASE_URL`. -Change it to the following (you have to replace `YOUR_SECRET_PASSWORD` with the password you have choosen in step 3): + Change it to the following (you have to replace `YOUR_SECRET_PASSWORD` with the password you have chosen in step 3): + ``` -DATABASE_URL=DATABASE_URL=mysql://partdb:YOUR_SECRET_PASSWORD@127.0.0.1:3306/partdb +DATABASE_URL=mysql://partdb:YOUR_SECRET_PASSWORD@127.0.0.1:3306/partdb ``` 5. Create the database schema with: + ```bash sudo -u www-data php bin/console doctrine:migrations:migrate ``` -6. The migration step should have shown you a password for the admin user, which you can use now to login to Part-DB. +6. The migration step should have shown you a password for the admin user, which you can use now to log in to Part-DB. diff --git a/docs/installation/kubernetes.md b/docs/installation/kubernetes.md new file mode 100644 index 00000000..86504af2 --- /dev/null +++ b/docs/installation/kubernetes.md @@ -0,0 +1,42 @@ +--- +title: Kubernetes / Helm +layout: default +parent: Installation +nav_order: 5 +--- + +# Kubernetes / Helm Charts + +If you are using Kubernetes, you can use the [helm charts](https://helm.sh/) provided in this [repository](https://github.com/Part-DB/helm-charts). + +## Usage + +[Helm](https://helm.sh) must be installed to use the charts. Please refer to +Helm's [documentation](https://helm.sh/docs) to get started. + +Once Helm has been set up correctly, add the repo as follows: + +`helm repo add part-db https://part-db.github.io/helm-charts` + +If you had already added this repo earlier, run `helm repo update` to retrieve +the latest versions of the packages. You can then run `helm search repo +part-db` to see the charts. + +To install the part-db chart: + + helm install my-part-db part-db/part-db + +To uninstall the chart: + + helm delete my-part-db + +This repository is also available at [ArtifactHUB](https://artifacthub.io/packages/search?repo=part-db). + +## Configuration + +See the README in the [chart directory](https://github.com/Part-DB/helm-charts/tree/main/charts/part-db) for more +information on the available configuration options. + +## Bugreports + +If you find issues related to the helm charts, please open an issue in the [helm-charts repository](https://github.com/Part-DB/helm-charts). \ No newline at end of file diff --git a/docs/installation/nginx.md b/docs/installation/nginx.md index 542c11eb..84305975 100644 --- a/docs/installation/nginx.md +++ b/docs/installation/nginx.md @@ -6,14 +6,18 @@ nav_order: 10 --- # Nginx + You can also use [nginx](https://www.nginx.com/) as webserver for Part-DB. Setup Part-DB with apache is a bit easier, so this is the method shown in the guides. This guide assumes that you already have a working nginx installation with PHP configured. ## Setup -1. Install composer and yarn like described in the [apache guide]({% link installation/installation_guide-debian.md %}#install-composer). + +1. Install composer and yarn as described in the [apache guide]({% link installation/installation_guide-debian.md + %}#install-composer). 2. Create a folder for Part-DB and install and configure it as described 3. Instead of creating the config for apache, add the following snippet to your nginx config: + ```nginx server { # Redirect all HTTP requests to HTTPS @@ -48,6 +52,11 @@ server { location ~ \.php$ { return 404; } + + # Set Content-Security-Policy for svg files, to block embedded javascript in there + location ~* \.svg$ { + add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"; + } error_log /var/log/nginx/parts.error.log; access_log /var/log/nginx/parts.access.log; @@ -64,4 +73,6 @@ server { ssl_prefer_server_ciphers on; } ``` -4. Restart nginx with `sudo systemctl restart nginx` and you should be able to access Part-DB under your configured domain. \ No newline at end of file + +4. Restart nginx with `sudo systemctl restart nginx` and you should be able to access Part-DB under your configured + domain. \ No newline at end of file diff --git a/docs/installation/proxmox.md b/docs/installation/proxmox.md new file mode 100644 index 00000000..865d2bf4 --- /dev/null +++ b/docs/installation/proxmox.md @@ -0,0 +1,31 @@ +--- +title: Proxmox VE LXC +layout: default +parent: Installation +nav_order: 6 +--- + +# Proxmox VE LXC + +{: .warning } +> The proxmox VE LXC script for Part-DB is developed and maintained by [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/) +> and not by the Part-DB developers. Keep in mind that the script is not officially supported by the Part-DB developers. + +If you are using Proxmox VE you can use the scripts provided by [Proxmox VE Helper-Scripts community](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db) +to easily install Part-DB in a LXC container. + +## Usage + +To create a new LXC container with Part-DB, you can use the following command in the Proxmox VE shell: + +```bash +bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/ct/part-db.sh)" +``` + +The same command can be used to update an existing Part-DB container. + +See the [helper script website](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db) for more information. + +## Bugreports + +If you find issues related to the proxmox VE LXC script, please open an issue in the [Proxmox VE Helper-Scripts repository](https://github.com/community-scripts/ProxmoxVE). diff --git a/docs/installation/reverse-proxy.md b/docs/installation/reverse-proxy.md index d42e5a2b..605b93fa 100644 --- a/docs/installation/reverse-proxy.md +++ b/docs/installation/reverse-proxy.md @@ -9,13 +9,32 @@ nav_order: 11 If you want to put Part-DB behind a reverse proxy, you have to configure Part-DB correctly to make it work properly. -You have to set the `TRUSTED_PROXIES` environment variable to the IP address of your reverse proxy -(either in your `docker-compose.yaml` in the case of docker, or `.env.local` in case of direct installation). +You have to set the `TRUSTED_PROXIES` environment variable to the IP address of your reverse proxy +(either in your `docker-compose.yaml` in the case of docker, or `.env.local` in case of direct installation). If you have multiple reverse proxies, you can set multiple IP addresses separated by a comma (or specify a range). -For example, if your reverse proxy has the IP address `192.168.2.10`, your value should be: +For example, if your reverse proxy has the IP address `192.168.2.10`, your value should be: + ``` TRUSTED_PROXIES=192.168.2.10 ``` -Set the `DEFAULT_URI` environment variable to the URL of your Part-DB installation, available from the outside (so via the reverse proxy). \ No newline at end of file +Set the `DEFAULT_URI` environment variable to the URL of your Part-DB installation, available from the outside (so via +the reverse proxy). + +## Part-DB in a subpath via reverse proxy + +If you put Part-DB into a subpath via the reverse proxy, you have to configure your webserver to include `X-Forwarded-Prefix` in the request headers. +For example if you put Part-DB behind a reverse proxy with the URL `https://example.com/partdb`, you have to set the `X-Forwarded-Prefix` header to `/partdb`. + +In apache, you can do this by adding the following line to your virtual host configuration: + +``` +RequestHeader set X-Forwarded-Prefix "/partdb" +``` + +and in nginx, you can do this by adding the following line to your server configuration: + +``` +proxy_set_header X-Forwarded-Prefix "/partdb"; +``` \ No newline at end of file diff --git a/docs/installation/saml_sso.md b/docs/installation/saml_sso.md index 22a3076e..d2e65e7f 100644 --- a/docs/installation/saml_sso.md +++ b/docs/installation/saml_sso.md @@ -7,43 +7,56 @@ nav_order: 12 # Single Sign-On via SAML -Part-DB supports Single Sign-On via SAML. This means that you can use your existing SAML identity provider to log in to Part-DB. -Using an intermediate SAML server like [Keycloak](https://www.keycloak.org/), also allows you to connect Part-DB to a LDAP or Active Directory server. +Part-DB supports Single Sign-On via SAML. This means that you can use your existing SAML identity provider to log in to +Part-DB. +Using an intermediate SAML server like [Keycloak](https://www.keycloak.org/), also allows you to connect Part-DB to an +LDAP or Active Directory server. {: .important } -> This feature is for advanced users only. Single Sign-On is useful for large organizations with many users, which are already using SAML for other services. +> This feature is for advanced users only. Single Sign-On is useful for large organizations with many users, which are +> already using SAML for other services. > If you have only one or a few users, you should use the built-in authentication system of Part-DB. -> This guide assumes that you already have an SAML identity provider set up and working, and have a basic understanding of how SAML works. +> This guide assumes that you already have an SAML identity provider set up and working, and have a basic understanding +> of how SAML works. {: .warning } > This feature is currently in beta. Please report any bugs you find. > So far it has only tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider. -This guide will show you how to configure Part-DB with [Keycloak](https://www.keycloak.org/) as the SAML identity provider, -but it should work with any SAML 2.0 compatible identity provider. +This guide will show you how to configure Part-DB with [Keycloak](https://www.keycloak.org/) as the SAML identity +provider, but it should work with any SAML 2.0 compatible identity provider. -This guide assumes that you have a working Keycloak installation with some users. If you don't, you can follow the [Keycloak Getting Started Guide](https://www.keycloak.org/docs/latest/getting_started/index.html). +This guide assumes that you have a working Keycloak installation with some users. If you don't, you can follow +the [Keycloak Getting Started Guide](https://www.keycloak.org/docs/latest/getting_started/index.html). {: .important } -> Part-DB associates local users with SAML users by their username. That means if the username of a SAML user changes, a new local user will be created (and the old account can not be accessed). -> You should make sure that the username of a SAML user does not change. If you use Keycloak make sure that the possibility to change the username is disabled (which is by default). -> If you really have to rename a SAML user, a Part-DB admin can rename the local user in the Part-DB in the admin panel, to match the new username of the SAML user. +> Part-DB associates local users with SAML users by their username. That means if the username of a SAML user changes, a +> new local user will be created (and the old account can not be accessed). +> You should make sure that the username of a SAML user does not change. If you use Keycloak make sure that the +> possibility to change the username is disabled (which is by default). +> If you really have to rename a SAML user, a Part-DB admin can rename the local user in the Part-DB in the admin panel, +> to match the new username of the SAML user. ## Configure basic SAML connection ### Create a new SAML client -1. First, you need to configure a new SAML client in Keycloak. Login in to your Keycloak admin console and go to the `Clients` page. -2. Click on `Create client` and select `SAML` as type from the dropdown menu. For the client ID, you can use anything you want, but it should be unique. -*It is recommended to set this value to the domain name of your Part-DB installation, with an attached `/sp` (e.g. `https://partdb.yourdomain.invalid/sp`)*. -The name field should be set to something human-readable, like `Part-DB`. -3. Click on `Save` to create the new client. + +1. First, you need to configure a new SAML client in Keycloak. Login in to your Keycloak admin console and go to + the `Clients` page. +2. Click on `Create client` and select `SAML` as type from the dropdown menu. For the client ID, you can use anything + you want, but it should be unique. + *It is recommended to set this value to the domain name of your Part-DB installation, with an attached `/sp` ( + e.g. `https://partdb.yourdomain.invalid/sp`)*. + The name field should be set to something human-readable, like `Part-DB`. +3. Click on `Save` to create a new client. ### Configure the SAML client 1. Now you need to configure the SAML client. Go to the `Settings` tab and set the following values: * Set `Home URL` to the homepage of your Part-DB installation (e.g. `https://partdb.yourdomain.invalid/`). - * Set `Valid redirect URIs` to your homepage with a wildcard at the end (e.g. `https://partdb.yourdomain.invalid/*`). - * Set `Valid post logout redirect URIs` to `+` to allow all urls from the `Valid redirect URIs`. + * Set `Valid redirect URIs` to your homepage with a wildcard at the end ( + e.g. `https://partdb.yourdomain.invalid/*`). + * Set `Valid post logout redirect URIs` to `+` to allow all URLs from the `Valid redirect URIs`. * Set `Name ID format` to `username` * Ensure `Force POST binding` is enabled. * Ensure `Sign documents` is enabled. @@ -52,30 +65,47 @@ The name field should be set to something human-readable, like `Part-DB`. Click on `Save` to save the changes. 2. Go to the `Advanced` tab and set the following values: - * Assertion Consumer Service POST Binding URL to your homepage with `/saml/acs` at the end (e.g. `https://partdb.yourdomain.invalid/saml/acs`). - * Logout Service POST Binding URL to your homepage with `/logout` at the end (e.g. `https://partdb.yourdomain.invalid/logout`). + * Assertion Consumer Service POST Binding URL to your homepage with `/saml/acs` at the end ( + e.g. `https://partdb.yourdomain.invalid/saml/acs`). + * Logout Service POST Binding URL to your homepage with `/logout` at the end ( + e.g. `https://partdb.yourdomain.invalid/logout`). 3. Go to Keys tab and ensure `Client Signature Required` is enabled. -4. In the Keys tab click on `Generate new keys`. This will generate a new key pair for the SAML client. The private key will be downloaded to your computer. +4. In the Keys tab click on `Generate new keys`. This will generate a new key pair for the SAML client. The private key + will be downloaded to your computer. ### Configure Part-DB to use SAML + 1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for edit -2. Set the `SAMLP_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the previous step. It should start with `MIEE` and end with `=`. -3. Set the `SAML_SP_X509_CERT` environment variable to the content of the Certificate field shown in the `Keys` tab of the SAML client in Keycloak. It should start with `MIIC` and end with `=`. -4. Set the `SAML_SP_ENTITY_ID` environment variable to the entityID of the SAML client in Keycloak (e.g. `https://partdb.yourdomain.invalid/sp`). -5. In Keycloak navigate to `Realm Settings` -> `SAML 2.0 Identity Provider` (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml/descriptor) to show the SAML metadata. -6. Copy the `entityID` value from the metadata to the `SAML_IDP_ENTITY_ID` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master`). -7. Copy the `Single Sign-On Service` value from the metadata to the `SAML_IDP_SINGLE_SIGN_ON_SERVICE` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml`). -8. Copy the `Single Logout Service` value from the metadata to the `SAML_IDP_SINGLE_LOGOUT_SERVICE` configuration variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml/logout`). -9. Copy the `X.509 Certificate` value from the metadata to the `SAML_IDP_X509_CERT` configuration variable of Part-DB (it should start with `MIIC` and should be pretty long). -10. Set the `DEFAULT_URI` to the homepage (on the publicly available domain) of your Part-DB installation (e.g. `https://partdb.yourdomain.invalid/`). It must end with a slash. +2. Set the `SAMLP_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the + previous step. It should start with `MIEE` and end with `=`. +3. Set the `SAML_SP_X509_CERT` environment variable to the content of the Certificate field shown in the `Keys` tab of + the SAML client in Keycloak. It should start with `MIIC` and end with `=`. +4. Set the `SAML_SP_ENTITY_ID` environment variable to the entityID of the SAML client in Keycloak ( + e.g. `https://partdb.yourdomain.invalid/sp`). +5. In Keycloak navigate to `Realm Settings` -> `SAML 2.0 Identity Provider` (by default something + like `https://idp.yourdomain.invalid/realms/master/protocol/saml/descriptor) to show the SAML metadata. +6. Copy the `entityID` value from the metadata to the `SAML_IDP_ENTITY_ID` configuration variable of Part-DB (by default + something like `https://idp.yourdomain.invalid/realms/master`). +7. Copy the `Single Sign-On Service` value from the metadata to the `SAML_IDP_SINGLE_SIGN_ON_SERVICE` configuration + variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml`). +8. Copy the `Single Logout Service` value from the metadata to the `SAML_IDP_SINGLE_LOGOUT_SERVICE` configuration + variable of Part-DB (by default something like `https://idp.yourdomain.invalid/realms/master/protocol/saml/logout`). +9. Copy the `X.509 Certificate` value from the metadata to the `SAML_IDP_X509_CERT` configuration variable of Part-DB ( + it should start with `MIIC` and should be pretty long). +10. Set the `DEFAULT_URI` to the homepage (on the publicly available domain) of your Part-DB installation ( + e.g. `https://partdb.yourdomain.invalid/`). It must end with a slash. 11. Set the `SAML_ENABLED` configuration in Part-DB to 1 to enable SAML authentication. -When you access the Part-DB login form now, you should see a new button to log in via SSO. Click on it to be redirected to the SAML identity provider and log in. +When you access the Part-DB login form now, you should see a new button to log in via SSO. Click on it to be redirected +to the SAML identity provider and log in. -In the following sections, you will learn how to configure that Part-DB uses the data provided by the SAML identity provider to fill out user informations. +In the following sections, you will learn how to configure that Part-DB uses the data provided by the SAML identity +provider to fill out user information. ### Set user information based on SAML attributes -Part-DB can set basic user information like the username, the real name and the email address based on the SAML attributes provided by the SAML identity provider. + +Part-DB can set basic user information like the username, the real name and the email address based on the SAML +attributes provided by the SAML identity provider. To do this, you need to configure your SAML identity provider to provide the following attributes: * `email` or `urn:oid:1.2.840.113549.1.9.1` for the email address @@ -83,68 +113,125 @@ To do this, you need to configure your SAML identity provider to provide the fol * `lastName` or `urn:oid:2.5.4.4` for the last name * `department` for the department field of the user -You can omit any of these attributes, but then the corresponding field will be empty (but can be overriden by an administrator). -These values are written to Part-DB database, whenever the user logs in via SAML (the user is created on the first login, and updated on every login). +You can omit any of these attributes, but then the corresponding field will be empty (but can be overwritten by an +administrator). +These values are written to Part-DB database, whenever the user logs in via SAML (the user is created on the first +login, and updated on every login). -To configure Keycloak to provide these attributes, you need to go to the `Client scopes` page and select the `sp-dedicatd` client scope (or create a new one). +To configure Keycloak to provide these attributes, you need to go to the `Client scopes` page and select +the `sp-dedicatd` client scope (or create a new one). In the scope configuration page, click on `Add mappers` and `From predefined mappers`. Select the following mappers: + * `X500 email` * `X500 givenName` * `X500 surname` -and click `Add`. Now Part-DB will be provided with the email, first name and last name of the user based on the Keycloak user database. +and click `Add`. Now Part-DB will be provided with the email, first name and last name of the user based on the Keycloak +user database. ### Configure permissions for SAML users -On the first login of a SAML user, Part-DB will create a new user in the database. This user will have the same username as the SAML user, but no password set. The user will be marked as a SAML user, so he can only login via SAML in the future. However in other aspects the user is a normal user, so Part-DB admins can set permissions for SAML users like for any other user and override permissions assigned via groups. -However for large organizations you maybe want to automatically assign permissions to SAML users based on the roles or groups configured in the identity provider. For this purpose Part-DB allows you to map SAML roles or groups to Part-DB groups. See the next section for details. +On the first login of a SAML user, Part-DB will create a new user in the database. This user will have the same username +as the SAML user, but no password set. The user will be marked as a SAML user, so he can only log in via SAML in the +future. However, in other aspects the user is a normal user, so Part-DB admins can set permissions for SAML users like +for any other user and override permissions assigned via groups. + +For large organizations, you maybe want to automatically assign permissions to SAML users based on the roles or +groups configured in the identity provider. For this purpose Part-DB allows you to map SAML roles or groups to Part-DB +groups. See the next section for details. ### Map SAML roles to Part-DB groups -Part-DB allows you to configure a mapping between SAML roles or groups and Part-DB groups. This allows you to automatically assign permissions to SAML users based on the roles or groups configured in the identity provider. For example if a user at your SAML provider has the role `admin`, you can configure Part-DB to assign the `admin` group to this user. This will give the user all permissions of the `admin` group. -For this you need first have to create the groups in Part-DB, to which you want to assign the users and configure their permissions. You will need the IDs of the groups, which you can find in the `System->Group` page of Part-DB in the Info tab. +Part-DB allows you to configure a mapping between SAML roles or groups and Part-DB groups. This allows you to +automatically assign permissions to SAML users based on the roles or groups configured in the identity provider. For +example, if a user at your SAML provider has the role `admin`, you can configure Part-DB to assign the `admin` group to +this user. This will give the user all permissions of the `admin` group. -The map is provided as [JSON](https://en.wikipedia.org/wiki/JSON) encoded map between the SAML role and the group ID, which has the form `{"saml_role": group_id, "*": group_id, ...}`. You can use the `*` key to assign a group to all users, which are not in any other group. The map is configured via the `SAML_ROLE_MAPPING` environment variable, which you can configure via the `.env.local` or `docker-compose.yml` file. Please note that you have to enclose the JSON string in single quotes here, as JSON itself uses double quotes (e.g. `SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }`). +For this, you need first have to create the groups in Part-DB, to which you want to assign the users and configure their +permissions. You will need the IDs of the groups, which you can find on the `System->Group` page of Part-DB in the Info +tab. -For example if you want to assign the group with ID 1 (by default admin) to every SAML user which has the role `admin`, the role with ID 3 (by default editor) to every SAML user with the role `editor` and everybody else to the group with ID 2 (by default readonly), you can configure the following map: +The map is provided as [JSON](https://en.wikipedia.org/wiki/JSON) encoded map between the SAML role and the group ID, +which has the form `{"saml_role": group_id, "*": group_id, ...}`. You can use the `*` key to assign a group to all +users, which are not in any other group. The map is configured via the `SAML_ROLE_MAPPING` environment variable, which +you can configure via the `.env.local` or `docker-compose.yml` file. Please note that you have to enclose the JSON +string in single quotes here, as JSON itself uses double quotes ( +e.g. `SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }`). + +For example, if you want to assign the group with ID 1 (by default admin) to every SAML user which has the role `admin`, +the role with ID 3 (by default editor) to every SAML user with the role `editor` and everybody else to the group with ID +2 (by default readonly), you can configure the following map: ``` SAML_ROLE_MAPPING='{"admin": 1, "editor": 3, "*": 2}' ``` -Please not that the order of the mapping is important. The first matching role will be assigned to the user. So if you have a user with the roles `admin` and `editor`, he will be assigned to the group with ID 1 (admin) and not to the group with ID 3 (editor), as the `admin` role comes first in the JSON map. -This mean that you should always put the most specific roles (e.g. admins) first of the map and the most general roles (e.g. normal users) later. +Please note that the order of the mapping is important. The first matching role will be assigned to the user. So if you +have a user with the roles `admin` and `editor`, he will be assigned to the group with ID 1 (admin) and not to the group +with ID 3 (editor), as the `admin` role comes first in the JSON map. +This mean that you should always put the most specific roles (e.g. admins) first of the map and the most general roles ( +e.g. normal users) later. -If you want to assign users with a certain role to a empty group, provide the group ID -1 as the value. This is not a valid group ID, so the user will not be assigned to any group. +If you want to assign users with a certain role to an empty group, provide the group ID -1 as the value. This is not a +valid group ID, so the user will not be assigned to any group. -The SAML roles (or groups depending on your configuration), have to be supplied via a SAML attribute `group`. You have to configure your SAML identity provider to provide this attribute. For example in Keycloak you can configure this attribute in the `Client scopes` page. Select the `sp-dedicated` client scope (or create a new one) and click on `Add mappers`. Select `Role mapping` or `Group membership`, change the field name and click `Add`. Now Part-DB will be provided with the groups of the user based on the Keycloak user database. +The SAML roles (or groups depending on your configuration), have to be supplied via a SAML attribute `group`. You have +to configure your SAML identity provider to provide this attribute. For example, in Keycloak you can configure this +attribute on the `Client scopes` page. Select the `sp-dedicated` client scope (or create a new one) and click +on `Add mappers`. Select `Role mapping` or `Group membership`, change the field name, and click `Add`. Now Part-DB will +be provided with the groups of the user based on the Keycloak user database. -By default, the group is assigned to the user on the first login and updated on every login based on the SAML attributes. This allows you to configure the groups in the SAML identity provider and the users will automatically stay up to date with their permissions. However, if you want to disable this behavior (and let the Part-DB admins configure the groups manually, after the first login), you can set the `SAML_UPDATE_GROUP_ON_LOGIN` environment variable to `false`. If you want to disable the automatic group assignment completly (so not even on the first login of a user), set the `SAML_ROLE_MAPPING` to `{}` (empty JSON object). +By default, the group is assigned to the user on the first login and updated on every login based on the SAML +attributes. This allows you to configure the groups in the SAML identity provider and the users will automatically stay +up to date with their permissions. However, if you want to disable this behavior (and let the Part-DB admins configure +the groups manually, after the first login), you can set the `SAML_UPDATE_GROUP_ON_LOGIN` environment variable +to `false`. If you want to disable the automatic group assignment completely (so not even on the first login of a user), +set the `SAML_ROLE_MAPPING` to `{}` (empty JSON object). ## Overview of possible SAML attributes used by Part-DB -The following table shows all SAML attributes, which can be usedby Part-DB. If your identity provider is configured to provide these attributes, you can use to automatically fill the corresponding fields of the user in Part-DB. + +The following table shows all SAML attributes, which can be used by Part-DB. If your identity provider is configured to +provide these attributes, you can use to automatically fill the corresponding fields of the user in Part-DB. | SAML attribute | Part-DB user field | Description | -|-------------------------------------------|-------------------|-------------------------------------------------------------------| -| `urn:oid:1.2.840.113549.1.9.1` or `email` | email | The email address of the user. | -| `urn:oid:2.5.4.42` or `firstName` | firstName | The first name of the user. | -| `urn:oid:2.5.4.4` or `lastName` | lastName | The last name of the user. | -| `department` | department | The department of the user. | -| `group` | group | The group of the user (determined by `SAML_ROLE_MAPPING` option). | +|-------------------------------------------|--------------------|-------------------------------------------------------------------| +| `urn:oid:1.2.840.113549.1.9.1` or `email` | email | The email address of the user. | +| `urn:oid:2.5.4.42` or `firstName` | firstName | The first name of the user. | +| `urn:oid:2.5.4.4` or `lastName` | lastName | The last name of the user. | +| `department` | department | The department of the user. | +| `group` | group | The group of the user (determined by `SAML_ROLE_MAPPING` option). | ## Use SAML Login for existing users -Part-DB distinguishes between local users and SAML users. Local users are users, which can login via Part-DB login form and which use the password (hash) saved in the Part-DB database. SAML users are stored in the database too (they are created on the first login of the user via SAML), but they use the SAML identity provider to authenticate the user and have no password stored in the database. When you try you will get an error message. -For security reasons it is not possible to authenticate via SAML as a local user (and vice versa). So if you have existing users in your Part-DB database and want them to be able to login via SAML in the future, you can use the `php bin/console partdb:user:convert-to-saml-user username` command to convert them to SAML users. This will remove the password hash from the database and mark them as SAML users, so they can login via SAML in the future. +Part-DB distinguishes between local users and SAML users. Local users are users, that can log in via the Part-DB login form +and use the password (hash) saved in the Part-DB database. SAML users are stored in the database too (they are +created on the first login of the user via SAML), but they use the SAML identity provider to authenticate the user and +have no password stored in the database. When you try you will get an error message. -The reverse is also possible: If you have existing SAML users and want them to be able to login via the Part-DB login form, you can use the `php bin/console partdb:user:convert-to-saml-user --to-local username` command to convert them to local users. You have to set an password for the user afterwards. +For security reasons, it is not possible to authenticate via SAML as a local user (and vice versa). So if you have +existing users in your Part-DB database and want them to be able to log in via SAML in the future, you can use +the `php bin/console partdb:user:convert-to-saml-user username` command to convert them to SAML users. This will remove +the password hash from the database and mark them as SAML users, so they can log in via SAML in the future. + +The reverse is also possible: If you have existing SAML users and want them to be able to log in via the Part-DB login +form, you can use the `php bin/console partdb:user:convert-to-saml-user --to-local username` command to convert them to +local users. You have to set a password for the user afterward. {: .important } -> It is recommended that you let the original admin user (ID: 2) be a local user, so you can still login to Part-DB if the SAML identity provider is not available. +> It is recommended that you let the original admin user (ID: 2) be a local user, so you can still login to Part-DB if +> the SAML identity provider is not available. ## Advanced SAML configuration -You can find some more advanced SAML configuration options in the `config/packages/hslavich_onelogin_saml.yaml` file. Refer to the file for more information. + +You can find some more advanced SAML configuration options in the `config/packages/nbgrp_onelogin_saml.yaml` file. Refer +to the file for more information. Normally you don't have to change anything here. -Please note that this file is not saved by the Part-DB backup tool, so you have to save it manually if you want to keep your changes. On docker containers you have to configure a volume mapping for it. +Please note that this file is not saved by the Part-DB backup tool, so you have to save it manually if you want to keep +your changes. On docker containers you have to configure a volume mapping for it. +## SAML behind a reverse proxy + +If you are running Part-DB behind a reverse proxy, configure the `TRUSTED_PROXIES` environment and other reverse proxy +settings as described in the [reverse proxy guide]({% link installation/reverse-proxy.md %}). +If you want to use SAML you also need to set `SAML_BEHIND_PROXY` to `true` to enable the SAML proxy mode. diff --git a/docs/partkeepr_migration.md b/docs/partkeepr_migration.md index 41a1ff40..e37f8055 100644 --- a/docs/partkeepr_migration.md +++ b/docs/partkeepr_migration.md @@ -11,41 +11,57 @@ nav_order: 101 This guide describes how to migrate from [PartKeepr](https://partkeepr.org/) to Part-DB. -Part-DB has a built-in migration tool, which can be used to migrate the data from an existing PartKeepr instance to -a new Part-DB instance. Most of the data can be migrated, however there are some limitations, you can find below. - +Part-DB has a built-in migration tool, which can be used to migrate the data from an existing PartKeepr instance to +a new Part-DB instance. Most of the data can be migrated, however, there are some limitations, that you can find below. + ## What can be imported -* Datastructures (Categories, Footprints, Storage Locations, Manufacturers, Distributors, Part Measurement Units) -* Basic part informations (Name, Description, Comment, etc.) -* Attachments and images of parts, projects, footprints, manufacturers and storage locations + +* Data structures (Categories, Footprints, Storage Locations, Manufacturers, Distributors, Part Measurement Units) +* Basic part information (Name, Description, Comment, etc.) +* Attachments and images of parts, projects, footprints, manufacturers, and storage locations * Part prices (distributor infos) * Part parameters * Projects (including parts and attachments) -* Users (optional): Passwords however will be not migrated, and need to be reset later +* Users (optional): Passwords however will not be migrated, and need to be reset later ## What can't be imported -* Metaparts (A dummy version of the metapart will be created in Part-DB, however it will not function as metapart) + +* Metaparts (A dummy version of the metapart will be created in Part-DB, however, it will not function as metapart) * Multiple manufacturers per part (only the last manufacturer of a part will be migrated) -* Overage information for project parts (the overage info will be set as comment in the project BOM, but will have no effect) +* Overage information for project parts (the overage info will be set as a comment in the project BOM, but will have no + effect) * Batch Jobs * Parameter Units (the units will be written into the parameters) * Project Reports and Project Runs -* Stock history +* Stock History * Any kind of PartKeepr preferences ## How to migrate -1. Install Part-DB like described in the installation guide. You can use any database backend you want (mysql or sqlite). Run the database migration, but do not create any new data yet. -2. Export your PartKeepr database as XML file using [mysqldump](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html): -When the MySQL database is running on the local computer and you are root you can just run the command `mysqldump --xml PARTKEEPR_DATABASE --result-file pk.xml`. -If your server is remote or your MySQL authentication is different, you need to run `mysqldump --xml -h PARTKEEPR_HOST -u PARTKEEPR_USER -p PARTKEEPR_DATABASE`, where you replace `PARTKEEPR_HOST` -with the hostname of your MySQL database and `PARTKEEPR_USER` with the username of MySQL user which has access to the PartKeepr database. You will be asked for the MySQL user password. -3. Go the Part-DB main folder and run the command `php bin/console partdb:migrations:import-partkeepr path/to/pk.xml`. This step will delete all existing data in the Part-DB database and import the contents of PartKeepr. -4. Copy the contents of `data/files/` from your PartKeepr installation to the `uploads/` folder of your Part-DB installation and the contents of `data/images` from PartKeepr to `public/media/` of Part-DB. + +1. Install Part-DB as described in the installation guide. You can use any database backend you want (MySQL or + SQLite). Run the database migration, but do not create any new data yet. +2. Export your PartKeepr database as XML file using [mysqldump](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html): + When the MySQL database is running on the local computer, and you are root you can just run the + command `mysqldump --xml PARTKEEPR_DATABASE --result-file pk.xml`. + If your server is remote or your MySQL authentication is different, you need to + run `mysqldump --xml -h PARTKEEPR_HOST -u PARTKEEPR_USER -p PARTKEEPR_DATABASE`, where you replace `PARTKEEPR_HOST` + with the hostname of your MySQL database and `PARTKEEPR_USER` with the username of MySQL user which has access to the + PartKeepr database. You will be asked for the MySQL user password. +3. Go to the Part-DB main folder and run the command `php bin/console partdb:migrations:import-partkeepr path/to/pk.xml`. + This step will delete all existing data in the Part-DB database and import the contents of PartKeepr. +4. Copy the contents of `data/files/` from your PartKeepr installation to the `uploads/` folder of your Part-DB + installation and the contents of `data/images` from PartKeepr to `public/media/` of Part-DB. 5. Clear the cache of Part-DB by running: `php bin/console cache:clear` -6. Go to the Part-DB web interface. You can login with the username `admin` and the password, which is shown during the installation process of Part-DB (step 1). You should be able to see all the data from PartKeepr. +6. Go to the Part-DB web interface. You can log in with the username `admin` and the password, which is shown during the + installation process of Part-DB (step 1). You should be able to see all the data from PartKeepr. ## Import users -If you want to import the users (mostly the username and email address) from PartKeepr, you can add the `--import-users` option on the database import command (step 3): `php bin/console partdb:migrations:import-partkeepr --import-users path/to/pk.xml`. -All imported users of PartKeepr will be assigned to a new group "PartKeepr Users", which has normal user permissions (so editing data, but no administrative tasks). You can change the group and permissions later in Part-DB users managment. -Passwords can not be imported from PartKeepr and all imported users get marked as disabled user. So to allow users to login, you need to enable them in the user management and assign a password. \ No newline at end of file +If you want to import the users (mostly the username and email address) from PartKeepr, you can add the `--import-users` +option on the database import command (step 3): +`php bin/console partdb:migrations:import-partkeepr --import-users path/to/pk.xml`. + +All imported users of PartKeepr will be assigned to a new group "PartKeepr Users", which has normal user permissions (so +editing data, but no administrative tasks). You can change the group and permissions later in Part-DB users management. +Passwords can not be imported from PartKeepr and all imported users get marked as disabled. So to allow users to +log in, you need to enable them in the user management and assign a password. \ No newline at end of file diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 9441d9b9..f20a7f22 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -4,35 +4,46 @@ title: Troubleshooting --- # Troubleshooting + Sometimes things go wrong and Part-DB shows an error message. This page should help you to solve the problem. ## Error messages -When a common, easy fixable error occurs (like a non up-to-date database), Part-DB will show you some short instructions on how to fix the problem. If you have a problem that is not listed here, please open an issue on GitHub. + +When a common, easy fixable error occurs (like a non-up-to-date database), Part-DB will show you some short instructions +on how to fix the problem. If you have a problem that is not listed here, please open an issue on GitHub. ## General procedure + If you encounter an error, try the following steps: -* Clear cache of Part-DB with the console command: + +* Clear the cache of Part-DB with the console command: + ```bash php bin/console cache:clear ``` -* Check if the database needs an update (and perform it when needed) with the console command: + +* Check if the database needs an update (and perform it when needed) with the console command: + ```bash php bin/console doctrine:migrations:migrate ``` If this does not help, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony). -## Search for user and reset password: -You can list all users with the following command: `php bin/console partdb:users:list` -To reset the password of a user you can use the following command: `php bin/console partdb:users:set-password [username]` +## Search for the user and reset the password: +You can list all users with the following command: `php bin/console partdb:users:list` +To reset the password of a user you can use the following +command: `php bin/console partdb:users:set-password [username]` ## Error logs + Detailed error logs can be found in the `var/log` directory. When Part-DB is installed directly, the errors are written to the `var/log/prod.log` file. -When Part-DB is installed with Docker, the errors are written directly to the console output. +When Part-DB is installed with Docker, the errors are written directly to the console output. You can see the logs with the following command, when you are in the folder with the `docker-compose.yml` file + ```bash docker-compose logs -f ``` @@ -40,4 +51,5 @@ docker-compose logs -f Please include the error logs in your issue on GitHub, if you open an issue. ## Report Issue + If an error occurs, or you found a bug, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony). diff --git a/docs/upgrade_legacy.md b/docs/upgrade_legacy.md index d43fa2ed..e1e43831 100644 --- a/docs/upgrade_legacy.md +++ b/docs/upgrade_legacy.md @@ -6,55 +6,85 @@ nav_order: 100 # Upgrade from legacy Part-DB version -Part-DB 1.0 was a complete rewrite of the old Part-DB (< 1.0.0), which you can find [here](https://github.com/Part-DB/Part-DB). A lot of things changed internally, but Part-DB was always developed with compatibility in mind, so you can migrate smoothly to the new Part-DB version, and utilize its new features and improvements. +Part-DB 1.0 was a complete rewrite of the old Part-DB (< 1.0.0), which you can +find [here](https://github.com/Part-DB/Part-DB). A lot of things changed internally, but Part-DB was always developed +with compatibility in mind, so you can migrate smoothly to the new Part-DB version, and utilize its new features and +improvements. -Some things changed however to the old version and some features are still missing, so be sure to read the following sections carefully before proceeding to upgrade. +Some things changed however to the old version and some features are still missing, so be sure to read the following +sections carefully before proceeding to upgrade. ## Changes -* PHP 7.4 or higher is required now (Part-DB 0.5 required PHP 5.4+, Part-DB 0.6 PHP 7.0). - PHP 7.4 (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.4 -* **Console access highly required.** The installation of composer and frontend dependencies require console access, also more sensitive stuff like database migration work via CLI now, so you should have console access on your server. + +* PHP 8.1 or higher is required now (Part-DB 0.5 required PHP 5.4+, Part-DB 0.6 PHP 7.0). + Releases are available for Windows too, so almost everybody should be able to use PHP 8.1 +* **Console access is highly recommended.** The installation of composer and frontend dependencies require console access, + also more sensitive stuff like database migration works via CLI now, 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. -* Configuration is now done via configuration files / environment variables instead of the WebUI (this maybe change in the future). -* Database updated are now done via console instead of the WebUI + 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 into event log anymore. For security reasons (exceptions can contain sensitive + information) 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 the database (before they were saved in a separate 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 have + changed. +* Configuration is now done via configuration files/environment variables instead of the WebUI (this may change in + the future). +* Database updates are now done via console instead of the WebUI * Permission system changed: **You will have to newly set the permissions of all users and groups!** -* Import / Export file format changed. Fields must be english now (unlike in legacy Part-DB versions, where german fields in CSV were possible) -and you maybe have to change the header line/field names of your CSV files. +* Import / Export file format changed. Fields must be English now (unlike in legacy Part-DB versions, where German + fields in CSV were possible) + and you may have to change the header line/field names of your CSV files. ## Missing features -* No possibility to mark parts for ordering (yet) + +* No possibility of marking parts for ordering (yet) * No support for 3D models of footprints (yet) -* No possibility to disable footprints, manufacturers globally (or per category). This should not have a big impact, when you forbid users to edit/create them. -* No resistor calculator or SMD labels tools +* No possibility to disable footprints, manufacturers globally (or per category). This should not have a big impact + when you forbid users to edit/create them. +* No resistor calculator or SMD label tools ## Upgrade process {: .warning } -> Once you have upgraded the database to the latest version, you will not be able to access the database with Part-DB 0.5.*. Doing so could lead to data corruption. So make a a backup before you proceed the upgrade, so you will be able to revert the upgrade, when you are not happy with the new version +> Once you have upgraded the database to the latest version, you will not be able to access the database with Part-DB +> 0.5.*. Doing so could lead to data corruption. So make a backup before you proceed the upgrade, so you will be able to +> revert the upgrade, when you are not happy with the new version > -> Beware that all user and group permissions will be reset, and you have to set the permissions again +> Beware that all user and group permissions will be reset, and you have to set the permissions again > the new Part-DB as many permissions changed, and automatic migration is not possible. - 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 and attachments. If somethings goes wrong during migration, you can use this backup to start over. If you have some more complex permission configuration, you maybe want to do screenshots of it, so you can redo it again later. - 3. Setup the new Part-DB like described in installation section. You will need to do the setup for a MySQL instance (either via docker or direct installation). Set the `DATABASE_URL` environment variable in your `.env.local` (or `docker-compose.yaml`) to your existing database. (e.g. `DATABASE_URL=mysql://PARTDB_USER:PASSWORD@localhost:3306/DATABASE_NAME`) - 4. Ensure that the correct base currency is configured (`BASE_CURRENCY` env), this must match the currency used in the old Part-DB version. If you used Euro, you do not need to change anything. - 5. Run `php bin/console cache:clear` and `php bin/console doctrine:migrations:migrate`. - 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 the `data/media` folder from the old Part-DB instance into `public/media` folder in the new version. - 6. Run `php bin/console cache:clear` - 7. You should be able to login to Part-DB now using your admin account and the old password. If you do not know the admin username, run `php bin/console partdb:users:list` and look for the user with ID 1. You can reset the password of this user using `php bin/console partdb:users:set-password [username]`. - 8. All other users besides the admin user are disabled (meaning they can not login). Go to "System->User" and "System->Group" and check the permissions of the users (and change them if needed). If you are done enable the users again, by removing the disabled checkmark in the password section. If you have a lot of users you can enable them all at once using `php bin/console partdb:users:enable --all` - -**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, after everything works. +1. Upgrade your existing Part-DB version the newest Part-DB 0.5.* version (at the moment Part-DB 0.5.8), as described + in the old Part-DB's repository. +2. Make a backup of your database and attachments. If something goes wrong during migration, you can use this backup to + start over. If you have some more complex permission configuration, you maybe want to do screenshots of it, so you + can redo it again later. +3. Set up the new Part-DB as described in the installation section. You will need to do the setup for a MySQL instance ( + either via docker or direct installation). Set the `DATABASE_URL` environment variable in your `.env.local` ( + or `docker-compose.yaml`) to your existing database. ( + e.g. `DATABASE_URL=mysql://PARTDB_USER:PASSWORD@localhost:3306/DATABASE_NAME`) +4. Ensure that the correct base currency is configured (`BASE_CURRENCY` env), this must match the currency used in the + old Part-DB version. If you used Euro, you do not need to change anything. +5. Run `php bin/console cache:clear` and `php bin/console doctrine:migrations:migrate`. +6. Run `php bin/console partdb:migrations:convert-bbcode` to convert the BBCode used in comments and part description to + the newly used markdown. +7. Copy the content of the `data/media` folder from the old Part-DB instance into `public/media` folder in the new + version. +8. Run `php bin/console cache:clear` +9. You should be able to log in to Part-DB now using your admin account and the old password. If you do not know the + admin username, run `php bin/console partdb:users:list` and look for the user with ID 1. You can reset the password + of this user using `php bin/console partdb:users:set-password [username]`. +10. All other users besides the admin user are disabled (meaning they can not log in). Go to "System->User" and "System-> + Group" and check the permissions of the users (and change them if needed). If you are done enable the users again, by + removing the disabled checkmark in the password section. If you have a lot of users you can enable them all at once + using `php bin/console partdb:users:enable --all` +**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, after +everything works. ## Issues -If you encounter any issues (especially during the database migration) or features do not work like intended, please open an issue ticket at GitHub. \ No newline at end of file + +If you encounter any issues (especially during the database migration) or features do not work like intended, please +open an issue ticket at GitHub. diff --git a/docs/usage/backup_restore.md b/docs/usage/backup_restore.md index edcd1a87..bef3792d 100644 --- a/docs/usage/backup_restore.md +++ b/docs/usage/backup_restore.md @@ -6,48 +6,75 @@ parent: Usage # Backup and Restore Data -When working productively you should backup the data and configuration of Part-DB regularly to prevent data loss. This is also useful, if you want to migrate your Part-DB instance from one server to another. In that case you just have to backup the data on server 1, move the backup to server 2, install Part-DB on server 2 and restore the backup. +When working productively you should back up the data and configuration of Part-DB regularly to prevent data loss. This +is also useful if you want to migrate your Part-DB instance from one server to another. In that case, you just have to +back up the data on server 1, move the backup to server 2, install Part-DB on server 2, and restore the backup. ## Backup (automatic / Part-DB supported) -Part-DB includes a command `php bin/console partdb:backup` which automatically collects all the needed data (described below) and saves them to a ZIP file. + +Part-DB includes a command `php bin/console partdb:backup` which automatically collects all the needed data (described +below) and saves them to a ZIP file. If you are using a MySQL/MariaDB database you need to have `mysqldump` installed and added to your `$PATH` env. ### Usage -To backup all possible data, run the following command: `php bin/console partdb:backup --full /path/to/backup/partdb_backup.zip`. -It is possible to do only partial backups (config, attachments, or database). See `php bin/console partdb:backup --help` for more infos about these options. +To back up all possible data, run the following +command: `php bin/console partdb:backup --full /path/to/backup/partdb_backup.zip`. + +It is possible to do only partial backups (config, attachments, or database). See `php bin/console partdb:backup --help` +for more info about these options. ## Backup (manual) -There are 3 parts which have to be backup-ed: The configuration files, which contains the instance specific options, the uploaded files of attachments, and the database containing the most data of Part-DB. + +3 parts have to be backup-ed: The configuration files, which contain the instance-specific options, the +uploaded files of attachments, and the database containing the most data of Part-DB. Everything else like thumbnails and cache files, are recreated automatically when needed. ### Configuration files -You have to copy the `.env.local` file and (if you have changed it) the `config/parameters.yaml` and `config/banner.md` to your backup location. + +You have to copy the `.env.local` file and (if you have changed it) the `config/parameters.yaml` and `config/banner.md` +to your backup location. ### Attachment files + You have to recursively copy the `uploads/` folder and the `public/media` folder to your backup location. ### Database -#### Sqlite -If you are using sqlite, it is sufficient to just copy your `app.db` from your database location (normally `var/app.db`) to your backup location. + +#### SQLite + +If you are using sqlite, it is sufficient to just copy your `app.db` from your database location (normally `var/app.db`) +to your backup location. #### MySQL / MariaDB -For MySQL / MariaDB you have to dump the database to an SQL file. You can do this manually with phpmyadmin, or you use [`mysqldump`](https://mariadb.com/kb/en/mariadb-dumpmysqldump/) to dump the database to an SQL file via command line interface (`mysqldump -uBACKUP -pPASSWORD DATABASE`) + +For MySQL / MariaDB you have to dump the database to an SQL file. You can do this manually with phpmyadmin, or you +use [`mysqldump`](https://mariadb.com/kb/en/mariadb-dumpmysqldump/) to dump the database to an SQL file via command line +interface (`mysqldump -uBACKUP -pPASSWORD DATABASE`) ## Restore -Install Part-DB as usual as described in the installation section, with the exception of the database creation / migration part. You have to use the same database type (sqlite or mysql) as on the back-uped server instance. + +Install Part-DB as usual as described in the installation section, except for the database creation/migration part. You +have to use the same database type (SQLite or MySQL) as on the backuped server instance. ### Restore configuration -Copy configuration files `.env.local`, (and if existing) `config/parameters.yaml` and `config/banner.md` from the backup to your new Part-DB instance and overwrite the existing files there. + +Copy configuration files `.env.local`, (and if existing) `config/parameters.yaml` and `config/banner.md` from the backup +to your new Part-DB instance and overwrite the existing files there. ### Restore attachment files + Copy the `uploads/` and the `public/media/` folder from your backup into your new Part-DB folder. ### Restore database -#### Sqlite + +#### SQLite + Copy the backup-ed `app.db` into the database folder normally `var/app.db` in Part-DB root folder. #### MySQL / MariaDB -Recreate a database and user with the same credentials as before (or update the database credentials in the `.env.local` file). + +Recreate a database and user with the same credentials as before (or update the database credentials in the `.env.local` +file). Import the dumped SQL file from the backup into your new database. \ No newline at end of file diff --git a/docs/usage/bom_import.md b/docs/usage/bom_import.md index 86e590d7..94a06d55 100644 --- a/docs/usage/bom_import.md +++ b/docs/usage/bom_import.md @@ -7,23 +7,30 @@ parent: Usage # Import Bill of Material (BOM) for Projects -Part-DB supports the import of Bill of Material (BOM) files for projects. This allows you to directly import a BOM file from your ECAD software into your Part-DB project. +Part-DB supports the import of Bill of Material (BOM) files for projects. This allows you to directly import a BOM file +from your ECAD software into your Part-DB project. - -The import process is currently semi-automatic. This means Part-DB will take the BOM file and create entries for all parts in the BOM file in your project and assign fields like -mountnames (e.g. 'C1, C2, C3'), quantity and more. -However, you still have to assign the parts from Part-DB database to the entries (if applicable) after the import by hand, +The import process is currently semi-automatic. This means Part-DB will take the BOM file and create entries for all +parts in the BOM file in your project and assign fields like +mount names (e.g. 'C1, C2, C3'), quantity and more. +However, you still have to assign the parts from Part-DB database to the entries (if applicable) after the import by +hand, as Part-DB can not know which part you had in mind when you designed your schematic. ## Usage + In the project view or edit click on the "Import BOM" button, below the BOM table. This will open a dialog where you can select the BOM file you want to import and some options for the import process: * **Type**: The format/type of the BOM file. See below for explanations of the different types. -* **Clear existing BOM entries before import**: If this is checked, all existing BOM entries, which are currently associated with the project, will be deleted before the import. +* **Clear existing BOM entries before import**: If this is checked, all existing BOM entries, which are currently + associated with the project, will be deleted before the import. ### Supported BOM file formats -* **KiCAD Pcbnew BOM (CSV file)**: A CSV file of the Bill of Material (BOM) generated by [KiCAD Pcbnew](https://www.kicad.org/). -Please note that you have to export the BOM from the PCB editor, the BOM generated by the schematic editor (Eeschema) has a different format and does not work with this type. -You can generate this BOM file by going to "File" -> "Fabrication Outputs" -> "Bill of Materials" in Pcbnew and save the file to your desired location. +* **KiCAD Pcbnew BOM (CSV file)**: A CSV file of the Bill of Material (BOM) generated + by [KiCAD Pcbnew](https://www.kicad.org/). + Please note that you have to export the BOM from the PCB editor, the BOM generated by the schematic editor (Eeschema) + has a different format and does not work with this type. + You can generate this BOM file by going to "File" -> "Fabrication Outputs" -> "Bill of Materials" in Pcbnew and save + the file to your desired location. diff --git a/docs/usage/console_commands.md b/docs/usage/console_commands.md index 7a23483a..e5197251 100644 --- a/docs/usage/console_commands.md +++ b/docs/usage/console_commands.md @@ -8,31 +8,72 @@ parent: Usage Part-DB provides some console commands to display various information or perform some tasks. The commands are invoked from the main directory of Part-DB with the command `php bin/console [command]` in the context -of the database user (so usually the webserver user), so you maybe have to use `sudo` or `su` to execute the commands. +of the database user (so usually the webserver user), so you maybe have to use `sudo` or `su` to execute the commands: + +```bash +sudo -u www-data php bin/console [command] +``` -You can get help for every command with the parameter `--help`. See `php bin/console` for a list of all available commands. +You can get help for every command with the parameter `--help`. See `php bin/console` for a list of all available +commands. + +If you are running Part-DB in a docker container, you must either execute the commands from a shell inside a container, +or use the `docker exec` command to execute the command directly inside the container. For example if you docker container +is named `partdb`, you can execute the command `php bin/console cache:clear` with the following command: + +```bash +docker exec --user=www-data partdb php bin/console cache:clear +``` + +{: .warning } +> If you run a root console inside the docker container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell. +> Otherwise Part-DB console might use the wrong configuration to execute commands. + +## Troubleshooting + +## User management commands -## User managment commands * `php bin/console partdb:users:list`: List all users of this Part-DB instance -* `php bin/console partdb:users:set-password [username]`: Set/Changes the password of the user with the given username. This allows administrators to reset a password of a user, if he forgot it. -* `php bin/console partdb:users:enable [username]`: Enable/Disable the user with the given username (use `--disable` to disable the user, which prevents login) +* `php bin/console partdb:users:set-password [username]`: Set/Changes the password of the user with the given username. + This allows administrators to reset a password of a user, if he forgot it. +* `php bin/console partdb:users:enable [username]`: Enable/Disable the user with the given username (use `--disable` to + disable the user, which prevents login) * `php bin/console partdb:users:permissions`: View/Change the permissions of the user with the given username -* `php bin/console partdb:users:upgrade-permissions-schema`: Upgrade the permissions schema of users to the latest version (this is normally automatically done when the user visits a page) +* `php bin/console partdb:users:upgrade-permissions-schema`: Upgrade the permissions schema of users to the latest + version (this is normally automatically done when the user visits a page) * `php bin/console partdb:logs:show`: Show the most recent entries of the Part-DB event log / recent activity -* `php bin/console partdb:user:convert-to-saml-user`: Convert a local user to a SAML/SSO user. This is needed, if you want to use SAML/SSO authentication for a user, which was created before you enabled SAML/SSO authentication. +* `php bin/console partdb:user:convert-to-saml-user`: Convert a local user to a SAML/SSO user. This is needed, if you + want to use SAML/SSO authentication for a user, which was created before you enabled SAML/SSO authentication. ## Currency commands -* `php bin/console partdb:currencies:update-exchange-rates`: Update the exchange rates of all currencies from the internet) + +* `php bin/console partdb:currencies:update-exchange-rates`: Update the exchange rates of all currencies from the + internet ## Installation/Maintenance commands + * `php bin/console partdb:backup`: Backup the database and the attachments * `php bin/console partdb:version`: Display the current version of Part-DB and the used PHP version -* `php bin/console partdb:check-requirements`: Check if the requirements for Part-DB are met (PHP version, PHP extensions, etc.) and make suggestions what could be improved -* `partdb:migrations:convert-bbcode`: Migrate the old BBCode markup codes used in legacy Part-DB versions (< 1.0.0) to the new markdown syntax -* `partdb:attachments:clean-unused`: Remove all attachments which are not used by any database entry (e.g. orphaned attachments) -* `partdb:cache:clear`: Clears all caches, so the next page load will be slower, but the cache will be rebuild. This can maybe fix some issues, when the cache were corrupted. This command is also needed after changing things in the `parameters.yaml` file or upgrading Part-DB. -* `partdb:migrations:import-partkeepr`: Imports an mysqldump XML dump of a PartKeepr database into Part-DB. This is only needed for users, which want to migrate from PartKeepr to Part-DB. *All existing data in the Part-DB database is deleted!* +* `php bin/console partdb:check-requirements`: Check if the requirements for Part-DB are met (PHP version, PHP + extensions, etc.) and make suggestions what could be improved +* `partdb:migrations:convert-bbcode`: Migrate the old BBCode markup codes used in legacy Part-DB versions (< 1.0.0) to + the new Markdown syntax +* `partdb:attachments:clean-unused`: Remove all attachments which are not used by any database entry (e.g. orphaned + attachments) +* `partdb:cache:clear`: Clears all caches, so the next page load will be slower, but the cache will be rebuilt. This can + maybe fix some issues, when the cache were corrupted. This command is also needed after changing things in + the `parameters.yaml` file or upgrading Part-DB. +* `partdb:migrations:import-partkeepr`: Imports a mysqldump XML dump of a PartKeepr database into Part-DB. This is only + needed for users, which want to migrate from PartKeepr to Part-DB. *All existing data in the Part-DB database is + deleted!* ## Database commands + * `php bin/console doctrine:migrations:migrate`: Migrate the database to the latest version -* `php bin/console doctrine:migrations:up-to-date`: Check if the database is up-to-date \ No newline at end of file +* `php bin/console doctrine:migrations:up-to-date`: Check if the database is up-to-date + +## Attachment commands + +* `php bin/console partdb:attachments:download`: Download all attachments, which are not already downloaded, to the + local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote and + also makes pictures thumbnails available for the frontend for them \ No newline at end of file diff --git a/docs/usage/eda_integration.md b/docs/usage/eda_integration.md new file mode 100644 index 00000000..9444e55f --- /dev/null +++ b/docs/usage/eda_integration.md @@ -0,0 +1,79 @@ +--- +layout: default +title: EDA / KiCad integration +parent: Usage +--- + +# EDA / KiCad integration + +Part-DB can function as a central database for [EDA](https://en.wikipedia.org/wiki/Electronic_design_automation) or ECAD software used to design electronic schematics and PCBs. +You can connect your EDA software and can view your available parts, with the data saved from Part-DB directly in your EDA software. +Part-DB allows to configure additional metadata for the EDA, to associate symbols and footprints for use inside the EDA software, so the part becomes +directly usable inside the EDA software. +This also allows to configure available and usable parts and their properties in a central place, which is especially useful in teams, where multiple persons design PCBs. + +**Currently only KiCad is supported!** + +## KiCad Setup + +{: .important } +> Part-DB uses the HTTP library feature of KiCad, which is experimental and not part of the stable KiCad 7 releases. If you want to use this feature, you need to install a KiCad nightly build (7.99 version). This feature will most likely also be part of KiCad 8. + +Part-DB should be accessible from the PCs with KiCAD. The URL should be stable (so no dynamically changing IP). +You require a user account in Part-DB, which has permission to access Part-DB API and create API tokens. Every user can have its own account, or you set up a shared read-only account. + +To connect KiCad with Part-DB do the following steps: + +1. Create an API token on the user settings page for the KiCAD application and copy/save it, when it is shown. Currently, KiCad can only read Part-DB database, so a token with a read-only scope is enough. +2. Add some EDA metadata to parts, categories, or footprints. Only parts with usable info will show up in KiCad. See below for more info. +3. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content: +``` +{ + "meta": { + "version": 1.0 + }, + "name": "Part-DB library", + "description": "This KiCAD library fetches information externally from ", + "source": { + "type": "REST_API", + "api_version": "v1", + "root_url": "http://kicad-instance.invalid/en/kicad-api/", + "token": "THE_GENERATED_API_TOKEN" + } +} +``` +4. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel. +5. Replace the `token` field value with the token you have generated in step 1. +6. Open KiCad and add this created file as a library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries) + +If you then place a new part, the library dialog opens, and you should be able to see the categories and parts from Part-DB. + +### How to associate footprints and symbols with parts + +Part-DB doesn't save any concrete footprints or symbols for the part. Instead, Part-DB just contains a reference string in the part metadata, which points to a symbol/footprint in KiCad's local library. + +You can define this on a per-part basis using the KiCad symbol and KiCad footprint field in the EDA tab of the part editor. Or you can define it at a category (symbol) or footprint level, to assign this value to all parts with this category and footprint. + +For example, to configure the values for a BC547 transistor you would put `Transistor_BJT:BC547` on the parts Kicad symbol to give it the right schematic symbol in EEschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in PcbNew. + +If you type in a character, you will get an autocomplete list of all symbols and footprints available in the KiCad standard library. You can also input your own value. + +### Parts and category visibility + +Only parts and their categories, on which there is any kind of EDA metadata are defined show up in KiCad. So if you want to see parts in KiCad, +you need to define at least a symbol, footprint, reference prefix, or value on a part, category or footprint. + +You can use the "Force visibility" checkbox on a part or category to override this behavior and force parts to be visible or hidden in KiCad. + +*Please note that KiCad caches the library categories. So if you change something, which would change the visible categories in KiCad, you have to reload EEschema to see the changes.* + +### Category depth in KiCad + +For performance reasons, only the most top-level categories of Part-DB are shown as categories in KiCad. All parts in the subcategories are shown in the top-level category. + +You can configure the depth of the categories shown in KiCad, via the `EDA_KICAD_CATEGORY_DEPTH` env option. The default value is 0, which means only the top-level categories are shown. +To show more levels of categories, you can set this value to a higher number. + +If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories. + +You can view the "real" category path of a part in the part details dialog in KiCad. diff --git a/docs/usage/getting_started.md b/docs/usage/getting_started.md index bc25123f..4bb8afb9 100644 --- a/docs/usage/getting_started.md +++ b/docs/usage/getting_started.md @@ -7,111 +7,145 @@ nav_order: 4 # Getting started After Part-DB you should begin with customizing the settings, and setting up the basic structures. -Before starting its useful to read a bit about the [concepts of Part-DB]({% link concepts.md %}). +Before starting, it's useful to read a bit about the [concepts of Part-DB]({% link concepts.md %}). 1. TOC {:toc} ## Customize config files -Before you start creating data structures, you should configure Part-DB to your needs by changing possible configuration options. -This is done either via changing the `.env.local` file in a direct installation or by changing the env variables in your `docker-compose.yaml` file. -A list of possible configuration options, can be found [here]({% link configuration.md %}). +Before you start creating data structures, you should configure Part-DB to your needs by changing possible configuration +options. +This is done either via changing the `.env.local` file in a direct installation or by changing the env variables in +your `docker-compose.yaml` file. +A list of possible configuration options can be found [here]({% link configuration.md %}). ## Change password, Set up Two-Factor-Authentication & Customize User settings -If you have not already done, you should change your user password. You can do this in the user settings (available in the navigation bar drop down with the user symbol). +If you have not already done so, you should change your user password. You can do this in the user settings (available in +the navigation bar drop-down with the user symbol). ![image]({% link assets/getting_started/change_password.png %}) -There you can also find the option, to set up Two Factor Authentication methods like Google Authenticator. Using this is highly recommended (especially if you have admin permissions) to increase the security of your account. (Two Factor Authentication even can be enforced for all members of a user group) +There you can also find the option, to set up Two-Factor Authentication methods like Google Authenticator. Using this is +highly recommended (especially if you have admin permissions) to increase the security of your account. (Two-factor authentication +even can be enforced for all members of a user group) -In the user settings panel you can change account infos like your username, your first and last name (which will be shown alongside your username to identify you better), department information and your email address. The email address is used to send password reset mails, if your system is configured to use this. +In the user settings panel, you can change account info like your username, your first and last name (which will be +shown alongside your username to identify you better), department information, and your email address. The email address +is used to send password reset mails if your system is configured to use this. ![image]({% link assets/getting_started/user_settings.png %}) -In the configuration tab you can also override global settings, like your preferred UI language (which will automatically be applied after login), the timezone you are in (and in which times will be shown for you), your preferred currency (all money values will be shown converted to this to you, if possible) and the theme that should be used. +In the configuration tab you can also override global settings, like your preferred UI language (which will +automatically be applied after login), the timezone you are in (and in which times will be shown for you), your +preferred currency (all money values will be shown converted to this to you, if possible) and the theme that should be +used. ## (Optional) Customize homepage banner -The banner which is shown on the homepage, can be customized/changed by changing the `config/banner.md` file with a text editor. You can use markdown and (safe) HTML here, to style and customize the banner. -You can even use Latex style equations by wrapping the expressions into `$` (like `$E=mc^2$`, which is rendered inline: $E=mc^2$) or `$$` (like `$$E=mc^2$$`) which will be rendered as a block, like so: $$E=mc^2$$ +The banner which is shown on the homepage, can be customized/changed by changing the `config/banner.md` file with a text +editor. You can use markdown and (safe) HTML here, to style and customize the banner. +You can even use LaTeX-style equations by wrapping the expressions into `$` (like `$E=mc^2$`, which is rendered inline: +$E=mc^2$) or `$$` (like `$$E=mc^2$$`) which will be rendered as a block, like so: $$E=mc^2$$ ## Create groups, users and customize permissions ### Users -When logged in as administrator, you can open the users menu in the `Tools` section of the sidebar under `System -> Users`. -At this page you can create new users, change their passwords and settings and change their permissions. -For each user which should use Part-DB you should setup a own account, so that tracking of what user did what works properly. +When logged in as administrator, you can open the users menu in the `Tools` section of the sidebar +under `System -> Users`. +On this page you can create new users, change their passwords and settings, and change their permissions. +For each user who should use Part-DB you should set up their own account so that tracking of what user did works +properly. ![image]({% link assets/getting_started/user_admin.png %}) - -You should check the permissions for every user and ensure that they are in the intended way, and no user has more permissions than he needs. -For each capability you can choose between allow, forbid and inherit. In the last case, the permission is determined by the group a user has (if no group is chosen, it equals forbid) +You should check the permissions for every user and ensure that they are in the intended way, and no user has more +permissions than he needs. +For each capability, you can choose between allow, forbid, and inherit. In the last case, the permission is determined by +the group a user has (if no group is chosen, it equals forbid) ![image]({% link assets/getting_started/user_permissions.png %}) - ### Anonymous user -The `anonymous` user is special, as its settings and permissions are used for everybody who is not logged in. By default the anonymous user has read capabilities for your parts. If your Part-DB instance is publicly available you maybe want to restrict the permissions. +The `anonymous` user is special, as its settings and permissions are used for everybody who is not logged in. By default, +the anonymous user has read capabilities for your parts. If your Part-DB instance is publicly available you maybe want +to restrict the permissions. ### Groups -If you have many users which should share the same permissions, it is useful to define the permissions using user groups, which you can create and edit in the `System -> Groups` menu. +If you have many users who should share the same permissions, it is useful to define the permissions using user +groups, which you can create and edit in the `System -> Groups` menu. -By default 3 groups are defined: -* `readonly` which users have only have read permissions (like viewing, searching parts, attachments, etc.) +By default, 3 groups are defined: + +* `readonly` which users only have read permissions (like viewing, searching parts, attachments, etc.) * `users` which users also have rights to edit/delete/create elements -* `admin` which users can do administrative operations (like creating new users, show global system log, etc.) +* `admin` which users can do administrative operations (like creating new users, showing global system log, etc.) -Users only use the setting of a capability from a group, if the user has a group associated and the capability on the user is set to `inherit` (which is the default if creating a new user). You can override the permissions settings of a group per user by explicitly settings the permission at the user. - -Groups are organized as trees, meaning a group can have parent and child permissions and child groups can inherit permissions from their parents. -To inherit the permissions from a parent group set the capability to inherit, otherwise set it explicitly to override the parents permission. +Users only use the setting of a capability from a group, if the user has a group associated and the capability on the +user is set to `inherit` (which is the default if creating a new user). You can override the permissions settings of a +group per user by explicitly setting the permission of the user. +Groups are organized as trees, meaning a group can have parent and child permissions and child groups can inherit +permissions from their parents. +To inherit the permissions from a parent group set the capability to inherit, otherwise, set it explicitly to override +the parents' permission. ## Create Attachment types -Every attachment (that is an file associated with a part, data structure, etc.) must have an attachment type. They can be used to group attachments logically, like differentiating between datasheets, pictures and other documents. +Every attachment (that is a file associated with a part, data structure, etc.) must have an attachment type. They can +be used to group attachments logically, like differentiating between datasheets, pictures, and other documents. You can create/edit attachment types in the tools sidebar under "Edit -> Attachment types": ![image]({% link assets/getting_started/attachment_type_admin.png %}) - -Depending on your usecase different entries here make sense. For part mananagment the following (additional) entries maybe make sense: + +Depending on your use case different entries here make sense. For part management the following (additional) entries may make sense: * Datasheets (restricted to pdfs, Allowed filetypes: `application/pdf`) * Pictures (for generic pictures of components, storage locations, etc., Allowed filetypes: `image/*` -For every attachment type a list of allowed file types, which can be uploaded to an attachment with this attachment type, can be defined. You can either pass a list of allowed file extensions (e.g. `.pdf, .zip, .docx`) and/or a list of [Mime Types](https://en.wikipedia.org/wiki/Media_type) (e.g. `application/pdf, image/jpeg`) or a combination of both here. To allow all browser supported images, you can use `image/*` wildcard here. +For every attachment type a list of allowed file types, which can be uploaded to an attachment with this attachment +type, can be defined. You can either pass a list of allowed file extensions (e.g. `.pdf, .zip, .docx`) and/or a list +of [Mime Types](https://en.wikipedia.org/wiki/Media_type) (e.g. `application/pdf, image/jpeg`) or a combination of both +here. To allow all browser-supported images, you can use `image/*` wildcard here. ## (Optional) Create Currencies -If you want to save priceinformations for parts in a currency different to your global currency (by default Euro), you have to define the additional currencies you want to use under `Edit -> Currencies`: +If you want to save price information for parts in a currency different from your global currency (by default Euro), you +have to define the additional currencies you want to use under `Edit -> Currencies`: ![image]({% link assets/getting_started/currencies_admin.png %}) -You create a new currency, name it however you want (it is recommended to use the official name of the currency) and select the currency ISO code from the list and save it. The currency symbol is determined automatically from chose ISO code. -You can define a exchange rate in terms of your base currency (e.g. how much euros is one unit of your currency worth) to convert the currencies values in your preferred display currency automatically. - +You create a new currency, name it however you want (it is recommended to use the official name of the currency), +select the currency ISO code from the list, and save it. The currency symbol is determined automatically from the chosen ISO +code. +You can define an exchange rate in terms of your base currency (e.g. how many euros is one unit of your currency worth) +to convert the currency values in your preferred display currency automatically. ## (Optional) Create Measurement Units -By default Part-DB assumes that the parts in inventory can be counted by individual indivisible pieces, like LEDs in a box or books in a shelf. -However if you want to manage things, that are divisible and and the instock is described by a physical quantity, like length for cables, or volumina of a liquid, you have to define additional measurement units. +By default, Part-DB assumes that the parts in inventory can be counted by individual indivisible pieces, like LEDs in a +box or books on a shelf. +However, if you want to manage things, that are divisible and the stock is described by a physical quantity, like +length for cables, or volumes of a liquid, you have to define additional measurement units. This is possible under `Edit -> Measurement Units`: ![image]({% link assets/getting_started/units_admin.png %}) -You can give the measurement unit a name and an optional unit symbol (like `m` for meters) which is shown when quantities in this unit are displayed. The option `Use SI prefix` is useful for almost all physical quantities, as big and small numbers are automatically formatted with SI-prefixes (like 1.5kg instead 1500 grams). +You can give the measurement unit a name and an optional unit symbol (like `m` for meters) which is shown when +quantities in this unit are displayed. The option `Use SI prefix` is useful for almost all physical quantities, as big +and small numbers are automatically formatted with SI prefixes (like 1.5kg instead 1500 grams). -The measurement unit can be selected for each part individually, by setting the option in the advanced tab of a part`s edit menu. +The measurement unit can be selected for each part individually, by setting the option in the advanced tab of a part`s +edit menu. ## Create Categories -A category is used to group parts logically by their function (e.g. all NPN transistors would be put in a "NPN-Transistors" category). +A category is used to group parts logically by their function (e.g. all NPN transistors would be put in a " +NPN-Transistors" category). Categories are hierarchical structures meaning that you can create logical trees to group categories together. See [Concepts]({% link concepts.md %}) for an example tree structure. @@ -121,43 +155,51 @@ Every part has to be assigned to a category, so you should create at least one c ## (Optional) Create Footprints -Footprints are used to describe the physical shape of a part, like a resistor or a capacitor. -They can be used to group parts by their physical shape and to find parts with in the same package. +Footprints are used to describe the physical shape of a part, like a resistor or a capacitor. +They can be used to group parts by their physical shape and to find parts within the same package. You can create/edit footprints in the tools sidebar under "Edit -> Footprints". -It is useful to create footprints for the most common packages, like SMD resistors, capacitors, etc. to make it easier to find parts with the same footprint. -You should create these as a tree structure, so that you can group footprints by their type. +It is useful to create footprints for the most common packages, like SMD resistors, capacitors, etc. to make it easier +to find parts with the same footprint. +You should create these as a tree structure so that you can group footprints by their type. See [Concepts]({% link concepts.md %}) for an example tree structure. -You can define attachments here which are associated with the footprint. The attachment set as preview image, will be +You can define attachments here which are associated with the footprint. The attachment set as the preview image, will be used whenever a visual representation of the footprint is needed (e.g. in the part list). -For many common footprints, you can use the built-in footprints, which can be found in the "Builtin footprint image gallery", which you can find in the tools menu. -Type the name of the image you want to use in the URL field of the attachment and select the image from the dropdown menu. +For many common footprints, you can use the built-in footprints, which can be found in the "Builtin footprint image +gallery", which you can find in the "tools" menu. +Type the name of the image you want to use in the URL field of the attachment and select the image from the dropdown +menu. ## (Optional) Create Storage locations -A storelocation represents a place where parts can be stored. +A storage location represents a place where parts can be stored. You can create/edit storage locations in the tools sidebar under "Edit -> Storage locations". ## (Optional) Create Manufacturers and suppliers -You can create/edit [manufacturers]({% link concepts.md %}#manufacturers) and [suppliers]({% link concepts.md %}#suppliers) in the tools sidebar under "Edit -> Manufacturers" and "Edit -> Suppliers". +You can create/edit [manufacturers]({% link concepts.md %}#manufacturers) and [suppliers]({% link concepts.md +%}#suppliers) in the tools sidebar under "Edit -> Manufacturers" and "Edit -> Suppliers". ## Create parts -You are now ready to create your first part. You can do this by clicking either by clicking "Edit -> New Part" in the tools sidebar tree -or by clicking the "Create new Part" above the (empty) part list, after clicking on one of your newly created categories. +You are now ready to create your first part. You can do this by clicking either by clicking "Edit -> New Part" in the +tools sidebar tree +or by clicking the "Create new Part" above the (empty) part list, after clicking on one of your newly created +categories. You will be presented with a form where you can enter the basic information about your part: ![image]({% link assets/getting_started/new_part.png %}) You have to enter at least a name for the part and choose a category for it, the other fields are optional. -However, it is recommended to fill out as much information as possible, as this will make it easier to find the part later. +However, it is recommended to fill out as much information as possible, as this will make it easier to find the part +later. You can choose from your created datastructures to add manufacturer information, supplier information, etc. to the part. You can also create new datastructures on the fly, if you want to add additional information to the part, by typing the -name of the new datastructure in the field and select the "New ..." option in the dropdown menu. See [tips]({% link usage/tips_tricks.md %}) for more information. \ No newline at end of file +name of the new datastructure in the field and select the "New ..." option in the dropdown menu. See [tips]({% link +usage/tips_tricks.md %}) for more information. \ No newline at end of file diff --git a/docs/usage/import_export.md b/docs/usage/import_export.md index 09e4b163..e43936cc 100644 --- a/docs/usage/import_export.md +++ b/docs/usage/import_export.md @@ -7,97 +7,155 @@ parent: Usage # Import & Export data -Part-DB offers the possibility to import existing data (parts, datastructures, etc.) from existing datasources into Part-DB. Data can also be exported from Part-DB into various formats. +Part-DB offers the possibility to import existing data (parts, data structures, etc.) from existing data sources into +Part-DB. Data can also be exported from Part-DB into various formats. ## Import {: .note } -> As data import is a very powerful feature and can easily fill up your database with lots of data, import is by default only available for -> administrators. If you want to allow other users to import data, or can not import data, check the permissions of the user. You can enable import for each data structure +> As data import is a very powerful feature and can easily fill up your database with lots of data, import is by default +> only available for +> administrators. If you want to allow other users to import data, or can not import data, check the permissions of the +> user. You can enable import for each data structure > individually in the permissions settings. -If you want to import data from PartKeepr you might want to look into the [PartKeepr migration guide]({% link upgrade_legacy.md %}). +If you want to import data from PartKeepr you might want to look into the [PartKeepr migration guide]({% link +upgrade_legacy.md %}). ### Import parts -Part-DB supports the import of parts from CSV files and other formats. This can be used to import existing parts from other databases or datasources into Part-DB. The import can be done via the "Tools -> Import parts" page, which you can find in the "Tools" sidebar panel. +Part-DB supports the import of parts from CSV files and other formats. This can be used to import existing parts from +other databases or data sources into Part-DB. The import can be done via the "Tools -> Import parts" page, which you can +find in the "Tools" sidebar panel. {: .important } -> When importing data, the data is immediatley written to database during the import process, when the data is formally valid. -> You will not be able to check the data before it is written to the database, so you should review the data before using the import tool. +> When importing data, the data is immediately written to database during the import process, when the data is formally +> valid. +> You will not be able to check the data before it is written to the database, so you should review the data before +> using the import tool. -You can upload the file which should be imported here and choose various options on how the data should be treated: -* **Format**: By default "auto" is selected here and Part-DB will try to detect the format of the file automatically based on its file extension. If you want to force a specific format or Part-DB can not auto-detect the format, you can select it here. -* **CSV delimiter**: If you upload an CSV file, you can select the delimiter character which is used to separate the columns in the CSV file. Depending on the CSV file, this might be a comma (`,`), semicolon (`;`). -* **Category override**: You can select (or create) a category here, to which all imported parts should be assigned, no matter what was specified in the import file. This can be useful if you want to assign all imports to a certain category or if no category is specified in the data. If you leave this field empty, the category will be determined by the import file (or the export will error, if no category is specified). -* **Mark all imported parts as "Needs review"**: If this is selected, all imported parts will be marked as "Needs review" after the import. This can be useful if you want to review all imported parts before using them. -* **Create unknown datastructures**: If this is selected Part-DB will create new datastructures (like categories, manufacturers, etc.) if no datastructure(s) with the same name and path already exists. If this is not selected, only existing datastructures will be used and if no matching datastrucure is found, the imported parts field will be empty. -* **Path delimiter**: Part-DB allows you to create/select nested datastructures (like categories, manufacturers, etc.) by using a path (e.g. `Category 1->Category 1.1`, which will select/create the `Category 1.1` whose parent is `Category 1`). This path is separated by the path delimiter. If you want to use a different path delimiter than the default one (which is `>`), you can select it here. -* **Abort on validation error**: If this is selected, the import will be aborted if a validation error occurs (e.g. if a required field is empty) for any of the imported parts and validation errors will be shown on top of the page. If this is not selected, the import will continue for the other parts and only the invalid parts will be skipped. +You can upload the file that should be imported here and choose various options on how the data should be treated: -After you have selected the options, you can start the import by clicking the "Import" button. When the import is finished, you will see the results of the import in the lower half of the page. You find a table with the imported parts (including links to them) there. +* **Format**: By default "auto" is selected here and Part-DB will try to detect the format of the file automatically + based on its file extension. If you want to force a specific format or Part-DB can not auto-detect the format, you can + select it here. +* **CSV delimiter**: If you upload a CSV file, you can select the delimiter character which is used to separate the + columns in the CSV file. Depending on the CSV file, this might be a comma (`,`) or semicolon (`;`). +* **Category override**: You can select (or create) a category here, to which all imported parts should be assigned, no + matter what was specified in the import file. This can be useful if you want to assign all imports to a certain + category or if no category is specified in the data. If you leave this field empty, the category will be determined by + the import file (or the export will error, if no category is specified). +* **Mark all imported parts as "Needs review"**: If this is selected, all imported parts will be marked as "Needs + review" after the import. This can be useful if you want to review all imported parts before using them. +* **Create unknown data structures**: If this is selected Part-DB will create new data structures (like categories, + manufacturers, etc.) if no data structure(s) with the same name and path already exists. If this is not selected, only + existing data structures will be used and if no matching data strucure is found, the imported parts field will be empty. +* **Path delimiter**: Part-DB allows you to create/select nested data structures (like categories, manufacturers, etc.) + by using a path (e.g. `Category 1->Category 1.1`, which will select/create the `Category 1.1` whose parent + is `Category 1`). This path is separated by the path delimiter. If you want to use a different path delimiter than the + default one (which is `>`), you can select it here. +* **Abort on validation error**: If this is selected, the import will be aborted if a validation error occurs (e.g. if a + required field is empty) for any of the imported parts and validation errors will be shown on top of the page. If this + is not selected, the import will continue for the other parts and only the invalid parts will be skipped. + +After you have selected the options, you can start the import by clicking the "Import" button. When the import is +finished, you will see the results of the import in the lower half of the page. You can find a table with the imported +parts (including links to them) there. #### Fields description -For the importing of parts, you can use the following fields which will be imported into each part. Please note that the field names are case sensitive (so `name` is not the same as `Name`). All fields (besides name) are optional, so you can leave them empty or do not include the column in your file. +For the importing of parts, you can use the following fields which will be imported into each part. Please note that the +field names are case-sensitive (so `name` is not the same as `Name`). All fields (besides name) are optional, so you can +leave them empty or do not include the column in your file. * **`name`** (required): The name of the part. This is the only required field, all other fields are optional. * **`description`**: The description of the part, you can use markdown/HTML syntax here for rich text formatting. * **`notes`** or **`comment`**: The notes of the part, you can use markdown/HTML syntax here for rich text formatting. -* **`category`**: The category of the part. This can be a path (e.g. `Category 1->Category 1.1`), which will select/create the `Category 1.1` whose parent is `Category 1`. If you want to use a different path delimiter than the default one (which is `->`), you can select it in the import options. If the category does not exist and the option "Create unknown datastructures" is selected, it will be created. +* **`category`**: The category of the part. This can be a path (e.g. `Category 1->Category 1.1`), which will + select/create the `Category 1.1` whose parent is `Category 1`. If you want to use a different path delimiter than the + default one (which is `->`), you can select it in the import options. If the category does not exist and the option " + Create unknown datastructures" is selected, it will be created. * **`footprint`**: The footprint of the part. Can be a path similar to the category field. * **`favorite`**: If this is set to `1`, the part will be marked as favorite. * **`manufacturer`**: The manufacturer of the part. Can be a path similar to the category field. * **`manufacturer_product_number`** or **`mpn`**: The manufacturer product number of the part. -* **`manufacturer_product_url`: The URL to the product page of the manufacturer of the part. -* **`manufacturing_status`**: The manufacturing status of the part, must be one of the following values: `announced`, `active`, `nrfnd`, `eol`, `discontinued` or left empty. +* **`manufacturer_product_url`**: The URL to the product page of the manufacturer of the part. +* **`manufacturing_status`**: The manufacturing status of the part, must be one of the following + values: `announced`, `active`, `nrfnd`, `eol`, `discontinued` or left empty. * **`needs_review`** or **`needs_review`**: If this is set to `1`, the part will be marked as "needs review". -* **`tags`**: A comma separated list of tags for the part. +* **`tags`**: A comma-separated list of tags for the part. * **`mass`**: The mass of the part in grams. * **`ipn`**: The IPN (Item Part Number) of the part. * **`minamount`**: The minimum amount of the part which should be in stock. * **`partUnit`**: The measurement unit of the part to use. Can be a path similar to the category field. -With the following fields you can specify storage locations and amount / quantiy in stock of the part. An PartLot will be created automatically from the data and assigned to the part. The following fields are helpers for an easy import for parts at one storage location. If you need to create a Part with multiple PartLots you have to use JSON format (or CSV) with nested objects: +With the following fields, you can specify storage locations and amount/quantity in stock of the part. A PartLot will +be created automatically from the data and assigned to the part. The following fields are helpers for an easy import of +parts at one storage location. If you need to create a Part with multiple PartLots you have to use JSON format (or CSV) +with nested objects: -**`storage_location`** or **`storelocation`**: The storage location of the part. Can be a path similar to the category field. -**`amount`**, **`quantity`** or **`instock`**: The amount of the part in stock. If this value is not set, the part lot will be marked with "unknown amount" +**`storage_location`** or **`storelocation`**: The storage location of the part. Can be a path similar to the category +field. +**`amount`**, **`quantity`** or **`instock`**: The amount of the part in stock. If this value is not set, the part lot +will be marked with "unknown amount" -The following fields can be used to specify the supplier/distributor, supplier product number and the price of the part. This is only possible for a single supplier/distributor and price with this fields. If you need to specify multiple suppliers/distributors or prices, you have to use JSON format (or CSV) with nested objects. -**Please note that the supplier fields is required, if you want to import prices or supplier product numbers.**. If the supplier is not specified, the price and supplier product number fields will be ignored: +The following fields can be used to specify the supplier/distributor, supplier product number, and the price of the part. +This is only possible for a single supplier/distributor and price with these fields. If you need to specify multiple +suppliers/distributors or prices, you have to use JSON format (or CSV) with nested objects. +**Please note that the supplier fields is required, if you want to import prices or supplier product numbers**. If the +supplier is not specified, the price and supplier product number fields will be ignored: * **`supplier`**: The supplier of the part. Can be a path similar to the category field. * **`supplier_product_number`** or **`supplier_part_number`** or * **`spn`**: The supplier product number of the part. * **`price`**: The price of the part in the base currency of the database (by default euro). #### Example data -Here you can find some example data for the import of parts, you can use it as a template for your own import (especially the CSV file). + +Here you can find some example data for the import of parts, you can use it as a template for your own import ( +especially the CSV file). * [Part import CSV example]({% link assets/usage/import_export/part_import_example.csv %}) with all possible fields ## Export -By default every user, who can read the datastructure, can also export the data of this datastructure, as this does not give the user any additional information. +By default, every user, who can read the datastructure, can also export the data of this datastructure, as this does not +give the user any additional information. ### Exporting data structures (categories, manufacturers, etc.) -You can export data structures (like categories, manufacturers, etc.) in the respective edit page (e.g. Tools Panel -> Edit -> Category). -If you select a certain datastructure from your list, you can export it (and optionally all sub-datastructures) in the "Export" tab. -If you want to export all datastructures of a certain type (e.g. all categories in your database), you can select the "Export all" function in the "Import / Export" tab of the "new element" page. + +You can export data structures (like categories, manufacturers, etc.) in the respective edit page (e.g. Tools Panel -> +Edit -> Category). +If you select a certain data structure from your list, you can export it (and optionally all sub data structures) in the " +Export" tab. +If you want to export all data structures of a certain type (e.g. all categories in your database), you can select the " +Export all" function in the "Import / Export" tab of the "new element" page. You can select between the following export formats: -* **CSV** (Comma Separated Values): A semicolon separated list of values, where every line represents an element. This format can be imported into Excel or LibreOffice Calc and is easy to work with. However it does not support nested datastructures or sub data (like parameters, attachments, etc.), very well (many columns are generated, as every possible sub data is exported as a separate column). -* **JSON** (JavaScript Object Notation): A text-based format, which is easy to work with programming laguages. It supports nested datastructures and sub data (like parameters, attachments, etc.) very well. However it is not easy to work with in Excel or LibreOffice Calc and you maybe need to write some code to work with the exported data efficiently. -* **YAML** (Yet another Markup Language): Very similar to JSON -* **XML** (Extensible Markup Language): Good support with nested datastructures. Similar usecase as JSON and YAML. -Also you can select between the following export levels: +* **CSV** (Comma Separated Values): A semicolon-separated list of values, where every line represents an element. This + format can be imported into Excel or LibreOffice Calc and is easy to work with. However, it does not support nested + data structures or sub data (like parameters, attachments, etc.), very well (many columns are generated, as every + possible sub-data is exported as a separate column). +* **JSON** (JavaScript Object Notation): A text-based format, which is easy to work with programming languages. It + supports nested data structures and sub-data (like parameters, attachments, etc.) very well. However, it is not easy to + work with in Excel or LibreOffice Calc and you may need to write some code to work with the exported data + efficiently. +* **YAML** (Yet Another Markup Language): Very similar to JSON +* **XML** (Extensible Markup Language): Good support with nested data structures. Similar use cases as JSON and YAML. + +Also, you can select between the following export levels: + * **Simple**: This will only export very basic information about the name (like the name, or description for parts) -* **Extended**: This will export all commonly used information about this datastructure (like notes, options, etc) -* **Full**: This will export all available information about this datastructure (like all parameters, attachments) +* **Extended**: This will export all commonly used information about this data structure (like notes, options, etc.) +* **Full**: This will export all available information about this data structure (like all parameters, attachments) -Please note that the level will also be applied to all sub data or children elements. So if you select "Full" for a part, all the associated categories, manufacturers, footprints, etc. will also be exported with all available information, this can lead to very large export files. +Please note that the level will also be applied to all sub-data or children elements. So if you select "Full" for a +part, all the associated categories, manufacturers, footprints, etc. will also be exported with all available +information, this can lead to very large export files. ### Exporting parts -You can export parts in all part tables. Select the parts you want via the checkbox in the table line and select the export format and level in the appearing menu. -See the section about exporting datastructures for more information about the export formats and levels. \ No newline at end of file +You can export parts in all part tables. Select the parts you want via the checkbox in the table line and select the +export format and level in the appearing menu. + +See the section about exporting data structures for more information about the export formats and levels. \ No newline at end of file diff --git a/docs/usage/information_provider_system.md b/docs/usage/information_provider_system.md new file mode 100644 index 00000000..8de83a8e --- /dev/null +++ b/docs/usage/information_provider_system.md @@ -0,0 +1,273 @@ +--- +title: Information provider system +layout: default +parent: Usage +--- + +# Information provider system + +Part-DB can create parts based on information from external sources: For example, with the right setup you can just +search for a part number +and Part-DB will query selected distributors and manufacturers for the part and create a part with the information it +found. +This way your Part-DB parts automatically get datasheet links, prices, parameters, and more, with just a few clicks. + +## Usage + +Before you can use the information provider system, you have to configure at least one information provider, which act +as data source. +See below for a list of available information providers and available configuration options. +For many providers it is enough, to set up the API keys in the env configuration, some require an additional OAuth +connection. +You can list all enabled information providers in the browser +at `https://your-partdb-instance.tld/tools/info_providers/providers` (you need the right permission for it, see below). + +To use the information provider system, your user need to have the right permissions. Go to the permission management +page of +a user or a group and assign the permissions of the "Info providers" group in the "Miscellaneous" tab. + +If you have the required permission you will find in the sidebar in the "Tools" section the entry "Create part from info +provider". +Click this and you will land on a search page. Enter the part number you want to search for and select the information +providers you want to use. + +After you click Search, you will be presented with the results and can select the result that fits best. +With a click on the blue plus button, you will be redirected to the part creation page with the information already +filled in. + +![image]({% link assets/usage/information_provider_system/animation.gif %}) + +If you want to update an existing part, go to the parts info page and click on the "Update from info provider" button in +the tools tab. You will be redirected to a search page, where you can search the info providers to automatically update this +part. + +## Alternative names + +Part-DB tries to automatically find existing elements from your database for the information it got from the providers +for fields like manufacturer, footprint, etc. +For this, it searches for an element with the same name (case-insensitive) as the information it got from the provider. So +e.g. if the provider returns "EXAMPLE CORP" as the manufacturer, +Part-DB will automatically select the element with the name "Example Corp" from your database. + +As the names of these fields differ from provider to provider (and maybe not even normalized for the same provider), you +can define multiple alternative names for an element (on their editing page). +For example, if you define a manufacturer "Example Corp" with the alternative names "Example Corp.", "Example Corp", "Example +Corp. Inc." and "Example Corporation", +then the provider can return any of these names and Part-DB will still automatically select the right element. + +If Part-DB finds no matching element, it will automatically create a new one, when you do not change the value before +saving. + +## Attachment types + +The information provider system uses attachment types to differentiate between datasheets and image attachments. +For this it will create a "Datasheet" and "Image" attachment type on the first run. You can change the names of these +types in the attachment type settings (as long as you keep the "Datasheet"/"Image" in the alternative names field). + +If you already have attachment types for images and datasheets and want the information provider system to use them, you +can +add the alternative names "Datasheet" and "Image" to the alternative names field of the attachment types. + +## Data providers + +The system tries to be as flexible as possible, so many different information sources can be used. +Each information source is called am "info provider" and handles the communication with the external source. +The providers are just a driver that handles the communication with the different external sources and converts them +into a common format Part-DB understands. +That way it is pretty easy to create new providers as they just need to do very little work. + +Normally the providers utilize an API of a service, and you need to create an account at the provider and get an API key. +Also, there are limits on how many requests you can do per day or month, depending on the provider and your contract +with them. + +The following providers are currently available and shipped with Part-DB: + +(All trademarks are property of their respective owners. Part-DB is not affiliated with any of the companies.) + +### Octopart + +The Octopart provider uses the [Octopart / Nexar API](https://nexar.com/api) to search for parts and get information. +To use it you have to create an account at Nexar and create a new application on +the [Nexar Portal](https://portal.nexar.com/). +The name does not matter, but it is important that the application has access to the "Supply" scope. +In the Authorization tab, you will find the client ID and client secret, which you have to put in the Part-DB env +configuration (see below). + +Please note that the Nexar API in the free plan is limited to 1000 results per month. +That means if you search for a keyword and results in 10 parts, then 10 will be subtracted from your monthly limit. You +can see your current usage on the Nexar portal. +Part-DB caches the search results internally, so if you have searched for a part before, it will not count against your +monthly limit again, when you create it from the search results. + +The following env configuration options are available: + +* `PROVIDER_OCTOPART_CLIENT_ID`: The client ID you got from Nexar (mandatory) +* `PROVIDER_OCTOPART_SECRET`: The client secret you got from Nexar (mandatory) +* `PROVIDER_OCTOPART_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, + default: `EUR`). If an offer is only available in a certain currency, + Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert + it to your preferred currency. +* `PROVIDER_OCTOPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code, + default: `DE`). To get the correct prices, you have to set this and the currency setting to the correct value. +* `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This + affects how quickly your monthly limit is used up. +* `PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS`: If set to `true`, only offers + from [authorized sellers](https://octopart.com/authorized) will be returned (optional, default: `false`). + +**Attention**: If you change the Octopart clientID after you have already used the provider, you have to remove the +OAuth token in the Part-DB database. Remove the entry in the table `oauth_tokens` with the name `ip_octopart_oauth`. + +### Digi-Key + +The Digi-Key provider uses the [Digi-Key API](https://developer.digikey.com/) to search for parts and get shopping +information from [Digi-Key](https://www.digikey.com/). +To use it you have to create an account at Digi-Key and get an API key on +the [Digi-Key API page](https://developer.digikey.com/). +You must create an organization there and create a "Production app". Most settings are not important, you just have to +grant access to the "Product Information" API. +You will get a Client ID and a Client Secret, which you have to put in the Part-DB env configuration (see below). + +The following env configuration options are available: + +* `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory) +* `PROVIDER_DIGIKEY_SECRET`: The client secret you got from Digi-Key (mandatory) +* `PROVIDER_DIGIKEY_CURRENCY`: The currency you want to get prices in (optional, default: `EUR`) +* `PROVIDER_DIGIKEY_LANGUAGE`: The language you want to get the descriptions in (optional, default: `en`) +* `PROVIDER_DIGIKEY_COUNTRY`: The country you want to get the prices for (optional, default: `DE`) + +The Digi-Key provider needs an additional OAuth connection. To do this, go to the information provider +list (`https://your-partdb-instance.tld/tools/info_providers/providers`), +go to Digi-Key provider (in the disabled page), and click on the "Connect OAuth" button. You will be redirected to +Digi-Key, where you have to log in and grant access to the app. +To do this your user needs the "Manage OAuth tokens" permission from the "System" section in the "System" tab. +The OAuth connection should only be needed once, but if you have any problems with the provider, just click the button +again, to establish a new connection. + +### TME + +The TME provider uses the API of [TME](https://www.tme.eu/) to search for parts and getting shopping information from +them. +To use it you have to create an account at TME and get an API key on the [TME API page](https://developers.tme.eu/en/). +You have to generate a new anonymous key there and enter the key and secret in the Part-DB env configuration (see +below). + +The following env configuration options are available: + +* `PROVIDER_TME_KEY`: The API key you got from TME (mandatory) +* `PROVIDER_TME_SECRET`: The API secret you got from TME (mandatory) +* `PROVIDER_TME_CURRENCY`: The currency you want to get prices in (optional, default: `EUR`) +* `PROVIDER_TME_LANGUAGE`: The language you want to get the descriptions in (`en`, `de` and `pl`) (optional, + default: `en`) +* `PROVIDER_TME_COUNTRY`: The country you want to get the prices for (optional, default: `DE`) +* `PROVIDER_TME_GET_GROSS_PRICES`: If this is set to `1` the prices will be gross prices (including tax), otherwise net + prices (optional, default: `0`) + +### Farnell / Element14 / Newark + +The Farnell provider uses the [Farnell API](https://partner.element14.com/) to search for parts and getting shopping +information from [Farnell](https://www.farnell.com/). +You have to create an account at Farnell and get an API key on the [Farnell API page](https://partner.element14.com/). +Register a new application there (settings does not matter, as long as you select the "Product Search API") and you will +get an API key. + +The following env configuration options are available: + +* `PROVIDER_ELEMENT14_KEY`: The API key you got from Farnell (mandatory) +* `PROVIDER_ELEMENT14_STORE_ID`: The store ID you want to use. This decides the language of results, currency and + country of prices (optional, default: `de.farnell.com`, + see [here](https://partner.element14.com/docs/Product_Search_API_REST__Description) for available values) + +### Mouser + +The Mouser provider uses the [Mouser API](https://www.mouser.de/api-home/) to search for parts and getting shopping +information from [Mouser](https://www.mouser.com/). +You have to create an account at Mouser and register for an API key for the Search API on +the [Mouser API page](https://www.mouser.de/api-home/). +You will receive an API token, which you have to put in the Part-DB env configuration (see below): +At the registration you choose a country, language, and currency in which you want to get the results. + +*Attention*: Currently (January 2024) the mouser API seems to be somewhat broken, in the way that it does not return any +information about datasheets and part specifications. Therefore Part-DB can not retrieve them, even if they are shown +at the mouser page. See [issue #503](https://github.com/Part-DB/Part-DB-server/issues/503) for more info. + +Following env configuration options are available: + +* `PROVIDER_MOUSER_KEY`: The API key you got from Mouser (mandatory) +* `PROVIDER_MOUSER_SEARCH_LIMIT`: The maximum number of results to return per search (maximum 50) +* `PROVIDER_MOUSER_SEARCH_OPTION`: You can choose an option here to restrict the search results to RoHs compliant and + available parts. Possible values are `None`, `Rohs`, `InStock`, `RohsAndInStock`. +* `PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE`: A bit of an obscure option. The original description of Mouser is: Used + when searching for keywords in the language specified when you signed up for Search API. + +### LCSC + +[LCSC](https://www.lcsc.com/) is a Chinese distributor of electronic parts. It does not offer a public API, but the LCSC +webshop uses an internal JSON based API to render the page. Part-DB can use this inofficial API to get part information +from LCSC. + +**Please note, that the use of this internal API is not intended or endorsed by LCS and it could break at any time. So use it at your own risk.** + +An API key is not required, it is enough to enable the provider using the following env configuration options: + +* `PROVIDER_LCSC_ENABLED`: Set this to `1` to enable the LCSC provider +* `PROVIDER_LCSC_CURRENCY`: The currency you want to get prices in (see LCSC webshop for available currencies, default: `EUR`) + +### OEMsecrets + +The oemsecrets provider uses the [oemsecrets API](https://www.oemsecrets.com/) to search for parts and getting shopping +information from them. Similar to octopart it aggregates offers from different distributors. + +You can apply for a free API key on the [oemsecrets API page](https://www.oemsecrets.com/api/) and put the key you get +in the Part-DB env configuration (see below). + +The following env configuration options are available: + +* `PROVIDER_OEMSECRETS_KEY`: The API key you got from oemsecrets (mandatory) +* `PROVIDER_OEMSECRETS_COUNTRY_CODE`: The two-letter code of the country you want to get the prices for +* `PROVIDER_OEMSECRETS_CURRENCY`: The currency you want to get prices in (optional, default: `EUR`) +* `PROVIDER_OEMSECRETS_ZERO_PRICE`: If set to `1`, parts with a price of 0 will be included in the search results, otherwise + they will be excluded (optional, default: `0`) +* `PROVIDER_OEMSECRETS_SET_PARAM`: If set to `1`, the provider will try to extract parameters from the part description +* `PROVIDER_OEMSECRETS_SORT_CRITERIA`: The criteria to sort the search results by. If set to 'C', it further sorts by +completeness (prioritizing items with the most detailed information). If set to 'M', it further sorts by manufacturer name. +If set to any other value, no sorting is performed. + +### Reichelt + +The reichelt provider uses webscraping from [reichelt.com](https://reichelt.com/) to get part information. +This is not an official API and could break at any time. So use it at your own risk. + +The following env configuration options are available: +* `PROVIDER_REICHELT_ENABLED`: Set this to `1` to enable the Reichelt provider +* `PROVIDER_REICHELT_CURRENCY`: The currency you want to get prices in. Only possible for countries which use Non-EUR (optional, default: `EUR`) +* `PROVIDER_REICHELT_COUNTRY`: The country you want to get the prices for (optional, default: `DE`) +* `PROVIDER_REICHELT_LANGUAGE`: The language you want to get the descriptions in (optional, default: `en`) +* `PROVIDER_REICHELT_INCLUDE_VAT`: If set to `1`, the prices will be gross prices (including tax), otherwise net prices (optional, default: `1`) + +### Pollin + +The pollin provider uses webscraping from [pollin.de](https://www.pollin.de/) to get part information. +This is not an official API and could break at any time. So use it at your own risk. + +The following env configuration options are available: +* `PROVIDER_POLLIN_ENABLED`: Set this to `1` to enable the Pollin provider + +### Custom provider + +To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long +as it is a valid Symfony service, it will be automatically loaded and can be used. +Besides some metadata functions, you have to implement the `searchByKeyword()` and `getDetails()` functions, which do +the actual API requests and return the information to Part-DB. +See the existing providers for examples. +If you created a new provider, feel free to create a pull request to add it to the Part-DB core. + +## Result caching + +To reduce the number of API calls against the providers, the results are cached: + +* The search results (exact search term) are cached for 7 days +* The product details are cached for 4 days + +If you need a fresh result, you can clear the cache by running `php .\bin\console cache:pool:clear info_provider.cache` +on the command line. +The default `php bin/console cache:clear` also clears the result cache, as it clears all caches. diff --git a/docs/usage/keybindings.md b/docs/usage/keybindings.md new file mode 100644 index 00000000..771d7684 --- /dev/null +++ b/docs/usage/keybindings.md @@ -0,0 +1,122 @@ +--- +title: Keybindings +layout: default +parent: Usage +--- + +# Keybindings + +This page lists all the keybindings of Part-DB. Currently, there are only the special character keybindings. + +## Special characters + +Using the keybindings below (Alt + key) you can insert special characters into the text fields of Part-DB. This works on +all text and search fields in Part-DB. + +### Greek letters + +| Key | Character | +|---------------------|-----------------------| +| **Alt + a** | α (Alpha) | +| **Alt + Shift + A** | Α (Alpha uppercase) | +| **Alt + b** | β (Beta) | +| **Alt + Shift + B** | Β (Beta uppercase) | +| **Alt + g** | γ (Gamma) | +| **Alt + Shift + G** | Γ (Gamma uppercase) | +| **Alt + d** | δ (Delta) | +| **Alt + Shift + D** | Δ (Delta uppercase) | +| **Alt + e** | ε (Epsilon) | +| **Alt + Shift + E** | Ε (Epsilon uppercase) | +| **Alt + z** | ζ (Zeta) | +| **Alt + Shift + Z** | Ζ (Zeta uppercase) | +| **Alt + h** | η (Eta) | +| **Alt + Shift + H** | Η (Eta uppercase) | +| **Alt + q** | θ (Theta) | +| **Alt + Shift + Q** | Θ (Theta uppercase) | +| **Alt + i** | ι (Iota) | +| **Alt + Shift + I** | Ι (Iota uppercase) | +| **Alt + k** | κ (Kappa) | +| **Alt + Shift + K** | Κ (Kappa uppercase) | +| **Alt + l** | λ (Lambda) | +| **Alt + Shift + L** | Λ (Lambda uppercase) | +| **Alt + m** | μ (Mu) | +| **Alt + Shift + M** | Μ (Mu uppercase) | +| **Alt + n** | ν (Nu) | +| **Alt + Shift + N** | Ν (Nu uppercase) | +| **Alt + x** | ξ (Xi) | +| **Alt + Shift + x** | Ξ (Xi uppercase) | +| **Alt + o** | ο (Omicron) | +| **Alt + Shift + O** | Ο (Omicron uppercase) | +| **Alt + p** | π (Pi) | +| **Alt + Shift + P** | Π (Pi uppercase) | +| **Alt + r** | ρ (Rho) | +| **Alt + Shift + R** | Ρ (Rho uppercase) | +| **Alt + s** | σ (Sigma) | +| **Alt + Shift + S** | Σ (Sigma uppercase) | +| **Alt + t** | τ (Tau) | +| **Alt + Shift + T** | Τ (Tau uppercase) | +| **Alt + u** | υ (Upsilon) | +| **Alt + Shift + U** | Υ (Upsilon uppercase) | +| **Alt + f** | φ (Phi) | +| **Alt + Shift + F** | Φ (Phi uppercase) | +| **Alt + y** | ψ (Psi) | +| **Alt + Shift + Y** | Ψ (Psi uppercase) | +| **Alt + c** | χ (Chi) | +| **Alt + Shift + C** | Χ (Chi uppercase) | +| **Alt + w** | ω (Omega) | +| **Alt + Shift + W** | Ω (Omega uppercase) | + +### Mathematical symbols + +| Key | Character | +|---------------------|-----------------------------| +| **Alt + 1** | ∑ (Sum symbol) | +| **Alt + Shift + 1** | ∏ (Product symbol) | +| **Alt + 2** | ∫ (Integral symbol) | +| **Alt + Shift + 2** | ∂ (Partial derivation) | +| **Alt + 3** | ≤ (Less or equal symbol) | +| **Alt + Shift + 3** | ≥ (Greater or equal symbol) | +| **Alt + 4** | ∞ (Infinity symbol) | +| **Alt + Shift + 4** | ∅ (Empty set symbol) | +| **Alt + 5** | ≈ (Approximately) | +| **Alt + Shift + 5** | ≠ (Not equal symbol) | +| **Alt + 6** | ∈ (Element of) | +| **Alt + Shift + 6** | ∉ (Not element of) | +| **Alt + 7** | ∨ (Logical or) | +| **Alt + Shift + 7** | ∧ (Logical and) | +| **Alt + 8** | ∠ (Angle symbol) | +| **Alt + Shift + 8** | ∝ (Proportional to) | +| **Alt + 9** | √ (Square root) | +| **Alt + Shift + 9** | ∛ (Cube root) | +| **Alt + 0** | ± (Plus minus) | +| **Alt + Shift + 0** | ∓ (Minus plus) | + +### Currency symbols + +Please note, the following keybindings are bound to a specific keycode. The key character is not the same on all +keyboards. +It is given here for a US keyboard layout. + +For a German keyboard layout, replace ; with ö, and ' with ä. + +| Key | Character | +|---------------------------------|----------------------------| +| **Alt + ;** (code 192) | € (Euro currency symbol) | +| **Alt + Shift + ;** (code 192) | £ (Pound currency symbol) | +| **Alt + '** (code 222) | ¥ (Yen currency symbol) | +| **Alt + Shift + '** (code 222) | $ (Dollar currency symbol) | + +### Others + +Please note the following keybindings are bound to a specific keycode. The key character is not the same on all +keyboards. +It is given here for a US keyboard layout. + +For a German keyboard layout, replace `[` with `0`, and `]` with `´`. + +| Key | Character | +|--------------------------------|--------------------| +| **Alt + [** (code 219) | © (Copyright char) | +| **Alt + Shift + [** (code 219) | ® (Registered char) | +| **Alt + ]** (code 221) | ™ (Trademark char) | +| **Alt + Shift + ]** (code 221) | ° (Degree char) | diff --git a/docs/usage/labels.md b/docs/usage/labels.md index 0d885f12..e84f4d7f 100644 --- a/docs/usage/labels.md +++ b/docs/usage/labels.md @@ -6,90 +6,284 @@ parent: Usage # Labels -Part-DB support the generation and printing of labels for parts, part lots and storelocation. -You can use the "Tools -> Labelgenerator" menu entry to create labels, or click the label generation link on the part. +Part-DB support the generation and printing of labels for parts, part lots and storage locations. +You can use the "Tools -> Label generator" menu entry to create labels or click the label generation link on the part. -You can define label templates by creating Label profiles. This way you can create many similar looking labels with for +You can define label templates by creating Label profiles. This way you can create many similar-looking labels with for many parts. -The content of the labels is defined by the templates content field. You can use the WYSIWYG editor to create and style the content (or write HTML code). +The content of the labels is defined by the template's content field. You can use the WYSIWYG editor to create and style +the content (or write HTML code). Using the "Label placeholder" menu in the editor, you can insert placeholders for the data of the parts. It will be replaced by the concrete data when the label is generated. - + ## Label placeholders + A placeholder has the format `[[PLACEHOLDER]]` and will be filled with the concrete data by Part-DB. -You can use the "Placeholders" dropdown in content editor, to automatically insert the placeholders. +You can use the "Placeholders" dropdown in the content editor, to automatically insert the placeholders. ### Common -| Placeholder | Description | Example | -|---|---|---| -| `[[USERNAME]]` | The user name of the currently logged in user | admin | -| `[[USERNAME_FULL]]` | The full name of the current user | John Doe (@admin) | -| `[[DATETIME]]` | The current date and time in the selected locale | 31.12.2017, 18:34:11 | -| `[[DATE]]` | The current date in the selected locale | 31.12.2017 | -| `[[TIME]]` | The current time in the selected locale | 18:34:11 | -| `[[INSTALL_NAME]]` | The name of the current installation (see $config['partdb_title']) | Part-DB | -| `[[INSTANCE_URL]]` | The URL of the current installation | https://demo.part-db.de | +| Placeholder | Description | Example | +|---------------------|--------------------------------------------------------------------|-------------------------| +| `[[USERNAME]]` | The user name of the currently logged in user | admin | +| `[[USERNAME_FULL]]` | The full name of the current user | John Doe (@admin) | +| `[[DATETIME]]` | The current date and time in the selected locale | 31.12.2017, 18:34:11 | +| `[[DATE]]` | The current date in the selected locale | 31.12.2017 | +| `[[TIME]]` | The current time in the selected locale | 18:34:11 | +| `[[INSTALL_NAME]]` | The name of the current installation (see $config['partdb_title']) | Part-DB | +| `[[INSTANCE_URL]]` | The URL of the current installation | https://demo.part-db.de | ### Parts -| Placeholder | Description | Example | -|---|---|---| -| `[[ID]]` | The internal ID of the part | 24 | -| `[[NAME]]` | The name of the part | ATMega328 | -| `[[CATEGORY]]` | The name of the category (without path) | AVRs | -| `[[CATEGORY_FULL]]` | The full path of the category | Aktiv->MCUs->AVRs | -| `[[MANUFACTURER]]` | The name of the manufacturer | Atmel | -| `[[MANUFACTURER_FULL]]` | The full path of the manufacturer | Halbleiterhersteller->Atmel | -| `[[FOOTPRINT]]` | The name of the footprint (without path) | DIP-32 | -| `[[FOOTPRINT_FULL]]` | The full path of the footprint | Bedrahtet->DIP->DIP-32 | -| `[[MASS]]` | The mass of the part | 123.4 g | -| `[[MPN]]` | The manufacturer product number | BC547ACT | -| `[[TAGS]]` | The tags of the part | SMD, Tag1 | -| `[[M_STATUS]]` | The manufacturing status of the part | Active | -| `[[DESCRIPTION]]` | The rich text description of the part | *NPN* | -| `[[DESCRIPTION_T]]` | The description as plain text | NPN | -| `[[COMMENT]]` | The rich text comment of the part | | -| `[[COMMENT_T]]` | The comment as plain text | | -| `[[LAST_MODIFIED]]` | The datetime when the element was last modified | 2/26/16, 5:38 PM | -| `[[CREATION_DATE]]` | The datetime when the element was created | 2/26/16, 5:38 PM | +| Placeholder | Description | Example | +|-------------------------|-------------------------------------------------|-----------------------------| +| `[[ID]]` | The internal ID of the part | 24 | +| `[[NAME]]` | The name of the part | ATMega328 | +| `[[CATEGORY]]` | The name of the category (without path) | AVRs | +| `[[CATEGORY_FULL]]` | The full path of the category | Aktiv->MCUs->AVRs | +| `[[MANUFACTURER]]` | The name of the manufacturer | Atmel | +| `[[MANUFACTURER_FULL]]` | The full path of the manufacturer | Halbleiterhersteller->Atmel | +| `[[FOOTPRINT]]` | The name of the footprint (without path) | DIP-32 | +| `[[FOOTPRINT_FULL]]` | The full path of the footprint | Bedrahtet->DIP->DIP-32 | +| `[[MASS]]` | The mass of the part | 123.4 g | +| `[[MPN]]` | The manufacturer product number | BC547ACT | +| `[[TAGS]]` | The tags of the part | SMD, Tag1 | +| `[[M_STATUS]]` | The manufacturing status of the part | Active | +| `[[DESCRIPTION]]` | The rich text description of the part | *NPN* | +| `[[DESCRIPTION_T]]` | The description as plain text | NPN | +| `[[COMMENT]]` | The rich text comment of the part | | +| `[[COMMENT_T]]` | The comment as plain text | | +| `[[LAST_MODIFIED]]` | The datetime when the element was last modified | 2/26/16, 5:38 PM | +| `[[CREATION_DATE]]` | The datetime when the element was created | 2/26/16, 5:38 PM | ### Part lot -| Placeholder | Description | Example | -|---|---|---| -| `[[LOT_ID]]` | Part lot ID | 123 | -| `[[LOT_NAME]]` | Part lot name | | -| `[[LOT_COMMENT]]` | Part lot comment | | -| `[[EXPIRATION_DATE]]` | Expiration date of the part lot | | -| `[[AMOUNT]]` | The amount of parts in this lot | 12 | -| `[[LOCATION]]` | The storage location of this part lot | Location A | -| `[[LOCATION_FULL]]` | The full path of the storage location | Location -> Location A | +| Placeholder | Description | Example | +|-----------------------|---------------------------------------|------------------------| +| `[[LOT_ID]]` | Part lot ID | 123 | +| `[[LOT_NAME]]` | Part lot name | | +| `[[LOT_COMMENT]]` | Part lot comment | | +| `[[EXPIRATION_DATE]]` | Expiration date of the part lot | | +| `[[AMOUNT]]` | The amount of parts in this lot | 12 | +| `[[LOCATION]]` | The storage location of this part lot | Location A | +| `[[LOCATION_FULL]]` | The full path of the storage location | Location -> Location A | ### Storelocation -| Placeholder | Description | Example | -|---|---|---| -| `[[ID]]` | ID of the storage location | | -| `[[NAME]]` | Name of the storage location | Location A | -| `[[FULL_PATH]]` | The full path of the storage location | Location -> Location A | -| `[[PARENT]]` | The name of the parent location | Location | -| `[[PARENT_FULL_PATH]]` | The full path of the storage location | | -| `[[COMMENT]]` | The comment of the storage location | | -| `[[COMMENT_T]]` | The plain text version of the comment | -| `[[LAST_MODIFIED]]` | The datetime when the element was last modified | 2/26/16, 5:38 PM | -| `[[CREATION_DATE]]` | The datetime when the element was created | 2/26/16, 5:38 PM | +| Placeholder | Description | Example | +|------------------------|-------------------------------------------------|------------------------| +| `[[ID]]` | ID of the storage location | | +| `[[NAME]]` | Name of the storage location | Location A | +| `[[FULL_PATH]]` | The full path of the storage location | Location -> Location A | +| `[[PARENT]]` | The name of the parent location | Location | +| `[[PARENT_FULL_PATH]]` | The full path of the storage location | | +| `[[COMMENT]]` | The comment of the storage location | | +| `[[COMMENT_T]]` | The plain text version of the comment | +| `[[LAST_MODIFIED]]` | The datetime when the element was last modified | 2/26/16, 5:38 PM | +| `[[CREATION_DATE]]` | The datetime when the element was created | 2/26/16, 5:38 PM | ## Twig mode -If you select "Twig" in parser mode under advanced settings, you can input a twig template in the lines field (activate source mode). You can use most of the twig tags and filters listed in [offical documentation](https://twig.symfony.com/doc/3.x/). +If you select "Twig" in parser mode under advanced settings, you can input a twig template in the lines field (activate +source mode). You can use most of the twig tags and filters listed +in [official documentation](https://twig.symfony.com/doc/3.x/). -The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}` (or `{% raw %}{{ variable.property }}{% endraw %}`): +Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create +the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment, +where only certain operations are allowed. Only read access to entities is allowed. However as it circumvents Part-DB normal permission system, +the twig mode is only available to users with the "Twig mode" permission. -| Variable name | Description | -|--------------------------------------| ----------- | -| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog | -| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in | -| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). | -| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated | \ No newline at end of file +The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` ( +or `{% raw %}{{ variable.property }}{% endraw %}`): + +| Variable name | Description | +|--------------------------------------------|--------------------------------------------------------------------------------------| +| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. | +| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in | +| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). | +| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated | +| `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels | +| `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm | +| `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm | + +### Use the placeholders in twig mode + +You can use the placeholders described above in the twig mode on `element` using the `{% raw %}{{ placeholder('PLACEHOLDER', element) }}{% endraw %}` +function or the ``{{ "[[PLACEHOLDER]]"|placeholders(element) }}`` filter: + +```twig +{% raw %} +{# The function can be used to get the a single placeholder value of an element, if the placeholder does not exist, null is returned #} +{{ placeholder('[[NAME]]', element) }} + +{# The filter can be used to replace all placeholders in a string with the values of the element #} +{{ "[[NAME]]: [[DESCRIPTION]]"|placeholders(element) }} + +{# Using the apply environment every placeholder in the apply block will be replaced automatically #} +{% apply placeholders(element) %} + [[NAME]]: [[DESCRIPTION]] +{% endapply %} + +{# If the block contains HTML use placeholders(element)|raw to prevent escaping of the HTML #} +{% apply placeholders(element)|raw %} + [[NAME]]: [[DESCRIPTION]] +{% endapply %} + +{% endraw %} +``` + +### Important entity fields in twig mode + +In twig mode you have access to many fields of the entity you are generating the label for and their associated entities. +Following are some important fields of the entities listed. See the [SandboxedTwigFactory service](https://github.com/Part-DB/Part-DB-server/blob/master/src/Services/LabelSystem/SandboxedTwigFactory.php) for the full list of allowed class methods. + +Please not that the field names might change in the future. + +#### Part + +| Field name | Description | +|---------------------|-----------------------------------------------------------------------------------------------| +| `id` | The internal ID of the part | +| `name` | The name of the part | +| `category` | The category of the part | +| `manufacturer` | The manufacturer of the part | +| `footprint` | The footprint of the part | +| `mass` | The mass of the part | +| `ManufacturerProductNumber` | The manufacturer product number of the part | +| `tags` | The tags of the part | +| `description` | The rich text (markdown) description of the part | +| `comment` | The rich text (markdown) comment of the part | +| `lastModified` | The datetime object when the part was last modified | +| `creationDate` | The datetime object when the part was created | +| `ipn` | The internal part number of the part | +| `partUnit` | The unit of the part | +| `amountSum` | The sum of the amount of all part lots of this part | +| `amountUnknwon` | Bool: True if there is at least one part lot with unknown amount | +| `partLots` | The part lots of the part | +| `parameters` | The parameters of the part | +| `orderdetails` | The order details of the part as array of Orderdetails | + +#### Part lot + +| Field name | Description | +|---------------------|-----------------------------------------------------------------------------------------------| +| `id` | The internal ID of the part lot | +| `name` | The name of the part lot | +| `comment` | The rich text (markdown) comment of the part lot | +| `expirationDate` | The expiration date of the part lot (as Datetime object) | +| `amount` | The amount of parts in this lot | +| `storageLocation` | The storage location of this part lot | +| `part` | The part of this part lot | +| `needsRefill` | Bool: True if the part lot needs a refill | +| `expired` | Bool: True if the part lot is expired | +| `vendorBarcode` | The vendor barcode field of the lot | + +#### Structural entities like categories, manufacturers, footprints, and storage locations + +| Field name | Description | +|---------------------|-----------------------------------------------------------------------------------------------| +| `id` | The internal ID of the entity | +| `name` | The name of the entity | +| `comment` | The rich text (markdown) comment of the entity | +| `parent` | The parent entity of the entity | +| `children` | The children entities of the entity | +| `lastModified` | The datetime object when the entity was last modified | +| `creationDate` | The datetime object when the entity was created | +| `level` | The level of the entity in the hierarchy | +| `fullPath` | The full path of the entity (you can pass the delimiter as parameter) | +| `pathArray` | The path of the entity as array of strings | + +#### Orderdetails + +| Field name | Description | +|---------------------|-----------------------------------------------------------------------------------------------| +| `id` | The internal ID of the order detail | +| `part` | The part of the order detail | +| `supplier` | The supplier/distributor of the order detail | +| `obsolete` | Bool: True if the order detail is obsolete | +| `pricedetails` | The price details of the order detail as array of Pricedetails | + +#### Pricedetails + +| Field name | Description | +|---------------------|-----------------------------------------------------------------------------------------------| +| `id` | The internal ID of the price detail | +| `price` | The price of the price detail | +| `currency` | The currency of the price detail | +| `currencyIsoCode` | The ISO code of the used currency | +| `pricePerUnit` | The price per unit of the price detail | +| `priceRelatedQuantity` | The related quantity of the price detail | +| `minDiscountQuantity` | The minimum discount quantity of the price detail | + +#### User + +| Field name | Description | +|---------------------|-----------------------------------------------------------------------------------------------| +| `id` | The internal ID of the user | +| `username` | The username of the user | +| `email` | The email of the user | +| `fullName` | The full name of the user | +| `lastName` | The last name of the user | +| `firstName` | The first name of the user | +| `department` | The department of the user | + + +### Part-DB specific twig functions and filters + +Part-DB offers some custom twig functions and filters, which can be used in the twig mode and ease the rendering of +certain data: + +#### Functions + +| Function name | Description | +|----------------------------------------------|-----------------------------------------------------------------------------------------------| +| `placeholder(placeholder, element)` | Get the value of a placeholder of an element | +| `entity_type(element)` | Get the type of an entity as string | +| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) | +| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. | + +### Filters + +| Filter name | Description | +|----------------------------------------------|-----------------------------------------------------------------------------------------------| +| `format_bytes` | Format a byte count to a human readable string | +| `format_money(price, currency)` | Format a price to a human readable string with the currency | +| `format_amount(amount, unit)` | Format an amount to a human readable string with the unit object | +| `format_si(value, unit_str)` | Format a value using SI prefixes and the given unit string | +| `placeholders(element)` | Replace all placeholders in a string with the values of the element | + +## Use custom fonts for PDF labels + +You can use your own fonts for label generation. To do this, put the TTF files of the fonts you want to use into +the `assets/fonts/dompdf` folder. +The filename will be used as name for the font family, and you can use a `_bold` (or `_b`), `_italic` (or `_i`) +or `_bold_italic` (or `_bi`) suffix to define +different styles of the font. So for example, if you copy the file `myfont.ttf` and `myfont_bold.ttf` into +the `assets/fonts/dompdf` folder, you can use the font family `myfont` with regular and bold style. +Afterward regenerate cache with `php bin/console cache:clear`, so the new fonts will be available for label generation. + +The fonts will not be available from the UI directly, you have to use it in the HTML directly either by defining +a `style="font-family: 'myfont';"` attribute on the HTML element or by using a CSS class. +You can define the font globally for the label, by adding following statement to the "Additional styles (CSS)" option in +the label generator settings: + +```css +* { + font-family: 'myfont'; +} +``` + +## Non-latin characters in PDF labels + +The default used font (DejaVu) does not support all characters. Especially characters from non-latin languages like +Chinese, Japanese, Korean, Arabic, Hebrew, Cyrillic, etc. are not supported. +For this, we use [Unifont](http://unifoundry.com/unifont.html) as fallback font. This font supports all (or most) Unicode +characters but is not as beautiful as DejaVu. + +If you want to use a different (more beautiful) font, you can use the [custom fonts](#use-custom-fonts-for-pdf-labels) +feature. +There is the [Noto](https://www.google.com/get/noto/) font family from Google, which supports a lot of languages and is +available in different styles (regular, bold, italic, bold-italic). +For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese, +and Korean characters. \ No newline at end of file diff --git a/docs/usage/tips_tricks.md b/docs/usage/tips_tricks.md index 81a31ec1..d033cbe8 100644 --- a/docs/usage/tips_tricks.md +++ b/docs/usage/tips_tricks.md @@ -8,42 +8,49 @@ parent: Usage Following you can find miscellaneous tips and tricks for using Part-DB. -## Create datastructures directly from part edit page +## Create data structures directly from part edit page -Instead of first creating a category, manufacturer, footprint, etc. and then creating the part, you can create the -datastructures directly from the part edit page: Just type the name of the datastructure you want to create into the -select field on the part edit page and press "Create new ...". The new datastructure will be created, when you save +Instead of first creating a category, manufacturer, footprint, etc., and then creating the part, you can create the +data structures directly from the part edit page: Just type the name of the data structure you want to create into the +select field on the part edit page and press "Create new ...". The new data structure will be created when you save the part changes. -You can create also create nested datastructures this way. For example, if you want to create a new category "AVRs", +You can create also create nested data structures this way. For example, if you want to create a new category "AVRs", as a subcategory of "MCUs", you can just type "MCUs->AVRs" into the category select field and press "Create new". The new category "AVRs" will be created as a subcategory of "MCUs". If the category "MCUs" does not exist, it will be created too. -## Builtin footprint images -Part-DB includes several builtin images for common footprints. You can use these images in your footprint datastructures, -by creating an attachment on the datastructure and selecting it as preview image. +## Built-in footprint images + +Part-DB includes several built-in images for common footprints. You can use these images in your footprint +data structures, +by creating an attachment on the data structure and selecting it as the preview image. Type the name of the footprint image you want to use into the URL field of the attachment and select it from the -dropdown menu. You can find a gallery of all builtin footprint images and their names in the "Builtin footprint image gallery", -which you can find in the "Tools" menu (you maybe need to give your user the permission to access this tool). +dropdown menu. You can find a gallery of all builtin footprint images and their names in the "Builtin footprint image +gallery", +which you can find in the "Tools" menu (you may need to give your user the permission to access this tool). ## Parametric search -In the "parameters" tab of the filter panel on parts list page, you can define constraints, which parameter values -have to fullfill. This allows you to search for parts with specific parameters (or parameter ranges), for example you + +In the "parameters" tab of the filter panel on parts list page, you can define constraints, and which parameter values +have to fulfill. This allows you to search for parts with specific parameters (or parameter ranges), for example, you can search for all parts with a voltage rating of greater than 5 V. -## View own users permissions +## View own user's permissions + If you want to see which permissions your user has, you can find a list of the permissions in the "Permissions" panel on the user info page. ## Use LaTeX equations -You can use LaTeX equations everywhere where markdown is supported (for example in the description or notes field of a part). + +You can use LaTeX equations everywhere where markdown is supported (for example in the description or notes field of a +part). [KaTeX](https://katex.org/) is used to render the equations. You can find a list of supported features in the [KaTeX documentation](https://katex.org/docs/supported.html). -To input a LaTeX equation, you have to wrap it in a pair of dollar signs (`$`). Single dollar signs mark inline equations, -double dollar signs mark displayed equations (which will be its own line and centered). For example, the following equation -will be rendered as an inline equation: +To input a LaTeX equation, you have to wrap it in a pair of dollar signs (`$`). Single dollar signs mark inline +equations, double dollar signs mark displayed equations (which will be their own line and centered). +For example, the following equation will be rendered as an inline equation: ``` $E=mc^2$ @@ -56,7 +63,8 @@ $$E=mc^2$$ ``` ## Update currency exchange rates automatically -Part-DB can update the currency exchange rates of all defined currencies programatically + +Part-DB can update the currency exchange rates of all defined currencies programmatically by calling the `php bin/console partdb:currencies:update-exchange-rates`. If you call this command regularly (e.g. with a cronjob), you can keep the exchange rates up-to-date. @@ -65,11 +73,26 @@ Please note that if you use a base currency, which is not the Euro, you have to free API used by default only supports the Euro as base currency. ## Enforce log comments + On almost any editing operation it is possible to add a comment describing, what or why you changed something. -This comment will be written to change log and can be viewed later. -If you want to enforce your users to add comments to certain operations, you can do this by setting the `ENFORCE_CHANGE_COMMENTS_FOR` option. +This comment will be written to changelog and can be viewed later. +If you want to force your users to add comments to certain operations, you can do this by setting +the `ENFORCE_CHANGE_COMMENTS_FOR` option. See the configuration reference for more information. ## Personal stocks and stock locations -For makerspaces and universities with a lot of users, where each user can have his own stock, which only he should be able to access, you can assign -the user as "owner" of a part lot. This way, only him is allowed to add or remove parts from this lot. \ No newline at end of file + +For maker spaces and universities with a lot of users, where each user can have his own stock, which only he should be +able to access, you can assign +the user as "owner" of a part lot. This way, only he is allowed to add or remove parts from this lot. + +## Update notifications + +Part-DB can show you a notification that there is a newer version than currently installed available. The notification +will be shown on the homepage and the server info page. +It is only be shown to users which has the `Show available Part-DB updates` permission. + +For the notification to work, Part-DB queries the GitHub API every 2 days to check for new releases. No data is sent to +GitHub besides the metadata required for the connection (so the public IP address of your computer running Part-DB). +If you don't want Part-DB to query the GitHub API, or if your server can not reach the internet, you can disable the +update notifications by setting the `CHECK_FOR_UPDATES` option to `false`. \ No newline at end of file diff --git a/migrations/Version1.php b/migrations/Version1.php index f1389801..fc4c7b2e 100644 --- a/migrations/Version1.php +++ b/migrations/Version1.php @@ -160,7 +160,7 @@ EOD; 21840,21840,21840,21840,21840,21520,21520,21520,20480,21520,20480, 20480,20480,20480,20480,21504,20480), ( - 2,'admin', '${admin_pw}','','', + 2,'admin', '$admin_pw','','', '','',1,1,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); @@ -235,4 +235,14 @@ EOD; { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20190902140506.php b/migrations/Version20190902140506.php index 9a4697fc..8984cb06 100644 --- a/migrations/Version20190902140506.php +++ b/migrations/Version20190902140506.php @@ -69,6 +69,12 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration //For attachments $this->addSql('DELETE FROM `attachements` WHERE attachements.class_name = "Part" AND (SELECT COUNT(parts.id) FROM parts WHERE parts.id = attachements.element_id) = 0;'); + //Add perms_labels column to groups table if not existing (it was not created in certain legacy versions) + //This prevents the migration failing (see https://github.com/Part-DB/Part-DB-server/issues/366 and https://github.com/Part-DB/Part-DB-server/issues/67) + if (!$this->doesColumnExist('groups', 'perms_labels')) { + $this->addSql('ALTER TABLE `groups` ADD `perms_labels` SMALLINT NOT NULL AFTER `perms_tools`'); + } + /************************************************************************************************************** * Doctrine generated SQL **************************************************************************************************************/ @@ -125,7 +131,8 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)'); $this->addSql('ALTER TABLE parts ADD CONSTRAINT parts_id_footprint_fk FOREIGN KEY (id_footprint) REFERENCES footprints (id)'); $this->addSql('ALTER TABLE parts ADD CONSTRAINT parts_id_manufacturer_fk FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id)'); - if ($this->doesFKExists('attachment_types', 'attachement_types_parent_id_fk')) { + //We have to use the old table name here, because the if is executed before any sql command added by addSQL is executed (and thus the table name is not changed yet) + if ($this->doesFKExists('attachement_types', 'attachement_types_parent_id_fk')) { $this->addSql('ALTER TABLE attachment_types DROP FOREIGN KEY attachement_types_parent_id_fk'); } $this->addSql('ALTER TABLE attachment_types ADD filetype_filter LONGTEXT NOT NULL, ADD not_selectable TINYINT(1) NOT NULL, CHANGE name name VARCHAR(255) NOT NULL, CHANGE comment comment LONGTEXT NOT NULL, CHANGE last_modified last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL'); @@ -179,7 +186,8 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)'); $this->addSql('ALTER TABLE suppliers ADD CONSTRAINT suppliers_parent_id_fk FOREIGN KEY (parent_id) REFERENCES suppliers (id)'); $this->addSql('DROP INDEX attachements_class_name_k ON attachments'); - if ($this->doesFKExists('attachments', 'attachements_type_id_fk')) { + //We have to use the old table name here, because the if is executed before any sql command added by addSQL is executed (and thus the table name is not changed yet) + if ($this->doesFKExists('attachements', 'attachements_type_id_fk')) { $this->addSql('ALTER TABLE attachments DROP FOREIGN KEY attachements_type_id_fk'); } $this->addSql('ALTER TABLE attachments ADD datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE type_id type_id INT DEFAULT NULL, CHANGE name name VARCHAR(255) NOT NULL, CHANGE filename filename VARCHAR(255) NOT NULL, CHANGE show_in_table show_in_table TINYINT(1) NOT NULL, CHANGE last_modified last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL'); @@ -216,22 +224,26 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON pricedetails (id_currency)'); $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON pricedetails (orderdetails_id)'); $this->addSql('DROP INDEX pricedetails_orderdetails_id_k ON pricedetails'); - $this->addSql('DROP INDEX name ON groups'); - $this->addSql('ALTER TABLE groups ADD not_selectable TINYINT(1) NOT NULL, CHANGE name name VARCHAR(255) NOT NULL, CHANGE comment comment LONGTEXT NOT NULL, CHANGE perms_labels perms_labels INT NOT NULL, CHANGE last_modified last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL'); - $this->addSql('ALTER TABLE groups ADD CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES `groups` (id)'); - $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON groups (parent_id)'); + $this->addSql('DROP INDEX name ON `groups`'); + $this->addSql('ALTER TABLE `groups` ADD not_selectable TINYINT(1) NOT NULL, CHANGE name name VARCHAR(255) NOT NULL, CHANGE comment comment LONGTEXT NOT NULL, CHANGE perms_labels perms_labels INT NOT NULL, CHANGE last_modified last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL'); + $this->addSql('ALTER TABLE `groups` ADD CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES `groups` (id)'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON `groups` (parent_id)'); //Fill empty timestamps with current date $tables = ['attachments', 'attachment_types', 'categories', 'devices', 'footprints', 'manufacturers', 'orderdetails', 'pricedetails', 'storelocations', 'suppliers', ]; foreach ($tables as $table) { - $this->addSql("UPDATE ${table} SET datetime_added = NOW() WHERE datetime_added = '0000-00-00 00:00:00'"); - $this->addSql("UPDATE ${table} SET last_modified = datetime_added WHERE last_modified = '0000-00-00 00:00:00'"); + $this->addSql("UPDATE $table SET datetime_added = NOW() WHERE datetime_added = '0000-00-00 00:00:00'"); + $this->addSql("UPDATE $table SET last_modified = datetime_added WHERE last_modified = '0000-00-00 00:00:00'"); } //Set the dbVersion to a high value, to prevent the old Part-DB versions to upgrade DB! $this->addSql("UPDATE `internal` SET `keyValue` = '99' WHERE `internal`.`keyName` = 'dbVersion'"); + + //Migrate theme config to new format + $this->addSql('UPDATE users SET users.config_theme = REPLACE(users.config_theme ,".min.css", "") WHERE users.config_theme LIKE "%.min.css"'); + $this->addSql('UPDATE users SET users.config_theme = REPLACE(users.config_theme ,".css", "") WHERE users.config_theme LIKE "%.css"'); } public function mySQLDown(Schema $schema): void @@ -357,10 +369,6 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration $this->addSql('ALTER TABLE `users` CHANGE name name VARCHAR(32) NOT NULL COLLATE utf8_general_ci, CHANGE need_pw_change need_pw_change TINYINT(1) DEFAULT \'0\' NOT NULL, CHANGE first_name first_name TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE last_name last_name TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE department department TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE email email TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE config_language config_language TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE config_timezone config_timezone TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE config_theme config_theme TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE config_currency config_currency TINYTEXT DEFAULT NULL COLLATE utf8_general_ci, CHANGE last_modified last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE perms_labels perms_labels SMALLINT NOT NULL'); $this->addSql('DROP INDEX uniq_1483a5e95e237e06 ON `users`'); $this->addSql('CREATE UNIQUE INDEX name ON `users` (name)'); - - //Migrate theme config to new format - $this->addSql('UPDATE users SET users.config_theme = REPLACE(users.config_theme ,".min.css", "") WHERE users.config_theme LIKE "%.min.css"'); - $this->addSql('UPDATE users SET users.config_theme = REPLACE(users.config_theme ,".css", "") WHERE users.config_theme LIKE "%.css"'); } public function sqLiteUp(Schema $schema): void @@ -372,4 +380,14 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20190913141126.php b/migrations/Version20190913141126.php index 58093338..31eed64a 100644 --- a/migrations/Version20190913141126.php +++ b/migrations/Version20190913141126.php @@ -88,4 +88,14 @@ final class Version20190913141126 extends AbstractMultiPlatformMigration { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20190924113252.php b/migrations/Version20190924113252.php index fe7c9c8b..8221c686 100644 --- a/migrations/Version20190924113252.php +++ b/migrations/Version20190924113252.php @@ -179,4 +179,14 @@ final class Version20190924113252 extends AbstractMultiPlatformMigration { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20191214153125.php b/migrations/Version20191214153125.php index 9a61c10d..83803d13 100644 --- a/migrations/Version20191214153125.php +++ b/migrations/Version20191214153125.php @@ -65,4 +65,14 @@ final class Version20191214153125 extends AbstractMultiPlatformMigration { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20200126191823.php b/migrations/Version20200126191823.php index c093a4d7..cf362ea5 100644 --- a/migrations/Version20200126191823.php +++ b/migrations/Version20200126191823.php @@ -68,4 +68,14 @@ final class Version20200126191823 extends AbstractMultiPlatformMigration { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20200311204104.php b/migrations/Version20200311204104.php index 2a90b62c..daa6fd28 100644 --- a/migrations/Version20200311204104.php +++ b/migrations/Version20200311204104.php @@ -56,4 +56,14 @@ final class Version20200311204104 extends AbstractMultiPlatformMigration { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20200409130946.php b/migrations/Version20200409130946.php index 72bdda9c..ef2dc7ab 100644 --- a/migrations/Version20200409130946.php +++ b/migrations/Version20200409130946.php @@ -42,4 +42,14 @@ final class Version20200409130946 extends AbstractMultiPlatformMigration { $this->warnIf(true, "Migration not needed for SQLite. Skipping..."); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20200502161750.php b/migrations/Version20200502161750.php index 93bb0d00..55117541 100644 --- a/migrations/Version20200502161750.php +++ b/migrations/Version20200502161750.php @@ -163,4 +163,14 @@ EOD; $this->addSql('DROP TABLE u2f_keys'); $this->addSql('DROP TABLE "users"'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20220925162725.php b/migrations/Version20220925162725.php index 52caa2a9..21ac8946 100644 --- a/migrations/Version20220925162725.php +++ b/migrations/Version20220925162725.php @@ -71,7 +71,10 @@ final class Version20220925162725 extends AbstractMultiPlatformMigration // this up() migration is auto-generated, please modify it to your needs $this->addSql('CREATE INDEX attachment_types_idx_name ON attachment_types (name)'); $this->addSql('CREATE INDEX attachment_types_idx_parent_name ON attachment_types (parent_id, name)'); + //We have to disable foreign key checks here, or we get a error on MySQL for legacy database migration. On MariaDB this works fine (with enabled foreign key checks), so I guess this is not so bad... + $this->addSql('SET foreign_key_checks = 0'); $this->addSql('ALTER TABLE attachments CHANGE type_id type_id INT NOT NULL'); + $this->addSql('SET foreign_key_checks = 1'); $this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON attachments (id, element_id, class_name)'); $this->addSql('CREATE INDEX attachments_idx_class_name_id ON attachments (class_name, id)'); $this->addSql('CREATE INDEX attachment_name_idx ON attachments (name)'); @@ -532,4 +535,14 @@ final class Version20220925162725 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON "users" (currency_id)'); $this->addSql('CREATE INDEX IDX_1483A5E96DEDCEC2 ON "users" (id_preview_attachement)'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20221003212851.php b/migrations/Version20221003212851.php index 3a7379ca..81e33c0e 100644 --- a/migrations/Version20221003212851.php +++ b/migrations/Version20221003212851.php @@ -47,4 +47,14 @@ final class Version20221003212851 extends AbstractMultiPlatformMigration { $this->addSql('DROP TABLE webauthn_keys'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20221114193325.php b/migrations/Version20221114193325.php index d80cc1d2..9766ccf3 100644 --- a/migrations/Version20221114193325.php +++ b/migrations/Version20221114193325.php @@ -4,24 +4,20 @@ declare(strict_types=1); namespace DoctrineMigrations; -use App\Entity\UserSystem\PermissionData; use App\Migration\AbstractMultiPlatformMigration; -use App\Security\Interfaces\HasPermissionsInterface; +use App\Migration\WithPermPresetsTrait; use App\Services\UserSystem\PermissionPresetsHelper; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Auto-generated Migration: Please modify to your needs! */ final class Version20221114193325 extends AbstractMultiPlatformMigration implements ContainerAwareInterface { - private ?ContainerInterface $container = null; - private ?PermissionPresetsHelper $permission_presets_helper = null; + use WithPermPresetsTrait; public function __construct(Connection $connection, LoggerInterface $logger) { @@ -33,34 +29,6 @@ final class Version20221114193325 extends AbstractMultiPlatformMigration impleme return 'Update the permission system to the new system. Please note that all permissions will be reset!'; } - private function getJSONPermDataFromPreset(string $preset): string - { - if ($this->permission_presets_helper === null) { - throw new \RuntimeException('PermissionPresetsHelper not set! There seems to be some issue with the dependency injection!'); - } - - //Create a virtual user on which we can apply the preset - $user = new class implements HasPermissionsInterface { - - public PermissionData $perm_data; - - public function __construct() - { - $this->perm_data = new PermissionData(); - } - - public function getPermissions(): PermissionData - { - return $this->perm_data; - } - }; - - //Apply the preset to the virtual user - $this->permission_presets_helper->applyPreset($user, $preset); - - //And return the json data - return json_encode($user->getPermissions()); - } private function addDataMigrationAndWarning(): void { @@ -83,9 +51,12 @@ final class Version20221114193325 extends AbstractMultiPlatformMigration impleme //Reset the permissions of the admin user, to allow admin permissions (like the admins group) $this->addSql("UPDATE `users` SET permissions_data = '$admin' WHERE id = 2;"); + //This warning should not be needed, anymore, as almost everybody should have updated to the new version by now, and this warning would just irritate new users of the software + /* $this->logger->warning('!!! All permissions were reset! Please change them to the desired state, immediately !!!'); $this->logger->warning('!!! For security reasons all users (except the admin user) were disabled. Login with admin user and reenable other users after checking their permissions !!!'); $this->logger->warning('!!! For more infos see: https://github.com/Part-DB/Part-DB-symfony/discussions/193 !!!'); + */ } public function mySQLUp(Schema $schema): void @@ -161,11 +132,15 @@ final class Version20221114193325 extends AbstractMultiPlatformMigration impleme $this->addSql('CREATE INDEX user_idx_username ON "users" (name)'); } - public function setContainer(ContainerInterface $container = null) + + + public function postgreSQLUp(Schema $schema): void { - if ($container) { - $this->container = $container; - $this->permission_presets_helper = $container->get(PermissionPresetsHelper::class); - } + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); } } diff --git a/migrations/Version20221204004815.php b/migrations/Version20221204004815.php index 20e151db..c6c7b6ab 100644 --- a/migrations/Version20221204004815.php +++ b/migrations/Version20221204004815.php @@ -55,4 +55,14 @@ final class Version20221204004815 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20221216224745.php b/migrations/Version20221216224745.php index 63bb535e..9ac3b8ba 100644 --- a/migrations/Version20221216224745.php +++ b/migrations/Version20221216224745.php @@ -67,6 +67,15 @@ final class Version20221216224745 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX log_idx_type ON log (type)'); $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + } + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); } } diff --git a/migrations/Version20230108165410.php b/migrations/Version20230108165410.php index 4124a95a..90f6314e 100644 --- a/migrations/Version20230108165410.php +++ b/migrations/Version20230108165410.php @@ -320,4 +320,14 @@ final class Version20230108165410 extends AbstractMultiPlatformMigration $this->addSql('ALTER TABLE projects RENAME TO devices'); $this->addSql('ALTER TABLE project_bom_entries RENAME TO device_parts'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20230219225340.php b/migrations/Version20230219225340.php index 94e08963..2740c85e 100644 --- a/migrations/Version20230219225340.php +++ b/migrations/Version20230219225340.php @@ -521,4 +521,14 @@ final class Version20230219225340 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); } + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + } diff --git a/migrations/Version20230220221024.php b/migrations/Version20230220221024.php index 01d65d51..b2209351 100644 --- a/migrations/Version20230220221024.php +++ b/migrations/Version20230220221024.php @@ -37,4 +37,14 @@ final class Version20230220221024 extends AbstractMultiPlatformMigration { $this->addSql('ALTER TABLE `users` DROP saml_user'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20230402170923.php b/migrations/Version20230402170923.php index 016a10d0..3325afb6 100644 --- a/migrations/Version20230402170923.php +++ b/migrations/Version20230402170923.php @@ -297,4 +297,14 @@ final class Version20230402170923 extends AbstractMultiPlatformMigration $this->addSql('DROP TABLE __temp__webauthn_keys'); $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20230408170059.php b/migrations/Version20230408170059.php index 88be7798..e3662e16 100644 --- a/migrations/Version20230408170059.php +++ b/migrations/Version20230408170059.php @@ -49,4 +49,14 @@ final class Version20230408170059 extends AbstractMultiPlatformMigration $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON "users" (id_preview_attachment)'); $this->addSql('CREATE INDEX user_idx_username ON "users" (name)'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20230408213957.php b/migrations/Version20230408213957.php index 976a79db..47807126 100644 --- a/migrations/Version20230408213957.php +++ b/migrations/Version20230408213957.php @@ -434,4 +434,14 @@ final class Version20230408213957 extends AbstractMultiPlatformMigration $this->addSql('DROP TABLE __temp__webauthn_keys'); $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } } diff --git a/migrations/Version20230417211732.php b/migrations/Version20230417211732.php new file mode 100644 index 00000000..5fa3b5f7 --- /dev/null +++ b/migrations/Version20230417211732.php @@ -0,0 +1,58 @@ +addSql('DELETE FROM attachments WHERE class_name = "PartDB\\\\Part" AND NOT EXISTS (SELECT id FROM parts WHERE id = attachments.element_id)'); + $this->addSql('DELETE FROM attachments WHERE class_name = "PartDB\\\\Device" AND NOT EXISTS (SELECT id FROM projects WHERE id = attachments.element_id)'); + + // Replace all attachments where class_name is the legacy "PartDB\Part" with the new version "Part" + //We have to use 4 backslashes here, as PHP reduces them to 2 backslashes, which MySQL interprets as an escaped backslash. + $this->addSql('UPDATE attachments SET class_name = "Part" WHERE class_name = "PartDB\\\\Part"'); + //Do the same with PartDB\Device and Device + $this->addSql('UPDATE attachments SET class_name = "Device" WHERE class_name = "PartDB\\\\Device"'); + + + } + + public function mySQLDown(Schema $schema): void + { + // We can not revert this migration, because we don't know the old class name. + } + + public function sqLiteUp(Schema $schema): void + { + //As legacy database can only be migrated to MySQL, we don't need to implement this method. + //Dont skip here, as this causes this migration always to be executed. Do nothing instead. + } + + public function sqLiteDown(Schema $schema): void + { + //As we done nothing, we don't need to implement this method. + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20230528000149.php b/migrations/Version20230528000149.php new file mode 100644 index 00000000..5e742383 --- /dev/null +++ b/migrations/Version20230528000149.php @@ -0,0 +1,75 @@ +addSql('ALTER TABLE webauthn_keys ADD other_ui LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:array)\''); + } + + public function mySQLDown(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE webauthn_keys DROP other_ui'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path) + , aaguid CLOB NOT NULL --(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL --(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, other_ui CLOB DEFAULT NULL --(DC2Type:array) + , CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL -- +(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL -- +(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL -- +(DC2Type:trust_path) + , aaguid CLOB NOT NULL -- +(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL -- +(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20230716184033.php b/migrations/Version20230716184033.php new file mode 100644 index 00000000..7c0d8a12 --- /dev/null +++ b/migrations/Version20230716184033.php @@ -0,0 +1,361 @@ +addSql('CREATE TABLE oauth_tokens (id INT AUTO_INCREMENT NOT NULL, token VARCHAR(255) DEFAULT NULL, expires_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', refresh_token VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, UNIQUE INDEX oauth_tokens_unique_name (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE attachment_types ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE categories ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE currencies ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE footprints ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE `groups` ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE manufacturers ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE measurement_units ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD provider_reference_provider_key VARCHAR(255) DEFAULT NULL, ADD provider_reference_provider_id VARCHAR(255) DEFAULT NULL, ADD provider_reference_provider_url VARCHAR(255) DEFAULT NULL, ADD provider_reference_last_updated DATETIME DEFAULT NULL'); + $this->addSql('ALTER TABLE projects ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE storelocations ADD alternative_names LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE suppliers ADD alternative_names LONGTEXT DEFAULT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('ALTER TABLE `attachment_types` DROP alternative_names'); + $this->addSql('ALTER TABLE `categories` DROP alternative_names'); + $this->addSql('ALTER TABLE currencies DROP alternative_names'); + $this->addSql('ALTER TABLE `footprints` DROP alternative_names'); + $this->addSql('ALTER TABLE `groups` DROP alternative_names'); + $this->addSql('ALTER TABLE `manufacturers` DROP alternative_names'); + $this->addSql('ALTER TABLE `measurement_units` DROP alternative_names'); + $this->addSql('ALTER TABLE `parts` DROP provider_reference_provider_key, DROP provider_reference_provider_id, DROP provider_reference_provider_url, DROP provider_reference_last_updated'); + $this->addSql('ALTER TABLE projects DROP alternative_names'); + $this->addSql('ALTER TABLE `storelocations` DROP alternative_names'); + $this->addSql('ALTER TABLE `suppliers` DROP alternative_names'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, token VARCHAR(255) DEFAULT NULL, expires_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) + , refresh_token VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL)'); + $this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)'); + $this->addSql('ALTER TABLE attachment_types ADD COLUMN alternative_names CLOB DEFAULT NULL'); + $this->addSql('ALTER TABLE categories ADD COLUMN alternative_names CLOB DEFAULT NULL'); + $this->addSql('CREATE TEMPORARY TABLE __temp__currencies AS SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM currencies'); + $this->addSql('DROP TABLE currencies'); + $this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , iso_code VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO currencies (id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM __temp__currencies'); + $this->addSql('DROP TABLE __temp__currencies'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('CREATE INDEX currency_idx_name ON currencies (name)'); + $this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)'); + $this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)'); + $this->addSql('ALTER TABLE footprints ADD COLUMN alternative_names CLOB DEFAULT NULL'); + $this->addSql('CREATE TEMPORARY TABLE __temp__groups AS SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM groups'); + $this->addSql('DROP TABLE groups'); + $this->addSql('CREATE TABLE groups (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL --(DC2Type:json) + , alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO groups (id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data) SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM __temp__groups'); + $this->addSql('DROP TABLE __temp__groups'); + $this->addSql('CREATE INDEX group_idx_parent_name ON groups (parent_id, name)'); + $this->addSql('CREATE INDEX group_idx_name ON groups (name)'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON groups (parent_id)'); + $this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON groups (id_preview_attachment)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM log'); + $this->addSql('DROP TABLE log'); + $this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_user INTEGER DEFAULT NULL, datetime DATETIME NOT NULL, level TINYINT NOT NULL --(DC2Type:tinyint) + , target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL --(DC2Type:json) + , type SMALLINT NOT NULL, username VARCHAR(255) NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO log (id, id_user, datetime, level, target_id, target_type, extra, type, username) SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM __temp__log'); + $this->addSql('DROP TABLE __temp__log'); + $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); + $this->addSql('CREATE INDEX log_idx_type ON log (type)'); + $this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)'); + $this->addSql('ALTER TABLE manufacturers ADD COLUMN alternative_names CLOB DEFAULT NULL'); + $this->addSql('ALTER TABLE measurement_units ADD COLUMN alternative_names CLOB DEFAULT NULL'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn FROM parts'); + $this->addSql('DROP TABLE parts'); + $this->addSql('CREATE TABLE parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url VARCHAR(255) NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, ipn VARCHAR(100) DEFAULT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO parts (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)'); + $this->addSql('CREATE INDEX parts_idx_name ON parts (name)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__pricedetails AS SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM pricedetails'); + $this->addSql('DROP TABLE pricedetails'); + $this->addSql('CREATE TABLE pricedetails (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, price NUMERIC(11, 5) NOT NULL --(DC2Type:big_decimal) + , price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO pricedetails (id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added) SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM __temp__pricedetails'); + $this->addSql('DROP TABLE __temp__pricedetails'); + $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON pricedetails (orderdetails_id)'); + $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON pricedetails (id_currency)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount ON pricedetails (min_discount_quantity)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON pricedetails (min_discount_quantity, price_related_quantity)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM project_bom_entries'); + $this->addSql('DROP TABLE project_bom_entries'); + $this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AFC547992F180363 FOREIGN KEY (id_device) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AFC54799C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO project_bom_entries (id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added) SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM __temp__project_bom_entries'); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + $this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)'); + $this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)'); + $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); + $this->addSql('ALTER TABLE projects ADD COLUMN alternative_names CLOB DEFAULT NULL'); + $this->addSql('ALTER TABLE storelocations ADD COLUMN alternative_names CLOB DEFAULT NULL'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM suppliers'); + $this->addSql('DROP TABLE suppliers'); + $this->addSql('CREATE TABLE suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO suppliers (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON suppliers (default_currency_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)'); + $this->addSql('CREATE INDEX supplier_idx_name ON suppliers (name)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON suppliers (parent_id, name)'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON suppliers (id_preview_attachment)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile FROM users'); + $this->addSql('DROP TABLE users'); + $this->addSql('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL --(DC2Type:json) + , google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL --(DC2Type:json) + , backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL --(DC2Type:json) + , saml_user BOOLEAN NOT NULL, about_me CLOB NOT NULL, show_email_on_profile BOOLEAN DEFAULT 0 NOT NULL, CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO users (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile FROM __temp__users'); + $this->addSql('DROP TABLE __temp__users'); + $this->addSql('CREATE INDEX user_idx_username ON users (name)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON users (name)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON users (group_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON users (currency_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON users (id_preview_attachment)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path) + , aaguid CLOB NOT NULL --(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL --(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, other_ui CLOB DEFAULT NULL --(DC2Type:array) + , CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('CREATE TEMPORARY TABLE __temp__attachment_types AS SELECT id, parent_id, id_preview_attachment, filetype_filter, comment, not_selectable, name, last_modified, datetime_added FROM "attachment_types"'); + $this->addSql('DROP TABLE "attachment_types"'); + $this->addSql('CREATE TABLE "attachment_types" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, filetype_filter CLOB NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_EFAED719727ACA70 FOREIGN KEY (parent_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EFAED719EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "attachment_types" (id, parent_id, id_preview_attachment, filetype_filter, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, filetype_filter, comment, not_selectable, name, last_modified, datetime_added FROM __temp__attachment_types'); + $this->addSql('DROP TABLE __temp__attachment_types'); + $this->addSql('CREATE INDEX IDX_EFAED719727ACA70 ON "attachment_types" (parent_id)'); + $this->addSql('CREATE INDEX IDX_EFAED719EA7100A1 ON "attachment_types" (id_preview_attachment)'); + $this->addSql('CREATE INDEX attachment_types_idx_name ON "attachment_types" (name)'); + $this->addSql('CREATE INDEX attachment_types_idx_parent_name ON "attachment_types" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__categories AS SELECT id, parent_id, id_preview_attachment, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment, comment, not_selectable, name, last_modified, datetime_added FROM "categories"'); + $this->addSql('DROP TABLE "categories"'); + $this->addSql('CREATE TABLE "categories" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, partname_hint CLOB NOT NULL, partname_regex CLOB NOT NULL, disable_footprints BOOLEAN NOT NULL, disable_manufacturers BOOLEAN NOT NULL, disable_autodatasheets BOOLEAN NOT NULL, disable_properties BOOLEAN NOT NULL, default_description CLOB NOT NULL, default_comment CLOB NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "categories" (id, parent_id, id_preview_attachment, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment, comment, not_selectable, name, last_modified, datetime_added FROM __temp__categories'); + $this->addSql('DROP TABLE __temp__categories'); + $this->addSql('CREATE INDEX IDX_3AF34668727ACA70 ON "categories" (parent_id)'); + $this->addSql('CREATE INDEX IDX_3AF34668EA7100A1 ON "categories" (id_preview_attachment)'); + $this->addSql('CREATE INDEX category_idx_name ON "categories" (name)'); + $this->addSql('CREATE INDEX category_idx_parent_name ON "categories" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__currencies AS SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM currencies'); + $this->addSql('DROP TABLE currencies'); + $this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL -- +(DC2Type:big_decimal) + , iso_code VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO currencies (id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM __temp__currencies'); + $this->addSql('DROP TABLE __temp__currencies'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)'); + $this->addSql('CREATE INDEX currency_idx_name ON currencies (name)'); + $this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__footprints AS SELECT id, parent_id, id_footprint_3d, id_preview_attachment, comment, not_selectable, name, last_modified, datetime_added FROM "footprints"'); + $this->addSql('DROP TABLE "footprints"'); + $this->addSql('CREATE TABLE "footprints" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_footprint_3d INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_A34D68A2727ACA70 FOREIGN KEY (parent_id) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A232A38C34 FOREIGN KEY (id_footprint_3d) REFERENCES "attachments" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A2EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "footprints" (id, parent_id, id_footprint_3d, id_preview_attachment, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_footprint_3d, id_preview_attachment, comment, not_selectable, name, last_modified, datetime_added FROM __temp__footprints'); + $this->addSql('DROP TABLE __temp__footprints'); + $this->addSql('CREATE INDEX IDX_A34D68A2727ACA70 ON "footprints" (parent_id)'); + $this->addSql('CREATE INDEX IDX_A34D68A232A38C34 ON "footprints" (id_footprint_3d)'); + $this->addSql('CREATE INDEX IDX_A34D68A2EA7100A1 ON "footprints" (id_preview_attachment)'); + $this->addSql('CREATE INDEX footprint_idx_name ON "footprints" (name)'); + $this->addSql('CREATE INDEX footprint_idx_parent_name ON "footprints" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__groups AS SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM "groups"'); + $this->addSql('DROP TABLE "groups"'); + $this->addSql('CREATE TABLE "groups" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL -- +(DC2Type:json) + , CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "groups" (id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data) SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM __temp__groups'); + $this->addSql('DROP TABLE __temp__groups'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON "groups" (parent_id)'); + $this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON "groups" (id_preview_attachment)'); + $this->addSql('CREATE INDEX group_idx_name ON "groups" (name)'); + $this->addSql('CREATE INDEX group_idx_parent_name ON "groups" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, id_user, username, datetime, level, target_id, target_type, extra, type FROM log'); + $this->addSql('DROP TABLE log'); + $this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_user INTEGER DEFAULT NULL, username VARCHAR(255) NOT NULL, datetime DATETIME NOT NULL, level TINYINT NOT NULL -- +(DC2Type:tinyint) + , target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL -- +(DC2Type:json) + , type SMALLINT NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO log (id, id_user, username, datetime, level, target_id, target_type, extra, type) SELECT id, id_user, username, datetime, level, target_id, target_type, extra, type FROM __temp__log'); + $this->addSql('DROP TABLE __temp__log'); + $this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)'); + $this->addSql('CREATE INDEX log_idx_type ON log (type)'); + $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); + $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__manufacturers AS SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM "manufacturers"'); + $this->addSql('DROP TABLE "manufacturers"'); + $this->addSql('CREATE TABLE "manufacturers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_94565B12727ACA70 FOREIGN KEY (parent_id) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_94565B12EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "manufacturers" (id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__manufacturers'); + $this->addSql('DROP TABLE __temp__manufacturers'); + $this->addSql('CREATE INDEX IDX_94565B12727ACA70 ON "manufacturers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_94565B12EA7100A1 ON "manufacturers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX manufacturer_name ON "manufacturers" (name)'); + $this->addSql('CREATE INDEX manufacturer_idx_parent_name ON "manufacturers" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__measurement_units AS SELECT id, parent_id, id_preview_attachment, unit, is_integer, use_si_prefix, comment, not_selectable, name, last_modified, datetime_added FROM "measurement_units"'); + $this->addSql('DROP TABLE "measurement_units"'); + $this->addSql('CREATE TABLE "measurement_units" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, unit VARCHAR(255) DEFAULT NULL, is_integer BOOLEAN NOT NULL, use_si_prefix BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_F5AF83CF727ACA70 FOREIGN KEY (parent_id) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F5AF83CFEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "measurement_units" (id, parent_id, id_preview_attachment, unit, is_integer, use_si_prefix, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, unit, is_integer, use_si_prefix, comment, not_selectable, name, last_modified, datetime_added FROM __temp__measurement_units'); + $this->addSql('DROP TABLE __temp__measurement_units'); + $this->addSql('CREATE INDEX IDX_F5AF83CF727ACA70 ON "measurement_units" (parent_id)'); + $this->addSql('CREATE INDEX IDX_F5AF83CFEA7100A1 ON "measurement_units" (id_preview_attachment)'); + $this->addSql('CREATE INDEX unit_idx_name ON "measurement_units" (name)'); + $this->addSql('CREATE INDEX unit_idx_parent_name ON "measurement_units" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order FROM "parts"'); + $this->addSql('DROP TABLE "parts"'); + $this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url VARCHAR(255) NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "parts" (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__pricedetails AS SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM "pricedetails"'); + $this->addSql('DROP TABLE "pricedetails"'); + $this->addSql('CREATE TABLE "pricedetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, price NUMERIC(11, 5) NOT NULL -- +(DC2Type:big_decimal) + , price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES "orderdetails" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "pricedetails" (id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added) SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM __temp__pricedetails'); + $this->addSql('DROP TABLE __temp__pricedetails'); + $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON "pricedetails" (id_currency)'); + $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON "pricedetails" (orderdetails_id)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount ON "pricedetails" (min_discount_quantity)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON "pricedetails" (min_discount_quantity, price_related_quantity)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM project_bom_entries'); + $this->addSql('DROP TABLE project_bom_entries'); + $this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL -- +(DC2Type:big_decimal) + , last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO project_bom_entries (id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added) SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM __temp__project_bom_entries'); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + $this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)'); + $this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)'); + $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__projects AS SELECT id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added FROM projects'); + $this->addSql('DROP TABLE projects'); + $this->addSql('CREATE TABLE projects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts BOOLEAN NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_5C93B3A4727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO projects (id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added FROM __temp__projects'); + $this->addSql('DROP TABLE __temp__projects'); + $this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)'); + $this->addSql('CREATE INDEX IDX_5C93B3A4EA7100A1 ON projects (id_preview_attachment)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__storelocations AS SELECT id, parent_id, storage_type_id, id_owner, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, part_owner_must_match, comment, not_selectable, name, last_modified, datetime_added FROM "storelocations"'); + $this->addSql('DROP TABLE "storelocations"'); + $this->addSql('CREATE TABLE "storelocations" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, storage_type_id INTEGER DEFAULT NULL, id_owner INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, is_full BOOLEAN NOT NULL, only_single_part BOOLEAN NOT NULL, limit_to_existing_parts BOOLEAN NOT NULL, part_owner_must_match BOOLEAN DEFAULT 0 NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_7517020727ACA70 FOREIGN KEY (parent_id) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020B270BFF1 FOREIGN KEY (storage_type_id) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_751702021E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "storelocations" (id, parent_id, storage_type_id, id_owner, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, part_owner_must_match, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, storage_type_id, id_owner, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, part_owner_must_match, comment, not_selectable, name, last_modified, datetime_added FROM __temp__storelocations'); + $this->addSql('DROP TABLE __temp__storelocations'); + $this->addSql('CREATE INDEX IDX_7517020727ACA70 ON "storelocations" (parent_id)'); + $this->addSql('CREATE INDEX IDX_7517020B270BFF1 ON "storelocations" (storage_type_id)'); + $this->addSql('CREATE INDEX IDX_751702021E5A74C ON "storelocations" (id_owner)'); + $this->addSql('CREATE INDEX IDX_7517020EA7100A1 ON "storelocations" (id_preview_attachment)'); + $this->addSql('CREATE INDEX location_idx_name ON "storelocations" (name)'); + $this->addSql('CREATE INDEX location_idx_parent_name ON "storelocations" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM "suppliers"'); + $this->addSql('DROP TABLE "suppliers"'); + $this->addSql('CREATE TABLE "suppliers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL -- +(DC2Type:big_decimal) + , address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "suppliers" (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON "suppliers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON "suppliers" (default_currency_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON "suppliers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX supplier_idx_name ON "suppliers" (name)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON "suppliers" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM "users"'); + $this->addSql('DROP TABLE "users"'); + $this->addSql('CREATE TABLE "users" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, about_me CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL -- +(DC2Type:json) + , google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, show_email_on_profile BOOLEAN DEFAULT 0 NOT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL -- +(DC2Type:json) + , backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, saml_user BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL -- +(DC2Type:json) + , CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "users" (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM __temp__users'); + $this->addSql('DROP TABLE __temp__users'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON "users" (name)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON "users" (group_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON "users" (currency_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON "users" (id_preview_attachment)'); + $this->addSql('CREATE INDEX user_idx_username ON "users" (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL -- +(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL -- +(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL -- +(DC2Type:trust_path) + , aaguid CLOB NOT NULL -- +(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL -- +(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, other_ui CLOB DEFAULT NULL -- +(DC2Type:array) + , name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20230730131708.php b/migrations/Version20230730131708.php new file mode 100644 index 00000000..a12c3b8d --- /dev/null +++ b/migrations/Version20230730131708.php @@ -0,0 +1,112 @@ +addSql('ALTER TABLE oauth_tokens CHANGE token token LONGTEXT DEFAULT NULL, CHANGE refresh_token refresh_token LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE orderdetails CHANGE supplier_product_url supplier_product_url LONGTEXT NOT NULL'); + $this->addSql('ALTER TABLE parts CHANGE manufacturer_product_url manufacturer_product_url LONGTEXT NOT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE oauth_tokens CHANGE token token VARCHAR(255) DEFAULT NULL, CHANGE refresh_token refresh_token VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE `orderdetails` CHANGE supplier_product_url supplier_product_url VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE `parts` CHANGE manufacturer_product_url manufacturer_product_url VARCHAR(255) NOT NULL'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__oauth_tokens AS SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM oauth_tokens'); + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, token CLOB DEFAULT NULL, expires_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) + , refresh_token CLOB DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL)'); + $this->addSql('INSERT INTO oauth_tokens (id, token, expires_at, refresh_token, name, last_modified, datetime_added) SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM __temp__oauth_tokens'); + $this->addSql('DROP TABLE __temp__oauth_tokens'); + $this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__orderdetails AS SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM orderdetails'); + $this->addSql('DROP TABLE orderdetails'); + $this->addSql('CREATE TABLE orderdetails (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, part_id INTEGER NOT NULL, id_supplier INTEGER DEFAULT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url CLOB NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO orderdetails (id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added) SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM __temp__orderdetails'); + $this->addSql('DROP TABLE __temp__orderdetails'); + $this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON orderdetails (supplierpartnr)'); + $this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON orderdetails (part_id)'); + $this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON orderdetails (id_supplier)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM parts'); + $this->addSql('DROP TABLE parts'); + $this->addSql('CREATE TABLE parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, ipn VARCHAR(100) DEFAULT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO parts (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON parts (name)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__oauth_tokens AS SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM oauth_tokens'); + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, token VARCHAR(255) DEFAULT NULL, expires_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) + , refresh_token VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL)'); + $this->addSql('INSERT INTO oauth_tokens (id, token, expires_at, refresh_token, name, last_modified, datetime_added) SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM __temp__oauth_tokens'); + $this->addSql('DROP TABLE __temp__oauth_tokens'); + $this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__orderdetails AS SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM "orderdetails"'); + $this->addSql('DROP TABLE "orderdetails"'); + $this->addSql('CREATE TABLE "orderdetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, part_id INTEGER NOT NULL, id_supplier INTEGER DEFAULT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "orderdetails" (id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added) SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM __temp__orderdetails'); + $this->addSql('DROP TABLE __temp__orderdetails'); + $this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON "orderdetails" (part_id)'); + $this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON "orderdetails" (id_supplier)'); + $this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON "orderdetails" (supplierpartnr)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM "parts"'); + $this->addSql('DROP TABLE "parts"'); + $this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url VARCHAR(255) NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "parts" (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20230816213201.php b/migrations/Version20230816213201.php new file mode 100644 index 00000000..d776da26 --- /dev/null +++ b/migrations/Version20230816213201.php @@ -0,0 +1,54 @@ +addSql('CREATE TABLE api_tokens (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, valid_until DATETIME DEFAULT NULL, token VARCHAR(68) NOT NULL, level SMALLINT NOT NULL, last_time_used DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, UNIQUE INDEX UNIQ_2CAD560E5F37A13B (token), INDEX IDX_2CAD560EA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE api_tokens ADD CONSTRAINT FK_2CAD560EA76ED395 FOREIGN KEY (user_id) REFERENCES `users` (id)'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE api_tokens DROP FOREIGN KEY FK_2CAD560EA76ED395'); + $this->addSql('DROP TABLE api_tokens'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TABLE api_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, valid_until DATETIME DEFAULT NULL, token VARCHAR(68) NOT NULL, level SMALLINT NOT NULL, last_time_used DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_2CAD560EA76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_2CAD560E5F37A13B ON api_tokens (token)'); + $this->addSql('CREATE INDEX IDX_2CAD560EA76ED395 ON api_tokens (user_id)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('DROP TABLE api_tokens'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20231114223101.php b/migrations/Version20231114223101.php new file mode 100644 index 00000000..c06cb279 --- /dev/null +++ b/migrations/Version20231114223101.php @@ -0,0 +1,81 @@ +addSql('CREATE TABLE part_association (id INT AUTO_INCREMENT NOT NULL, owner_id INT NOT NULL, other_id INT NOT NULL, type SMALLINT NOT NULL, other_type VARCHAR(255) DEFAULT NULL, comment LONGTEXT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_61B952E07E3C61F9 (owner_id), INDEX IDX_61B952E0998D9879 (other_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE part_association ADD CONSTRAINT FK_61B952E07E3C61F9 FOREIGN KEY (owner_id) REFERENCES `parts` (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE part_association ADD CONSTRAINT FK_61B952E0998D9879 FOREIGN KEY (other_id) REFERENCES `parts` (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE part_lots ADD vendor_barcode VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE part_association DROP FOREIGN KEY FK_61B952E07E3C61F9'); + $this->addSql('ALTER TABLE part_association DROP FOREIGN KEY FK_61B952E0998D9879'); + $this->addSql('DROP TABLE part_association'); + $this->addSql('DROP INDEX part_lots_idx_barcode ON part_lots'); + $this->addSql('ALTER TABLE part_lots DROP vendor_barcode'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TABLE part_association (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, owner_id INTEGER NOT NULL, other_id INTEGER NOT NULL, type SMALLINT NOT NULL, other_type VARCHAR(255) DEFAULT NULL, comment CLOB DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_61B952E07E3C61F9 FOREIGN KEY (owner_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_61B952E0998D9879 FOREIGN KEY (other_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_61B952E07E3C61F9 ON part_association (owner_id)'); + $this->addSql('CREATE INDEX IDX_61B952E0998D9879 ON part_association (other_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES storelocations (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added) SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('DROP TABLE part_association'); + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added) SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20231130180903.php b/migrations/Version20231130180903.php new file mode 100644 index 00000000..0eccb5aa --- /dev/null +++ b/migrations/Version20231130180903.php @@ -0,0 +1,98 @@ +addSql('ALTER TABLE categories ADD eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, ADD eda_info_invisible TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_bom TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_board TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_sim TINYINT(1) DEFAULT NULL, ADD eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE footprints ADD eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, ADD eda_info_value VARCHAR(255) DEFAULT NULL, ADD eda_info_invisible TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_bom TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_board TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_sim TINYINT(1) DEFAULT NULL, ADD eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, ADD eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE `categories` DROP eda_info_reference_prefix, DROP eda_info_invisible, DROP eda_info_exclude_from_bom, DROP eda_info_exclude_from_board, DROP eda_info_exclude_from_sim, DROP eda_info_kicad_symbol'); + $this->addSql('ALTER TABLE `footprints` DROP eda_info_kicad_footprint'); + $this->addSql('ALTER TABLE `parts` DROP eda_info_reference_prefix, DROP eda_info_value, DROP eda_info_invisible, DROP eda_info_exclude_from_bom, DROP eda_info_exclude_from_board, DROP eda_info_exclude_from_sim, DROP eda_info_kicad_symbol, DROP eda_info_kicad_footprint'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE categories ADD COLUMN eda_info_reference_prefix VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE categories ADD COLUMN eda_info_invisible BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE categories ADD COLUMN eda_info_exclude_from_bom BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE categories ADD COLUMN eda_info_exclude_from_board BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE categories ADD COLUMN eda_info_exclude_from_sim BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE categories ADD COLUMN eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE footprints ADD COLUMN eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_reference_prefix VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_value VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_invisible BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_exclude_from_bom BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_exclude_from_board BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_exclude_from_sim BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD COLUMN eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__categories AS SELECT id, parent_id, id_preview_attachment, name, last_modified, datetime_added, comment, not_selectable, alternative_names, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment FROM "categories"'); + $this->addSql('DROP TABLE "categories"'); + $this->addSql('CREATE TABLE "categories" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, partname_hint CLOB NOT NULL, partname_regex CLOB NOT NULL, disable_footprints BOOLEAN NOT NULL, disable_manufacturers BOOLEAN NOT NULL, disable_autodatasheets BOOLEAN NOT NULL, disable_properties BOOLEAN NOT NULL, default_description CLOB NOT NULL, default_comment CLOB NOT NULL, CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "categories" (id, parent_id, id_preview_attachment, name, last_modified, datetime_added, comment, not_selectable, alternative_names, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment) SELECT id, parent_id, id_preview_attachment, name, last_modified, datetime_added, comment, not_selectable, alternative_names, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment FROM __temp__categories'); + $this->addSql('DROP TABLE __temp__categories'); + $this->addSql('CREATE INDEX IDX_3AF34668727ACA70 ON "categories" (parent_id)'); + $this->addSql('CREATE INDEX IDX_3AF34668EA7100A1 ON "categories" (id_preview_attachment)'); + $this->addSql('CREATE INDEX category_idx_name ON "categories" (name)'); + $this->addSql('CREATE INDEX category_idx_parent_name ON "categories" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__footprints AS SELECT id, parent_id, id_preview_attachment, id_footprint_3d, name, last_modified, datetime_added, comment, not_selectable, alternative_names FROM "footprints"'); + $this->addSql('DROP TABLE "footprints"'); + $this->addSql('CREATE TABLE "footprints" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_footprint_3d INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_A34D68A2727ACA70 FOREIGN KEY (parent_id) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A2EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A232A38C34 FOREIGN KEY (id_footprint_3d) REFERENCES "attachments" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "footprints" (id, parent_id, id_preview_attachment, id_footprint_3d, name, last_modified, datetime_added, comment, not_selectable, alternative_names) SELECT id, parent_id, id_preview_attachment, id_footprint_3d, name, last_modified, datetime_added, comment, not_selectable, alternative_names FROM __temp__footprints'); + $this->addSql('DROP TABLE __temp__footprints'); + $this->addSql('CREATE INDEX IDX_A34D68A2727ACA70 ON "footprints" (parent_id)'); + $this->addSql('CREATE INDEX IDX_A34D68A2EA7100A1 ON "footprints" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_A34D68A232A38C34 ON "footprints" (id_footprint_3d)'); + $this->addSql('CREATE INDEX footprint_idx_name ON "footprints" (name)'); + $this->addSql('CREATE INDEX footprint_idx_parent_name ON "footprints" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM "parts"'); + $this->addSql('DROP TABLE "parts"'); + $this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "parts" (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20240427222442.php b/migrations/Version20240427222442.php new file mode 100644 index 00000000..92b2733d --- /dev/null +++ b/migrations/Version20240427222442.php @@ -0,0 +1,75 @@ +addSql('ALTER TABLE webauthn_keys ADD backup_eligible TINYINT(1) DEFAULT NULL, ADD backup_status TINYINT(1) DEFAULT NULL, ADD uv_initialized TINYINT(1) DEFAULT NULL, ADD last_time_used DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + } + + public function mySQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE webauthn_keys DROP backup_eligible, DROP backup_status, DROP uv_initialized, DROP last_time_used'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path) + , aaguid CLOB NOT NULL --(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL --(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, other_ui CLOB DEFAULT NULL --(DC2Type:array) + , backup_eligible BOOLEAN DEFAULT NULL, backup_status BOOLEAN DEFAULT NULL, uv_initialized BOOLEAN DEFAULT NULL, last_time_used DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) + , CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL -- +(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL -- +(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL -- +(DC2Type:trust_path) + , aaguid CLOB NOT NULL -- +(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL -- +(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, other_ui CLOB DEFAULT NULL -- +(DC2Type:array) + , name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->warnIf(true, "Migration not needed for Postgres. Skipping..."); + } +} diff --git a/migrations/Version20240606203053.php b/migrations/Version20240606203053.php new file mode 100644 index 00000000..83370ad6 --- /dev/null +++ b/migrations/Version20240606203053.php @@ -0,0 +1,654 @@ +addSql("CREATE COLLATION numeric (provider = icu, locale = 'en-u-kn-true');"); + + $this->addSql('CREATE TABLE api_tokens (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, valid_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, token VARCHAR(68) NOT NULL, level SMALLINT NOT NULL, last_time_used TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_2CAD560E5F37A13B ON api_tokens (token)'); + $this->addSql('CREATE INDEX IDX_2CAD560EA76ED395 ON api_tokens (user_id)'); + $this->addSql('CREATE TABLE "attachment_types" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, filetype_filter TEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_EFAED719727ACA70 ON "attachment_types" (parent_id)'); + $this->addSql('CREATE INDEX IDX_EFAED719EA7100A1 ON "attachment_types" (id_preview_attachment)'); + $this->addSql('CREATE INDEX attachment_types_idx_name ON "attachment_types" (name)'); + $this->addSql('CREATE INDEX attachment_types_idx_parent_name ON "attachment_types" (parent_id, name)'); + $this->addSql('CREATE TABLE "attachments" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, original_filename VARCHAR(255) DEFAULT NULL, path VARCHAR(255) NOT NULL, show_in_table BOOLEAN NOT NULL, type_id INT NOT NULL, class_name VARCHAR(255) NOT NULL, element_id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_47C4FAD6C54C8C93 ON "attachments" (type_id)'); + $this->addSql('CREATE INDEX IDX_47C4FAD61F1F2A24 ON "attachments" (element_id)'); + $this->addSql('CREATE INDEX attachments_idx_id_element_id_class_name ON "attachments" (id, element_id, class_name)'); + $this->addSql('CREATE INDEX attachments_idx_class_name_id ON "attachments" (class_name, id)'); + $this->addSql('CREATE INDEX attachment_name_idx ON "attachments" (name)'); + $this->addSql('CREATE INDEX attachment_element_idx ON "attachments" (class_name, element_id)'); + $this->addSql('CREATE TABLE "categories" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, partname_hint TEXT NOT NULL, partname_regex TEXT NOT NULL, disable_footprints BOOLEAN NOT NULL, disable_manufacturers BOOLEAN NOT NULL, disable_autodatasheets BOOLEAN NOT NULL, disable_properties BOOLEAN NOT NULL, default_description TEXT NOT NULL, default_comment TEXT NOT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_3AF34668727ACA70 ON "categories" (parent_id)'); + $this->addSql('CREATE INDEX IDX_3AF34668EA7100A1 ON "categories" (id_preview_attachment)'); + $this->addSql('CREATE INDEX category_idx_name ON "categories" (name)'); + $this->addSql('CREATE INDEX category_idx_parent_name ON "categories" (parent_id, name)'); + $this->addSql('CREATE TABLE currencies (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL, iso_code VARCHAR(255) NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)'); + $this->addSql('CREATE INDEX currency_idx_name ON currencies (name)'); + $this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)'); + $this->addSql('CREATE TABLE "footprints" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, id_footprint_3d INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_A34D68A2727ACA70 ON "footprints" (parent_id)'); + $this->addSql('CREATE INDEX IDX_A34D68A2EA7100A1 ON "footprints" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_A34D68A232A38C34 ON "footprints" (id_footprint_3d)'); + $this->addSql('CREATE INDEX footprint_idx_name ON "footprints" (name)'); + $this->addSql('CREATE INDEX footprint_idx_parent_name ON "footprints" (parent_id, name)'); + $this->addSql('CREATE TABLE "groups" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, permissions_data JSON NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON "groups" (parent_id)'); + $this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON "groups" (id_preview_attachment)'); + $this->addSql('CREATE INDEX group_idx_name ON "groups" (name)'); + $this->addSql('CREATE INDEX group_idx_parent_name ON "groups" (parent_id, name)'); + $this->addSql('CREATE TABLE label_profiles (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, show_in_dropdown BOOLEAN NOT NULL, options_width DOUBLE PRECISION NOT NULL, options_height DOUBLE PRECISION NOT NULL, options_barcode_type VARCHAR(255) NOT NULL, options_picture_type VARCHAR(255) NOT NULL, options_supported_element VARCHAR(255) NOT NULL, options_additional_css TEXT NOT NULL, options_lines_mode VARCHAR(255) NOT NULL, options_lines TEXT NOT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C93E9CF5EA7100A1 ON label_profiles (id_preview_attachment)'); + $this->addSql('CREATE TABLE log (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, username VARCHAR(255) NOT NULL, datetime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, level SMALLINT NOT NULL, target_id INT NOT NULL, target_type SMALLINT NOT NULL, extra JSON NOT NULL, id_user INT DEFAULT NULL, type SMALLINT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)'); + $this->addSql('CREATE INDEX log_idx_type ON log (type)'); + $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); + $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + $this->addSql('CREATE TABLE "manufacturers" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_94565B12727ACA70 ON "manufacturers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_94565B12EA7100A1 ON "manufacturers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX manufacturer_name ON "manufacturers" (name)'); + $this->addSql('CREATE INDEX manufacturer_idx_parent_name ON "manufacturers" (parent_id, name)'); + $this->addSql('CREATE TABLE "measurement_units" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, unit VARCHAR(255) DEFAULT NULL, is_integer BOOLEAN NOT NULL, use_si_prefix BOOLEAN NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_F5AF83CF727ACA70 ON "measurement_units" (parent_id)'); + $this->addSql('CREATE INDEX IDX_F5AF83CFEA7100A1 ON "measurement_units" (id_preview_attachment)'); + $this->addSql('CREATE INDEX unit_idx_name ON "measurement_units" (name)'); + $this->addSql('CREATE INDEX unit_idx_parent_name ON "measurement_units" (parent_id, name)'); + $this->addSql('CREATE TABLE oauth_tokens (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, token TEXT DEFAULT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, refresh_token TEXT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)'); + $this->addSql('CREATE TABLE "orderdetails" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url TEXT NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, part_id INT NOT NULL, id_supplier INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON "orderdetails" (part_id)'); + $this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON "orderdetails" (id_supplier)'); + $this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON "orderdetails" (supplierpartnr)'); + $this->addSql('CREATE TABLE parameters (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, symbol VARCHAR(255) NOT NULL, value_min DOUBLE PRECISION DEFAULT NULL, value_typical DOUBLE PRECISION DEFAULT NULL, value_max DOUBLE PRECISION DEFAULT NULL, unit VARCHAR(255) NOT NULL, value_text VARCHAR(255) NOT NULL, param_group VARCHAR(255) NOT NULL, type SMALLINT NOT NULL, element_id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_69348FE1F1F2A24 ON parameters (element_id)'); + $this->addSql('CREATE INDEX parameter_name_idx ON parameters (name)'); + $this->addSql('CREATE INDEX parameter_group_idx ON parameters (param_group)'); + $this->addSql('CREATE INDEX parameter_type_element_idx ON parameters (type, element_id)'); + $this->addSql('CREATE TABLE part_association (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, type SMALLINT NOT NULL, other_type VARCHAR(255) DEFAULT NULL, comment TEXT DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, owner_id INT NOT NULL, other_id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_61B952E07E3C61F9 ON part_association (owner_id)'); + $this->addSql('CREATE INDEX IDX_61B952E0998D9879 ON part_association (other_id)'); + $this->addSql('CREATE TABLE part_lots (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, description TEXT NOT NULL, comment TEXT NOT NULL, expiration_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, id_store_location INT DEFAULT NULL, id_part INT NOT NULL, id_owner INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + $this->addSql('CREATE TABLE "parts" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags TEXT NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description TEXT NOT NULL, comment TEXT NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url TEXT NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INT NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, id_category INT NOT NULL, id_footprint INT DEFAULT NULL, id_part_unit INT DEFAULT NULL, id_manufacturer INT DEFAULT NULL, order_orderdetails_id INT DEFAULT NULL, built_project_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)'); + $this->addSql('CREATE TABLE "pricedetails" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, price NUMERIC(11, 5) NOT NULL, price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, id_currency INT DEFAULT NULL, orderdetails_id INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON "pricedetails" (id_currency)'); + $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON "pricedetails" (orderdetails_id)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount ON "pricedetails" (min_discount_quantity)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON "pricedetails" (min_discount_quantity, price_related_quantity)'); + $this->addSql('CREATE TABLE project_bom_entries (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames TEXT NOT NULL, name VARCHAR(255) DEFAULT NULL, comment TEXT NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, id_device INT DEFAULT NULL, id_part INT DEFAULT NULL, price_currency_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)'); + $this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)'); + $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); + $this->addSql('CREATE TABLE projects (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, order_quantity INT NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts BOOLEAN NOT NULL, description TEXT NOT NULL, parent_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)'); + $this->addSql('CREATE INDEX IDX_5C93B3A4EA7100A1 ON projects (id_preview_attachment)'); + $this->addSql('CREATE TABLE "storelocations" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, is_full BOOLEAN NOT NULL, only_single_part BOOLEAN NOT NULL, limit_to_existing_parts BOOLEAN NOT NULL, part_owner_must_match BOOLEAN DEFAULT false NOT NULL, parent_id INT DEFAULT NULL, storage_type_id INT DEFAULT NULL, id_owner INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_7517020727ACA70 ON "storelocations" (parent_id)'); + $this->addSql('CREATE INDEX IDX_7517020B270BFF1 ON "storelocations" (storage_type_id)'); + $this->addSql('CREATE INDEX IDX_751702021E5A74C ON "storelocations" (id_owner)'); + $this->addSql('CREATE INDEX IDX_7517020EA7100A1 ON "storelocations" (id_preview_attachment)'); + $this->addSql('CREATE INDEX location_idx_name ON "storelocations" (name)'); + $this->addSql('CREATE INDEX location_idx_parent_name ON "storelocations" (parent_id, name)'); + $this->addSql('CREATE TABLE "suppliers" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, comment TEXT NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names TEXT DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL, parent_id INT DEFAULT NULL, default_currency_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON "suppliers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON "suppliers" (default_currency_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON "suppliers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX supplier_idx_name ON "suppliers" (name)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON "suppliers" (parent_id, name)'); + $this->addSql('CREATE TABLE u2f_keys (key_handle VARCHAR(128) NOT NULL, public_key VARCHAR(255) NOT NULL, certificate TEXT NOT NULL, counter VARCHAR(255) NOT NULL, id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4F4ADB4BA76ED395 ON u2f_keys (user_id)'); + $this->addSql('CREATE UNIQUE INDEX user_unique ON u2f_keys (user_id, key_handle)'); + $this->addSql('CREATE TABLE "users" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a TEXT NOT NULL, config_instock_comment_w TEXT NOT NULL, about_me TEXT NOT NULL, trusted_device_cookie_version INT NOT NULL, backup_codes JSON NOT NULL, google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, show_email_on_profile BOOLEAN DEFAULT false NOT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, settings JSON NOT NULL, backup_codes_generation_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, pw_reset_expires TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, saml_user BOOLEAN NOT NULL, name VARCHAR(180) NOT NULL, permissions_data JSON NOT NULL, group_id INT DEFAULT NULL, id_preview_attachment INT DEFAULT NULL, currency_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON "users" (name)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON "users" (group_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON "users" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON "users" (currency_id)'); + $this->addSql('CREATE INDEX user_idx_username ON "users" (name)'); + $this->addSql('CREATE TABLE webauthn_keys (public_key_credential_id TEXT NOT NULL, type VARCHAR(255) NOT NULL, transports TEXT NOT NULL, attestation_type VARCHAR(255) NOT NULL, trust_path JSON NOT NULL, aaguid TEXT NOT NULL, credential_public_key TEXT NOT NULL, user_handle VARCHAR(255) NOT NULL, counter INT NOT NULL, other_ui TEXT DEFAULT NULL, backup_eligible BOOLEAN DEFAULT NULL, backup_status BOOLEAN DEFAULT NULL, uv_initialized BOOLEAN DEFAULT NULL, id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, last_time_used TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + $this->addSql('ALTER TABLE api_tokens ADD CONSTRAINT FK_2CAD560EA76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "attachment_types" ADD CONSTRAINT FK_EFAED719727ACA70 FOREIGN KEY (parent_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "attachment_types" ADD CONSTRAINT FK_EFAED719EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "attachments" ADD CONSTRAINT FK_47C4FAD6C54C8C93 FOREIGN KEY (type_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "categories" ADD CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "categories" ADD CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE currencies ADD CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE currencies ADD CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "footprints" ADD CONSTRAINT FK_A34D68A2727ACA70 FOREIGN KEY (parent_id) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "footprints" ADD CONSTRAINT FK_A34D68A2EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "footprints" ADD CONSTRAINT FK_A34D68A232A38C34 FOREIGN KEY (id_footprint_3d) REFERENCES "attachments" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "groups" ADD CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "groups" ADD CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE label_profiles ADD CONSTRAINT FK_C93E9CF5EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE log ADD CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "manufacturers" ADD CONSTRAINT FK_94565B12727ACA70 FOREIGN KEY (parent_id) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "manufacturers" ADD CONSTRAINT FK_94565B12EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "measurement_units" ADD CONSTRAINT FK_F5AF83CF727ACA70 FOREIGN KEY (parent_id) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "measurement_units" ADD CONSTRAINT FK_F5AF83CFEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "orderdetails" ADD CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "orderdetails" ADD CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE part_association ADD CONSTRAINT FK_61B952E07E3C61F9 FOREIGN KEY (owner_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE part_association ADD CONSTRAINT FK_61B952E0998D9879 FOREIGN KEY (other_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE part_lots ADD CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE part_lots ADD CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE part_lots ADD CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "parts" ADD CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "parts" ADD CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "parts" ADD CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "parts" ADD CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "parts" ADD CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "parts" ADD CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "parts" ADD CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "pricedetails" ADD CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "pricedetails" ADD CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES "orderdetails" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE project_bom_entries ADD CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE project_bom_entries ADD CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE project_bom_entries ADD CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE projects ADD CONSTRAINT FK_5C93B3A4727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE projects ADD CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "storelocations" ADD CONSTRAINT FK_7517020727ACA70 FOREIGN KEY (parent_id) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "storelocations" ADD CONSTRAINT FK_7517020B270BFF1 FOREIGN KEY (storage_type_id) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "storelocations" ADD CONSTRAINT FK_751702021E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "storelocations" ADD CONSTRAINT FK_7517020EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "suppliers" ADD CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "suppliers" ADD CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "suppliers" ADD CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE u2f_keys ADD CONSTRAINT FK_4F4ADB4BA76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "users" ADD CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "users" ADD CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "users" ADD CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE webauthn_keys ADD CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + + //Create the initial groups and users + //Retrieve the json representations of the presets + $admin = $this->getJSONPermDataFromPreset(PermissionPresetsHelper::PRESET_ADMIN); + $editor = $this->getJSONPermDataFromPreset(PermissionPresetsHelper::PRESET_EDITOR); + $read_only = $this->getJSONPermDataFromPreset(PermissionPresetsHelper::PRESET_READ_ONLY); + + + $sql = <<addSql($sql); + + //Increase the sequence for the groups, to avoid conflicts later + $this->addSql('SELECT setval(\'groups_id_seq\', 4)'); + + + $admin_pw = $this->getInitalAdminPW(); + + $sql = <<addSql($sql); + + //Increase the sequence for the users, to avoid conflicts later + $this->addSql('SELECT setval(\'users_id_seq\', 3)'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE api_tokens DROP CONSTRAINT FK_2CAD560EA76ED395'); + $this->addSql('ALTER TABLE "attachment_types" DROP CONSTRAINT FK_EFAED719727ACA70'); + $this->addSql('ALTER TABLE "attachment_types" DROP CONSTRAINT FK_EFAED719EA7100A1'); + $this->addSql('ALTER TABLE "attachments" DROP CONSTRAINT FK_47C4FAD6C54C8C93'); + $this->addSql('ALTER TABLE "categories" DROP CONSTRAINT FK_3AF34668727ACA70'); + $this->addSql('ALTER TABLE "categories" DROP CONSTRAINT FK_3AF34668EA7100A1'); + $this->addSql('ALTER TABLE currencies DROP CONSTRAINT FK_37C44693727ACA70'); + $this->addSql('ALTER TABLE currencies DROP CONSTRAINT FK_37C44693EA7100A1'); + $this->addSql('ALTER TABLE "footprints" DROP CONSTRAINT FK_A34D68A2727ACA70'); + $this->addSql('ALTER TABLE "footprints" DROP CONSTRAINT FK_A34D68A2EA7100A1'); + $this->addSql('ALTER TABLE "footprints" DROP CONSTRAINT FK_A34D68A232A38C34'); + $this->addSql('ALTER TABLE "groups" DROP CONSTRAINT FK_F06D3970727ACA70'); + $this->addSql('ALTER TABLE "groups" DROP CONSTRAINT FK_F06D3970EA7100A1'); + $this->addSql('ALTER TABLE label_profiles DROP CONSTRAINT FK_C93E9CF5EA7100A1'); + $this->addSql('ALTER TABLE log DROP CONSTRAINT FK_8F3F68C56B3CA4B'); + $this->addSql('ALTER TABLE "manufacturers" DROP CONSTRAINT FK_94565B12727ACA70'); + $this->addSql('ALTER TABLE "manufacturers" DROP CONSTRAINT FK_94565B12EA7100A1'); + $this->addSql('ALTER TABLE "measurement_units" DROP CONSTRAINT FK_F5AF83CF727ACA70'); + $this->addSql('ALTER TABLE "measurement_units" DROP CONSTRAINT FK_F5AF83CFEA7100A1'); + $this->addSql('ALTER TABLE "orderdetails" DROP CONSTRAINT FK_489AFCDC4CE34BEC'); + $this->addSql('ALTER TABLE "orderdetails" DROP CONSTRAINT FK_489AFCDCCBF180EB'); + $this->addSql('ALTER TABLE part_association DROP CONSTRAINT FK_61B952E07E3C61F9'); + $this->addSql('ALTER TABLE part_association DROP CONSTRAINT FK_61B952E0998D9879'); + $this->addSql('ALTER TABLE part_lots DROP CONSTRAINT FK_EBC8F9435D8F4B37'); + $this->addSql('ALTER TABLE part_lots DROP CONSTRAINT FK_EBC8F943C22F6CC4'); + $this->addSql('ALTER TABLE part_lots DROP CONSTRAINT FK_EBC8F94321E5A74C'); + $this->addSql('ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FEEA7100A1'); + $this->addSql('ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FE5697F554'); + $this->addSql('ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FE7E371A10'); + $this->addSql('ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FE2626CEF9'); + $this->addSql('ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FE1ECB93AE'); + $this->addSql('ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FE81081E9B'); + $this->addSql('ALTER TABLE "parts" DROP CONSTRAINT FK_6940A7FEE8AE70D9'); + $this->addSql('ALTER TABLE "pricedetails" DROP CONSTRAINT FK_C68C4459398D64AA'); + $this->addSql('ALTER TABLE "pricedetails" DROP CONSTRAINT FK_C68C44594A01DDC7'); + $this->addSql('ALTER TABLE project_bom_entries DROP CONSTRAINT FK_1AA2DD312F180363'); + $this->addSql('ALTER TABLE project_bom_entries DROP CONSTRAINT FK_1AA2DD31C22F6CC4'); + $this->addSql('ALTER TABLE project_bom_entries DROP CONSTRAINT FK_1AA2DD313FFDCD60'); + $this->addSql('ALTER TABLE projects DROP CONSTRAINT FK_5C93B3A4727ACA70'); + $this->addSql('ALTER TABLE projects DROP CONSTRAINT FK_5C93B3A4EA7100A1'); + $this->addSql('ALTER TABLE "storelocations" DROP CONSTRAINT FK_7517020727ACA70'); + $this->addSql('ALTER TABLE "storelocations" DROP CONSTRAINT FK_7517020B270BFF1'); + $this->addSql('ALTER TABLE "storelocations" DROP CONSTRAINT FK_751702021E5A74C'); + $this->addSql('ALTER TABLE "storelocations" DROP CONSTRAINT FK_7517020EA7100A1'); + $this->addSql('ALTER TABLE "suppliers" DROP CONSTRAINT FK_AC28B95C727ACA70'); + $this->addSql('ALTER TABLE "suppliers" DROP CONSTRAINT FK_AC28B95CECD792C0'); + $this->addSql('ALTER TABLE "suppliers" DROP CONSTRAINT FK_AC28B95CEA7100A1'); + $this->addSql('ALTER TABLE u2f_keys DROP CONSTRAINT FK_4F4ADB4BA76ED395'); + $this->addSql('ALTER TABLE "users" DROP CONSTRAINT FK_1483A5E9FE54D947'); + $this->addSql('ALTER TABLE "users" DROP CONSTRAINT FK_1483A5E9EA7100A1'); + $this->addSql('ALTER TABLE "users" DROP CONSTRAINT FK_1483A5E938248176'); + $this->addSql('ALTER TABLE webauthn_keys DROP CONSTRAINT FK_799FD143A76ED395'); + $this->addSql('DROP TABLE api_tokens'); + $this->addSql('DROP TABLE "attachment_types"'); + $this->addSql('DROP TABLE "attachments"'); + $this->addSql('DROP TABLE "categories"'); + $this->addSql('DROP TABLE currencies'); + $this->addSql('DROP TABLE "footprints"'); + $this->addSql('DROP TABLE "groups"'); + $this->addSql('DROP TABLE label_profiles'); + $this->addSql('DROP TABLE log'); + $this->addSql('DROP TABLE "manufacturers"'); + $this->addSql('DROP TABLE "measurement_units"'); + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('DROP TABLE "orderdetails"'); + $this->addSql('DROP TABLE parameters'); + $this->addSql('DROP TABLE part_association'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('DROP TABLE "parts"'); + $this->addSql('DROP TABLE "pricedetails"'); + $this->addSql('DROP TABLE project_bom_entries'); + $this->addSql('DROP TABLE projects'); + $this->addSql('DROP TABLE "storelocations"'); + $this->addSql('DROP TABLE "suppliers"'); + $this->addSql('DROP TABLE u2f_keys'); + $this->addSql('DROP TABLE "users"'); + $this->addSql('DROP TABLE webauthn_keys'); + } + + public function mySQLUp(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE currencies CHANGE exchange_rate exchange_rate NUMERIC(11, 5) DEFAULT NULL'); + //Set empty JSON fields to "{}" to avoid issues with failing JSON validation + $this->addSql('UPDATE `groups` SET permissions_data = "{}" WHERE permissions_data = ""'); + $this->addSql('ALTER TABLE `groups` CHANGE permissions_data permissions_data JSON NOT NULL'); + //Set the empty JSON fields to "{}" to avoid issues with failing JSON validation + $this->addSql('UPDATE `log` SET extra = "{}" WHERE extra = ""'); + $this->addSql('ALTER TABLE `log` CHANGE level level TINYINT NOT NULL, CHANGE extra extra JSON NOT NULL'); + $this->addSql('ALTER TABLE oauth_tokens CHANGE expires_at expires_at DATETIME DEFAULT NULL'); + $this->addSql('ALTER TABLE pricedetails CHANGE price price NUMERIC(11, 5) NOT NULL'); + $this->addSql('ALTER TABLE project_bom_entries CHANGE price price NUMERIC(11, 5) DEFAULT NULL'); + $this->addSql('ALTER TABLE suppliers CHANGE shipping_costs shipping_costs NUMERIC(11, 5) DEFAULT NULL'); + //Set the empty JSON fields to "{}" to avoid issues with failing JSON validation + $this->addSql('UPDATE `users` SET settings = "{}" WHERE settings = ""'); + $this->addSql('UPDATE `users` SET backup_codes = "{}" WHERE backup_codes = ""'); + $this->addSql('UPDATE `users` SET permissions_data = "{}" WHERE permissions_data = ""'); + $this->addSql('ALTER TABLE `users` CHANGE settings settings JSON NOT NULL, CHANGE backup_codes backup_codes JSON NOT NULL, CHANGE permissions_data permissions_data JSON NOT NULL'); + $this->addSql('ALTER TABLE webauthn_keys CHANGE public_key_credential_id public_key_credential_id LONGTEXT NOT NULL, CHANGE transports transports LONGTEXT NOT NULL, CHANGE trust_path trust_path JSON NOT NULL, CHANGE aaguid aaguid TINYTEXT NOT NULL, CHANGE credential_public_key credential_public_key LONGTEXT NOT NULL, CHANGE other_ui other_ui LONGTEXT DEFAULT NULL, CHANGE last_time_used last_time_used DATETIME DEFAULT NULL'); + + // Add the natural sort emulation function to the database (based on this stackoverflow: https://stackoverflow.com/questions/153633/natural-sort-in-mysql/58154535#58154535) + //This version here is wrong, and will be replaced by the correct version in the next migration (we need to use nowdoc instead of heredoc, otherwise the slashes will be wrongly escaped!!) + $this->addSql(<<0, only the first n numbers in the input string will be converted for nat-sort (so strings that differ only after the first n numbers will not nat-sort amongst themselves). + Total sort-ordering is preserved, i.e. if s1!=s2, then NatSortKey(s1,n)!=NatSortKey(s2,n), for any given n. + Numbers may contain ',' as a thousands separator, and '.' as a decimal point. To reverse these (as appropriate for some European locales), the code would require modification. + Numbers preceded by '+' sort with numbers not preceded with either a '+' or '-' sign. + Negative numbers (preceded with '-') sort before positive numbers, but are sorted in order of ascending absolute value (so -7 sorts BEFORE -1001). + Numbers with leading zeros sort after the same number with no (or fewer) leading zeros. + Decimal-part-only numbers (like .75) are recognised, provided the decimal point is not immediately preceded by either another '.', or by a letter-type character. + Numbers with thousand separators sort after the same number without them. + Thousand separators are only recognised in numbers with no leading zeros that don't immediately follow a ',', and when they format the number correctly. + (When not recognised as a thousand separator, a ',' will instead be treated as separating two distinct numbers). + Version-number-like sequences consisting of 3 or more numbers separated by '.' are treated as distinct entities, and each component number will be nat-sorted. + The entire entity will sort after any number beginning with the first component (so e.g. 10.2.1 sorts after both 10 and 10.995, but before 11) + Note that The first number component in an entity like this is also permitted to contain thousand separators. + + To achieve this, numbers within the input string are prefixed and suffixed according to the following format: + - The number is prefixed by a 2-digit base-36 number representing its length, excluding leading zeros. If there is a decimal point, this length only includes the integer part of the number. + - A 3-character suffix is appended after the number (after the decimals if present). + - The first character is a space, or a '+' sign if the number was preceded by '+'. Any preceding '+' sign is also removed from the front of the number. + - This is followed by a 2-digit base-36 number that encodes the number of leading zeros and whether the number was expressed in comma-separated form (e.g. 1,000,000.25 vs 1000000.25) + - The value of this 2-digit number is: (number of leading zeros)*2 + (1 if comma-separated, 0 otherwise) + - For version number sequences, each component number has the prefix in front of it, and the separating dots are removed. + Then there is a single suffix that consists of a ' ' or '+' character, followed by a pair base-36 digits for each number component in the sequence. + + e.g. here is how some simple sample strings get converted: + 'Foo055' --> 'Foo0255 02' + 'Absolute zero is around -273 centigrade' --> 'Absolute zero is around -03273 00 centigrade' + 'The $1,000,000 prize' --> 'The $071000000 01 prize' + '+99.74 degrees' --> '0299.74+00 degrees' + 'I have 0 apples' --> 'I have 00 02 apples' + '.5 is the same value as 0000.5000' --> '00.5 00 is the same value as 00.5000 08' + 'MariaDB v10.3.0018' --> 'MariaDB v02100130218 000004' + + The restriction to numbers of up to 359 digits comes from the fact that the first character of the base-36 prefix MUST be a decimal digit, and so the highest permitted prefix value is '9Z' or 359 decimal. + The code could be modified to handle longer numbers by increasing the size of (both) the prefix and suffix. + A higher base could also be used (by replacing CONV() with a custom function), provided that the collation you are using sorts the "digits" of the base in the correct order, starting with 0123456789. + However, while the maximum number length may be increased this way, note that the technique this function uses is NOT applicable where strings may contain numbers of unlimited length. + + The function definition does not specify the charset or collation to be used for string-type parameters or variables: The default database charset & collation at the time the function is defined will be used. + This is to make the function code more portable. However, there are some important restrictions: + + - Collation is important here only when comparing (or storing) the output value from this function, but it MUST order the characters " +0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" in that order for the natural sort to work. + This is true for most collations, but not all of them, e.g. in Lithuanian 'Y' comes before 'J' (according to Wikipedia). + To adapt the function to work with such collations, replace CONV() in the function code with a custom function that emits "digits" above 9 that are characters ordered according to the collation in use. + + - For efficiency, the function code uses LENGTH() rather than CHAR_LENGTH() to measure the length of strings that consist only of digits 0-9, '.', and ',' characters. + This works for any single-byte charset, as well as any charset that maps standard ASCII characters to single bytes (such as utf8 or utf8mb4). + If using a charset that maps these characters to multiple bytes (such as, e.g. utf16 or utf32), you MUST replace all instances of LENGTH() in the function definition with CHAR_LENGTH() + + Length of the output: + + Each number converted adds 5 characters (2 prefix + 3 suffix) to the length of the string. n is the maximum count of numbers to convert; + This parameter is provided as a means to limit the maximum output length (to input length + 5*n). + If you do not require the total-ordering property, you could edit the code to use suffixes of 1 character (space or plus) only; this would reduce the maximum output length for any given n. + Since a string of length L has at most ((L+1) DIV 2) individual numbers in it (every 2nd character a digit), for n<=0 the maximum output length is (inputlength + 5*((inputlength+1) DIV 2)) + So for the current input length of 100, the maximum output length is 350. + If changing the input length, the output length must be modified according to the above formula. The DECLARE statements for x,y,r, and suf must also be modified, as the code comments indicate. + ****/ + + DECLARE x,y varchar(1000); # need to be same length as input s + DECLARE r varchar(3500) DEFAULT ''; # return value: needs to be same length as return type + DECLARE suf varchar(1001); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components) + DECLARE i,j,k int UNSIGNED; + IF n<=0 THEN SET n := -1; END IF; # n<=0 means "process all numbers" + LOOP + SET i := REGEXP_INSTR(s,'\\d'); # find position of next digit + IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF; # no more numbers to process -> we're done + SET n := n-1, suf := ' '; + IF i>1 THEN + IF SUBSTRING(s,i-1,1)='.' AND (i=2 OR SUBSTRING(s,i-2,1) RLIKE '[^.\\p{L}\\p{N}\\p{M}\\x{608}\\x{200C}\\x{200D}\\x{2100}-\\x{214F}\\x{24B6}-\\x{24E9}\\x{1F130}-\\x{1F149}\\x{1F150}-\\x{1F169}\\x{1F170}-\\x{1F189}]') AND (SUBSTRING(s,i) NOT RLIKE '^\\d++\\.\\d') THEN SET i:=i-1; END IF; # Allow decimal number (but not version string) to begin with a '.', provided preceding char is neither another '.', nor a member of the unicode character classes: "Alphabetic", "Letter", "Block=Letterlike Symbols" "Number", "Mark", "Join_Control" + IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF; # move any preceding '+' into the suffix, so equal numbers with and without preceding "+" signs sort together + SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i); # add everything before the number to r and strip it from the start of s; preceding '+' is dropped (not included in either r or s) + END IF; + SET x := REGEXP_SUBSTR(s,IF(SUBSTRING(s,1,1) IN ('0','.') OR (SUBSTRING(r,-1)=',' AND suf=' '),'^\\d*+(?:\\.\\d++)*','^(?:[1-9]\\d{0,2}(?:,\\d{3}(?!\\d))++|\\d++)(?:\\.\\d++)*+')); # capture the number + following decimals (including multiple consecutive '.' sequences) + SET s := SUBSTRING(s,CHAR_LENGTH(x)+1); # NOTE: CHAR_LENGTH() can be safely used instead of CHAR_LENGTH() here & below PROVIDED we're using a charset that represents digits, ',' and '.' characters using single bytes (e.g. latin1, utf8) + SET i := INSTR(x,'.'); + IF i=0 THEN SET y := ''; ELSE SET y := SUBSTRING(x,i); SET x := SUBSTRING(x,1,i-1); END IF; # move any following decimals into y + SET i := CHAR_LENGTH(x); + SET x := REPLACE(x,',',''); + SET j := CHAR_LENGTH(x); + SET x := TRIM(LEADING '0' FROM x); # strip leading zeros + SET k := CHAR_LENGTH(x); + SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0')); # (j-k)*2 + IF(i=j,0,1) = (count of leading zeros)*2 + (1 if there are thousands-separators, 0 otherwise) Note the first term is bounded to <= base-36 'ZY' as it must fit within 2 characters + SET i := LOCATE('.',y,2); + IF i=0 THEN + SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x,y,suf); # k = count of digits in number, bounded to be <= '9Z' base-36 + ELSE # encode a version number (like 3.12.707, etc) + SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36 + WHILE CHAR_LENGTH(y)>0 AND n!=0 DO + IF i=0 THEN SET x := SUBSTRING(y,2); SET y := ''; ELSE SET x := SUBSTRING(y,2,i-2); SET y := SUBSTRING(y,i); SET i := LOCATE('.',y,2); END IF; + SET j := CHAR_LENGTH(x); + SET x := TRIM(LEADING '0' FROM x); # strip leading zeros + SET k := CHAR_LENGTH(x); + SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36 + SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0')); # (j-k)*2 = (count of leading zeros)*2, bounded to fit within 2 base-36 digits + SET n := n-1; + END WHILE; + SET r := CONCAT(r,y,suf); + END IF; + END LOOP; + END + EOD + ); + + } + + public function mySQLDown(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE currencies CHANGE exchange_rate exchange_rate NUMERIC(11, 5) DEFAULT NULL COMMENT \'(DC2Type:big_decimal)\''); + $this->addSql('ALTER TABLE `groups` CHANGE permissions_data permissions_data LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\''); + $this->addSql('ALTER TABLE log CHANGE level level TINYINT(1) NOT NULL COMMENT \'(DC2Type:tinyint)\', CHANGE extra extra LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\''); + $this->addSql('ALTER TABLE oauth_tokens CHANGE expires_at expires_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE `pricedetails` CHANGE price price NUMERIC(11, 5) NOT NULL COMMENT \'(DC2Type:big_decimal)\''); + $this->addSql('ALTER TABLE project_bom_entries CHANGE price price NUMERIC(11, 5) DEFAULT NULL COMMENT \'(DC2Type:big_decimal)\''); + $this->addSql('ALTER TABLE `suppliers` CHANGE shipping_costs shipping_costs NUMERIC(11, 5) DEFAULT NULL COMMENT \'(DC2Type:big_decimal)\''); + $this->addSql('ALTER TABLE `users` CHANGE backup_codes backup_codes LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', CHANGE settings settings LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', CHANGE permissions_data permissions_data LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\''); + $this->addSql('ALTER TABLE webauthn_keys CHANGE public_key_credential_id public_key_credential_id LONGTEXT NOT NULL COMMENT \'(DC2Type:base64)\', CHANGE transports transports LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE trust_path trust_path LONGTEXT NOT NULL COMMENT \'(DC2Type:trust_path)\', CHANGE aaguid aaguid TINYTEXT NOT NULL COMMENT \'(DC2Type:aaguid)\', CHANGE credential_public_key credential_public_key LONGTEXT NOT NULL COMMENT \'(DC2Type:base64)\', CHANGE other_ui other_ui LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:array)\', CHANGE last_time_used last_time_used DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + + //Drop custom function + $this->addSql('DROP FUNCTION IF EXISTS NatSortKey'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__currencies AS SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM currencies'); + $this->addSql('DROP TABLE currencies'); + $this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL, iso_code VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO currencies (id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added, alternative_names) SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM __temp__currencies'); + $this->addSql('DROP TABLE __temp__currencies'); + $this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)'); + $this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)'); + $this->addSql('CREATE INDEX currency_idx_name ON currencies (name)'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__groups AS SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data, alternative_names FROM groups'); + $this->addSql('DROP TABLE groups'); + $this->addSql('CREATE TABLE groups (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO groups (id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data, alternative_names) SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data, alternative_names FROM __temp__groups'); + $this->addSql('DROP TABLE __temp__groups'); + $this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON groups (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON groups (parent_id)'); + $this->addSql('CREATE INDEX group_idx_name ON groups (name)'); + $this->addSql('CREATE INDEX group_idx_parent_name ON groups (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM log'); + $this->addSql('DROP TABLE log'); + $this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_user INTEGER DEFAULT NULL, datetime DATETIME NOT NULL, level SMALLINT NOT NULL, target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL, type SMALLINT NOT NULL, username VARCHAR(255) NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO log (id, id_user, datetime, level, target_id, target_type, extra, type, username) SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM __temp__log'); + $this->addSql('DROP TABLE __temp__log'); + $this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)'); + $this->addSql('CREATE INDEX log_idx_type ON log (type)'); + $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); + $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__oauth_tokens AS SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM oauth_tokens'); + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, token CLOB DEFAULT NULL, expires_at DATETIME DEFAULT NULL, refresh_token CLOB DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL)'); + $this->addSql('INSERT INTO oauth_tokens (id, token, expires_at, refresh_token, name, last_modified, datetime_added) SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM __temp__oauth_tokens'); + $this->addSql('DROP TABLE __temp__oauth_tokens'); + $this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__pricedetails AS SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM pricedetails'); + $this->addSql('DROP TABLE pricedetails'); + $this->addSql('CREATE TABLE pricedetails (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, price NUMERIC(11, 5) NOT NULL, price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO pricedetails (id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added) SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM __temp__pricedetails'); + $this->addSql('DROP TABLE __temp__pricedetails'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON pricedetails (min_discount_quantity, price_related_quantity)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount ON pricedetails (min_discount_quantity)'); + $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON pricedetails (id_currency)'); + $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON pricedetails (orderdetails_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM project_bom_entries'); + $this->addSql('DROP TABLE project_bom_entries'); + $this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AFC547992F180363 FOREIGN KEY (id_device) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AFC54799C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO project_bom_entries (id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added) SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM __temp__project_bom_entries'); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); + $this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)'); + $this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM suppliers'); + $this->addSql('DROP TABLE suppliers'); + $this->addSql('CREATE TABLE suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO suppliers (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added, alternative_names FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON suppliers (id_preview_attachment)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON suppliers (parent_id, name)'); + $this->addSql('CREATE INDEX supplier_idx_name ON suppliers (name)'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON suppliers (default_currency_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile FROM users'); + $this->addSql('DROP TABLE users'); + $this->addSql('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL, google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL, backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL, saml_user BOOLEAN NOT NULL, about_me CLOB NOT NULL, show_email_on_profile BOOLEAN DEFAULT 0 NOT NULL, CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO users (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile FROM __temp__users'); + $this->addSql('DROP TABLE __temp__users'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON users (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON users (currency_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON users (group_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON users (name)'); + $this->addSql('CREATE INDEX user_idx_username ON users (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui, backup_eligible, backup_status, uv_initialized, last_time_used FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL, type VARCHAR(255) NOT NULL, transports CLOB NOT NULL, attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL, aaguid CLOB NOT NULL, credential_public_key CLOB NOT NULL, user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, other_ui CLOB DEFAULT NULL, backup_eligible BOOLEAN DEFAULT NULL, backup_status BOOLEAN DEFAULT NULL, uv_initialized BOOLEAN DEFAULT NULL, last_time_used DATETIME DEFAULT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui, backup_eligible, backup_status, uv_initialized, last_time_used) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui, backup_eligible, backup_status, uv_initialized, last_time_used FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__currencies AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, exchange_rate, iso_code, parent_id, id_preview_attachment FROM currencies'); + $this->addSql('DROP TABLE currencies'); + $this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , iso_code VARCHAR(255) NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO currencies (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, exchange_rate, iso_code, parent_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, exchange_rate, iso_code, parent_id, id_preview_attachment FROM __temp__currencies'); + $this->addSql('DROP TABLE __temp__currencies'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)'); + $this->addSql('CREATE INDEX currency_idx_name ON currencies (name)'); + $this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__groups AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, enforce_2fa, permissions_data, parent_id, id_preview_attachment FROM "groups"'); + $this->addSql('DROP TABLE "groups"'); + $this->addSql('CREATE TABLE "groups" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, permissions_data CLOB NOT NULL --(DC2Type:json) + , parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "groups" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, enforce_2fa, permissions_data, parent_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, enforce_2fa, permissions_data, parent_id, id_preview_attachment FROM __temp__groups'); + $this->addSql('DROP TABLE __temp__groups'); + $this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON "groups" (parent_id)'); + $this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON "groups" (id_preview_attachment)'); + $this->addSql('CREATE INDEX group_idx_name ON "groups" (name)'); + $this->addSql('CREATE INDEX group_idx_parent_name ON "groups" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, username, datetime, level, target_id, target_type, extra, id_user, type FROM log'); + $this->addSql('DROP TABLE log'); + $this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(255) NOT NULL, datetime DATETIME NOT NULL, level BOOLEAN NOT NULL --(DC2Type:tinyint) + , target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL --(DC2Type:json) + , id_user INTEGER DEFAULT NULL, type SMALLINT NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO log (id, username, datetime, level, target_id, target_type, extra, id_user, type) SELECT id, username, datetime, level, target_id, target_type, extra, id_user, type FROM __temp__log'); + $this->addSql('DROP TABLE __temp__log'); + $this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)'); + $this->addSql('CREATE INDEX log_idx_type ON log (type)'); + $this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)'); + $this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__oauth_tokens AS SELECT id, name, last_modified, datetime_added, token, expires_at, refresh_token FROM oauth_tokens'); + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, token CLOB DEFAULT NULL, expires_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) + , refresh_token CLOB DEFAULT NULL)'); + $this->addSql('INSERT INTO oauth_tokens (id, name, last_modified, datetime_added, token, expires_at, refresh_token) SELECT id, name, last_modified, datetime_added, token, expires_at, refresh_token FROM __temp__oauth_tokens'); + $this->addSql('DROP TABLE __temp__oauth_tokens'); + $this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__pricedetails AS SELECT id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added, id_currency, orderdetails_id FROM "pricedetails"'); + $this->addSql('DROP TABLE "pricedetails"'); + $this->addSql('CREATE TABLE "pricedetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, price NUMERIC(11, 5) NOT NULL --(DC2Type:big_decimal) + , price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES "orderdetails" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "pricedetails" (id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added, id_currency, orderdetails_id) SELECT id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added, id_currency, orderdetails_id FROM __temp__pricedetails'); + $this->addSql('DROP TABLE __temp__pricedetails'); + $this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON "pricedetails" (id_currency)'); + $this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON "pricedetails" (orderdetails_id)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount ON "pricedetails" (min_discount_quantity)'); + $this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON "pricedetails" (min_discount_quantity, price_related_quantity)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, quantity, mountnames, name, comment, price, last_modified, datetime_added, id_device, id_part, price_currency_id FROM project_bom_entries'); + $this->addSql('DROP TABLE project_bom_entries'); + $this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO project_bom_entries (id, quantity, mountnames, name, comment, price, last_modified, datetime_added, id_device, id_part, price_currency_id) SELECT id, quantity, mountnames, name, comment, price, last_modified, datetime_added, id_device, id_part, price_currency_id FROM __temp__project_bom_entries'); + $this->addSql('DROP TABLE __temp__project_bom_entries'); + $this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)'); + $this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)'); + $this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment FROM "suppliers"'); + $this->addSql('DROP TABLE "suppliers"'); + $this->addSql('CREATE TABLE "suppliers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal) + , parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "suppliers" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, address, phone_number, fax_number, email_address, website, auto_product_url, shipping_costs, parent_id, default_currency_id, id_preview_attachment FROM __temp__suppliers'); + $this->addSql('DROP TABLE __temp__suppliers'); + $this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON "suppliers" (parent_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON "suppliers" (default_currency_id)'); + $this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON "suppliers" (id_preview_attachment)'); + $this->addSql('CREATE INDEX supplier_idx_name ON "suppliers" (name)'); + $this->addSql('CREATE INDEX supplier_idx_parent_name ON "suppliers" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, last_modified, datetime_added, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, settings, backup_codes_generation_date, pw_reset_expires, saml_user, name, permissions_data, group_id, id_preview_attachment, currency_id FROM "users"'); + $this->addSql('DROP TABLE "users"'); + $this->addSql('CREATE TABLE "users" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, about_me CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL --(DC2Type:json) + , google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, show_email_on_profile BOOLEAN DEFAULT 0 NOT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, settings CLOB NOT NULL --(DC2Type:json) + , backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, saml_user BOOLEAN NOT NULL, name VARCHAR(180) NOT NULL, permissions_data CLOB NOT NULL --(DC2Type:json) + , group_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "users" (id, last_modified, datetime_added, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, settings, backup_codes_generation_date, pw_reset_expires, saml_user, name, permissions_data, group_id, id_preview_attachment, currency_id) SELECT id, last_modified, datetime_added, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, settings, backup_codes_generation_date, pw_reset_expires, saml_user, name, permissions_data, group_id, id_preview_attachment, currency_id FROM __temp__users'); + $this->addSql('DROP TABLE __temp__users'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON "users" (name)'); + $this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON "users" (group_id)'); + $this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON "users" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_1483A5E938248176 ON "users" (currency_id)'); + $this->addSql('CREATE INDEX user_idx_username ON "users" (name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, backup_eligible, backup_status, uv_initialized, id, name, last_time_used, last_modified, datetime_added, user_id FROM webauthn_keys'); + $this->addSql('DROP TABLE webauthn_keys'); + $this->addSql('CREATE TABLE webauthn_keys (public_key_credential_id CLOB NOT NULL --(DC2Type:base64) + , type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array) + , attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path) + , aaguid CLOB NOT NULL --(DC2Type:aaguid) + , credential_public_key CLOB NOT NULL --(DC2Type:base64) + , user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, other_ui CLOB DEFAULT NULL --(DC2Type:array) + , backup_eligible BOOLEAN DEFAULT NULL, backup_status BOOLEAN DEFAULT NULL, uv_initialized BOOLEAN DEFAULT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_time_used DATETIME DEFAULT NULL --(DC2Type:datetime_immutable) + , last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, user_id INTEGER DEFAULT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO webauthn_keys (public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, backup_eligible, backup_status, uv_initialized, id, name, last_time_used, last_modified, datetime_added, user_id) SELECT public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, backup_eligible, backup_status, uv_initialized, id, name, last_time_used, last_modified, datetime_added, user_id FROM __temp__webauthn_keys'); + $this->addSql('DROP TABLE __temp__webauthn_keys'); + $this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)'); + } +} diff --git a/migrations/Version20240728145604.php b/migrations/Version20240728145604.php new file mode 100644 index 00000000..42309d70 --- /dev/null +++ b/migrations/Version20240728145604.php @@ -0,0 +1,165 @@ +addSql('DROP FUNCTION IF EXISTS NatSortKey'); + + //The difference to the original function is the correct length of the suf variable and correct escaping + //We now use heredoc syntax to avoid escaping issues with the \ (which resulted in "range out of order in character class"). + $this->addSql(<<<'EOD' + CREATE DEFINER=CURRENT_USER FUNCTION `NatSortKey`(`s` VARCHAR(1000) CHARSET utf8mb4, `n` INT) RETURNS varchar(3500) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci + DETERMINISTIC + SQL SECURITY INVOKER + BEGIN + /**** + Converts numbers in the input string s into a format such that sorting results in a nat-sort. + Numbers of up to 359 digits (before the decimal point, if one is present) are supported. Sort results are undefined if the input string contains numbers longer than this. + For n>0, only the first n numbers in the input string will be converted for nat-sort (so strings that differ only after the first n numbers will not nat-sort amongst themselves). + Total sort-ordering is preserved, i.e. if s1!=s2, then NatSortKey(s1,n)!=NatSortKey(s2,n), for any given n. + Numbers may contain ',' as a thousands separator, and '.' as a decimal point. To reverse these (as appropriate for some European locales), the code would require modification. + Numbers preceded by '+' sort with numbers not preceded with either a '+' or '-' sign. + Negative numbers (preceded with '-') sort before positive numbers, but are sorted in order of ascending absolute value (so -7 sorts BEFORE -1001). + Numbers with leading zeros sort after the same number with no (or fewer) leading zeros. + Decimal-part-only numbers (like .75) are recognised, provided the decimal point is not immediately preceded by either another '.', or by a letter-type character. + Numbers with thousand separators sort after the same number without them. + Thousand separators are only recognised in numbers with no leading zeros that don't immediately follow a ',', and when they format the number correctly. + (When not recognised as a thousand separator, a ',' will instead be treated as separating two distinct numbers). + Version-number-like sequences consisting of 3 or more numbers separated by '.' are treated as distinct entities, and each component number will be nat-sorted. + The entire entity will sort after any number beginning with the first component (so e.g. 10.2.1 sorts after both 10 and 10.995, but before 11) + Note that The first number component in an entity like this is also permitted to contain thousand separators. + + To achieve this, numbers within the input string are prefixed and suffixed according to the following format: + - The number is prefixed by a 2-digit base-36 number representing its length, excluding leading zeros. If there is a decimal point, this length only includes the integer part of the number. + - A 3-character suffix is appended after the number (after the decimals if present). + - The first character is a space, or a '+' sign if the number was preceded by '+'. Any preceding '+' sign is also removed from the front of the number. + - This is followed by a 2-digit base-36 number that encodes the number of leading zeros and whether the number was expressed in comma-separated form (e.g. 1,000,000.25 vs 1000000.25) + - The value of this 2-digit number is: (number of leading zeros)*2 + (1 if comma-separated, 0 otherwise) + - For version number sequences, each component number has the prefix in front of it, and the separating dots are removed. + Then there is a single suffix that consists of a ' ' or '+' character, followed by a pair base-36 digits for each number component in the sequence. + + e.g. here is how some simple sample strings get converted: + 'Foo055' --> 'Foo0255 02' + 'Absolute zero is around -273 centigrade' --> 'Absolute zero is around -03273 00 centigrade' + 'The $1,000,000 prize' --> 'The $071000000 01 prize' + '+99.74 degrees' --> '0299.74+00 degrees' + 'I have 0 apples' --> 'I have 00 02 apples' + '.5 is the same value as 0000.5000' --> '00.5 00 is the same value as 00.5000 08' + 'MariaDB v10.3.0018' --> 'MariaDB v02100130218 000004' + + The restriction to numbers of up to 359 digits comes from the fact that the first character of the base-36 prefix MUST be a decimal digit, and so the highest permitted prefix value is '9Z' or 359 decimal. + The code could be modified to handle longer numbers by increasing the size of (both) the prefix and suffix. + A higher base could also be used (by replacing CONV() with a custom function), provided that the collation you are using sorts the "digits" of the base in the correct order, starting with 0123456789. + However, while the maximum number length may be increased this way, note that the technique this function uses is NOT applicable where strings may contain numbers of unlimited length. + + The function definition does not specify the charset or collation to be used for string-type parameters or variables: The default database charset & collation at the time the function is defined will be used. + This is to make the function code more portable. However, there are some important restrictions: + + - Collation is important here only when comparing (or storing) the output value from this function, but it MUST order the characters " +0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" in that order for the natural sort to work. + This is true for most collations, but not all of them, e.g. in Lithuanian 'Y' comes before 'J' (according to Wikipedia). + To adapt the function to work with such collations, replace CONV() in the function code with a custom function that emits "digits" above 9 that are characters ordered according to the collation in use. + + - For efficiency, the function code uses LENGTH() rather than CHAR_LENGTH() to measure the length of strings that consist only of digits 0-9, '.', and ',' characters. + This works for any single-byte charset, as well as any charset that maps standard ASCII characters to single bytes (such as utf8 or utf8mb4). + If using a charset that maps these characters to multiple bytes (such as, e.g. utf16 or utf32), you MUST replace all instances of LENGTH() in the function definition with CHAR_LENGTH() + + Length of the output: + + Each number converted adds 5 characters (2 prefix + 3 suffix) to the length of the string. n is the maximum count of numbers to convert; + This parameter is provided as a means to limit the maximum output length (to input length + 5*n). + If you do not require the total-ordering property, you could edit the code to use suffixes of 1 character (space or plus) only; this would reduce the maximum output length for any given n. + Since a string of length L has at most ((L+1) DIV 2) individual numbers in it (every 2nd character a digit), for n<=0 the maximum output length is (inputlength + 5*((inputlength+1) DIV 2)) + So for the current input length of 100, the maximum output length is 350. + If changing the input length, the output length must be modified according to the above formula. The DECLARE statements for x,y,r, and suf must also be modified, as the code comments indicate. + ****/ + + DECLARE x,y varchar(1000); # need to be same length as input s + DECLARE r varchar(3500) DEFAULT ''; # return value: needs to be same length as return type + DECLARE suf varchar(1001); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components) + DECLARE i,j,k int UNSIGNED; + IF n<=0 THEN SET n := -1; END IF; # n<=0 means "process all numbers" + LOOP + SET i := REGEXP_INSTR(s,'\\d'); # find position of next digit + IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF; # no more numbers to process -> we're done + SET n := n-1, suf := ' '; + IF i>1 THEN + IF SUBSTRING(s,i-1,1)='.' AND (i=2 OR SUBSTRING(s,i-2,1) RLIKE '[^.\\p{L}\\p{N}\\p{M}\\x{608}\\x{200C}\\x{200D}\\x{2100}-\\x{214F}\\x{24B6}-\\x{24E9}\\x{1F130}-\\x{1F149}\\x{1F150}-\\x{1F169}\\x{1F170}-\\x{1F189}]') AND (SUBSTRING(s,i) NOT RLIKE '^\\d++\\.\\d') THEN SET i:=i-1; END IF; # Allow decimal number (but not version string) to begin with a '.', provided preceding char is neither another '.', nor a member of the unicode character classes: "Alphabetic", "Letter", "Block=Letterlike Symbols" "Number", "Mark", "Join_Control" + IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF; # move any preceding '+' into the suffix, so equal numbers with and without preceding "+" signs sort together + SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i); # add everything before the number to r and strip it from the start of s; preceding '+' is dropped (not included in either r or s) + END IF; + SET x := REGEXP_SUBSTR(s,IF(SUBSTRING(s,1,1) IN ('0','.') OR (SUBSTRING(r,-1)=',' AND suf=' '),'^\\d*+(?:\\.\\d++)*','^(?:[1-9]\\d{0,2}(?:,\\d{3}(?!\\d))++|\\d++)(?:\\.\\d++)*+')); # capture the number + following decimals (including multiple consecutive '.' sequences) + SET s := SUBSTRING(s,CHAR_LENGTH(x)+1); # NOTE: CHAR_LENGTH() can be safely used instead of CHAR_LENGTH() here & below PROVIDED we're using a charset that represents digits, ',' and '.' characters using single bytes (e.g. latin1, utf8) + SET i := INSTR(x,'.'); + IF i=0 THEN SET y := ''; ELSE SET y := SUBSTRING(x,i); SET x := SUBSTRING(x,1,i-1); END IF; # move any following decimals into y + SET i := CHAR_LENGTH(x); + SET x := REPLACE(x,',',''); + SET j := CHAR_LENGTH(x); + SET x := TRIM(LEADING '0' FROM x); # strip leading zeros + SET k := CHAR_LENGTH(x); + SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0')); # (j-k)*2 + IF(i=j,0,1) = (count of leading zeros)*2 + (1 if there are thousands-separators, 0 otherwise) Note the first term is bounded to <= base-36 'ZY' as it must fit within 2 characters + SET i := LOCATE('.',y,2); + IF i=0 THEN + SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x,y,suf); # k = count of digits in number, bounded to be <= '9Z' base-36 + ELSE # encode a version number (like 3.12.707, etc) + SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36 + WHILE CHAR_LENGTH(y)>0 AND n!=0 DO + IF i=0 THEN SET x := SUBSTRING(y,2); SET y := ''; ELSE SET x := SUBSTRING(y,2,i-2); SET y := SUBSTRING(y,i); SET i := LOCATE('.',y,2); END IF; + SET j := CHAR_LENGTH(x); + SET x := TRIM(LEADING '0' FROM x); # strip leading zeros + SET k := CHAR_LENGTH(x); + SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36 + SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0')); # (j-k)*2 = (count of leading zeros)*2, bounded to fit within 2 base-36 digits + SET n := n-1; + END WHILE; + SET r := CONCAT(r,y,suf); + END IF; + END LOOP; + END + EOD + ); + } + + public function mySQLDown(Schema $schema): void + { + //Not needed + } + + public function sqLiteUp(Schema $schema): void + { + //Not needed + } + + public function sqLiteDown(Schema $schema): void + { + //Not needed + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } +} diff --git a/migrations/Version20250220215048.php b/migrations/Version20250220215048.php new file mode 100644 index 00000000..90a73eb1 --- /dev/null +++ b/migrations/Version20250220215048.php @@ -0,0 +1,42 @@ +addSql('ALTER TABLE attachments ADD internal_path VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE attachments ADD external_path VARCHAR(255) DEFAULT NULL'); + + //Copy the data from path to external_path and remove the path column + $this->addSql('UPDATE attachments SET external_path=path'); + $this->addSql('ALTER TABLE attachments DROP COLUMN path'); + + + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\''); + $this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('UPDATE attachments SET external_path=internal_path WHERE internal_path IS NOT NULL'); + $this->addSql('ALTER TABLE attachments DROP COLUMN internal_path'); + $this->addSql('ALTER TABLE attachments RENAME COLUMN external_path TO path'); + } +} diff --git a/migrations/Version20250222165240.php b/migrations/Version20250222165240.php new file mode 100644 index 00000000..57cd3970 --- /dev/null +++ b/migrations/Version20250222165240.php @@ -0,0 +1,31 @@ +addSql("UPDATE attachments SET class_name = 'Part' WHERE class_name = 'PartDB\Part'"); + $this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = 'PartDB\Device'"); + } + + public function down(Schema $schema): void + { + //No down required, as the new format can also be read by older Part-DB version + } +} diff --git a/package.json b/package.json index ca99967e..38656c72 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,21 @@ "@babel/preset-env": "^7.19.4", "@fortawesome/fontawesome-free": "^6.1.1", "@hotwired/stimulus": "^3.0.0", - "@hotwired/turbo": "^7.0.1", + "@hotwired/turbo": "^8.0.1", "@popperjs/core": "^2.10.2", "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets", "@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets", - "@symfony/webpack-encore": "^4.1.0", + "@symfony/webpack-encore": "^5.0.0", "bootstrap": "^5.1.3", "core-js": "^3.23.0", + "intl-messageformat": "^10.2.5", "jquery": "^3.5.1", "popper.js": "^1.14.7", "regenerator-runtime": "^0.13.9", "webpack": "^5.74.0", "webpack-bundle-analyzer": "^4.3.0", - "webpack-cli": "^4.10.0", + "webpack-cli": "^5.1.0", "webpack-notifier": "^1.15.0" }, "license": "AGPL-3.0-or-later", @@ -28,62 +30,75 @@ "build": "encore production --progress" }, "dependencies": { - "@ckeditor/ckeditor5-alignment": "^36.0.0", - "@ckeditor/ckeditor5-autoformat": "^36.0.0", - "@ckeditor/ckeditor5-basic-styles": "^36.0.0", - "@ckeditor/ckeditor5-block-quote": "^36.0.0", - "@ckeditor/ckeditor5-code-block": "^36.0.0", - "@ckeditor/ckeditor5-dev-utils": "^33.0.0", - "@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13", - "@ckeditor/ckeditor5-editor-classic": "^36.0.0", - "@ckeditor/ckeditor5-essentials": "^36.0.0", - "@ckeditor/ckeditor5-find-and-replace": "^36.0.0", - "@ckeditor/ckeditor5-font": "^36.0.0", - "@ckeditor/ckeditor5-heading": "^36.0.0", - "@ckeditor/ckeditor5-highlight": "^36.0.0", - "@ckeditor/ckeditor5-horizontal-line": "^36.0.0", - "@ckeditor/ckeditor5-html-embed": "^36.0.0", - "@ckeditor/ckeditor5-html-support": "^36.0.0", - "@ckeditor/ckeditor5-image": "^36.0.0", - "@ckeditor/ckeditor5-indent": "^36.0.0", - "@ckeditor/ckeditor5-link": "^36.0.0", - "@ckeditor/ckeditor5-list": "^36.0.0", - "@ckeditor/ckeditor5-markdown-gfm": "^36.0.0", - "@ckeditor/ckeditor5-media-embed": "^36.0.0", - "@ckeditor/ckeditor5-paragraph": "^36.0.0", - "@ckeditor/ckeditor5-paste-from-office": "^36.0.0", - "@ckeditor/ckeditor5-remove-format": "^36.0.0", - "@ckeditor/ckeditor5-source-editing": "^36.0.0", - "@ckeditor/ckeditor5-special-characters": "^36.0.0", - "@ckeditor/ckeditor5-table": "^36.0.0", - "@ckeditor/ckeditor5-theme-lark": "^36.0.0", - "@ckeditor/ckeditor5-upload": "^36.0.0", - "@ckeditor/ckeditor5-watchdog": "^36.0.0", - "@ckeditor/ckeditor5-word-count": "^36.0.0", + "@algolia/autocomplete-js": "^1.17.0", + "@algolia/autocomplete-plugin-recent-searches": "^1.17.0", + "@algolia/autocomplete-theme-classic": "^1.17.0", + "@ckeditor/ckeditor5-alignment": "^44.0.0", + "@ckeditor/ckeditor5-autoformat": "^44.0.0", + "@ckeditor/ckeditor5-basic-styles": "^44.0.0", + "@ckeditor/ckeditor5-block-quote": "^44.0.0", + "@ckeditor/ckeditor5-code-block": "^44.0.0", + "@ckeditor/ckeditor5-dev-translations": "^43.0.1", + "@ckeditor/ckeditor5-dev-utils": "^43.0.1", + "@ckeditor/ckeditor5-editor-classic": "^44.0.0", + "@ckeditor/ckeditor5-essentials": "^44.0.0", + "@ckeditor/ckeditor5-find-and-replace": "^44.0.0", + "@ckeditor/ckeditor5-font": "^44.0.0", + "@ckeditor/ckeditor5-heading": "^44.0.0", + "@ckeditor/ckeditor5-highlight": "^44.0.0", + "@ckeditor/ckeditor5-horizontal-line": "^44.0.0", + "@ckeditor/ckeditor5-html-embed": "^44.0.0", + "@ckeditor/ckeditor5-html-support": "^44.0.0", + "@ckeditor/ckeditor5-image": "^44.0.0", + "@ckeditor/ckeditor5-indent": "^44.0.0", + "@ckeditor/ckeditor5-link": "^44.0.0", + "@ckeditor/ckeditor5-list": "^44.0.0", + "@ckeditor/ckeditor5-markdown-gfm": "^44.0.0", + "@ckeditor/ckeditor5-media-embed": "^44.0.0", + "@ckeditor/ckeditor5-paragraph": "^44.0.0", + "@ckeditor/ckeditor5-paste-from-office": "^44.0.0", + "@ckeditor/ckeditor5-remove-format": "^44.0.0", + "@ckeditor/ckeditor5-source-editing": "^44.0.0", + "@ckeditor/ckeditor5-special-characters": "^44.0.0", + "@ckeditor/ckeditor5-table": "^44.0.0", + "@ckeditor/ckeditor5-theme-lark": "^44.0.0", + "@ckeditor/ckeditor5-upload": "^44.0.0", + "@ckeditor/ckeditor5-watchdog": "^44.0.0", + "@ckeditor/ckeditor5-word-count": "^44.0.0", "@jbtronics/bs-treeview": "^1.0.1", + "@part-db/html5-qrcode": "^3.1.0", + "@zxcvbn-ts/core": "^3.0.2", + "@zxcvbn-ts/language-common": "^3.0.3", + "@zxcvbn-ts/language-de": "^3.0.1", + "@zxcvbn-ts/language-en": "^3.0.1", + "@zxcvbn-ts/language-fr": "^3.0.1", + "@zxcvbn-ts/language-ja": "^3.0.1", + "barcode-detector": "^2.3.1", "bootbox": "^6.0.0", "bootswatch": "^5.1.3", "bs-custom-file-input": "^1.3.4", "clipboard": "^2.0.4", - "compression-webpack-plugin": "^10.0.0", - "darkmode-js": "^1.5.0", - "datatables.net-bs5": "^1.10.20", - "datatables.net-buttons-bs5": "^2.2.2", - "datatables.net-colreorder-bs5": "^1.5.1", - "datatables.net-fixedheader-bs5": "^3.1.5", - "datatables.net-responsive-bs5": "^2.2.3", - "datatables.net-select-bs5": "^1.2.7", - "dompurify": "^2.0.6", - "emoji.json": "^14.0.0", - "exports-loader": "^3.0.0", - "html5-qrcode": "^2.2.1", + "compression-webpack-plugin": "^11.1.0", + "datatables.net": "^2.0.0", + "datatables.net-bs5": "^2.0.0", + "datatables.net-buttons-bs5": "^3.0.0", + "datatables.net-colreorder-bs5": "^2.0.0", + "datatables.net-fixedheader-bs5": "^4.0.0", + "datatables.net-responsive-bs5": "^3.0.0", + "datatables.net-select-bs5": "^2.0.0", + "dompurify": "^3.0.3", + "emoji.json": "^15.0.0", + "exports-loader": "^5.0.0", + "json-formatter-js": "^2.3.4", "jszip": "^3.2.0", "katex": "^0.16.0", - "marked": "^4.0.3", + "marked": "^15.0.4", + "marked-gfm-heading-id": "^4.1.1", + "marked-mangle": "^1.0.1", "pdfmake": "^0.2.2", - "stimulus-use": "^0.51.1", + "stimulus-use": "^0.52.0", "tom-select": "^2.1.0", "ts-loader": "^9.2.6", - "typescript": "^4.0.2" + "typescript": "^5.7.2" } } diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 00000000..fc7b3524 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,63 @@ +parameters: + + level: 5 + + paths: + - src + # - tests + + excludePaths: + - src/DataTables/Adapter/* + - src/Configuration/* + - src/Doctrine/Purger/* + - src/DataTables/Adapters/TwoStepORMAdapter.php + - src/Form/Fixes/* + - src/Translation/Fixes/* + + + + inferPrivatePropertyTypeFromConstructor: true + treatPhpDocTypesAsCertain: false + + symfony: + containerXmlPath: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml' + + doctrine: + objectManagerLoader: tests/object-manager.php + allowNullablePropertyForRequiredField: true + + checkUninitializedProperties: true + + checkFunctionNameCase: false + + reportMaybesInPropertyPhpDocTypes: false + reportMaybesInMethodSignatures: false + + strictRules: + disallowedLooseComparison: false + booleansInConditions: false + uselessCast: false + requireParentConstructorCall: true + overwriteVariablesWithLoop: false + closureUsesThis: false + matchingInheritedMethodNames: true + numericOperandsInArithmeticOperators: true + switchConditionsMatchingType: false + noVariableVariables: false + disallowedEmpty: false + disallowedShortTernary: false + + ignoreErrors: + # Ignore errors caused by complex mapping with AbstractStructuralDBElement + - '#AbstractStructuralDBElement does not have a field named \$parent#' + #- '#AbstractStructuralDBElement does not have a field named \$name#' + + # Ignore errors related to the use of the ParametersTrait in Part entity + - '#expects .*PartParameter, .*AbstractParameter given.#' + - '#Part::getParameters\(\) should return .*AbstractParameter#' + + # Ignore doctrine type mapping mismatch + - '#Property .* type mapping mismatch: property can contain .* but database expects .*#' + + # Ignore error of unused WithPermPresetsTrait, as it is used in the migrations which are not analyzed by Phpstan + - '#Trait App\\Migration\\WithPermPresetsTrait is used zero times and is not analysed#' diff --git a/phpstan.neon b/phpstan.neon deleted file mode 100644 index e0e0e662..00000000 --- a/phpstan.neon +++ /dev/null @@ -1,11 +0,0 @@ -parameters: - inferPrivatePropertyTypeFromConstructor: true - treatPhpDocTypesAsCertain: false - - symfony: - container_xml_path: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml' - - excludes_analyse: - - src/DataTables/Adapter/* - - src/Configuration/* - - src/Doctrine/Purger/* \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 94534e9d..7ee7596f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,27 @@ - - - - src - - + - - + + + + + src + + + tests diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 872169ac..00000000 --- a/psalm.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/.htaccess b/public/.htaccess index ee3b5450..bfaab5de 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -118,3 +118,10 @@ DirectoryIndex index.php # RedirectTemp cannot be used instead + +# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there + + + Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';" + + \ No newline at end of file diff --git a/public/img/default_avatar.png b/public/img/default_avatar.png deleted file mode 100644 index 17d6a106..00000000 Binary files a/public/img/default_avatar.png and /dev/null differ diff --git a/public/img/default_avatar.svg b/public/img/default_avatar.svg new file mode 100644 index 00000000..4586017b --- /dev/null +++ b/public/img/default_avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/kicad/footprints.txt b/public/kicad/footprints.txt new file mode 100644 index 00000000..8f0f944c --- /dev/null +++ b/public/kicad/footprints.txt @@ -0,0 +1,14213 @@ +# This file contains all the KiCad footprints available in the official library +# Generated by footprints.sh +# on Sun Feb 16 21:19:56 CET 2025 +Audio_Module:Reverb_BTDR-1H +Audio_Module:Reverb_BTDR-1V +Battery:BatteryClip_Keystone_54_D16-19mm +Battery:BatteryHolder_Bulgin_BX0036_1xC +Battery:BatteryHolder_ComfortableElectronic_CH273-2450_1x2450 +Battery:BatteryHolder_Eagle_12BH611-GR +Battery:BatteryHolder_Keystone_103_1x20mm +Battery:BatteryHolder_Keystone_1042_1x18650 +Battery:BatteryHolder_Keystone_104_1x23mm +Battery:BatteryHolder_Keystone_1057_1x2032 +Battery:BatteryHolder_Keystone_1058_1x2032 +Battery:BatteryHolder_Keystone_105_1x2430 +Battery:BatteryHolder_Keystone_1060_1x2032 +Battery:BatteryHolder_Keystone_106_1x20mm +Battery:BatteryHolder_Keystone_107_1x23mm +Battery:BatteryHolder_Keystone_2460_1xAA +Battery:BatteryHolder_Keystone_2462_2xAA +Battery:BatteryHolder_Keystone_2466_1xAAA +Battery:BatteryHolder_Keystone_2468_2xAAA +Battery:BatteryHolder_Keystone_2479_3xAAA +Battery:BatteryHolder_Keystone_2993 +Battery:BatteryHolder_Keystone_2998_1x6.8mm +Battery:BatteryHolder_Keystone_3000_1x12mm +Battery:BatteryHolder_Keystone_3001_1x12mm +Battery:BatteryHolder_Keystone_3002_1x2032 +Battery:BatteryHolder_Keystone_3008_1x2450 +Battery:BatteryHolder_Keystone_3009_1x2450 +Battery:BatteryHolder_Keystone_3034_1x20mm +Battery:BatteryHolder_Keystone_500 +Battery:BatteryHolder_Keystone_590 +Battery:BatteryHolder_LINX_BAT-HLD-012-SMT +Battery:BatteryHolder_MPD_BA9VPC_1xPP3 +Battery:BatteryHolder_MPD_BC12AAPC_2xAA +Battery:BatteryHolder_MPD_BC2003_1x2032 +Battery:BatteryHolder_MPD_BC2AAPC_2xAA +Battery:BatteryHolder_MPD_BH-18650-PC2 +Battery:BatteryHolder_Multicomp_BC-2001_1x2032 +Battery:BatteryHolder_MYOUNG_BS-07-A1BJ001_CR2032 +Battery:BatteryHolder_Renata_SMTU2032-LF_1x2032 +Battery:BatteryHolder_Seiko_MS621F +Battery:BatteryHolder_TruPower_BH-331P_3xAA +Battery:Battery_CR1225 +Battery:Battery_Panasonic_CR1025-VSK_Vertical_CircularHoles +Battery:Battery_Panasonic_CR1220-VCN_Vertical_CircularHoles +Battery:Battery_Panasonic_CR1632-V1AN_Vertical_CircularHoles +Battery:Battery_Panasonic_CR2025-V1AK_Vertical_CircularHoles +Battery:Battery_Panasonic_CR2032-HFN_Horizontal_CircularHoles +Battery:Battery_Panasonic_CR2032-VS1N_Vertical_CircularHoles +Battery:Battery_Panasonic_CR2354-VCN_Vertical_CircularHoles +Battery:Battery_Panasonic_CR2450-VAN_Vertical_CircularHoles +Battery:Battery_Panasonic_CR2477-VCN_Vertical_CircularHoles +Battery:Battery_Panasonic_CR3032-VCN_Vertical_CircularHoles +Button_Switch_Keyboard:SW_Cherry_MX_1.00u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_1.00u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_1.25u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_1.25u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_1.50u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_1.50u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_1.75u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_1.75u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_2.00u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_2.00u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_2.00u_Vertical_PCB +Button_Switch_Keyboard:SW_Cherry_MX_2.00u_Vertical_Plate +Button_Switch_Keyboard:SW_Cherry_MX_2.25u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_2.25u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_2.75u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_2.75u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_6.25u_PCB +Button_Switch_Keyboard:SW_Cherry_MX_6.25u_Plate +Button_Switch_Keyboard:SW_Cherry_MX_ISOEnter_PCB +Button_Switch_Keyboard:SW_Cherry_MX_ISOEnter_Plate +Button_Switch_Keyboard:SW_Matias_1.00u +Button_Switch_Keyboard:SW_Matias_1.25u +Button_Switch_Keyboard:SW_Matias_1.50u +Button_Switch_Keyboard:SW_Matias_1.75u +Button_Switch_Keyboard:SW_Matias_2.00u +Button_Switch_Keyboard:SW_Matias_2.25u +Button_Switch_Keyboard:SW_Matias_2.75u +Button_Switch_Keyboard:SW_Matias_6.25u +Button_Switch_Keyboard:SW_Matias_ISOEnter +Button_Switch_SMD:Nidec_Copal_CAS-120A +Button_Switch_SMD:Nidec_Copal_SH-7010A +Button_Switch_SMD:Nidec_Copal_SH-7010B +Button_Switch_SMD:Nidec_Copal_SH-7040B +Button_Switch_SMD:Panasonic_EVQPUJ_EVQPUA +Button_Switch_SMD:Panasonic_EVQPUK_EVQPUB +Button_Switch_SMD:Panasonic_EVQPUL_EVQPUC +Button_Switch_SMD:Panasonic_EVQPUM_EVQPUD +Button_Switch_SMD:SW_DIP_SPSTx01_Slide_6.7x4.1mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx01_Slide_6.7x4.1mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx01_Slide_9.78x4.72mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx01_Slide_Copal_CHS-01A_W5.08mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx01_Slide_Copal_CHS-01B_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx01_Slide_Copal_CVS-01xB_W5.9mm_P1mm +Button_Switch_SMD:SW_DIP_SPSTx01_Slide_Omron_A6S-110x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_6.7x6.64mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_6.7x6.64mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_9.78x7.26mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_Copal_CHS-02A_W5.08mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_Copal_CHS-02B_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_Copal_CVS-02xB_W5.9mm_P1mm +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_KingTek_DSHP02TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_KingTek_DSHP02TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_Omron_A6H-2101_W6.15mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx02_Slide_Omron_A6S-210x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx03_Slide_6.7x9.18mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx03_Slide_6.7x9.18mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx03_Slide_9.78x9.8mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx03_Slide_Copal_CVS-03xB_W5.9mm_P1mm +Button_Switch_SMD:SW_DIP_SPSTx03_Slide_KingTek_DSHP03TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx03_Slide_KingTek_DSHP03TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx03_Slide_Omron_A6S-310x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_6.7x11.72mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_6.7x11.72mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_9.78x12.34mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_Copal_CHS-04A_W5.08mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_Copal_CHS-04B_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_Copal_CVS-04xB_W5.9mm_P1mm +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_KingTek_DSHP04TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_KingTek_DSHP04TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_Omron_A6H-4101_W6.15mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx04_Slide_Omron_A6S-410x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx05_Slide_6.7x14.26mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx05_Slide_6.7x14.26mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx05_Slide_9.78x14.88mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx05_Slide_KingTek_DSHP05TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx05_Slide_KingTek_DSHP05TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx05_Slide_Omron_A6S-510x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_6.7x16.8mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_6.7x16.8mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_9.78x17.42mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_Copal_CHS-06A_W5.08mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_Copal_CHS-06B_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_KingTek_DSHP06TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_KingTek_DSHP06TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_Omron_A6H-6101_W6.15mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx06_Slide_Omron_A6S-610x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx07_Slide_6.7x19.34mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx07_Slide_6.7x19.34mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx07_Slide_9.78x19.96mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx07_Slide_KingTek_DSHP07TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx07_Slide_KingTek_DSHP07TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx07_Slide_Omron_A6S-710x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_6.7x21.88mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_6.7x21.88mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_9.78x22.5mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_Copal_CHS-08A_W5.08mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_Copal_CHS-08B_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_Copal_CVS-08xB_W5.9mm_P1mm +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_KingTek_DSHP08TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_KingTek_DSHP08TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_Omron_A6H-8101_W6.15mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx08_Slide_Omron_A6S-810x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx09_Slide_6.7x24.42mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx09_Slide_6.7x24.42mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx09_Slide_9.78x25.04mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx09_Slide_KingTek_DSHP09TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx09_Slide_KingTek_DSHP09TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx09_Slide_Omron_A6S-910x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_6.7x26.96mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_6.7x26.96mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_9.78x27.58mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_Copal_CHS-10A_W5.08mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_Copal_CHS-10B_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_KingTek_DSHP10TJ_W5.25mm_P1.27mm_JPin +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_KingTek_DSHP10TS_W7.62mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_Omron_A6H-10101_W6.15mm_P1.27mm +Button_Switch_SMD:SW_DIP_SPSTx10_Slide_Omron_A6S-1010x_W8.9mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx11_Slide_6.7x29.5mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx11_Slide_6.7x29.5mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx11_Slide_9.78x30.12mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DIP_SPSTx12_Slide_6.7x32.04mm_W6.73mm_P2.54mm_LowProfile_JPin +Button_Switch_SMD:SW_DIP_SPSTx12_Slide_6.7x32.04mm_W8.61mm_P2.54mm_LowProfile +Button_Switch_SMD:SW_DIP_SPSTx12_Slide_9.78x32.66mm_W8.61mm_P2.54mm +Button_Switch_SMD:SW_DPDT_CK_JS202011JCQN +Button_Switch_SMD:SW_MEC_5GSH9 +Button_Switch_SMD:SW_Push_1P1T-MP_NO_Horizontal_Alps_SKRTLAE010 +Button_Switch_SMD:SW_Push_1P1T-SH_NO_CK_KMR2xxG +Button_Switch_SMD:SW_Push_1P1T_NO_CK_KMR2 +Button_Switch_SMD:SW_Push_1P1T_NO_CK_KSC6xxJ +Button_Switch_SMD:SW_Push_1P1T_NO_CK_KSC7xxJ +Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx43PSMTR +Button_Switch_SMD:SW_Push_1P1T_NO_Vertical_Wuerth_434133025816 +Button_Switch_SMD:SW_Push_1P1T_XKB_TS-1187A +Button_Switch_SMD:SW_Push_1TS009xxxx-xxxx-xxxx_6x6x5mm +Button_Switch_SMD:SW_Push_SPST_NO_Alps_SKRK +Button_Switch_SMD:SW_SP3T_PCM13 +Button_Switch_SMD:SW_SPDT_CK_JS102011SAQN +Button_Switch_SMD:SW_SPDT_PCM12 +Button_Switch_SMD:SW_SPDT_REED_MSDM-DT +Button_Switch_SMD:SW_SPST_B3S-1000 +Button_Switch_SMD:SW_SPST_B3S-1100 +Button_Switch_SMD:SW_SPST_B3SL-1002P +Button_Switch_SMD:SW_SPST_B3SL-1022P +Button_Switch_SMD:SW_SPST_B3U-1000P-B +Button_Switch_SMD:SW_SPST_B3U-1000P +Button_Switch_SMD:SW_SPST_B3U-1100P-B +Button_Switch_SMD:SW_SPST_B3U-1100P +Button_Switch_SMD:SW_SPST_B3U-3000P-B +Button_Switch_SMD:SW_SPST_B3U-3000P +Button_Switch_SMD:SW_SPST_B3U-3100P-B +Button_Switch_SMD:SW_SPST_B3U-3100P +Button_Switch_SMD:SW_SPST_CK_KMS2xxG +Button_Switch_SMD:SW_SPST_CK_KMS2xxGP +Button_Switch_SMD:SW_SPST_CK_KXT3 +Button_Switch_SMD:SW_SPST_CK_RS282G05A3 +Button_Switch_SMD:SW_SPST_EVPBF +Button_Switch_SMD:SW_SPST_EVQP0 +Button_Switch_SMD:SW_SPST_EVQP2 +Button_Switch_SMD:SW_SPST_EVQP7A +Button_Switch_SMD:SW_SPST_EVQP7C +Button_Switch_SMD:SW_SPST_EVQPE1 +Button_Switch_SMD:SW_SPST_EVQQ2 +Button_Switch_SMD:SW_SPST_FSMSM +Button_Switch_SMD:SW_SPST_Omron_B3FS-100xP +Button_Switch_SMD:SW_SPST_Omron_B3FS-101xP +Button_Switch_SMD:SW_SPST_Omron_B3FS-105xP +Button_Switch_SMD:SW_SPST_Panasonic_EVQPL_3PL_5PL_PT_A08 +Button_Switch_SMD:SW_SPST_Panasonic_EVQPL_3PL_5PL_PT_A15 +Button_Switch_SMD:SW_SPST_PTS645 +Button_Switch_SMD:SW_SPST_PTS647_Sx38 +Button_Switch_SMD:SW_SPST_PTS647_Sx50 +Button_Switch_SMD:SW_SPST_PTS647_Sx70 +Button_Switch_SMD:SW_SPST_PTS810 +Button_Switch_SMD:SW_SPST_REED_CT05-XXXX-G1 +Button_Switch_SMD:SW_SPST_REED_CT05-XXXX-J1 +Button_Switch_SMD:SW_SPST_REED_CT10-XXXX-G1 +Button_Switch_SMD:SW_SPST_REED_CT10-XXXX-G2 +Button_Switch_SMD:SW_SPST_REED_CT10-XXXX-G4 +Button_Switch_SMD:SW_SPST_SKQG_WithoutStem +Button_Switch_SMD:SW_SPST_SKQG_WithStem +Button_Switch_SMD:SW_SPST_TL3305A +Button_Switch_SMD:SW_SPST_TL3305B +Button_Switch_SMD:SW_SPST_TL3305C +Button_Switch_SMD:SW_SPST_TL3342 +Button_Switch_SMD:SW_Tactile_SPST_NO_Straight_CK_PTS636Sx25SMTRLFS +Button_Switch_THT:KSA_Tactile_SPST +Button_Switch_THT:Nidec_Copal_SH-7010C +Button_Switch_THT:Push_E-Switch_KS01Q01 +Button_Switch_THT:SW_CK_JS202011AQN_DPDT_Angled +Button_Switch_THT:SW_CK_JS202011CQN_DPDT_Straight +Button_Switch_THT:SW_CW_GPTS203211B +Button_Switch_THT:SW_DIP_SPSTx01_Piano_10.8x4.1mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx01_Slide_6.7x4.1mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx01_Slide_9.78x4.72mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx02_Piano_10.8x6.64mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx02_Piano_CTS_Series194-2MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx02_Slide_6.7x6.64mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx02_Slide_9.78x7.26mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx03_Piano_10.8x9.18mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx03_Piano_CTS_Series194-3MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx03_Slide_6.7x9.18mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx03_Slide_9.78x9.8mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx04_Piano_10.8x11.72mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx04_Piano_CTS_Series194-4MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx04_Slide_6.7x11.72mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx04_Slide_9.78x12.34mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx05_Piano_10.8x14.26mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx05_Piano_CTS_Series194-5MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx05_Slide_6.7x14.26mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx05_Slide_9.78x14.88mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx06_Piano_10.8x16.8mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx06_Piano_CTS_Series194-6MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx06_Slide_6.7x16.8mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx06_Slide_9.78x17.42mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx07_Piano_10.8x19.34mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx07_Piano_CTS_Series194-7MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx07_Slide_6.7x19.34mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx07_Slide_9.78x19.96mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx08_Piano_10.8x21.88mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx08_Piano_CTS_Series194-8MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx08_Slide_6.7x21.88mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx08_Slide_9.78x22.5mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx09_Piano_10.8x24.42mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx09_Piano_CTS_Series194-9MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx09_Slide_6.7x24.42mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx09_Slide_9.78x25.04mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx10_Piano_10.8x26.96mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx10_Piano_CTS_Series194-10MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx10_Slide_6.7x26.96mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx10_Slide_9.78x27.58mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx11_Piano_10.8x29.5mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx11_Piano_CTS_Series194-11MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx11_Slide_6.7x29.5mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx11_Slide_9.78x30.12mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx12_Piano_10.8x32.04mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx12_Piano_CTS_Series194-12MSTN_W7.62mm_P2.54mm +Button_Switch_THT:SW_DIP_SPSTx12_Slide_6.7x32.04mm_W7.62mm_P2.54mm_LowProfile +Button_Switch_THT:SW_DIP_SPSTx12_Slide_9.78x32.66mm_W7.62mm_P2.54mm +Button_Switch_THT:SW_E-Switch_EG1224_SPDT_Angled +Button_Switch_THT:SW_E-Switch_EG1271_SPDT +Button_Switch_THT:SW_E-Switch_EG2219_DPDT_Angled +Button_Switch_THT:SW_Lever_1P2T_NKK_GW12LxH +Button_Switch_THT:SW_MEC_5GTH9 +Button_Switch_THT:SW_NKK_BB15AH +Button_Switch_THT:SW_NKK_G1xJP +Button_Switch_THT:SW_NKK_GW12LJP +Button_Switch_THT:SW_NKK_NR01 +Button_Switch_THT:SW_PUSH-12mm +Button_Switch_THT:SW_PUSH-12mm_Wuerth-430476085716 +Button_Switch_THT:SW_PUSH_1P1T_6x3.5mm_H4.3_APEM_MJTP1243 +Button_Switch_THT:SW_PUSH_1P1T_6x3.5mm_H5.0_APEM_MJTP1250 +Button_Switch_THT:SW_Push_1P1T_NO_LED_E-Switch_TL1250 +Button_Switch_THT:SW_Push_1P2T_Vertical_E-Switch_800UDP8P1A1M6 +Button_Switch_THT:SW_Push_2P1T_Toggle_CK_PVA1xxH1xxxxxxV2 +Button_Switch_THT:SW_Push_2P1T_Toggle_CK_PVA1xxH2xxxxxxV2 +Button_Switch_THT:SW_Push_2P1T_Toggle_CK_PVA1xxH3xxxxxxV2 +Button_Switch_THT:SW_Push_2P1T_Toggle_CK_PVA1xxH4xxxxxxV2 +Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2OAH5xxxxxxV2 +Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2xxH1xxxxxxV2 +Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2xxH2xxxxxxV2 +Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2xxH3xxxxxxV2 +Button_Switch_THT:SW_Push_2P2T_Toggle_CK_PVA2xxH4xxxxxxV2 +Button_Switch_THT:SW_Push_2P2T_Vertical_E-Switch_800UDP8P1A1M6 +Button_Switch_THT:SW_PUSH_6mm +Button_Switch_THT:SW_PUSH_6mm_H13mm +Button_Switch_THT:SW_PUSH_6mm_H4.3mm +Button_Switch_THT:SW_PUSH_6mm_H5mm +Button_Switch_THT:SW_PUSH_6mm_H7.3mm +Button_Switch_THT:SW_PUSH_6mm_H8.5mm +Button_Switch_THT:SW_PUSH_6mm_H8mm +Button_Switch_THT:SW_PUSH_6mm_H9.5mm +Button_Switch_THT:SW_PUSH_E-Switch_FS5700DP_DPDT +Button_Switch_THT:SW_PUSH_LCD_E3_SAxxxx +Button_Switch_THT:SW_PUSH_LCD_E3_SAxxxx_SocketPins +Button_Switch_THT:SW_Slide-03_Wuerth-WS-SLTV_10x2.5x6.4_P2.54mm +Button_Switch_THT:SW_Slide_SPDT_Angled_CK_OS102011MA1Q +Button_Switch_THT:SW_Slide_SPDT_Straight_CK_OS102011MS2Q +Button_Switch_THT:SW_SPST_Omron_B3F-315x_Angled +Button_Switch_THT:SW_SPST_Omron_B3F-40xx +Button_Switch_THT:SW_SPST_Omron_B3F-50xx +Button_Switch_THT:SW_Tactile_SKHH_Angled +Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx31-2LFS +Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx39-2LFS +Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx58-2LFS +Button_Switch_THT:SW_Tactile_SPST_Angled_PTS645Vx83-2LFS +Button_Switch_THT:SW_Tactile_Straight_KSA0Axx1LFTR +Button_Switch_THT:SW_Tactile_Straight_KSL0Axx1LFTR +Button_Switch_THT:SW_TH_Tactile_Omron_B3F-10xx +Button_Switch_THT:SW_XKB_DM1-16UC-1 +Button_Switch_THT:SW_XKB_DM1-16UD-1 +Button_Switch_THT:SW_XKB_DM1-16UP-1 +Buzzer_Beeper:Buzzer_12x9.5RM7.6 +Buzzer_Beeper:Buzzer_15x7.5RM7.6 +Buzzer_Beeper:Buzzer_CUI_CPT-9019S-SMT +Buzzer_Beeper:Buzzer_D14mm_H7mm_P10mm +Buzzer_Beeper:Buzzer_Mallory_AST1109MLTRQ +Buzzer_Beeper:Buzzer_Mallory_AST1240MLQ +Buzzer_Beeper:Buzzer_Murata_PKLCS1212E +Buzzer_Beeper:Buzzer_Murata_PKMCS0909E +Buzzer_Beeper:Buzzer_TDK_PS1240P02BT_D12.2mm_H6.5mm +Buzzer_Beeper:Indicator_PUI_AI-1440-TWT-24V-2-R +Buzzer_Beeper:MagneticBuzzer_CUI_CMT-8504-100-SMT +Buzzer_Beeper:MagneticBuzzer_CUI_CST-931RP-A +Buzzer_Beeper:MagneticBuzzer_Kingstate_KCG0601 +Buzzer_Beeper:MagneticBuzzer_Kobitone_254-EMB73-RO +Buzzer_Beeper:MagneticBuzzer_Kobitone_254-EMB84Q-RO +Buzzer_Beeper:MagneticBuzzer_ProjectsUnlimited_AI-4228-TWT-R +Buzzer_Beeper:MagneticBuzzer_ProSignal_ABI-009-RC +Buzzer_Beeper:MagneticBuzzer_ProSignal_ABI-010-RC +Buzzer_Beeper:MagneticBuzzer_ProSignal_ABT-410-RC +Buzzer_Beeper:MagneticBuzzer_PUI_AT-0927-TT-6-R +Buzzer_Beeper:MagneticBuzzer_PUI_SMT-1028-T-2-R +Buzzer_Beeper:MagneticBuzzer_StarMicronics_HMB-06_HMB-12 +Buzzer_Beeper:PUIAudio_SMT_0825_S_4_R +Buzzer_Beeper:Speaker_CUI_CMR-1206S-67 +Calibration_Scale:Gauge_100mm_Grid_Type1_CopperTop +Calibration_Scale:Gauge_100mm_Type1_CopperTop +Calibration_Scale:Gauge_100mm_Type1_SilkScreenTop +Calibration_Scale:Gauge_100mm_Type2_CopperTop +Calibration_Scale:Gauge_100mm_Type2_SilkScreenTop +Calibration_Scale:Gauge_10mm_Type1_CopperTop +Calibration_Scale:Gauge_10mm_Type1_SilkScreenTop +Calibration_Scale:Gauge_10mm_Type2_CopperTop +Calibration_Scale:Gauge_10mm_Type2_SilkScreenTop +Calibration_Scale:Gauge_10mm_Type3_CopperTop +Calibration_Scale:Gauge_10mm_Type3_SilkScreenTop +Calibration_Scale:Gauge_10mm_Type4_CopperTop +Calibration_Scale:Gauge_10mm_Type4_SilkScreenTop +Calibration_Scale:Gauge_10mm_Type5_CopperTop +Calibration_Scale:Gauge_10mm_Type5_SilkScreenTop +Calibration_Scale:Gauge_50mm_Type1_CopperTop +Calibration_Scale:Gauge_50mm_Type1_SilkScreenTop +Calibration_Scale:Gauge_50mm_Type2_CopperTop +Calibration_Scale:Gauge_50mm_Type2_SilkScreenTop +Capacitor_SMD:CP_Elec_10x10.5 +Capacitor_SMD:CP_Elec_10x10 +Capacitor_SMD:CP_Elec_10x12.5 +Capacitor_SMD:CP_Elec_10x12.6 +Capacitor_SMD:CP_Elec_10x14.3 +Capacitor_SMD:CP_Elec_10x7.7 +Capacitor_SMD:CP_Elec_10x7.9 +Capacitor_SMD:CP_Elec_16x17.5 +Capacitor_SMD:CP_Elec_16x22 +Capacitor_SMD:CP_Elec_18x17.5 +Capacitor_SMD:CP_Elec_18x22 +Capacitor_SMD:CP_Elec_3x5.3 +Capacitor_SMD:CP_Elec_3x5.4 +Capacitor_SMD:CP_Elec_4x3.9 +Capacitor_SMD:CP_Elec_4x3 +Capacitor_SMD:CP_Elec_4x4.5 +Capacitor_SMD:CP_Elec_4x5.3 +Capacitor_SMD:CP_Elec_4x5.4 +Capacitor_SMD:CP_Elec_4x5.7 +Capacitor_SMD:CP_Elec_4x5.8 +Capacitor_SMD:CP_Elec_5x3.9 +Capacitor_SMD:CP_Elec_5x3 +Capacitor_SMD:CP_Elec_5x4.4 +Capacitor_SMD:CP_Elec_5x4.5 +Capacitor_SMD:CP_Elec_5x5.3 +Capacitor_SMD:CP_Elec_5x5.4 +Capacitor_SMD:CP_Elec_5x5.7 +Capacitor_SMD:CP_Elec_5x5.8 +Capacitor_SMD:CP_Elec_5x5.9 +Capacitor_SMD:CP_Elec_6.3x3.9 +Capacitor_SMD:CP_Elec_6.3x3 +Capacitor_SMD:CP_Elec_6.3x4.5 +Capacitor_SMD:CP_Elec_6.3x4.9 +Capacitor_SMD:CP_Elec_6.3x5.2 +Capacitor_SMD:CP_Elec_6.3x5.3 +Capacitor_SMD:CP_Elec_6.3x5.4 +Capacitor_SMD:CP_Elec_6.3x5.4_Nichicon +Capacitor_SMD:CP_Elec_6.3x5.7 +Capacitor_SMD:CP_Elec_6.3x5.8 +Capacitor_SMD:CP_Elec_6.3x5.9 +Capacitor_SMD:CP_Elec_6.3x7.7 +Capacitor_SMD:CP_Elec_6.3x9.9 +Capacitor_SMD:CP_Elec_8x10.5 +Capacitor_SMD:CP_Elec_8x10 +Capacitor_SMD:CP_Elec_8x11.9 +Capacitor_SMD:CP_Elec_8x5.4 +Capacitor_SMD:CP_Elec_8x6.2 +Capacitor_SMD:CP_Elec_8x6.5 +Capacitor_SMD:CP_Elec_8x6.7 +Capacitor_SMD:CP_Elec_8x6.9 +Capacitor_SMD:CP_Elec_CAP-XX_DMF3Zxxxxxxxx3D +Capacitor_SMD:C_01005_0402Metric +Capacitor_SMD:C_01005_0402Metric_Pad0.57x0.30mm_HandSolder +Capacitor_SMD:C_0201_0603Metric +Capacitor_SMD:C_0201_0603Metric_Pad0.64x0.40mm_HandSolder +Capacitor_SMD:C_0402_1005Metric +Capacitor_SMD:C_0402_1005Metric_Pad0.74x0.62mm_HandSolder +Capacitor_SMD:C_0504_1310Metric +Capacitor_SMD:C_0504_1310Metric_Pad0.83x1.28mm_HandSolder +Capacitor_SMD:C_0603_1608Metric +Capacitor_SMD:C_0603_1608Metric_Pad1.08x0.95mm_HandSolder +Capacitor_SMD:C_0805_2012Metric +Capacitor_SMD:C_0805_2012Metric_Pad1.18x1.45mm_HandSolder +Capacitor_SMD:C_1206_3216Metric +Capacitor_SMD:C_1206_3216Metric_Pad1.33x1.80mm_HandSolder +Capacitor_SMD:C_1210_3225Metric +Capacitor_SMD:C_1210_3225Metric_Pad1.33x2.70mm_HandSolder +Capacitor_SMD:C_1808_4520Metric +Capacitor_SMD:C_1808_4520Metric_Pad1.72x2.30mm_HandSolder +Capacitor_SMD:C_1812_4532Metric +Capacitor_SMD:C_1812_4532Metric_Pad1.57x3.40mm_HandSolder +Capacitor_SMD:C_1825_4564Metric +Capacitor_SMD:C_1825_4564Metric_Pad1.57x6.80mm_HandSolder +Capacitor_SMD:C_2220_5750Metric +Capacitor_SMD:C_2220_5750Metric_Pad1.97x5.40mm_HandSolder +Capacitor_SMD:C_2225_5664Metric +Capacitor_SMD:C_2225_5664Metric_Pad1.80x6.60mm_HandSolder +Capacitor_SMD:C_3640_9110Metric +Capacitor_SMD:C_3640_9110Metric_Pad2.10x10.45mm_HandSolder +Capacitor_SMD:C_Elec_10x10.2 +Capacitor_SMD:C_Elec_3x5.4 +Capacitor_SMD:C_Elec_4x5.4 +Capacitor_SMD:C_Elec_4x5.8 +Capacitor_SMD:C_Elec_5x5.4 +Capacitor_SMD:C_Elec_5x5.8 +Capacitor_SMD:C_Elec_6.3x5.4 +Capacitor_SMD:C_Elec_6.3x5.8 +Capacitor_SMD:C_Elec_6.3x7.7 +Capacitor_SMD:C_Elec_8x10.2 +Capacitor_SMD:C_Elec_8x5.4 +Capacitor_SMD:C_Elec_8x6.2 +Capacitor_SMD:C_Trimmer_Murata_TZB4-A +Capacitor_SMD:C_Trimmer_Murata_TZB4-B +Capacitor_SMD:C_Trimmer_Murata_TZC3 +Capacitor_SMD:C_Trimmer_Murata_TZR1 +Capacitor_SMD:C_Trimmer_Murata_TZW4 +Capacitor_SMD:C_Trimmer_Murata_TZY2 +Capacitor_SMD:C_Trimmer_Sprague-Goodman_SGC3 +Capacitor_SMD:C_Trimmer_Voltronics_JN +Capacitor_SMD:C_Trimmer_Voltronics_JQ +Capacitor_SMD:C_Trimmer_Voltronics_JR +Capacitor_SMD:C_Trimmer_Voltronics_JV +Capacitor_SMD:C_Trimmer_Voltronics_JZ +Capacitor_Tantalum_SMD:CP_EIA-1608-08_AVX-J +Capacitor_Tantalum_SMD:CP_EIA-1608-08_AVX-J_Pad1.25x1.05mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-1608-10_AVX-L +Capacitor_Tantalum_SMD:CP_EIA-1608-10_AVX-L_Pad1.25x1.05mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-2012-12_Kemet-R +Capacitor_Tantalum_SMD:CP_EIA-2012-12_Kemet-R_Pad1.30x1.05mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-2012-15_AVX-P +Capacitor_Tantalum_SMD:CP_EIA-2012-15_AVX-P_Pad1.30x1.05mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3216-10_Kemet-I +Capacitor_Tantalum_SMD:CP_EIA-3216-10_Kemet-I_Pad1.58x1.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3216-12_Kemet-S +Capacitor_Tantalum_SMD:CP_EIA-3216-12_Kemet-S_Pad1.58x1.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A +Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A_Pad1.58x1.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3528-12_Kemet-T +Capacitor_Tantalum_SMD:CP_EIA-3528-12_Kemet-T_Pad1.50x2.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3528-15_AVX-H +Capacitor_Tantalum_SMD:CP_EIA-3528-15_AVX-H_Pad1.50x2.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B +Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B_Pad1.50x2.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-6032-15_Kemet-U +Capacitor_Tantalum_SMD:CP_EIA-6032-15_Kemet-U_Pad2.25x2.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-6032-20_AVX-F +Capacitor_Tantalum_SMD:CP_EIA-6032-20_AVX-F_Pad2.25x2.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C +Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C_Pad2.25x2.35mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7132-20_AVX-U +Capacitor_Tantalum_SMD:CP_EIA-7132-20_AVX-U_Pad2.72x3.50mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7132-28_AVX-C +Capacitor_Tantalum_SMD:CP_EIA-7132-28_AVX-C_Pad2.72x3.50mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-15_AVX-R +Capacitor_Tantalum_SMD:CP_EIA-7260-15_AVX-R_Pad2.68x6.30mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-20_AVX-M +Capacitor_Tantalum_SMD:CP_EIA-7260-20_AVX-M_Pad2.68x6.30mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-28_AVX-M +Capacitor_Tantalum_SMD:CP_EIA-7260-28_AVX-M_Pad2.68x6.30mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7260-38_AVX-R +Capacitor_Tantalum_SMD:CP_EIA-7260-38_AVX-R_Pad2.68x6.30mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-15_Kemet-W +Capacitor_Tantalum_SMD:CP_EIA-7343-15_Kemet-W_Pad2.25x2.55mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-20_Kemet-V +Capacitor_Tantalum_SMD:CP_EIA-7343-20_Kemet-V_Pad2.25x2.55mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-30_AVX-N +Capacitor_Tantalum_SMD:CP_EIA-7343-30_AVX-N_Pad2.25x2.55mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D +Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D_Pad2.25x2.55mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-40_Kemet-Y +Capacitor_Tantalum_SMD:CP_EIA-7343-40_Kemet-Y_Pad2.25x2.55mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-X +Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-X_Pad2.25x2.55mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7360-38_Kemet-E +Capacitor_Tantalum_SMD:CP_EIA-7360-38_Kemet-E_Pad2.25x4.25mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7361-38_AVX-V +Capacitor_Tantalum_SMD:CP_EIA-7361-38_AVX-V_Pad2.18x3.30mm_HandSolder +Capacitor_Tantalum_SMD:CP_EIA-7361-438_AVX-U +Capacitor_Tantalum_SMD:CP_EIA-7361-438_AVX-U_Pad2.18x3.30mm_HandSolder +Capacitor_THT:CP_Axial_L10.0mm_D4.5mm_P15.00mm_Horizontal +Capacitor_THT:CP_Axial_L10.0mm_D6.0mm_P15.00mm_Horizontal +Capacitor_THT:CP_Axial_L11.0mm_D5.0mm_P18.00mm_Horizontal +Capacitor_THT:CP_Axial_L11.0mm_D6.0mm_P18.00mm_Horizontal +Capacitor_THT:CP_Axial_L11.0mm_D8.0mm_P15.00mm_Horizontal +Capacitor_THT:CP_Axial_L18.0mm_D10.0mm_P25.00mm_Horizontal +Capacitor_THT:CP_Axial_L18.0mm_D6.5mm_P25.00mm_Horizontal +Capacitor_THT:CP_Axial_L18.0mm_D8.0mm_P25.00mm_Horizontal +Capacitor_THT:CP_Axial_L20.0mm_D10.0mm_P26.00mm_Horizontal +Capacitor_THT:CP_Axial_L20.0mm_D13.0mm_P26.00mm_Horizontal +Capacitor_THT:CP_Axial_L21.0mm_D8.0mm_P28.00mm_Horizontal +Capacitor_THT:CP_Axial_L25.0mm_D10.0mm_P30.00mm_Horizontal +Capacitor_THT:CP_Axial_L26.5mm_D20.0mm_P33.00mm_Horizontal +Capacitor_THT:CP_Axial_L29.0mm_D10.0mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L29.0mm_D13.0mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L29.0mm_D16.0mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L29.0mm_D20.0mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L30.0mm_D10.0mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L30.0mm_D12.5mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L30.0mm_D15.0mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L30.0mm_D18.0mm_P35.00mm_Horizontal +Capacitor_THT:CP_Axial_L34.5mm_D20.0mm_P41.00mm_Horizontal +Capacitor_THT:CP_Axial_L37.0mm_D13.0mm_P43.00mm_Horizontal +Capacitor_THT:CP_Axial_L37.0mm_D16.0mm_P43.00mm_Horizontal +Capacitor_THT:CP_Axial_L37.0mm_D20.0mm_P43.00mm_Horizontal +Capacitor_THT:CP_Axial_L38.0mm_D18.0mm_P44.00mm_Horizontal +Capacitor_THT:CP_Axial_L38.0mm_D21.0mm_P44.00mm_Horizontal +Capacitor_THT:CP_Axial_L40.0mm_D16.0mm_P48.00mm_Horizontal +Capacitor_THT:CP_Axial_L42.0mm_D23.0mm_P45.00mm_Horizontal +Capacitor_THT:CP_Axial_L42.0mm_D26.0mm_P45.00mm_Horizontal +Capacitor_THT:CP_Axial_L42.0mm_D29.0mm_P45.00mm_Horizontal +Capacitor_THT:CP_Axial_L42.0mm_D32.0mm_P45.00mm_Horizontal +Capacitor_THT:CP_Axial_L42.0mm_D35.0mm_P45.00mm_Horizontal +Capacitor_THT:CP_Axial_L42.5mm_D20.0mm_P49.00mm_Horizontal +Capacitor_THT:CP_Axial_L46.0mm_D20.0mm_P52.00mm_Horizontal +Capacitor_THT:CP_Axial_L55.0mm_D23.0mm_P60.00mm_Horizontal +Capacitor_THT:CP_Axial_L55.0mm_D26.0mm_P60.00mm_Horizontal +Capacitor_THT:CP_Axial_L55.0mm_D29.0mm_P60.00mm_Horizontal +Capacitor_THT:CP_Axial_L55.0mm_D32.0mm_P60.00mm_Horizontal +Capacitor_THT:CP_Axial_L55.0mm_D35.0mm_P60.00mm_Horizontal +Capacitor_THT:CP_Axial_L67.0mm_D23.0mm_P75.00mm_Horizontal +Capacitor_THT:CP_Axial_L67.0mm_D26.0mm_P75.00mm_Horizontal +Capacitor_THT:CP_Axial_L67.0mm_D29.0mm_P75.00mm_Horizontal +Capacitor_THT:CP_Axial_L67.0mm_D32.0mm_P75.00mm_Horizontal +Capacitor_THT:CP_Axial_L67.0mm_D35.0mm_P75.00mm_Horizontal +Capacitor_THT:CP_Axial_L80.0mm_D23.0mm_P85.00mm_Horizontal +Capacitor_THT:CP_Axial_L80.0mm_D26.0mm_P85.00mm_Horizontal +Capacitor_THT:CP_Axial_L80.0mm_D29.0mm_P85.00mm_Horizontal +Capacitor_THT:CP_Axial_L80.0mm_D32.0mm_P85.00mm_Horizontal +Capacitor_THT:CP_Axial_L80.0mm_D35.0mm_P85.00mm_Horizontal +Capacitor_THT:CP_Axial_L93.0mm_D23.0mm_P100.00mm_Horizontal +Capacitor_THT:CP_Axial_L93.0mm_D26.0mm_P100.00mm_Horizontal +Capacitor_THT:CP_Axial_L93.0mm_D29.0mm_P100.00mm_Horizontal +Capacitor_THT:CP_Axial_L93.0mm_D32.0mm_P100.00mm_Horizontal +Capacitor_THT:CP_Axial_L93.0mm_D35.0mm_P100.00mm_Horizontal +Capacitor_THT:CP_Radial_D10.0mm_P2.50mm +Capacitor_THT:CP_Radial_D10.0mm_P2.50mm_P5.00mm +Capacitor_THT:CP_Radial_D10.0mm_P3.50mm +Capacitor_THT:CP_Radial_D10.0mm_P3.80mm +Capacitor_THT:CP_Radial_D10.0mm_P5.00mm +Capacitor_THT:CP_Radial_D10.0mm_P5.00mm_P7.50mm +Capacitor_THT:CP_Radial_D10.0mm_P7.50mm +Capacitor_THT:CP_Radial_D12.5mm_P2.50mm +Capacitor_THT:CP_Radial_D12.5mm_P5.00mm +Capacitor_THT:CP_Radial_D12.5mm_P7.50mm +Capacitor_THT:CP_Radial_D13.0mm_P2.50mm +Capacitor_THT:CP_Radial_D13.0mm_P5.00mm +Capacitor_THT:CP_Radial_D13.0mm_P7.50mm +Capacitor_THT:CP_Radial_D14.0mm_P5.00mm +Capacitor_THT:CP_Radial_D14.0mm_P7.50mm +Capacitor_THT:CP_Radial_D16.0mm_P7.50mm +Capacitor_THT:CP_Radial_D17.0mm_P7.50mm +Capacitor_THT:CP_Radial_D18.0mm_P7.50mm +Capacitor_THT:CP_Radial_D22.0mm_P10.00mm_3pin_SnapIn +Capacitor_THT:CP_Radial_D22.0mm_P10.00mm_SnapIn +Capacitor_THT:CP_Radial_D24.0mm_P10.00mm_3pin_SnapIn +Capacitor_THT:CP_Radial_D24.0mm_P10.00mm_SnapIn +Capacitor_THT:CP_Radial_D25.0mm_P10.00mm_3pin_SnapIn +Capacitor_THT:CP_Radial_D25.0mm_P10.00mm_SnapIn +Capacitor_THT:CP_Radial_D26.0mm_P10.00mm_3pin_SnapIn +Capacitor_THT:CP_Radial_D26.0mm_P10.00mm_SnapIn +Capacitor_THT:CP_Radial_D30.0mm_P10.00mm_3pin_SnapIn +Capacitor_THT:CP_Radial_D30.0mm_P10.00mm_SnapIn +Capacitor_THT:CP_Radial_D35.0mm_P10.00mm_3pin_SnapIn +Capacitor_THT:CP_Radial_D35.0mm_P10.00mm_SnapIn +Capacitor_THT:CP_Radial_D4.0mm_P1.50mm +Capacitor_THT:CP_Radial_D4.0mm_P2.00mm +Capacitor_THT:CP_Radial_D40.0mm_P10.00mm_3pin_SnapIn +Capacitor_THT:CP_Radial_D40.0mm_P10.00mm_SnapIn +Capacitor_THT:CP_Radial_D5.0mm_P2.00mm +Capacitor_THT:CP_Radial_D5.0mm_P2.50mm +Capacitor_THT:CP_Radial_D6.3mm_P2.50mm +Capacitor_THT:CP_Radial_D7.5mm_P2.50mm +Capacitor_THT:CP_Radial_D8.0mm_P2.50mm +Capacitor_THT:CP_Radial_D8.0mm_P3.50mm +Capacitor_THT:CP_Radial_D8.0mm_P3.80mm +Capacitor_THT:CP_Radial_D8.0mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D10.5mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D10.5mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D4.5mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D4.5mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D5.0mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D5.0mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D5.5mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D5.5mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D6.0mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D6.0mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D7.0mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D7.0mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D8.0mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D8.0mm_P5.00mm +Capacitor_THT:CP_Radial_Tantal_D9.0mm_P2.50mm +Capacitor_THT:CP_Radial_Tantal_D9.0mm_P5.00mm +Capacitor_THT:C_Axial_L12.0mm_D10.5mm_P15.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D10.5mm_P20.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D6.5mm_P15.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D6.5mm_P20.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D7.5mm_P15.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D7.5mm_P20.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D8.5mm_P15.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D8.5mm_P20.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D9.5mm_P15.00mm_Horizontal +Capacitor_THT:C_Axial_L12.0mm_D9.5mm_P20.00mm_Horizontal +Capacitor_THT:C_Axial_L17.0mm_D6.5mm_P20.00mm_Horizontal +Capacitor_THT:C_Axial_L17.0mm_D6.5mm_P25.00mm_Horizontal +Capacitor_THT:C_Axial_L17.0mm_D7.0mm_P20.00mm_Horizontal +Capacitor_THT:C_Axial_L17.0mm_D7.0mm_P25.00mm_Horizontal +Capacitor_THT:C_Axial_L19.0mm_D7.5mm_P25.00mm_Horizontal +Capacitor_THT:C_Axial_L19.0mm_D8.0mm_P25.00mm_Horizontal +Capacitor_THT:C_Axial_L19.0mm_D9.0mm_P25.00mm_Horizontal +Capacitor_THT:C_Axial_L19.0mm_D9.5mm_P25.00mm_Horizontal +Capacitor_THT:C_Axial_L22.0mm_D10.5mm_P27.50mm_Horizontal +Capacitor_THT:C_Axial_L22.0mm_D9.5mm_P27.50mm_Horizontal +Capacitor_THT:C_Axial_L3.8mm_D2.6mm_P10.00mm_Horizontal +Capacitor_THT:C_Axial_L3.8mm_D2.6mm_P12.50mm_Horizontal +Capacitor_THT:C_Axial_L3.8mm_D2.6mm_P15.00mm_Horizontal +Capacitor_THT:C_Axial_L3.8mm_D2.6mm_P7.50mm_Horizontal +Capacitor_THT:C_Axial_L5.1mm_D3.1mm_P10.00mm_Horizontal +Capacitor_THT:C_Axial_L5.1mm_D3.1mm_P12.50mm_Horizontal +Capacitor_THT:C_Axial_L5.1mm_D3.1mm_P15.00mm_Horizontal +Capacitor_THT:C_Axial_L5.1mm_D3.1mm_P7.50mm_Horizontal +Capacitor_THT:C_Disc_D10.0mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D10.5mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D10.5mm_W5.0mm_P5.00mm +Capacitor_THT:C_Disc_D10.5mm_W5.0mm_P7.50mm +Capacitor_THT:C_Disc_D11.0mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D11.0mm_W5.0mm_P5.00mm +Capacitor_THT:C_Disc_D11.0mm_W5.0mm_P7.50mm +Capacitor_THT:C_Disc_D12.0mm_W4.4mm_P7.75mm +Capacitor_THT:C_Disc_D12.5mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D12.5mm_W5.0mm_P7.50mm +Capacitor_THT:C_Disc_D14.5mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D14.5mm_W5.0mm_P7.50mm +Capacitor_THT:C_Disc_D16.0mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D16.0mm_W5.0mm_P7.50mm +Capacitor_THT:C_Disc_D3.0mm_W1.6mm_P2.50mm +Capacitor_THT:C_Disc_D3.0mm_W2.0mm_P2.50mm +Capacitor_THT:C_Disc_D3.4mm_W2.1mm_P2.50mm +Capacitor_THT:C_Disc_D3.8mm_W2.6mm_P2.50mm +Capacitor_THT:C_Disc_D4.3mm_W1.9mm_P5.00mm +Capacitor_THT:C_Disc_D4.7mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P2.50mm +Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D5.1mm_W3.2mm_P5.00mm +Capacitor_THT:C_Disc_D6.0mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D6.0mm_W4.4mm_P5.00mm +Capacitor_THT:C_Disc_D7.0mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D7.5mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D7.5mm_W4.4mm_P5.00mm +Capacitor_THT:C_Disc_D7.5mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D7.5mm_W5.0mm_P5.00mm +Capacitor_THT:C_Disc_D7.5mm_W5.0mm_P7.50mm +Capacitor_THT:C_Disc_D8.0mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D8.0mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D8.0mm_W5.0mm_P5.00mm +Capacitor_THT:C_Disc_D8.0mm_W5.0mm_P7.50mm +Capacitor_THT:C_Disc_D9.0mm_W2.5mm_P5.00mm +Capacitor_THT:C_Disc_D9.0mm_W5.0mm_P10.00mm +Capacitor_THT:C_Disc_D9.0mm_W5.0mm_P5.00mm +Capacitor_THT:C_Disc_D9.0mm_W5.0mm_P7.50mm +Capacitor_THT:C_Radial_D10.0mm_H12.5mm_P5.00mm +Capacitor_THT:C_Radial_D10.0mm_H16.0mm_P5.00mm +Capacitor_THT:C_Radial_D10.0mm_H20.0mm_P5.00mm +Capacitor_THT:C_Radial_D12.5mm_H20.0mm_P5.00mm +Capacitor_THT:C_Radial_D12.5mm_H25.0mm_P5.00mm +Capacitor_THT:C_Radial_D16.0mm_H25.0mm_P7.50mm +Capacitor_THT:C_Radial_D16.0mm_H31.5mm_P7.50mm +Capacitor_THT:C_Radial_D18.0mm_H35.5mm_P7.50mm +Capacitor_THT:C_Radial_D4.0mm_H5.0mm_P1.50mm +Capacitor_THT:C_Radial_D4.0mm_H7.0mm_P1.50mm +Capacitor_THT:C_Radial_D5.0mm_H11.0mm_P2.00mm +Capacitor_THT:C_Radial_D5.0mm_H5.0mm_P2.00mm +Capacitor_THT:C_Radial_D5.0mm_H7.0mm_P2.00mm +Capacitor_THT:C_Radial_D6.3mm_H11.0mm_P2.50mm +Capacitor_THT:C_Radial_D6.3mm_H5.0mm_P2.50mm +Capacitor_THT:C_Radial_D6.3mm_H7.0mm_P2.50mm +Capacitor_THT:C_Radial_D8.0mm_H11.5mm_P3.50mm +Capacitor_THT:C_Radial_D8.0mm_H7.0mm_P3.50mm +Capacitor_THT:C_Rect_L10.0mm_W2.5mm_P7.50mm_MKS4 +Capacitor_THT:C_Rect_L10.0mm_W3.0mm_P7.50mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L10.0mm_W3.0mm_P7.50mm_MKS4 +Capacitor_THT:C_Rect_L10.0mm_W4.0mm_P7.50mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L10.0mm_W4.0mm_P7.50mm_MKS4 +Capacitor_THT:C_Rect_L10.0mm_W5.0mm_P5.00mm_P7.50mm +Capacitor_THT:C_Rect_L10.3mm_W4.5mm_P7.50mm_MKS4 +Capacitor_THT:C_Rect_L10.3mm_W5.0mm_P7.50mm_MKS4 +Capacitor_THT:C_Rect_L10.3mm_W5.7mm_P7.50mm_MKS4 +Capacitor_THT:C_Rect_L10.3mm_W7.2mm_P7.50mm_MKS4 +Capacitor_THT:C_Rect_L11.0mm_W2.8mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W3.4mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W3.5mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W4.2mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W4.3mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W5.1mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W5.3mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W6.3mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W6.4mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W7.3mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.0mm_W8.8mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W2.0mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W2.6mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W2.8mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W3.2mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W3.5mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W3.6mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W4.0mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W4.3mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W4.5mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W5.0mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W5.1mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W5.2mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W5.6mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W6.4mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W6.6mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W6.9mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W7.3mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W7.5mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W7.8mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W8.0mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W8.8mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W9.5mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L11.5mm_W9.8mm_P10.00mm_MKT +Capacitor_THT:C_Rect_L13.0mm_W3.0mm_P10.00mm_FKS3_FKP3_MKS4 +Capacitor_THT:C_Rect_L13.0mm_W4.0mm_P10.00mm_FKS3_FKP3_MKS4 +Capacitor_THT:C_Rect_L13.0mm_W5.0mm_P10.00mm_FKS3_FKP3_MKS4 +Capacitor_THT:C_Rect_L13.0mm_W6.0mm_P10.00mm_FKS3_FKP3_MKS4 +Capacitor_THT:C_Rect_L13.0mm_W6.5mm_P7.50mm_P10.00mm +Capacitor_THT:C_Rect_L13.0mm_W8.0mm_P10.00mm_FKS3_FKP3_MKS4 +Capacitor_THT:C_Rect_L13.5mm_W4.0mm_P10.00mm_FKS3_FKP3_MKS4 +Capacitor_THT:C_Rect_L13.5mm_W5.0mm_P10.00mm_FKS3_FKP3_MKS4 +Capacitor_THT:C_Rect_L16.5mm_W10.7mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W10.9mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W11.2mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W11.8mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W13.5mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W13.7mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W13.9mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W4.7mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W4.9mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W5.0mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W6.0mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W7.0mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W7.3mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W8.7mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W8.9mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W9.0mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L16.5mm_W9.2mm_P15.00mm_MKT +Capacitor_THT:C_Rect_L18.0mm_W11.0mm_P15.00mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L18.0mm_W5.0mm_P15.00mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L18.0mm_W6.0mm_P15.00mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L18.0mm_W7.0mm_P15.00mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L18.0mm_W8.0mm_P15.00mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L18.0mm_W9.0mm_P15.00mm_FKS3_FKP3 +Capacitor_THT:C_Rect_L19.0mm_W11.0mm_P15.00mm_MKS4 +Capacitor_THT:C_Rect_L19.0mm_W5.0mm_P15.00mm_MKS4 +Capacitor_THT:C_Rect_L19.0mm_W6.0mm_P15.00mm_MKS4 +Capacitor_THT:C_Rect_L19.0mm_W7.0mm_P15.00mm_MKS4 +Capacitor_THT:C_Rect_L19.0mm_W8.0mm_P15.00mm_MKS4 +Capacitor_THT:C_Rect_L19.0mm_W9.0mm_P15.00mm_MKS4 +Capacitor_THT:C_Rect_L24.0mm_W10.1mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W10.3mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W10.9mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W12.2mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W12.6mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W12.8mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W7.0mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W8.3mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L24.0mm_W8.6mm_P22.50mm_MKT +Capacitor_THT:C_Rect_L26.5mm_W10.5mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L26.5mm_W11.5mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L26.5mm_W5.0mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L26.5mm_W6.0mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L26.5mm_W7.0mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L26.5mm_W8.5mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L27.0mm_W11.0mm_P22.00mm +Capacitor_THT:C_Rect_L27.0mm_W9.0mm_P22.00mm +Capacitor_THT:C_Rect_L27.0mm_W9.0mm_P23.00mm +Capacitor_THT:C_Rect_L28.0mm_W10.0mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L28.0mm_W12.0mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L28.0mm_W8.0mm_P22.50mm_MKS4 +Capacitor_THT:C_Rect_L29.0mm_W11.0mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W11.9mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W12.2mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W13.0mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W13.8mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W14.2mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W16.0mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W7.6mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W7.8mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W7.9mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W9.1mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L29.0mm_W9.6mm_P27.50mm_MKT +Capacitor_THT:C_Rect_L31.5mm_W11.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L31.5mm_W13.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L31.5mm_W15.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L31.5mm_W17.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L31.5mm_W20.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L31.5mm_W9.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L32.0mm_W15.0mm_P27.00mm +Capacitor_THT:C_Rect_L33.0mm_W13.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L33.0mm_W15.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L33.0mm_W20.0mm_P27.50mm_MKS4 +Capacitor_THT:C_Rect_L4.0mm_W2.5mm_P2.50mm +Capacitor_THT:C_Rect_L4.6mm_W2.0mm_P2.50mm_MKS02_FKP02 +Capacitor_THT:C_Rect_L4.6mm_W3.0mm_P2.50mm_MKS02_FKP02 +Capacitor_THT:C_Rect_L4.6mm_W3.8mm_P2.50mm_MKS02_FKP02 +Capacitor_THT:C_Rect_L4.6mm_W4.6mm_P2.50mm_MKS02_FKP02 +Capacitor_THT:C_Rect_L4.6mm_W5.5mm_P2.50mm_MKS02_FKP02 +Capacitor_THT:C_Rect_L41.5mm_W11.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W13.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W15.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W17.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W19.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W20.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W24.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W31.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W35.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W40.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L41.5mm_W9.0mm_P37.50mm_MKS4 +Capacitor_THT:C_Rect_L7.0mm_W2.0mm_P5.00mm +Capacitor_THT:C_Rect_L7.0mm_W2.5mm_P5.00mm +Capacitor_THT:C_Rect_L7.0mm_W3.5mm_P2.50mm_P5.00mm +Capacitor_THT:C_Rect_L7.0mm_W3.5mm_P5.00mm +Capacitor_THT:C_Rect_L7.0mm_W4.5mm_P5.00mm +Capacitor_THT:C_Rect_L7.0mm_W6.0mm_P5.00mm +Capacitor_THT:C_Rect_L7.0mm_W6.5mm_P5.00mm +Capacitor_THT:C_Rect_L7.2mm_W11.0mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W2.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W3.0mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W3.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W4.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W5.5mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W7.2mm_P5.00mm_FKS2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.2mm_W8.5mm_P5.00mm_FKP2_FKP2_MKS2_MKP2 +Capacitor_THT:C_Rect_L7.5mm_W6.5mm_P5.00mm +Capacitor_THT:C_Rect_L9.0mm_W2.5mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W2.6mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W2.7mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W3.2mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W3.3mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W3.4mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W3.6mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W3.8mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W3.9mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W4.0mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W4.2mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W4.9mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W5.1mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W5.7mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W6.4mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W6.7mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W7.7mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W8.5mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W9.5mm_P7.50mm_MKT +Capacitor_THT:C_Rect_L9.0mm_W9.8mm_P7.50mm_MKT +Capacitor_THT:DX_5R5HxxxxU_D11.5mm_P10.00mm +Capacitor_THT:DX_5R5VxxxxU_D11.5mm_P5.00mm +Capacitor_THT:DX_5R5VxxxxU_D19.0mm_P5.00mm +Connector:Banana_Cliff_FCR7350B_S16N-PC_Horizontal +Connector:Banana_Cliff_FCR7350G_S16N-PC_Horizontal +Connector:Banana_Cliff_FCR7350L_S16N-PC_Horizontal +Connector:Banana_Cliff_FCR7350N_S16N-PC_Horizontal +Connector:Banana_Cliff_FCR7350R_S16N-PC_Horizontal +Connector:Banana_Cliff_FCR7350Y_S16N-PC_Horizontal +Connector:Banana_Jack_1Pin +Connector:Banana_Jack_2Pin +Connector:Banana_Jack_3Pin +Connector:CalTest_CT3151 +Connector:Connector_SFP_and_Cage +Connector:CUI_PD-30 +Connector:CUI_PD-30S +Connector:CUI_PD-30S_CircularHoles +Connector:DTF13-12Px +Connector:FanPinHeader_1x03_P2.54mm_Vertical +Connector:FanPinHeader_1x04_P2.54mm_Vertical +Connector:GB042-34S-H10 +Connector:IHI_B6A-PCB-45_Vertical +Connector:Joint-Tech_C5080WR-04P_1x04_P5.08mm_Vertical +Connector:JWT_A3963_1x02_P3.96mm_Vertical +Connector:NS-Tech_Grove_1x04_P2mm_Vertical +Connector:OCN_OK-01GM030-04_2x15_P0.4mm_Vertical +Connector:SpringContact_Harwin_S1941-46R +Connector:Tag-Connect_TC2030-IDC-FP_2x03_P1.27mm_Vertical +Connector:Tag-Connect_TC2030-IDC-NL_2x03_P1.27mm_Vertical +Connector:Tag-Connect_TC2050-IDC-FP_2x05_P1.27mm_Vertical +Connector:Tag-Connect_TC2050-IDC-NL_2x05_P1.27mm_Vertical +Connector:Tag-Connect_TC2050-IDC-NL_2x05_P1.27mm_Vertical_with_bottom_clip +Connector:Tag-Connect_TC2070-IDC-FP_2x07_P1.27mm_Vertical +Connector_AMASS:AMASS_MR30PW-FB_1x03_P3.50mm_Horizontal +Connector_AMASS:AMASS_MR30PW-M_1x03_P3.50mm_Horizontal +Connector_AMASS:AMASS_XT30PW-F_1x02_P2.50mm_Horizontal +Connector_AMASS:AMASS_XT30PW-M_1x02_P2.50mm_Horizontal +Connector_AMASS:AMASS_XT30U-F_1x02_P5.0mm_Vertical +Connector_AMASS:AMASS_XT30U-M_1x02_P5.0mm_Vertical +Connector_AMASS:AMASS_XT30UPB-F_1x02_P5.0mm_Vertical +Connector_AMASS:AMASS_XT30UPB-M_1x02_P5.0mm_Vertical +Connector_AMASS:AMASS_XT60-F_1x02_P7.20mm_Vertical +Connector_AMASS:AMASS_XT60-M_1x02_P7.20mm_Vertical +Connector_AMASS:AMASS_XT60IPW-M_1x03_P7.20mm_Horizontal +Connector_AMASS:AMASS_XT60PW-F_1x02_P7.20mm_Horizontal +Connector_AMASS:AMASS_XT60PW-M_1x02_P7.20mm_Horizontal +Connector_AMASS:AMASS_XT90PW-M_1x02_P10.90mm_Horizontal +Connector_Amphenol:Amphenol_M8S-03PMMR-SF8001 +Connector_Audio:Jack_3.5mm_CUI_SJ-3523-SMT_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ-3524-SMT_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3513N_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3514N_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3515N_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3523N_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3524N_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3525N_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3533NG_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3533NG_Horizontal_CircularHoles +Connector_Audio:Jack_3.5mm_CUI_SJ1-3535NG_Horizontal +Connector_Audio:Jack_3.5mm_CUI_SJ1-3535NG_Horizontal_CircularHoles +Connector_Audio:Jack_3.5mm_CUI_SJ2-3593D-SMT_Horizontal +Connector_Audio:Jack_3.5mm_KoreanHropartsElec_PJ-320D-4A_Horizontal +Connector_Audio:Jack_3.5mm_Ledino_KB3SPRS_Horizontal +Connector_Audio:Jack_3.5mm_Lumberg_1503_02_Horizontal +Connector_Audio:Jack_3.5mm_Lumberg_1503_03_Horizontal +Connector_Audio:Jack_3.5mm_Lumberg_1503_07_Horizontal +Connector_Audio:Jack_3.5mm_PJ31060-I_Horizontal +Connector_Audio:Jack_3.5mm_PJ311_Horizontal +Connector_Audio:Jack_3.5mm_PJ320D_Horizontal +Connector_Audio:Jack_3.5mm_PJ320E_Horizontal +Connector_Audio:Jack_3.5mm_QingPu_WQP-PJ398SM_Vertical_CircularHoles +Connector_Audio:Jack_3.5mm_Switronic_ST-005-G_horizontal +Connector_Audio:Jack_3.5mm_Technik_TWP-3002_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NJ2FD-V_Vertical +Connector_Audio:Jack_6.35mm_Neutrik_NJ3FD-V_Vertical +Connector_Audio:Jack_6.35mm_Neutrik_NJ5FD-V_Vertical +Connector_Audio:Jack_6.35mm_Neutrik_NJ6FD-V_Vertical +Connector_Audio:Jack_6.35mm_Neutrik_NJ6TB-V_Vertical +Connector_Audio:Jack_6.35mm_Neutrik_NMJ4HCD2_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ4HFD2_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ4HFD3_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ4HHD2_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ6HCD2_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ6HCD3_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ6HFD2-AU_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ6HFD2_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ6HFD3_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ6HFD4_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NMJ6HHD2_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ3HF-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ4HF-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ4HF_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ4HH-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ4HH_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HF-1-AU_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HF-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HF-AU_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HF_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HH-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HH-AU_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HH_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HM-1-AU_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HM-1-PRE_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NRJ6HM-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NSJ12HC_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NSJ12HF-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NSJ12HH-1_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NSJ12HL_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NSJ8HC_Horizontal +Connector_Audio:Jack_6.35mm_Neutrik_NSJ8HL_Horizontal +Connector_Audio:Jack_speakON-6.35mm_Neutrik_NLJ2MDXX-H_Horizontal +Connector_Audio:Jack_speakON-6.35mm_Neutrik_NLJ2MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL2MDXX-H-3_Horizontal +Connector_Audio:Jack_speakON_Neutrik_NL2MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-H-2_Horizontal +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-H-3_Horizontal +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V-2_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V-3_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL4MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL8MDXX-V-3_Vertical +Connector_Audio:Jack_speakON_Neutrik_NL8MDXX-V_Vertical +Connector_Audio:Jack_speakON_Neutrik_NLT4MD-V_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ10FI-H-0_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ10FI-H_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ10FI-V-0_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ10FI-V_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ5FI-H-0_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ5FI-H_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ5FI-V-0_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ5FI-V_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FA-H-0_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FA-H-DA_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FA-H_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FA-V-0_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FA-V-DA_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FA-V_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FI-H-0_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FI-H_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FI-V-0_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ6FI-V_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ9FI-H-0_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ9FI-H_Horizontal +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ9FI-V-0_Vertical +Connector_Audio:Jack_XLR-6.35mm_Neutrik_NCJ9FI-V_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAAH-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAAH1-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAAH1-DA_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAAH1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAAH2-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAAH2_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAAV-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAAV1-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAAV1-DA_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAAV1_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAAV2-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAAV2_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAH-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAH1-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAH1-DA_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAH1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAH2-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAH2-DA_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAH2_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHL-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHL1-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHL1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHR-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHR1-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHR1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHR2-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAHR2_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FAV-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAV1-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAV1-DA_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAV1_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAV2-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAV2-DA_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAV2_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBH1-B_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBH1-DA_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBH1-E_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBH1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBH2-B_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBH2-DA_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBH2-E_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBH2_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBHL1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3FBV1-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBV1-B_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBV1-DA_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBV1_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBV2-B_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBV2-DA_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBV2-SW_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3FBV2_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MAAH-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAAH-1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAAV-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MAAV-1_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MAAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MAFH-PH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAH-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAHL_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAHR_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAMH-PH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MAV-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MBH-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBH-1_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBH-B_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBH-E_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBHL-B_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBHL_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBHR-B_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBHR_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC3MBV-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MBV-1_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MBV-B_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MBV-E_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MBV-SW_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC3MBV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC4FAH-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC4FAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC4FAV-0_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC4FAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC4FBH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC4FBV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC4MAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC4MAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC4MBH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC4MBV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5FAH-0_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5FAH-DA_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5FAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5FAV-DA_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5FAV-SW_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5FAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5FBH-B_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5FBH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5FBV-B_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5FBV-SW_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5FBV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5MAH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5MAV-SW_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5MAV_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5MBH-B_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5MBH_Horizontal +Connector_Audio:Jack_XLR_Neutrik_NC5MBV-B_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5MBV-SW_Vertical +Connector_Audio:Jack_XLR_Neutrik_NC5MBV_Vertical +Connector_Audio:MiniXLR-5_Switchcraft_TRAPC_Horizontal +Connector_Audio:Plug_3.5mm_CUI_SP-3541 +Connector_BarrelJack:BarrelJack_CLIFF_FC681465S_SMT_Horizontal +Connector_BarrelJack:BarrelJack_CUI_PJ-036AH-SMT_Horizontal +Connector_BarrelJack:BarrelJack_CUI_PJ-063AH_Horizontal +Connector_BarrelJack:BarrelJack_CUI_PJ-063AH_Horizontal_CircularHoles +Connector_BarrelJack:BarrelJack_CUI_PJ-079BH_Horizontal +Connector_BarrelJack:BarrelJack_CUI_PJ-102AH_Horizontal +Connector_BarrelJack:BarrelJack_GCT_DCJ200-10-A_Horizontal +Connector_BarrelJack:BarrelJack_Horizontal +Connector_BarrelJack:BarrelJack_Kycon_KLDX-0202-xC_Horizontal +Connector_BarrelJack:BarrelJack_SwitchcraftConxall_RAPC10U_Horizontal +Connector_BarrelJack:BarrelJack_Wuerth_694102107102_1.0x3.9mm +Connector_BarrelJack:BarrelJack_Wuerth_694103107102_1.35x3.9mm +Connector_BarrelJack:BarrelJack_Wuerth_694106106102_2.0x5.5mm +Connector_BarrelJack:BarrelJack_Wuerth_694108106102_2.5x5.5mm +Connector_BarrelJack:BarrelJack_Wuerth_6941xx301002 +Connector_Card:CF-Card_3M_N7E50-A516xx-30 +Connector_Card:CF-Card_3M_N7E50-E516xx-30 +Connector_Card:microSD_HC_Hirose_DM3AT-SF-PEJM5 +Connector_Card:microSD_HC_Hirose_DM3BT-DSF-PEJS +Connector_Card:microSD_HC_Hirose_DM3D-SF +Connector_Card:microSD_HC_Molex_104031-0811 +Connector_Card:microSD_HC_Molex_47219-2001 +Connector_Card:microSD_HC_Wuerth_693072010801 +Connector_Card:microSIM_JAE_SF53S006VCBR2000 +Connector_Card:nanoSIM_GCT_SIM8060-6-0-14-00 +Connector_Card:nanoSIM_GCT_SIM8060-6-1-14-00 +Connector_Card:nanoSIM_Hinged_CUI_NSIM-2-C +Connector_Card:SD-SIM_microSD-microSIM_Molex_104168-1620 +Connector_Card:SD_Card_Device_16mm_SlotDepth +Connector_Card:SD_Hirose_DM1AA_SF_PEJ82 +Connector_Card:SD_Kyocera_145638009211859+ +Connector_Card:SD_Kyocera_145638009511859+ +Connector_Card:SD_Kyocera_145638109211859+ +Connector_Card:SD_Kyocera_145638109511859+ +Connector_Card:SD_TE_2041021 +Connector_Coaxial:BNC_Amphenol_031-5539_Vertical +Connector_Coaxial:BNC_Amphenol_031-6575_Horizontal +Connector_Coaxial:BNC_Amphenol_B6252HB-NPP3G-50_Horizontal +Connector_Coaxial:BNC_PanelMountable_Vertical +Connector_Coaxial:BNC_TEConnectivity_1478035_Horizontal +Connector_Coaxial:BNC_TEConnectivity_1478204_Vertical +Connector_Coaxial:BNC_Win_364A2x95_Horizontal +Connector_Coaxial:CoaxialSwitch_Hirose_MS-156C3_Horizontal +Connector_Coaxial:LEMO-EPG.00.302.NLN +Connector_Coaxial:LEMO-EPL.00.250.NTN +Connector_Coaxial:MMCX_Molex_73415-0961_Horizontal_0.8mm-PCB +Connector_Coaxial:MMCX_Molex_73415-0961_Horizontal_1.0mm-PCB +Connector_Coaxial:MMCX_Molex_73415-0961_Horizontal_1.6mm-PCB +Connector_Coaxial:MMCX_Molex_73415-1471_Vertical +Connector_Coaxial:SMA_Amphenol_132134-10_Vertical +Connector_Coaxial:SMA_Amphenol_132134-11_Vertical +Connector_Coaxial:SMA_Amphenol_132134-14_Vertical +Connector_Coaxial:SMA_Amphenol_132134-16_Vertical +Connector_Coaxial:SMA_Amphenol_132134_Vertical +Connector_Coaxial:SMA_Amphenol_132203-12_Horizontal +Connector_Coaxial:SMA_Amphenol_132289_EdgeMount +Connector_Coaxial:SMA_Amphenol_132291-12_Vertical +Connector_Coaxial:SMA_Amphenol_132291_Vertical +Connector_Coaxial:SMA_Amphenol_901-143_Horizontal +Connector_Coaxial:SMA_Amphenol_901-144_Vertical +Connector_Coaxial:SMA_BAT_Wireless_BWSMA-KWE-Z001 +Connector_Coaxial:SMA_Molex_73251-1153_EdgeMount_Horizontal +Connector_Coaxial:SMA_Molex_73251-2120_EdgeMount_Horizontal +Connector_Coaxial:SMA_Molex_73251-2200_Horizontal +Connector_Coaxial:SMA_Samtec_SMA-J-P-H-ST-EM1_EdgeMount +Connector_Coaxial:SMA_Wurth_60312002114503_Vertical +Connector_Coaxial:SMA_Wurth_60312102114405_Vertical +Connector_Coaxial:SMB_Jack_Vertical +Connector_Coaxial:U.FL_Hirose_U.FL-R-SMT-1_Vertical +Connector_Coaxial:U.FL_Molex_MCRF_73412-0110_Vertical +Connector_Coaxial:WR-MMCX_Wuerth_66011102111302_Horizontal +Connector_Coaxial:WR-MMCX_Wuerth_66012102111404_Vertical +Connector_DIN:DIN41612_B2_2x16_Female_Vertical_THT +Connector_DIN:DIN41612_B2_2x16_Male_Horizontal_THT +Connector_DIN:DIN41612_B2_2x8_Female_Vertical_THT +Connector_DIN:DIN41612_B2_2x8_Male_Horizontal_THT +Connector_DIN:DIN41612_B3_2x10_Female_Vertical_THT +Connector_DIN:DIN41612_B3_2x10_Male_Horizontal_THT +Connector_DIN:DIN41612_B3_2x5_Female_Vertical_THT +Connector_DIN:DIN41612_B3_2x5_Male_Horizontal_THT +Connector_DIN:DIN41612_B_1x32_Female_Vertical_THT +Connector_DIN:DIN41612_B_1x32_Male_Horizontal_THT +Connector_DIN:DIN41612_B_2x16_Female_Vertical_THT +Connector_DIN:DIN41612_B_2x16_Male_Horizontal_THT +Connector_DIN:DIN41612_B_2x32_Female_Vertical_THT +Connector_DIN:DIN41612_B_2x32_Male_Horizontal_THT +Connector_DIN:DIN41612_C2_2x16_Female_Vertical_THT +Connector_DIN:DIN41612_C2_2x16_Male_Horizontal_THT +Connector_DIN:DIN41612_C2_3x16_Female_Vertical_THT +Connector_DIN:DIN41612_C2_3x16_Male_Horizontal_THT +Connector_DIN:DIN41612_C3_2x10_Female_Vertical_THT +Connector_DIN:DIN41612_C3_2x10_Male_Horizontal_THT +Connector_DIN:DIN41612_C3_3x10_Female_Vertical_THT +Connector_DIN:DIN41612_C3_3x10_Male_Horizontal_THT +Connector_DIN:DIN41612_C_1x32_Female_Vertical_THT +Connector_DIN:DIN41612_C_1x32_Male_Horizontal_THT +Connector_DIN:DIN41612_C_2x16_Female_Vertical_THT +Connector_DIN:DIN41612_C_2x16_Male_Horizontal_THT +Connector_DIN:DIN41612_C_2x32_Female_Vertical_THT +Connector_DIN:DIN41612_C_2x32_Male_Horizontal_THT +Connector_DIN:DIN41612_C_3x16_Female_Vertical_THT +Connector_DIN:DIN41612_C_3x16_Male_Horizontal_THT +Connector_DIN:DIN41612_C_3x32_Female_Vertical_THT +Connector_DIN:DIN41612_C_3x32_Male_Horizontal_THT +Connector_DIN:DIN41612_D_2x16_Female_Vertical_THT +Connector_DIN:DIN41612_D_2x16_Male_Horizontal_THT +Connector_DIN:DIN41612_D_2x8_Female_Vertical_THT +Connector_DIN:DIN41612_D_2x8_Male_Horizontal_THT +Connector_DIN:DIN41612_E_2x16_Female_Vertical_THT +Connector_DIN:DIN41612_E_2x16_Male_Horizontal_THT +Connector_DIN:DIN41612_E_2x16_RowsAC_Female_Vertical_THT +Connector_DIN:DIN41612_E_2x16_RowsAC_Male_Horizontal_THT +Connector_DIN:DIN41612_E_3x16_Female_Vertical_THT +Connector_DIN:DIN41612_E_3x16_Male_Horizontal_THT +Connector_DIN:DIN41612_F_2x16_Female_Vertical_THT +Connector_DIN:DIN41612_F_2x16_Male_Horizontal_THT +Connector_DIN:DIN41612_F_2x16_RowsZD_Female_Vertical_THT +Connector_DIN:DIN41612_F_2x16_RowsZD_Male_Horizontal_THT +Connector_DIN:DIN41612_F_3x16_Female_Vertical_THT +Connector_DIN:DIN41612_F_3x16_Male_Horizontal_THT +Connector_DIN:DIN41612_Q2_2x16_Female_Horizontal_THT +Connector_DIN:DIN41612_Q2_2x16_Male_Vertical_THT +Connector_DIN:DIN41612_Q3_2x10_Female_Horizontal_THT +Connector_DIN:DIN41612_Q3_2x10_Male_Vertical_THT +Connector_DIN:DIN41612_Q_2x32_Female_Horizontal_THT +Connector_DIN:DIN41612_Q_2x32_Male_Vertical_THT +Connector_DIN:DIN41612_R2_2x16_Female_Horizontal_THT +Connector_DIN:DIN41612_R2_2x16_Male_Vertical_THT +Connector_DIN:DIN41612_R2_3x16_Female_Horizontal_THT +Connector_DIN:DIN41612_R2_3x16_Male_Vertical_THT +Connector_DIN:DIN41612_R3_2x10_Female_Horizontal_THT +Connector_DIN:DIN41612_R3_2x10_Male_Vertical_THT +Connector_DIN:DIN41612_R3_3x10_Female_Horizontal_THT +Connector_DIN:DIN41612_R3_3x10_Male_Vertical_THT +Connector_DIN:DIN41612_R_1x32_Female_Horizontal_THT +Connector_DIN:DIN41612_R_1x32_Male_Vertical_THT +Connector_DIN:DIN41612_R_2x16_Female_Horizontal_THT +Connector_DIN:DIN41612_R_2x16_Male_Vertical_THT +Connector_DIN:DIN41612_R_2x32_Female_Horizontal_THT +Connector_DIN:DIN41612_R_2x32_Male_Vertical_THT +Connector_DIN:DIN41612_R_3x16_Female_Horizontal_THT +Connector_DIN:DIN41612_R_3x16_Male_Vertical_THT +Connector_DIN:DIN41612_R_3x32_Female_Horizontal_THT +Connector_DIN:DIN41612_R_3x32_Male_Vertical_THT +Connector_Dsub:DSUB-15-HD_Pins_Horizontal_P2.29x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-15-HD_Pins_Horizontal_P2.29x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-15-HD_Pins_Vertical_P2.29x1.98mm_MountingHoles +Connector_Dsub:DSUB-15-HD_Socket_Horizontal_P2.29x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-15-HD_Socket_Horizontal_P2.29x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-15-HD_Socket_Vertical_P2.29x1.98mm_MountingHoles +Connector_Dsub:DSUB-15_Pins_EdgeMount_P2.77mm +Connector_Dsub:DSUB-15_Pins_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-15_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-15_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-15_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-15_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-15_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-15_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-15_Pins_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-15_Pins_Vertical_P2.77x2.84mm_MountingHoles +Connector_Dsub:DSUB-15_Socket_EdgeMount_P2.77mm +Connector_Dsub:DSUB-15_Socket_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-15_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-15_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-15_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-15_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-15_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-15_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-15_Socket_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-15_Socket_Vertical_P2.77x2.84mm_MountingHoles +Connector_Dsub:DSUB-25_Pins_EdgeMount_P2.77mm +Connector_Dsub:DSUB-25_Pins_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-25_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-25_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-25_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-25_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-25_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-25_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-25_Pins_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-25_Pins_Vertical_P2.77x2.84mm_MountingHoles +Connector_Dsub:DSUB-25_Socket_EdgeMount_P2.77mm +Connector_Dsub:DSUB-25_Socket_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-25_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-25_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-25_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-25_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-25_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-25_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-25_Socket_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-25_Socket_Vertical_P2.77x2.84mm_MountingHoles +Connector_Dsub:DSUB-26-HD_Pins_Horizontal_P2.29x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-26-HD_Pins_Horizontal_P2.29x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-26-HD_Pins_Vertical_P2.29x1.98mm_MountingHoles +Connector_Dsub:DSUB-26-HD_Socket_Horizontal_P2.29x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-26-HD_Socket_Horizontal_P2.29x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-26-HD_Socket_Vertical_P2.29x1.98mm_MountingHoles +Connector_Dsub:DSUB-37_Pins_EdgeMount_P2.77mm +Connector_Dsub:DSUB-37_Pins_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-37_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-37_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-37_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-37_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-37_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-37_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-37_Pins_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-37_Pins_Vertical_P2.77x2.84mm_MountingHoles +Connector_Dsub:DSUB-37_Socket_EdgeMount_P2.77mm +Connector_Dsub:DSUB-37_Socket_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-37_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-37_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-37_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-37_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-37_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-37_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-37_Socket_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-37_Socket_Vertical_P2.77x2.84mm_MountingHoles +Connector_Dsub:DSUB-44-HD_Pins_Horizontal_P2.29x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-44-HD_Pins_Horizontal_P2.29x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-44-HD_Pins_Vertical_P2.29x1.98mm_MountingHoles +Connector_Dsub:DSUB-44-HD_Socket_Horizontal_P2.29x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-44-HD_Socket_Horizontal_P2.29x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-44-HD_Socket_Vertical_P2.29x1.98mm_MountingHoles +Connector_Dsub:DSUB-62-HD_Pins_Horizontal_P2.41x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-62-HD_Pins_Horizontal_P2.41x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-62-HD_Pins_Vertical_P2.41x1.98mm_MountingHoles +Connector_Dsub:DSUB-62-HD_Socket_Horizontal_P2.41x1.90mm_EdgePinOffset3.03mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-62-HD_Socket_Horizontal_P2.41x2.54mm_EdgePinOffset8.35mm_Housed_MountingHolesOffset10.89mm +Connector_Dsub:DSUB-62-HD_Socket_Vertical_P2.41x1.98mm_MountingHoles +Connector_Dsub:DSUB-9_Pins_EdgeMount_P2.77mm +Connector_Dsub:DSUB-9_Pins_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-9_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-9_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-9_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-9_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-9_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-9_Pins_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-9_Pins_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-9_Pins_Vertical_P2.77x2.84mm_MountingHoles +Connector_Dsub:DSUB-9_Socket_EdgeMount_P2.77mm +Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.54mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset15.98mm +Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset14.56mm_Housed_MountingHolesOffset8.20mm +Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset4.94mm_Housed_MountingHolesOffset4.94mm +Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset7.70mm_Housed_MountingHolesOffset9.12mm +Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.40mm +Connector_Dsub:DSUB-9_Socket_Horizontal_P2.77x2.84mm_EdgePinOffset9.90mm_Housed_MountingHolesOffset11.32mm +Connector_Dsub:DSUB-9_Socket_Vertical_P2.77x2.84mm +Connector_Dsub:DSUB-9_Socket_Vertical_P2.77x2.84mm_MountingHoles +Connector_FFC-FPC:Hirose_FH12-10S-0.5SH_1x10-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-11S-0.5SH_1x11-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-12S-0.5SH_1x12-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-13S-0.5SH_1x13-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-14S-0.5SH_1x14-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-15S-0.5SH_1x15-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-16S-0.5SH_1x16-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-17S-0.5SH_1x17-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-18S-0.5SH_1x18-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-19S-0.5SH_1x19-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-20S-0.5SH_1x20-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-22S-0.5SH_1x22-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-24S-0.5SH_1x24-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-25S-0.5SH_1x25-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-26S-0.5SH_1x26-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-28S-0.5SH_1x28-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-30S-0.5SH_1x30-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-32S-0.5SH_1x32-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-33S-0.5SH_1x33-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-34S-0.5SH_1x34-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-35S-0.5SH_1x35-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-36S-0.5SH_1x36-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-40S-0.5SH_1x40-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-45S-0.5SH_1x45-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-50S-0.5SH_1x50-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-53S-0.5SH_1x53-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-6S-0.5SH_1x06-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH12-8S-0.5SH_1x08-1MP_P0.50mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-13S-0.3SHW_2Rows-13Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-15S-0.3SHW_2Rows-15Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-17S-0.3SHW_2Rows-17Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-19S-0.3SHW_2Rows-19Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-21S-0.3SHW_2Rows-21Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-23S-0.3SHW_2Rows-23Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-25S-0.3SHW_2Rows-25Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-27S-0.3SHW_2Rows-27Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-29S-0.3SHW_2Rows-29Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-31S-0.3SHW_2Rows-31Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-33S-0.3SHW_2Rows-33Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-35S-0.3SHW_2Rows-35Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-37S-0.3SHW_2Rows-37Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-39S-0.3SHW_2Rows-39Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-41S-0.3SHW_2Rows-41Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-45S-0.3SHW_2Rows-45Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-51S-0.3SHW_2Rows-51Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-55S-0.3SHW_2Rows-55Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-57S-0.3SHW_2Rows-57Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-61S-0.3SHW_2Rows-61Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH26-71S-0.3SHW_2Rows-71Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Hirose_FH41-30S-0.5SH_1x30_1MP_1SH_P0.5mm_Horizontal +Connector_FFC-FPC:JAE_FF0825SA1_2Rows-25Pins_P0.40mm_Horizontal +Connector_FFC-FPC:JAE_FF0829SA1_2Rows-29Pins_P0.40mm_Horizontal +Connector_FFC-FPC:JAE_FF0841SA1_2Rows-41Pins_P0.40mm_Horizontal +Connector_FFC-FPC:JAE_FF0851SA1_2Rows-51Pins_P0.40mm_Horizontal +Connector_FFC-FPC:JAE_FF0871SA1_2Rows-71Pins_P0.40mm_Horizontal +Connector_FFC-FPC:JAE_FF0881SA1_2Rows-81Pins_P0.40mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S04FCA-00_1x4-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S05FCA-00_1x5-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S06FCA-00_1x6-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S07FCA-00_1x7-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S08FCA-00_1x8-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S09FCA-00_1x9-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S10FCA-00_1x10-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S11FCA-00_1x11-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S12FCA-00_1x12-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S13FCA-00_1x13-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S14FCA-00_1x14-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S15FCA-00_1x15-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S16FCA-00_1x16-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S17FCA-00_1x17-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S18FCA-00_1x18-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S19FCA-00_1x19-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S20FCA-00_1x20-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S21FCA-00_1x21-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S22FCA-00_1x22-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S23FCA-00_1x23-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S24FCA-00_1x24-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S25FCA-00_1x25-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S26FCA-00_1x26-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S27FCA-00_1x27-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S28FCA-00_1x28-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:JUSHUO_AFA07-S29FCA-00_1x29-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:Jushuo_AFC07-S06FCA-00_1x6-1MP_P0.50_Horizontal +Connector_FFC-FPC:Jushuo_AFC07-S24FCA-00_1x24-1MP_P0.50_Horizontal +Connector_FFC-FPC:Molex_200528-0040_1x04-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0050_1x05-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0060_1x06-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0070_1x07-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0080_1x08-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0090_1x09-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0100_1x10-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0110_1x11-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0120_1x12-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0130_1x13-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0140_1x14-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0150_1x15-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0160_1x16-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0170_1x17-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0180_1x18-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0190_1x19-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0200_1x20-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0210_1x21-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0220_1x22-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0230_1x23-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0240_1x24-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0250_1x25-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0260_1x26-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0270_1x27-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0280_1x28-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0290_1x29-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_200528-0300_1x30-1MP_P1.00mm_Horizontal +Connector_FFC-FPC:Molex_502231-1500_1x15-1SH_P0.5mm_Vertical +Connector_FFC-FPC:Molex_502231-2400_1x24-1SH_P0.5mm_Vertical +Connector_FFC-FPC:Molex_502231-3300_1x33-1SH_P0.5mm_Vertical +Connector_FFC-FPC:Molex_502244-1530_1x15-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Molex_502244-2430_1x24-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Molex_502244-3330_1x33-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Molex_502250-1791_2Rows-17Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-2191_2Rows-21Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-2391_2Rows-23Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-2791_2Rows-27Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-3391_2Rows-33Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-3591_2Rows-35Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-3991_2Rows-39Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-4191_2Rows-41Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_502250-5191_2Rows-51Pins-1MP_P0.60mm_Horizontal +Connector_FFC-FPC:Molex_52559-3652_2x18-1MP_P0.5mm_Vertical +Connector_FFC-FPC:Molex_54132-5033_1x50-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Molex_54548-1071_1x10-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:Omron_XF2M-4015-1A_1x40-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_0-1734839-5_1x05-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_0-1734839-6_1x06-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_0-1734839-7_1x07-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_0-1734839-8_1x08-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_0-1734839-9_1x09-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-0_1x10-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-1_1x11-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-2_1x12-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-3_1x13-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-4_1x14-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-5_1x15-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-6_1x16-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-7_1x17-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-8_1x18-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-1734839-9_1x19-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_1-84952-0_1x10-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-1_1x11-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-2_1x12-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-3_1x13-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-4_1x14-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-5_1x15-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-6_1x16-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-7_1x17-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-8_1x18-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84952-9_1x19-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-0_1x10-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-1_1x11-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-2_1x12-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-3_1x13-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-4_1x14-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-5_1x15-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-6_1x16-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-7_1x17-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-8_1x18-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_1-84953-9_1x19-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-0_1x20-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-1_1x21-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-2_1x22-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-3_1x23-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-4_1x24-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-5_1x25-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-6_1x26-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-7_1x27-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-8_1x28-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-1734839-9_1x29-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_2-84952-0_1x20-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-1_1x21-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-2_1x22-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-3_1x23-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-4_1x24-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-5_1x25-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-6_1x26-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-7_1x27-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-8_1x28-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84952-9_1x29-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-0_1x20-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-1_1x21-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-2_1x22-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-3_1x23-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-4_1x24-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-5_1x25-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-6_1x26-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-7_1x27-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-8_1x28-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_2-84953-9_1x29-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-0_1x30-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-1_1x31-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-2_1x32-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-3_1x33-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-4_1x34-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-5_1x35-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-6_1x36-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-7_1x37-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-8_1x38-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-1734839-9_1x39-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_3-84952-0_1x30-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_3-84953-0_1x30-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-0_1x40-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-1_1x41-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-2_1x42-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-3_1x43-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-4_1x44-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-5_1x45-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-6_1x46-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-7_1x47-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-8_1x48-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_4-1734839-9_1x49-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_5-1734839-0_1x50-1MP_P0.5mm_Horizontal +Connector_FFC-FPC:TE_84952-4_1x04-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84952-5_1x05-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84952-6_1x06-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84952-7_1x07-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84952-8_1x08-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84952-9_1x09-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84953-4_1x04-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84953-5_1x05-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84953-6_1x06-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84953-7_1x07-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84953-8_1x08-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:TE_84953-9_1x09-1MP_P1.0mm_Horizontal +Connector_FFC-FPC:Wuerth_68611214422_1x12-1MP_P1.0mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110213001xxx_1x02-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110213002xxx_1x02-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110213010xxx_1x02-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110313001xxx_1x03-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110313002xxx_1x03-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110313010xxx_1x03-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110413001xxx_1x04-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110413002xxx_1x04-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110413010xxx_1x04-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110513001xxx_1x05-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110513002xxx_1x05-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110513010xxx_1x05-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110613001xxx_1x06-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110613002xxx_1x06-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110613010xxx_1x06-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110713001xxx_1x07-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110713002xxx_1x07-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110713010xxx_1x07-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110813001xxx_1x08-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110813002xxx_1x08-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110813010xxx_1x08-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110913001xxx_1x09-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14110913002xxx_1x09-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14110913010xxx_1x09-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14111013001xxx_1x10-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14111013002xxx_1x10-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14111013010xxx_1x10-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14111113001xxx_1x11-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14111113002xxx_1x11-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14111113010xxx_1x11-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14111213001xxx_1x12-MP_P2.54mm_Vertical +Connector_Harting:Harting_har-flexicon_14111213002xxx_1x12-MP_P2.54mm_Horizontal +Connector_Harting:Harting_har-flexicon_14111213010xxx_1x12-MP_P2.54mm_Horizontal +Connector_Harwin:Harwin_Gecko-G125-FVX0605L0X_2x03_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-FVX1005L0X_2x05_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-FVX1205L0X_2x06_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-FVX1605L0X_2x08_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-FVX2005L0X_2x10_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-FVX2605L0X_2x13_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-FVX3405L0X_2x17_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-FVX5005L0X_2x25_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX0605L0X_2x03_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX0605L1X_2x03_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX1005L0X_2x05_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX1005L1X_2x05_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX1205L0X_2x06_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX1205L1X_2x06_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX1605L0X_2x08_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX1605L1X_2x08_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX2005L0X_2x10_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX2005L1X_2x10_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX2605L0X_2x13_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX2605L1X_2x13_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX3405L0X_2x17_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX3405L1X_2x17_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX5005L0X_2x25_P1.25mm_Vertical +Connector_Harwin:Harwin_Gecko-G125-MVX5005L1X_2x25_P1.25mm_Vertical +Connector_Harwin:Harwin_LTek-Male_02_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_02_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_03_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_03_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_04_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_04_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_05_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_05_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_06_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_06_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_07_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_07_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_17_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_17_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_22_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_22_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x02_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x02_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x03_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x03_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x04_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x04_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x05_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x05_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x06_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x06_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x07_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x07_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x08_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x08_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x09_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x09_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x10_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x10_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x13_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x13_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x17_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x17_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_LTek-Male_2x22_P2.00mm_Vertical +Connector_Harwin:Harwin_LTek-Male_2x22_P2.00mm_Vertical_StrainRelief +Connector_Harwin:Harwin_M20-7810245_2x02_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7810345_2x03_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7810445_2x04_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7810545_2x05_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7810645_2x06_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7810745_2x07_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7810845_2x08_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7810945_2x09_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7811045_2x10_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7811245_2x12_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7811545_2x15_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-7812045_2x20_P2.54mm_Vertical +Connector_Harwin:Harwin_M20-89003xx_1x03_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89004xx_1x04_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89005xx_1x05_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89006xx_1x06_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89007xx_1x07_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89008xx_1x08_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89009xx_1x09_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89010xx_1x10_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89011xx_1x11_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89012xx_1x12_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89013xx_1x13_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89014xx_1x14_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89015xx_1x15_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89016xx_1x16_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89017xx_1x17_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89018xx_1x18_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89019xx_1x19_P2.54mm_Horizontal +Connector_Harwin:Harwin_M20-89020xx_1x20_P2.54mm_Horizontal +Connector_Hirose:Hirose_BM23FR0.6-16DP-0.35V_2x08_1MP_Vertical +Connector_Hirose:Hirose_BM23FR0.6-16DS-0.35V_2x08_P0.35_1MP_Vertical +Connector_Hirose:Hirose_BM24_BM24-40DP-2-0.35V_2x20_P0.35mm_PowerPin2_Vertical +Connector_Hirose:Hirose_BM24_BM24-40DS-2-0.35V_2x20_P0.35mm_PowerPin2_Vertical +Connector_Hirose:Hirose_DF11-10DP-2DSA_2x05_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-12DP-2DSA_2x06_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-14DP-2DSA_2x07_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-16DP-2DSA_2x08_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-18DP-2DSA_2x09_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-20DP-2DSA_2x10_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-22DP-2DSA_2x11_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-24DP-2DSA_2x12_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-26DP-2DSA_2x13_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-28DP-2DSA_2x14_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-30DP-2DSA_2x15_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-32DP-2DSA_2x16_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-4DP-2DSA_2x02_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-6DP-2DSA_2x03_P2.00mm_Vertical +Connector_Hirose:Hirose_DF11-8DP-2DSA_2x04_P2.00mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-10DS-0.5V_2x05_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-14DS-0.5V_2x07_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-20DS-0.5V_2x10_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-30DS-0.5V_2x15_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-32DS-0.5V_2x16_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-36DS-0.5V_2x18_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-40DS-0.5V_2x20_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-50DS-0.5V_2x25_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12C3.0-60DS-0.5V_2x30_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-10DP-0.5V_2x05_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-14DP-0.5V_2x07_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-20DP-0.5V_2x10_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-30DP-0.5V_2x15_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-32DP-0.5V_2x16_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-36DP-0.5V_2x18_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-40DP-0.5V_2x20_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-50DP-0.5V_2x25_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-60DP-0.5V_2x30_P0.50mm_Vertical +Connector_Hirose:Hirose_DF12_DF12E3.0-80DP-0.5V_2x40_P0.50mm_Vertical +Connector_Hirose:Hirose_DF13-02P-1.25DSA_1x02_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-02P-1.25DS_1x02_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-03P-1.25DSA_1x03_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-03P-1.25DS_1x03_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-04P-1.25DSA_1x04_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-04P-1.25DS_1x04_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-05P-1.25DSA_1x05_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-05P-1.25DS_1x05_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-06P-1.25DSA_1x06_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-06P-1.25DS_1x06_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-07P-1.25DSA_1x07_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-07P-1.25DS_1x07_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-08P-1.25DSA_1x08_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-08P-1.25DS_1x08_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-09P-1.25DSA_1x09_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-09P-1.25DS_1x09_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-10P-1.25DSA_1x10_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-10P-1.25DS_1x10_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-11P-1.25DSA_1x11_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-11P-1.25DS_1x11_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-12P-1.25DSA_1x12_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-12P-1.25DS_1x12_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-13P-1.25DSA_1x13_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-14P-1.25DSA_1x14_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-14P-1.25DS_1x14_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13-15P-1.25DSA_1x15_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13-15P-1.25DS_1x15_P1.25mm_Horizontal +Connector_Hirose:Hirose_DF13C_CL535-0402-2-51_1x02-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0403-5-51_1x03-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0404-8-51_1x04-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0405-0-51_1x05-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0406-3-51_1x06-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0407-6-51_1x07-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0408-9-51_1x08-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0409-1-51_1x09-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0410-4-51_1x10-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0411-3-51_1x11-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0412-6-51_1x12-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0414-1-51_1x14-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF13C_CL535-0415-4-51_1x15-1MP_P1.25mm_Vertical +Connector_Hirose:Hirose_DF3EA-02P-2H_1x02-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-03P-2H_1x03-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-04P-2H_1x04-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-05P-2H_1x05-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-06P-2H_1x06-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-07P-2H_1x07-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-08P-2H_1x08-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-09P-2H_1x09-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-10P-2H_1x10-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-11P-2H_1x11-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-12P-2H_1x12-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-13P-2H_1x13-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-14P-2H_1x14-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF3EA-15P-2H_1x15-1MP_P2.00mm_Horizontal +Connector_Hirose:Hirose_DF52-10S-0.8H_1x10-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-11S-0.8H_1x11-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-12S-0.8H_1x12-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-14S-0.8H_1x14-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-15S-0.8H_1x15-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-2S-0.8H_1x02-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-3S-0.8H_1x03-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-4S-0.8H_1x04-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-5S-0.8H_1x05-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-6S-0.8H_1x06-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-7S-0.8H_1x07-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-8S-0.8H_1x08-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF52-9S-0.8H_1x09-1MP_P0.80mm_Horizontal +Connector_Hirose:Hirose_DF63-5P-3.96DSA_1x05_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63-6P-3.96DSA_1x06_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63M-1P-3.96DSA_1x01_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63M-2P-3.96DSA_1x02_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63M-3P-3.96DSA_1x03_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63M-4P-3.96DSA_1x04_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63R-1P-3.96DSA_1x01_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63R-2P-3.96DSA_1x02_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63R-3P-3.96DSA_1x03_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63R-4P-3.96DSA_1x04_P3.96mm_Vertical +Connector_Hirose:Hirose_DF63R-5P-3.96DSA_1x05_P3.96mm_Vertical +Connector_Hirose_FX8:Hirose_FX8-100P-SV_2x50_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-100S-SV_2x50_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-120P-SV_2x60_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-120S-SV_2x60_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-140P-SV_2x70_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-140S-SV_2x70_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-60P-SV_2x30_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-60S-SV_2x30_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-80P-SV_2x40_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-80S-SV_2x40_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-90P-SV_2x45_P0.6mm +Connector_Hirose_FX8:Hirose_FX8-90S-SV_2x45_P0.6mm +Connector_IDC:IDC-Header_2x03_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x03_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x03_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x04_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x04_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x04_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x05-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x05-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x05-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x05-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x05-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x05_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x05_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x05_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x05_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x05_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x05_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x05_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x05_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x06-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x06-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x06-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x06-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x06-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x06_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x06_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x06_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x06_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x06_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x06_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x06_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x06_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x07-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x07-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x07-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x07-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x07-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x07_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x07_P2.54mm_Horizontal_Lock +Connector_IDC:IDC-Header_2x07_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x07_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x07_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x07_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x07_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x07_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x07_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x08-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x08-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x08-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x08-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x08-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x08_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x08_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x08_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x08_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x08_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x08_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x08_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x08_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x09_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x09_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x09_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x10-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x10-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x10-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x10-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x10-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x10_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x10_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x10_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x10_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x10_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x10_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x10_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x10_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x11_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x11_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x11_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x12-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x12-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x12-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x12-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x12-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x12_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x12_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x12_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x12_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x12_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x12_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x12_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x12_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x13-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x13-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x13-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x13-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x13-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x13_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x13_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x13_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x13_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x13_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x13_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x13_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x13_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x15-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x15-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x15-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x15-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x15-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x15_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x15_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x15_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x15_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x15_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x15_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x15_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x17-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x17-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x17-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x17-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x17-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x17_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x17_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x17_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x17_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x17_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x17_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x17_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x20-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x20-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x20-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x20-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x20-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x20_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x20_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x20_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x20_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x20_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x20_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x20_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x20_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x22_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x22_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x22_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x25-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x25-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x25-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x25-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x25-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x25_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x25_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x25_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x25_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x25_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x25_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x25_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x25_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x30-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x30-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x30-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x30-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x30-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x30_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x30_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x30_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x30_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x30_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x30_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x30_P2.54mm_Vertical +Connector_IDC:IDC-Header_2x30_P2.54mm_Vertical_SMD +Connector_IDC:IDC-Header_2x32-1MP_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x32-1MP_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x32-1MP_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x32-1MP_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x32-1MP_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x32_P2.54mm_Horizontal +Connector_IDC:IDC-Header_2x32_P2.54mm_Latch12.0mm_Vertical +Connector_IDC:IDC-Header_2x32_P2.54mm_Latch6.5mm_Vertical +Connector_IDC:IDC-Header_2x32_P2.54mm_Latch9.5mm_Vertical +Connector_IDC:IDC-Header_2x32_P2.54mm_Latch_Horizontal +Connector_IDC:IDC-Header_2x32_P2.54mm_Latch_Vertical +Connector_IDC:IDC-Header_2x32_P2.54mm_Vertical +Connector_JAE:JAE_LY20-10P-DLT1_2x05_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-10P-DT1_2x05_P2.00mm_Vertical +Connector_JAE:JAE_LY20-12P-DLT1_2x06_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-12P-DT1_2x06_P2.00mm_Vertical +Connector_JAE:JAE_LY20-14P-DLT1_2x07_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-14P-DT1_2x07_P2.00mm_Vertical +Connector_JAE:JAE_LY20-16P-DLT1_2x08_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-16P-DT1_2x08_P2.00mm_Vertical +Connector_JAE:JAE_LY20-18P-DLT1_2x09_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-18P-DT1_2x09_P2.00mm_Vertical +Connector_JAE:JAE_LY20-20P-DLT1_2x10_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-20P-DT1_2x10_P2.00mm_Vertical +Connector_JAE:JAE_LY20-22P-DLT1_2x11_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-22P-DT1_2x11_P2.00mm_Vertical +Connector_JAE:JAE_LY20-24P-DLT1_2x12_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-24P-DT1_2x12_P2.00mm_Vertical +Connector_JAE:JAE_LY20-26P-DLT1_2x13_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-26P-DT1_2x13_P2.00mm_Vertical +Connector_JAE:JAE_LY20-28P-DLT1_2x14_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-28P-DT1_2x14_P2.00mm_Vertical +Connector_JAE:JAE_LY20-30P-DLT1_2x15_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-30P-DT1_2x15_P2.00mm_Vertical +Connector_JAE:JAE_LY20-32P-DLT1_2x16_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-32P-DT1_2x16_P2.00mm_Vertical +Connector_JAE:JAE_LY20-34P-DLT1_2x17_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-34P-DT1_2x17_P2.00mm_Vertical +Connector_JAE:JAE_LY20-36P-DLT1_2x18_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-36P-DT1_2x18_P2.00mm_Vertical +Connector_JAE:JAE_LY20-38P-DLT1_2x19_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-38P-DT1_2x19_P2.00mm_Vertical +Connector_JAE:JAE_LY20-40P-DLT1_2x20_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-40P-DT1_2x20_P2.00mm_Vertical +Connector_JAE:JAE_LY20-42P-DLT1_2x21_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-42P-DT1_2x21_P2.00mm_Vertical +Connector_JAE:JAE_LY20-44P-DLT1_2x22_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-44P-DT1_2x22_P2.00mm_Vertical +Connector_JAE:JAE_LY20-4P-DLT1_2x02_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-4P-DT1_2x02_P2.00mm_Vertical +Connector_JAE:JAE_LY20-6P-DLT1_2x03_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-6P-DT1_2x03_P2.00mm_Vertical +Connector_JAE:JAE_LY20-8P-DLT1_2x04_P2.00mm_Horizontal +Connector_JAE:JAE_LY20-8P-DT1_2x04_P2.00mm_Vertical +Connector_JAE:JAE_MM70-314-310B1 +Connector_JAE:JAE_SIM_Card_SF72S006 +Connector_JAE_WP7B:JAE_WP7B-P034VA1-R8000_2x17-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P034VA1-R8000_Longpads_2x17-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P040VA1-R8000_2x20-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P040VA1-R8000_Longpads_2x20-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P050VA1-R8000_2x25-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P050VA1-R8000_Longpads_2x25-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P060VA1-R8000_2x30-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P060VA1-R8000_Longpads_2x30-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P070VA1-R8000_2x35-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-P070VA1-R8000_Longpads_2x35-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S034VA1-R8000_2x17-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S034VA1-R8000_Longpads_2x17-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S040VA1-R8000_2x20-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S040VA1-R8000_Longpads_2x20-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S050VA1-R8000_2x25-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S050VA1-R8000_Longpads_2x25-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S060VA1-R8000_2x30-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S060VA1-R8000_Longpads_2x30-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S070VA1-R8000_2x35-1MP_P0.4mm +Connector_JAE_WP7B:JAE_WP7B-S070VA1-R8000_Longpads_2x35-1MP_P0.4mm +Connector_JST:JST_ACH_BM01B-ACHSS-A-GAN-ETF_1x01-1MP_P1.20mm_Vertical +Connector_JST:JST_ACH_BM02B-ACHSS-GAN-ETF_1x02-1MP_P1.20mm_Vertical +Connector_JST:JST_ACH_BM03B-ACHSS-GAN-ETF_1x03-1MP_P1.20mm_Vertical +Connector_JST:JST_ACH_BM04B-ACHSS-A-GAN-ETF_1x04-1MP_P1.20mm_Vertical +Connector_JST:JST_ACH_BM05B-ACHSS-A-GAN-ETF_1x05-1MP_P1.20mm_Vertical +Connector_JST:JST_AUH_BM03B-AUHKS-GA-TB_1x03-1MP_P1.50mm_Vertical +Connector_JST:JST_AUH_BM05B-AUHKS-GA-TB_1x05-1MP_P1.50mm_Vertical +Connector_JST:JST_EH_B10B-EH-A_1x10_P2.50mm_Vertical +Connector_JST:JST_EH_B11B-EH-A_1x11_P2.50mm_Vertical +Connector_JST:JST_EH_B12B-EH-A_1x12_P2.50mm_Vertical +Connector_JST:JST_EH_B13B-EH-A_1x13_P2.50mm_Vertical +Connector_JST:JST_EH_B14B-EH-A_1x14_P2.50mm_Vertical +Connector_JST:JST_EH_B15B-EH-A_1x15_P2.50mm_Vertical +Connector_JST:JST_EH_B2B-EH-A_1x02_P2.50mm_Vertical +Connector_JST:JST_EH_B3B-EH-A_1x03_P2.50mm_Vertical +Connector_JST:JST_EH_B4B-EH-A_1x04_P2.50mm_Vertical +Connector_JST:JST_EH_B5B-EH-A_1x05_P2.50mm_Vertical +Connector_JST:JST_EH_B6B-EH-A_1x06_P2.50mm_Vertical +Connector_JST:JST_EH_B7B-EH-A_1x07_P2.50mm_Vertical +Connector_JST:JST_EH_B8B-EH-A_1x08_P2.50mm_Vertical +Connector_JST:JST_EH_B9B-EH-A_1x09_P2.50mm_Vertical +Connector_JST:JST_EH_S10B-EH_1x10_P2.50mm_Horizontal +Connector_JST:JST_EH_S11B-EH_1x11_P2.50mm_Horizontal +Connector_JST:JST_EH_S12B-EH_1x12_P2.50mm_Horizontal +Connector_JST:JST_EH_S13B-EH_1x13_P2.50mm_Horizontal +Connector_JST:JST_EH_S14B-EH_1x14_P2.50mm_Horizontal +Connector_JST:JST_EH_S15B-EH_1x15_P2.50mm_Horizontal +Connector_JST:JST_EH_S2B-EH_1x02_P2.50mm_Horizontal +Connector_JST:JST_EH_S3B-EH_1x03_P2.50mm_Horizontal +Connector_JST:JST_EH_S4B-EH_1x04_P2.50mm_Horizontal +Connector_JST:JST_EH_S5B-EH_1x05_P2.50mm_Horizontal +Connector_JST:JST_EH_S6B-EH_1x06_P2.50mm_Horizontal +Connector_JST:JST_EH_S7B-EH_1x07_P2.50mm_Horizontal +Connector_JST:JST_EH_S8B-EH_1x08_P2.50mm_Horizontal +Connector_JST:JST_EH_S9B-EH_1x09_P2.50mm_Horizontal +Connector_JST:JST_GH_BM02B-GHS-TBT_1x02-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM03B-GHS-TBT_1x03-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM04B-GHS-TBT_1x04-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM05B-GHS-TBT_1x05-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM06B-GHS-TBT_1x06-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM07B-GHS-TBT_1x07-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM08B-GHS-TBT_1x08-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM09B-GHS-TBT_1x09-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM10B-GHS-TBT_1x10-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM11B-GHS-TBT_1x11-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM12B-GHS-TBT_1x12-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM13B-GHS-TBT_1x13-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM14B-GHS-TBT_1x14-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_BM15B-GHS-TBT_1x15-1MP_P1.25mm_Vertical +Connector_JST:JST_GH_SM02B-GHS-TB_1x02-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM03B-GHS-TB_1x03-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM05B-GHS-TB_1x05-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM06B-GHS-TB_1x06-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM07B-GHS-TB_1x07-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM08B-GHS-TB_1x08-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM09B-GHS-TB_1x09-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM10B-GHS-TB_1x10-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM11B-GHS-TB_1x11-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM12B-GHS-TB_1x12-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM13B-GHS-TB_1x13-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM14B-GHS-TB_1x14-1MP_P1.25mm_Horizontal +Connector_JST:JST_GH_SM15B-GHS-TB_1x15-1MP_P1.25mm_Horizontal +Connector_JST:JST_J2100_B06B-J21DK-GGXR_2x03_P2.50x4.00mm_Vertical +Connector_JST:JST_J2100_B08B-J21DK-GGXR_2x04_P2.50x4.00mm_Vertical +Connector_JST:JST_J2100_B10B-J21DK-GGXR_2x05_P2.50x4.00mm_Vertical +Connector_JST:JST_J2100_B12B-J21DK-GGXR_2x06_P2.50x4.00mm_Vertical +Connector_JST:JST_J2100_B16B-J21DK-GGXR_2x08_P2.50x4.00mm_Vertical +Connector_JST:JST_J2100_B20B-J21DK-GGXR_2x10_P2.50x4.00mm_Vertical +Connector_JST:JST_J2100_S06B-J21DK-GGXR_2x03_P2.50mm_Horizontal +Connector_JST:JST_J2100_S08B-J21DK-GGXR_2x04_P2.50mm_Horizontal +Connector_JST:JST_J2100_S10B-J21DK-GGXR_2x05_P2.50mm_Horizontal +Connector_JST:JST_J2100_S12B-J21DK-GGXR_2x06_P2.50mm_Horizontal +Connector_JST:JST_J2100_S16B-J21DK-GGXR_2x08_P2.50mm_Horizontal +Connector_JST:JST_J2100_S20B-J21DK-GGXR_2x10_P2.50mm_Horizontal +Connector_JST:JST_JWPF_B02B-JWPF-SK-R_1x02_P2.00mm_Vertical +Connector_JST:JST_JWPF_B03B-JWPF-SK-R_1x03_P2.00mm_Vertical +Connector_JST:JST_JWPF_B04B-JWPF-SK-R_1x04_P2.00mm_Vertical +Connector_JST:JST_JWPF_B06B-JWPF-SK-R_2x03_P2.00mm_Vertical +Connector_JST:JST_JWPF_B08B-JWPF-SK-R_2x04_P2.00mm_Vertical +Connector_JST:JST_LEA_SM02B-LEASS-TF_1x02-1MP_P4.20mm_Horizontal +Connector_JST:JST_NV_B02P-NV_1x02_P5.00mm_Vertical +Connector_JST:JST_NV_B03P-NV_1x03_P5.00mm_Vertical +Connector_JST:JST_NV_B04P-NV_1x04_P5.00mm_Vertical +Connector_JST:JST_PHD_B10B-PHDSS_2x05_P2.00mm_Vertical +Connector_JST:JST_PHD_B12B-PHDSS_2x06_P2.00mm_Vertical +Connector_JST:JST_PHD_B14B-PHDSS_2x07_P2.00mm_Vertical +Connector_JST:JST_PHD_B16B-PHDSS_2x08_P2.00mm_Vertical +Connector_JST:JST_PHD_B18B-PHDSS_2x09_P2.00mm_Vertical +Connector_JST:JST_PHD_B20B-PHDSS_2x10_P2.00mm_Vertical +Connector_JST:JST_PHD_B22B-PHDSS_2x11_P2.00mm_Vertical +Connector_JST:JST_PHD_B24B-PHDSS_2x12_P2.00mm_Vertical +Connector_JST:JST_PHD_B26B-PHDSS_2x13_P2.00mm_Vertical +Connector_JST:JST_PHD_B28B-PHDSS_2x14_P2.00mm_Vertical +Connector_JST:JST_PHD_B30B-PHDSS_2x15_P2.00mm_Vertical +Connector_JST:JST_PHD_B32B-PHDSS_2x16_P2.00mm_Vertical +Connector_JST:JST_PHD_B34B-PHDSS_2x17_P2.00mm_Vertical +Connector_JST:JST_PHD_B8B-PHDSS_2x04_P2.00mm_Vertical +Connector_JST:JST_PHD_S10B-PHDSS_2x05_P2.00mm_Horizontal +Connector_JST:JST_PHD_S12B-PHDSS_2x06_P2.00mm_Horizontal +Connector_JST:JST_PHD_S14B-PHDSS_2x07_P2.00mm_Horizontal +Connector_JST:JST_PHD_S16B-PHDSS_2x08_P2.00mm_Horizontal +Connector_JST:JST_PHD_S18B-PHDSS_2x09_P2.00mm_Horizontal +Connector_JST:JST_PHD_S20B-PHDSS_2x10_P2.00mm_Horizontal +Connector_JST:JST_PHD_S22B-PHDSS_2x11_P2.00mm_Horizontal +Connector_JST:JST_PHD_S24B-PHDSS_2x12_P2.00mm_Horizontal +Connector_JST:JST_PHD_S26B-PHDSS_2x13_P2.00mm_Horizontal +Connector_JST:JST_PHD_S28B-PHDSS_2x14_P2.00mm_Horizontal +Connector_JST:JST_PHD_S30B-PHDSS_2x15_P2.00mm_Horizontal +Connector_JST:JST_PHD_S32B-PHDSS_2x16_P2.00mm_Horizontal +Connector_JST:JST_PHD_S34B-PHDSS_2x17_P2.00mm_Horizontal +Connector_JST:JST_PHD_S8B-PHDSS_2x04_P2.00mm_Horizontal +Connector_JST:JST_PH_B10B-PH-K_1x10_P2.00mm_Vertical +Connector_JST:JST_PH_B10B-PH-SM4-TB_1x10-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B11B-PH-K_1x11_P2.00mm_Vertical +Connector_JST:JST_PH_B11B-PH-SM4-TB_1x11-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B12B-PH-K_1x12_P2.00mm_Vertical +Connector_JST:JST_PH_B12B-PH-SM4-TB_1x12-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B13B-PH-K_1x13_P2.00mm_Vertical +Connector_JST:JST_PH_B13B-PH-SM4-TB_1x13-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B14B-PH-K_1x14_P2.00mm_Vertical +Connector_JST:JST_PH_B14B-PH-SM4-TB_1x14-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B15B-PH-K_1x15_P2.00mm_Vertical +Connector_JST:JST_PH_B15B-PH-SM4-TB_1x15-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B16B-PH-K_1x16_P2.00mm_Vertical +Connector_JST:JST_PH_B16B-PH-SM4-TB_1x16-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B2B-PH-K_1x02_P2.00mm_Vertical +Connector_JST:JST_PH_B2B-PH-SM4-TB_1x02-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B3B-PH-K_1x03_P2.00mm_Vertical +Connector_JST:JST_PH_B3B-PH-SM4-TB_1x03-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical +Connector_JST:JST_PH_B4B-PH-SM4-TB_1x04-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B5B-PH-K_1x05_P2.00mm_Vertical +Connector_JST:JST_PH_B5B-PH-SM4-TB_1x05-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B6B-PH-K_1x06_P2.00mm_Vertical +Connector_JST:JST_PH_B6B-PH-SM4-TB_1x06-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B7B-PH-K_1x07_P2.00mm_Vertical +Connector_JST:JST_PH_B7B-PH-SM4-TB_1x07-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B8B-PH-K_1x08_P2.00mm_Vertical +Connector_JST:JST_PH_B8B-PH-SM4-TB_1x08-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_B9B-PH-K_1x09_P2.00mm_Vertical +Connector_JST:JST_PH_B9B-PH-SM4-TB_1x09-1MP_P2.00mm_Vertical +Connector_JST:JST_PH_S10B-PH-K_1x10_P2.00mm_Horizontal +Connector_JST:JST_PH_S10B-PH-SM4-TB_1x10-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S11B-PH-K_1x11_P2.00mm_Horizontal +Connector_JST:JST_PH_S11B-PH-SM4-TB_1x11-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S12B-PH-K_1x12_P2.00mm_Horizontal +Connector_JST:JST_PH_S12B-PH-SM4-TB_1x12-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S13B-PH-K_1x13_P2.00mm_Horizontal +Connector_JST:JST_PH_S13B-PH-SM4-TB_1x13-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S14B-PH-K_1x14_P2.00mm_Horizontal +Connector_JST:JST_PH_S14B-PH-SM4-TB_1x14-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S15B-PH-K_1x15_P2.00mm_Horizontal +Connector_JST:JST_PH_S15B-PH-SM4-TB_1x15-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S16B-PH-K_1x16_P2.00mm_Horizontal +Connector_JST:JST_PH_S2B-PH-K_1x02_P2.00mm_Horizontal +Connector_JST:JST_PH_S2B-PH-SM4-TB_1x02-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S3B-PH-K_1x03_P2.00mm_Horizontal +Connector_JST:JST_PH_S3B-PH-SM4-TB_1x03-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S4B-PH-K_1x04_P2.00mm_Horizontal +Connector_JST:JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S5B-PH-K_1x05_P2.00mm_Horizontal +Connector_JST:JST_PH_S5B-PH-SM4-TB_1x05-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S6B-PH-K_1x06_P2.00mm_Horizontal +Connector_JST:JST_PH_S6B-PH-SM4-TB_1x06-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S7B-PH-K_1x07_P2.00mm_Horizontal +Connector_JST:JST_PH_S7B-PH-SM4-TB_1x07-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S8B-PH-K_1x08_P2.00mm_Horizontal +Connector_JST:JST_PH_S8B-PH-SM4-TB_1x08-1MP_P2.00mm_Horizontal +Connector_JST:JST_PH_S9B-PH-K_1x09_P2.00mm_Horizontal +Connector_JST:JST_PH_S9B-PH-SM4-TB_1x09-1MP_P2.00mm_Horizontal +Connector_JST:JST_PUD_B08B-PUDSS_2x04_P2.00mm_Vertical +Connector_JST:JST_PUD_B10B-PUDSS_2x05_P2.00mm_Vertical +Connector_JST:JST_PUD_B12B-PUDSS_2x06_P2.00mm_Vertical +Connector_JST:JST_PUD_B14B-PUDSS_2x07_P2.00mm_Vertical +Connector_JST:JST_PUD_B16B-PUDSS_2x08_P2.00mm_Vertical +Connector_JST:JST_PUD_B18B-PUDSS_2x09_P2.00mm_Vertical +Connector_JST:JST_PUD_B20B-PUDSS_2x10_P2.00mm_Vertical +Connector_JST:JST_PUD_B22B-PUDSS_2x11_P2.00mm_Vertical +Connector_JST:JST_PUD_B24B-PUDSS_2x12_P2.00mm_Vertical +Connector_JST:JST_PUD_B26B-PUDSS_2x13_P2.00mm_Vertical +Connector_JST:JST_PUD_B28B-PUDSS_2x14_P2.00mm_Vertical +Connector_JST:JST_PUD_B30B-PUDSS_2x15_P2.00mm_Vertical +Connector_JST:JST_PUD_B32B-PUDSS_2x16_P2.00mm_Vertical +Connector_JST:JST_PUD_B34B-PUDSS_2x17_P2.00mm_Vertical +Connector_JST:JST_PUD_B36B-PUDSS_2x18_P2.00mm_Vertical +Connector_JST:JST_PUD_B38B-PUDSS_2x19_P2.00mm_Vertical +Connector_JST:JST_PUD_B40B-PUDSS_2x20_P2.00mm_Vertical +Connector_JST:JST_PUD_S08B-PUDSS-1_2x04_P2.00mm_Horizontal +Connector_JST:JST_PUD_S10B-PUDSS-1_2x05_P2.00mm_Horizontal +Connector_JST:JST_PUD_S12B-PUDSS-1_2x06_P2.00mm_Horizontal +Connector_JST:JST_PUD_S14B-PUDSS-1_2x07_P2.00mm_Horizontal +Connector_JST:JST_PUD_S16B-PUDSS-1_2x08_P2.00mm_Horizontal +Connector_JST:JST_PUD_S18B-PUDSS-1_2x09_P2.00mm_Horizontal +Connector_JST:JST_PUD_S20B-PUDSS-1_2x10_P2.00mm_Horizontal +Connector_JST:JST_PUD_S22B-PUDSS-1_2x11_P2.00mm_Horizontal +Connector_JST:JST_PUD_S24B-PUDSS-1_2x12_P2.00mm_Horizontal +Connector_JST:JST_PUD_S26B-PUDSS-1_2x13_P2.00mm_Horizontal +Connector_JST:JST_PUD_S28B-PUDSS-1_2x14_P2.00mm_Horizontal +Connector_JST:JST_PUD_S30B-PUDSS-1_2x15_P2.00mm_Horizontal +Connector_JST:JST_PUD_S32B-PUDSS-1_2x16_P2.00mm_Horizontal +Connector_JST:JST_PUD_S34B-PUDSS-1_2x17_P2.00mm_Horizontal +Connector_JST:JST_PUD_S36B-PUDSS-1_2x18_P2.00mm_Horizontal +Connector_JST:JST_PUD_S38B-PUDSS-1_2x19_P2.00mm_Horizontal +Connector_JST:JST_PUD_S40B-PUDSS-1_2x20_P2.00mm_Horizontal +Connector_JST:JST_SFH_SM02B-SFHRS-TF_1x02-1MP_P4.20mm_Horizontal +Connector_JST:JST_SHL_SM02B-SHLS-TF_1x02-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM05B-SHLS-TF_1x05-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM06B-SHLS-TF_1x06-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM07B-SHLS-TF_1x07-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM08B-SHLS-TF_1x08-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM10B-SHLS-TF_1x10-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM11B-SHLS-TF_1x11-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM12B-SHLS-TF_1x12-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM14B-SHLS-TF_1x14-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM16B-SHLS-TF_1x16-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM20B-SHLS-TF_1x20-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM22B-SHLS-TF_1x22-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM26B-SHLS-TF_1x26-1MP_P1.00mm_Horizontal +Connector_JST:JST_SHL_SM30B-SHLS-TF_1x30-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_BM02B-SRSS-TB_1x02-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM03B-SRSS-TB_1x03-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM04B-SRSS-TB_1x04-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM05B-SRSS-TB_1x05-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM06B-SRSS-TB_1x06-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM07B-SRSS-TB_1x07-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM08B-SRSS-TB_1x08-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM09B-SRSS-TB_1x09-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM10B-SRSS-TB_1x10-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM11B-SRSS-TB_1x11-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM12B-SRSS-TB_1x12-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM13B-SRSS-TB_1x13-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM14B-SRSS-TB_1x14-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_BM15B-SRSS-TB_1x15-1MP_P1.00mm_Vertical +Connector_JST:JST_SH_SM02B-SRSS-TB_1x02-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM03B-SRSS-TB_1x03-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM05B-SRSS-TB_1x05-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM06B-SRSS-TB_1x06-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM07B-SRSS-TB_1x07-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM08B-SRSS-TB_1x08-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM09B-SRSS-TB_1x09-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM10B-SRSS-TB_1x10-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM11B-SRSS-TB_1x11-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM12B-SRSS-TB_1x12-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM13B-SRSS-TB_1x13-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM14B-SRSS-TB_1x14-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM15B-SRSS-TB_1x15-1MP_P1.00mm_Horizontal +Connector_JST:JST_SH_SM20B-SRSS-TB_1x20-1MP_P1.00mm_Horizontal +Connector_JST:JST_SUR_BM02B-SURS-TF_1x02-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM03B-SURS-TF_1x03-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM04B-SURS-TF_1x04-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM05B-SURS-TF_1x05-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM06B-SURS-TF_1x06-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM08B-SURS-TF_1x08-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM10B-SURS-TF_1x10-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM12B-SURS-TF_1x12-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM14B-SURS-TF_1x14-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM15B-SURS-TF_1x15-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM16B-SURS-TF_1x16-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM17B-SURS-TF_1x17-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_BM20B-SURS-TF_1x20-1MP_P0.80mm_Vertical +Connector_JST:JST_SUR_SM02B-SURS-TF_1x02-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM03B-SURS-TF_1x03-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM04B-SURS-TF_1x04-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM05B-SURS-TF_1x05-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM06B-SURS-TF_1x06-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM08B-SURS-TF_1x08-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM10B-SURS-TF_1x10-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM12B-SURS-TF_1x12-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM14B-SURS-TF_1x14-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM15B-SURS-TF_1x15-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM16B-SURS-TF_1x16-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM17B-SURS-TF_1x17-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM20B-SURS-TF_1x20-1MP_P0.80mm_Horizontal +Connector_JST:JST_SUR_SM22B-SURS-TF_1x22-1MP_P0.80mm_Horizontal +Connector_JST:JST_VH_B10P-VH-B_1x10_P3.96mm_Vertical +Connector_JST:JST_VH_B10P-VH-FB-B_1x10_P3.96mm_Vertical +Connector_JST:JST_VH_B10P-VH_1x10_P3.96mm_Vertical +Connector_JST:JST_VH_B10PS-VH_1x10_P3.96mm_Horizontal +Connector_JST:JST_VH_B11P-VH-B_1x11_P3.96mm_Vertical +Connector_JST:JST_VH_B2P-VH-B_1x02_P3.96mm_Vertical +Connector_JST:JST_VH_B2P-VH-FB-B_1x02_P3.96mm_Vertical +Connector_JST:JST_VH_B2P-VH_1x02_P3.96mm_Vertical +Connector_JST:JST_VH_B2P3-VH_1x02_P7.92mm_Vertical +Connector_JST:JST_VH_B2PS-VH_1x02_P3.96mm_Horizontal +Connector_JST:JST_VH_B3P-VH-B_1x03_P3.96mm_Vertical +Connector_JST:JST_VH_B3P-VH-FB-B_1x03_P3.96mm_Vertical +Connector_JST:JST_VH_B3P-VH_1x03_P3.96mm_Vertical +Connector_JST:JST_VH_B3PS-VH_1x03_P3.96mm_Horizontal +Connector_JST:JST_VH_B4P-VH-B_1x04_P3.96mm_Vertical +Connector_JST:JST_VH_B4P-VH-FB-B_1x04_P3.96mm_Vertical +Connector_JST:JST_VH_B4P-VH_1x04_P3.96mm_Vertical +Connector_JST:JST_VH_B4PS-VH_1x04_P3.96mm_Horizontal +Connector_JST:JST_VH_B5P-VH-B_1x05_P3.96mm_Vertical +Connector_JST:JST_VH_B5P-VH-FB-B_1x05_P3.96mm_Vertical +Connector_JST:JST_VH_B5P-VH_1x05_P3.96mm_Vertical +Connector_JST:JST_VH_B5PS-VH_1x05_P3.96mm_Horizontal +Connector_JST:JST_VH_B6P-VH-B_1x06_P3.96mm_Vertical +Connector_JST:JST_VH_B6P-VH-FB-B_1x06_P3.96mm_Vertical +Connector_JST:JST_VH_B6P-VH_1x06_P3.96mm_Vertical +Connector_JST:JST_VH_B6PS-VH_1x06_P3.96mm_Horizontal +Connector_JST:JST_VH_B7P-VH-B_1x07_P3.96mm_Vertical +Connector_JST:JST_VH_B7P-VH-FB-B_1x07_P3.96mm_Vertical +Connector_JST:JST_VH_B7P-VH_1x07_P3.96mm_Vertical +Connector_JST:JST_VH_B7PS-VH_1x07_P3.96mm_Horizontal +Connector_JST:JST_VH_B8P-VH-B_1x08_P3.96mm_Vertical +Connector_JST:JST_VH_B8P-VH-FB-B_1x08_P3.96mm_Vertical +Connector_JST:JST_VH_B8P-VH_1x08_P3.96mm_Vertical +Connector_JST:JST_VH_B8PS-VH_1x08_P3.96mm_Horizontal +Connector_JST:JST_VH_B9P-VH-B_1x09_P3.96mm_Vertical +Connector_JST:JST_VH_B9P-VH-FB-B_1x09_P3.96mm_Vertical +Connector_JST:JST_VH_B9P-VH_1x09_P3.96mm_Vertical +Connector_JST:JST_VH_B9PS-VH_1x09_P3.96mm_Horizontal +Connector_JST:JST_VH_S2P-VH_1x02_P3.96mm_Horizontal +Connector_JST:JST_VH_S3P-VH_1x03_P3.96mm_Horizontal +Connector_JST:JST_VH_S4P-VH_1x04_P3.96mm_Horizontal +Connector_JST:JST_VH_S5P-VH_1x05_P3.96mm_Horizontal +Connector_JST:JST_VH_S6P-VH_1x06_P3.96mm_Horizontal +Connector_JST:JST_VH_S7P-VH_1x07_P3.96mm_Horizontal +Connector_JST:JST_XAG_SM05B-XAGKS-BN-TB_1x05-1MP_P2.50mm_Horizontal +Connector_JST:JST_XA_B02B-XASK-1-A_1x02_P2.50mm_Vertical +Connector_JST:JST_XA_B02B-XASK-1_1x02_P2.50mm_Vertical +Connector_JST:JST_XA_B03B-XASK-1-A_1x03_P2.50mm_Vertical +Connector_JST:JST_XA_B03B-XASK-1_1x03_P2.50mm_Vertical +Connector_JST:JST_XA_B04B-XASK-1-A_1x04_P2.50mm_Vertical +Connector_JST:JST_XA_B04B-XASK-1_1x04_P2.50mm_Vertical +Connector_JST:JST_XA_B05B-XASK-1-A_1x05_P2.50mm_Vertical +Connector_JST:JST_XA_B05B-XASK-1_1x05_P2.50mm_Vertical +Connector_JST:JST_XA_B06B-XASK-1-A_1x06_P2.50mm_Vertical +Connector_JST:JST_XA_B06B-XASK-1_1x06_P2.50mm_Vertical +Connector_JST:JST_XA_B07B-XASK-1-A_1x07_P2.50mm_Vertical +Connector_JST:JST_XA_B07B-XASK-1_1x07_P2.50mm_Vertical +Connector_JST:JST_XA_B08B-XASK-1-A_1x08_P2.50mm_Vertical +Connector_JST:JST_XA_B08B-XASK-1_1x08_P2.50mm_Vertical +Connector_JST:JST_XA_B09B-XASK-1-A_1x09_P2.50mm_Vertical +Connector_JST:JST_XA_B09B-XASK-1_1x09_P2.50mm_Vertical +Connector_JST:JST_XA_B10B-XASK-1-A_1x10_P2.50mm_Vertical +Connector_JST:JST_XA_B10B-XASK-1_1x10_P2.50mm_Vertical +Connector_JST:JST_XA_B11B-XASK-1-A_1x11_P2.50mm_Vertical +Connector_JST:JST_XA_B11B-XASK-1_1x11_P2.50mm_Vertical +Connector_JST:JST_XA_B12B-XASK-1-A_1x12_P2.50mm_Vertical +Connector_JST:JST_XA_B12B-XASK-1_1x12_P2.50mm_Vertical +Connector_JST:JST_XA_B13B-XASK-1-A_1x13_P2.50mm_Vertical +Connector_JST:JST_XA_B13B-XASK-1_1x13_P2.50mm_Vertical +Connector_JST:JST_XA_B14B-XASK-1-A_1x14_P2.50mm_Vertical +Connector_JST:JST_XA_B14B-XASK-1_1x14_P2.50mm_Vertical +Connector_JST:JST_XA_B15B-XASK-1-A_1x15_P2.50mm_Vertical +Connector_JST:JST_XA_B15B-XASK-1_1x15_P2.50mm_Vertical +Connector_JST:JST_XA_B18B-XASK-1_1x18_P2.50mm_Vertical +Connector_JST:JST_XA_B20B-XASK-1-A_1x20_P2.50mm_Vertical +Connector_JST:JST_XA_B20B-XASK-1_1x20_P2.50mm_Vertical +Connector_JST:JST_XA_S02B-XASK-1N-BN_1x02_P2.50mm_Horizontal +Connector_JST:JST_XA_S02B-XASK-1_1x02_P2.50mm_Horizontal +Connector_JST:JST_XA_S03B-XASK-1N-BN_1x03_P2.50mm_Horizontal +Connector_JST:JST_XA_S03B-XASK-1_1x03_P2.50mm_Horizontal +Connector_JST:JST_XA_S04B-XASK-1N-BN_1x04_P2.50mm_Horizontal +Connector_JST:JST_XA_S04B-XASK-1_1x04_P2.50mm_Horizontal +Connector_JST:JST_XA_S05B-XASK-1N-BN_1x05_P2.50mm_Horizontal +Connector_JST:JST_XA_S05B-XASK-1_1x05_P2.50mm_Horizontal +Connector_JST:JST_XA_S06B-XASK-1N-BN_1x06_P2.50mm_Horizontal +Connector_JST:JST_XA_S06B-XASK-1_1x06_P2.50mm_Horizontal +Connector_JST:JST_XA_S07B-XASK-1N-BN_1x07_P2.50mm_Horizontal +Connector_JST:JST_XA_S07B-XASK-1_1x07_P2.50mm_Horizontal +Connector_JST:JST_XA_S08B-XASK-1N-BN_1x08_P2.50mm_Horizontal +Connector_JST:JST_XA_S08B-XASK-1_1x08_P2.50mm_Horizontal +Connector_JST:JST_XA_S09B-XASK-1N-BN_1x09_P2.50mm_Horizontal +Connector_JST:JST_XA_S09B-XASK-1_1x09_P2.50mm_Horizontal +Connector_JST:JST_XA_S10B-XASK-1N-BN_1x10_P2.50mm_Horizontal +Connector_JST:JST_XA_S10B-XASK-1_1x10_P2.50mm_Horizontal +Connector_JST:JST_XA_S11B-XASK-1N-BN_1x11_P2.50mm_Horizontal +Connector_JST:JST_XA_S11B-XASK-1_1x11_P2.50mm_Horizontal +Connector_JST:JST_XA_S12B-XASK-1N-BN_1x12_P2.50mm_Horizontal +Connector_JST:JST_XA_S12B-XASK-1_1x12_P2.50mm_Horizontal +Connector_JST:JST_XA_S13B-XASK-1N-BN_1x13_P2.50mm_Horizontal +Connector_JST:JST_XA_S13B-XASK-1_1x13_P2.50mm_Horizontal +Connector_JST:JST_XA_S14B-XASK-1N-BN_1x14_P2.50mm_Horizontal +Connector_JST:JST_XA_S14B-XASK-1_1x14_P2.50mm_Horizontal +Connector_JST:JST_XH_B10B-XH-AM_1x10_P2.50mm_Vertical +Connector_JST:JST_XH_B10B-XH-A_1x10_P2.50mm_Vertical +Connector_JST:JST_XH_B11B-XH-A_1x11_P2.50mm_Vertical +Connector_JST:JST_XH_B12B-XH-AM_1x12_P2.50mm_Vertical +Connector_JST:JST_XH_B12B-XH-A_1x12_P2.50mm_Vertical +Connector_JST:JST_XH_B13B-XH-A_1x13_P2.50mm_Vertical +Connector_JST:JST_XH_B14B-XH-A_1x14_P2.50mm_Vertical +Connector_JST:JST_XH_B15B-XH-A_1x15_P2.50mm_Vertical +Connector_JST:JST_XH_B16B-XH-A_1x16_P2.50mm_Vertical +Connector_JST:JST_XH_B1B-XH-AM_1x01_P2.50mm_Vertical +Connector_JST:JST_XH_B20B-XH-A_1x20_P2.50mm_Vertical +Connector_JST:JST_XH_B2B-XH-AM_1x02_P2.50mm_Vertical +Connector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical +Connector_JST:JST_XH_B3B-XH-AM_1x03_P2.50mm_Vertical +Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical +Connector_JST:JST_XH_B4B-XH-AM_1x04_P2.50mm_Vertical +Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical +Connector_JST:JST_XH_B5B-XH-AM_1x05_P2.50mm_Vertical +Connector_JST:JST_XH_B5B-XH-A_1x05_P2.50mm_Vertical +Connector_JST:JST_XH_B6B-XH-AM_1x06_P2.50mm_Vertical +Connector_JST:JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical +Connector_JST:JST_XH_B7B-XH-AM_1x07_P2.50mm_Vertical +Connector_JST:JST_XH_B7B-XH-A_1x07_P2.50mm_Vertical +Connector_JST:JST_XH_B8B-XH-AM_1x08_P2.50mm_Vertical +Connector_JST:JST_XH_B8B-XH-A_1x08_P2.50mm_Vertical +Connector_JST:JST_XH_B9B-XH-AM_1x09_P2.50mm_Vertical +Connector_JST:JST_XH_B9B-XH-A_1x09_P2.50mm_Vertical +Connector_JST:JST_XH_S10B-XH-A-1_1x10_P2.50mm_Horizontal +Connector_JST:JST_XH_S10B-XH-A_1x10_P2.50mm_Horizontal +Connector_JST:JST_XH_S11B-XH-A-1_1x11_P2.50mm_Horizontal +Connector_JST:JST_XH_S11B-XH-A_1x11_P2.50mm_Horizontal +Connector_JST:JST_XH_S12B-XH-A-1_1x12_P2.50mm_Horizontal +Connector_JST:JST_XH_S12B-XH-A_1x12_P2.50mm_Horizontal +Connector_JST:JST_XH_S13B-XH-A-1_1x13_P2.50mm_Horizontal +Connector_JST:JST_XH_S13B-XH-A_1x13_P2.50mm_Horizontal +Connector_JST:JST_XH_S14B-XH-A-1_1x14_P2.50mm_Horizontal +Connector_JST:JST_XH_S14B-XH-A_1x14_P2.50mm_Horizontal +Connector_JST:JST_XH_S15B-XH-A-1_1x15_P2.50mm_Horizontal +Connector_JST:JST_XH_S15B-XH-A_1x15_P2.50mm_Horizontal +Connector_JST:JST_XH_S16B-XH-A_1x16_P2.50mm_Horizontal +Connector_JST:JST_XH_S2B-XH-A-1_1x02_P2.50mm_Horizontal +Connector_JST:JST_XH_S2B-XH-A_1x02_P2.50mm_Horizontal +Connector_JST:JST_XH_S3B-XH-A-1_1x03_P2.50mm_Horizontal +Connector_JST:JST_XH_S3B-XH-A_1x03_P2.50mm_Horizontal +Connector_JST:JST_XH_S3B-XH-SM4-TB_1x03-1MP_P2.50mm_Horizontal +Connector_JST:JST_XH_S4B-XH-A-1_1x04_P2.50mm_Horizontal +Connector_JST:JST_XH_S4B-XH-A_1x04_P2.50mm_Horizontal +Connector_JST:JST_XH_S4B-XH-SM4-TB_1x04-1MP_P2.50mm_Horizontal +Connector_JST:JST_XH_S5B-XH-A-1_1x05_P2.50mm_Horizontal +Connector_JST:JST_XH_S5B-XH-A_1x05_P2.50mm_Horizontal +Connector_JST:JST_XH_S6B-XH-A-1_1x06_P2.50mm_Horizontal +Connector_JST:JST_XH_S6B-XH-A_1x06_P2.50mm_Horizontal +Connector_JST:JST_XH_S6B-XH-SM4-TB_1x06-1MP_P2.50mm_Horizontal +Connector_JST:JST_XH_S7B-XH-A-1_1x07_P2.50mm_Horizontal +Connector_JST:JST_XH_S7B-XH-A_1x07_P2.50mm_Horizontal +Connector_JST:JST_XH_S8B-XH-A-1_1x08_P2.50mm_Horizontal +Connector_JST:JST_XH_S8B-XH-A_1x08_P2.50mm_Horizontal +Connector_JST:JST_XH_S9B-XH-A-1_1x09_P2.50mm_Horizontal +Connector_JST:JST_XH_S9B-XH-A_1x09_P2.50mm_Horizontal +Connector_JST:JST_ZE_B02B-ZESK-1D_1x02_P1.50mm_Vertical +Connector_JST:JST_ZE_B03B-ZESK-1D_1x03_P1.50mm_Vertical +Connector_JST:JST_ZE_B03B-ZESK-D_1x03_P1.50mm_Vertical +Connector_JST:JST_ZE_B04B-ZESK-1D_1x04_P1.50mm_Vertical +Connector_JST:JST_ZE_B04B-ZESK-D_1x04_P1.50mm_Vertical +Connector_JST:JST_ZE_B05B-ZESK-1D_1x05_P1.50mm_Vertical +Connector_JST:JST_ZE_B05B-ZESK-D_1x05_P1.50mm_Vertical +Connector_JST:JST_ZE_B06B-ZESK-1D_1x06_P1.50mm_Vertical +Connector_JST:JST_ZE_B06B-ZESK-D_1x06_P1.50mm_Vertical +Connector_JST:JST_ZE_B07B-ZESK-1D_1x07_P1.50mm_Vertical +Connector_JST:JST_ZE_B07B-ZESK-D_1x07_P1.50mm_Vertical +Connector_JST:JST_ZE_B08B-ZESK-1D_1x08_P1.50mm_Vertical +Connector_JST:JST_ZE_B08B-ZESK-D_1x08_P1.50mm_Vertical +Connector_JST:JST_ZE_B09B-ZESK-1D_1x09_P1.50mm_Vertical +Connector_JST:JST_ZE_B09B-ZESK-D_1x09_P1.50mm_Vertical +Connector_JST:JST_ZE_B10B-ZESK-1D_1x10_P1.50mm_Vertical +Connector_JST:JST_ZE_B10B-ZESK-D_1x10_P1.50mm_Vertical +Connector_JST:JST_ZE_B11B-ZESK-1D_1x11_P1.50mm_Vertical +Connector_JST:JST_ZE_B11B-ZESK-D_1x11_P1.50mm_Vertical +Connector_JST:JST_ZE_B12B-ZESK-1D_1x12_P1.50mm_Vertical +Connector_JST:JST_ZE_B12B-ZESK-D_1x12_P1.50mm_Vertical +Connector_JST:JST_ZE_B13B-ZESK-1D_1x13_P1.50mm_Vertical +Connector_JST:JST_ZE_B13B-ZESK-D_1x13_P1.50mm_Vertical +Connector_JST:JST_ZE_B14B-ZESK-1D_1x14_P1.50mm_Vertical +Connector_JST:JST_ZE_B14B-ZESK-D_1x14_P1.50mm_Vertical +Connector_JST:JST_ZE_B15B-ZESK-1D_1x15_P1.50mm_Vertical +Connector_JST:JST_ZE_B15B-ZESK-D_1x15_P1.50mm_Vertical +Connector_JST:JST_ZE_B16B-ZESK-1D_1x16_P1.50mm_Vertical +Connector_JST:JST_ZE_B16B-ZESK-D_1x16_P1.50mm_Vertical +Connector_JST:JST_ZE_BM02B-ZESS-TBT_1x02-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM03B-ZESS-TBT_1x03-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM04B-ZESS-TBT_1x04-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM05B-ZESS-TBT_1x05-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM06B-ZESS-TBT_1x06-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM07B-ZESS-TBT_1x07-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM08B-ZESS-TBT_1x08-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM09B-ZESS-TBT_1x09-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM10B-ZESS-TBT_1x10-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM11B-ZESS-TBT_1x11-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM12B-ZESS-TBT_1x12-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM13B-ZESS-TBT_1x13-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM14B-ZESS-TBT_1x14-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM15B-ZESS-TBT_1x15-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_BM16B-ZESS-TBT_1x16-1MP_P1.50mm_Vertical +Connector_JST:JST_ZE_S02B-ZESK-2D_1x02_P1.50mm_Horizontal +Connector_JST:JST_ZE_S03B-ZESK-2D_1x03_P1.50mm_Horizontal +Connector_JST:JST_ZE_S04B-ZESK-2D_1x04_P1.50mm_Horizontal +Connector_JST:JST_ZE_S05B-ZESK-2D_1x05_P1.50mm_Horizontal +Connector_JST:JST_ZE_S06B-ZESK-2D_1x06_P1.50mm_Horizontal +Connector_JST:JST_ZE_S07B-ZESK-2D_1x07_P1.50mm_Horizontal +Connector_JST:JST_ZE_S08B-ZESK-2D_1x08_P1.50mm_Horizontal +Connector_JST:JST_ZE_S09B-ZESK-2D_1x09_P1.50mm_Horizontal +Connector_JST:JST_ZE_S10B-ZESK-2D_1x10_P1.50mm_Horizontal +Connector_JST:JST_ZE_S11B-ZESK-2D_1x11_P1.50mm_Horizontal +Connector_JST:JST_ZE_S12B-ZESK-2D_1x12_P1.50mm_Horizontal +Connector_JST:JST_ZE_S13B-ZESK-2D_1x13_P1.50mm_Horizontal +Connector_JST:JST_ZE_S14B-ZESK-2D_1x14_P1.50mm_Horizontal +Connector_JST:JST_ZE_S15B-ZESK-2D_1x15_P1.50mm_Horizontal +Connector_JST:JST_ZE_S16B-ZESK-2D_1x16_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM02B-ZESS-TB_1x02-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM03B-ZESS-TB_1x03-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM04B-ZESS-TB_1x04-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM05B-ZESS-TB_1x05-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM06B-ZESS-TB_1x06-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM07B-ZESS-TB_1x07-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM08B-ZESS-TB_1x08-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM09B-ZESS-TB_1x09-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM10B-ZESS-TB_1x10-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM11B-ZESS-TB_1x11-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM12B-ZESS-TB_1x12-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM13B-ZESS-TB_1x13-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM14B-ZESS-TB_1x14-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM15B-ZESS-TB_1x15-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZE_SM16B-ZESS-TB_1x16-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_B10B-ZR-SM4-TF_1x10-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B10B-ZR_1x10_P1.50mm_Vertical +Connector_JST:JST_ZH_B11B-ZR-SM4-TF_1x11-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B11B-ZR_1x11_P1.50mm_Vertical +Connector_JST:JST_ZH_B12B-ZR-SM4-TF_1x12-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B12B-ZR_1x12_P1.50mm_Vertical +Connector_JST:JST_ZH_B13B-ZR-SM4-TF_1x13-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B2B-ZR-SM4-TF_1x02-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B2B-ZR_1x02_P1.50mm_Vertical +Connector_JST:JST_ZH_B3B-ZR-SM4-TF_1x03-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B3B-ZR_1x03_P1.50mm_Vertical +Connector_JST:JST_ZH_B4B-ZR-SM4-TF_1x04-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B4B-ZR_1x04_P1.50mm_Vertical +Connector_JST:JST_ZH_B5B-ZR-SM4-TF_1x05-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B5B-ZR_1x05_P1.50mm_Vertical +Connector_JST:JST_ZH_B6B-ZR-SM4-TF_1x06-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B6B-ZR_1x06_P1.50mm_Vertical +Connector_JST:JST_ZH_B7B-ZR-SM4-TF_1x07-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B7B-ZR_1x07_P1.50mm_Vertical +Connector_JST:JST_ZH_B8B-ZR-SM4-TF_1x08-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B8B-ZR_1x08_P1.50mm_Vertical +Connector_JST:JST_ZH_B9B-ZR-SM4-TF_1x09-1MP_P1.50mm_Vertical +Connector_JST:JST_ZH_B9B-ZR_1x09_P1.50mm_Vertical +Connector_JST:JST_ZH_S10B-ZR-SM4A-TF_1x10-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S11B-ZR-SM4A-TF_1x11-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S12B-ZR-SM4A-TF_1x12-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S13B-ZR-SM4A-TF_1x13-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S2B-ZR-SM4A-TF_1x02-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S3B-ZR-SM4A-TF_1x03-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S4B-ZR-SM4A-TF_1x04-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S5B-ZR-SM4A-TF_1x05-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S6B-ZR-SM4A-TF_1x06-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S7B-ZR-SM4A-TF_1x07-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S8B-ZR-SM4A-TF_1x08-1MP_P1.50mm_Horizontal +Connector_JST:JST_ZH_S9B-ZR-SM4A-TF_1x09-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502382-0270_1x02-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-0370_1x03-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-0470_1x04-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-0570_1x05-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-0670_1x06-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-0770_1x07-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-0870_1x08-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-0970_1x09-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-1070_1x10-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-1170_1x11-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-1270_1x12-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-1370_1x13-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-1470_1x14-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502382-1570_1x15-1MP_P1.25mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502386-0270_1x02-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-0370_1x03-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-0470_1x04-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-0570_1x05-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-0670_1x06-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-0770_1x07-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-0870_1x08-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-0970_1x09-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-1070_1x10-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-1170_1x11-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-1270_1x12-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-1370_1x13-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-1470_1x14-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502386-1570_1x15-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502443-0270_1x02-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-0370_1x03-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-0470_1x04-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-0570_1x05-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-0670_1x06-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-0770_1x07-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-0870_1x08-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-0970_1x09-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-1270_1x12-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-1370_1x13-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-1470_1x14-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502443-1570_1x15-1MP_P2.00mm_Vertical +Connector_Molex:Molex_CLIK-Mate_502494-0270_1x02-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-0370_1x03-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-0470_1x04-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-0670_1x06-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-0870_1x08-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-1070_1x10-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-1270_1x12-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-1370_1x13-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-1470_1x14-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502494-1570_1x15-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0270_1x02-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0370_1x03-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0470_1x04-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0570_1x05-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0670_1x06-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0770_1x07-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0870_1x08-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-0970_1x09-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-1070_1x10-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-1170_1x11-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-1270_1x12-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-1370_1x13-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-1470_1x14-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_502585-1570_1x15-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_CLIK-Mate_505405-0270_1x02-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-0370_1x03-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-0470_1x04-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-0570_1x05-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-0670_1x06-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-0770_1x07-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-0870_1x08-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-0970_1x09-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-1070_1x10-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-1170_1x11-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-1270_1x12-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-1370_1x13-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-1470_1x14-1MP_P1.50mm_Vertical +Connector_Molex:Molex_CLIK-Mate_505405-1570_1x15-1MP_P1.50mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-02A_1x02_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-03A_1x03_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-05A_1x05_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-06A_1x06_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-07A_1x07_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-08A_1x08_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-09A_1x09_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-10A_1x10_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-11A_1x11_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-12A_1x12_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-13A_1x13_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-14A_1x14_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-15A_1x15_P2.54mm_Vertical +Connector_Molex:Molex_KK-254_AE-6410-16A_1x16_P2.54mm_Vertical +Connector_Molex:Molex_KK-396_5273-02A_1x02_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-03A_1x03_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-04A_1x04_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-05A_1x05_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-06A_1x06_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-07A_1x07_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-08A_1x08_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-09A_1x09_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-10A_1x10_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-11A_1x11_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_5273-12A_1x12_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0002_1x02_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0003_1x03_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0004_1x04_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0005_1x05_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0006_1x06_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0007_1x07_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0008_1x08_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0009_1x09_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0010_1x10_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0011_1x11_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0012_1x12_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0013_1x13_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0014_1x14_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0015_1x15_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0016_1x16_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0017_1x17_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41791-0018_1x18_P3.96mm_Vertical +Connector_Molex:Molex_KK-396_A-41792-0002_1x02_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0003_1x03_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0004_1x04_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0005_1x05_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0006_1x06_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0007_1x07_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0008_1x08_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0009_1x09_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0010_1x10_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0011_1x11_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0012_1x12_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0013_1x13_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0014_1x14_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0015_1x15_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0016_1x16_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0017_1x17_P3.96mm_Horizontal +Connector_Molex:Molex_KK-396_A-41792-0018_1x18_P3.96mm_Horizontal +Connector_Molex:Molex_Mega-Fit_76825-0002_2x01_P5.70mm_Horizontal +Connector_Molex:Molex_Mega-Fit_76825-0004_2x02_P5.70mm_Horizontal +Connector_Molex:Molex_Mega-Fit_76825-0006_2x03_P5.70mm_Horizontal +Connector_Molex:Molex_Mega-Fit_76825-0008_2x04_P5.70mm_Horizontal +Connector_Molex:Molex_Mega-Fit_76825-0010_2x05_P5.70mm_Horizontal +Connector_Molex:Molex_Mega-Fit_76825-0012_2x06_P5.70mm_Horizontal +Connector_Molex:Molex_Mega-Fit_76829-0002_2x01_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0004_2x02_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0006_2x03_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0008_2x04_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0010_2x05_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0012_2x06_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0102_2x01_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0104_2x02_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0106_2x03_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0108_2x04_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0110_2x05_P5.70mm_Vertical +Connector_Molex:Molex_Mega-Fit_76829-0112_2x06_P5.70mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0200_2x01_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0210_2x01-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0212_2x01_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0215_2x01_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0218_2x01-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0221_2x01-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0400_2x02_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0410_2x02-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0412_2x02_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0415_2x02_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0418_2x02-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0421_2x02-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0600_2x03_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0610_2x03-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0612_2x03_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0615_2x03_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0618_2x03-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0621_2x03-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0800_2x04_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0810_2x04-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-0812_2x04_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0815_2x04_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0818_2x04-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-0821_2x04-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1000_2x05_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1010_2x05-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1012_2x05_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1015_2x05_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1018_2x05-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1021_2x05-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1200_2x06_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1210_2x06-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1212_2x06_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1215_2x06_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1218_2x06-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1221_2x06-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1400_2x07_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1410_2x07-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1412_2x07_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1415_2x07_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1418_2x07-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1421_2x07-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1600_2x08_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1610_2x08-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1612_2x08_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1615_2x08_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1618_2x08-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1621_2x08-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1800_2x09_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1810_2x09-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-1812_2x09_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1815_2x09_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1818_2x09-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-1821_2x09-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2000_2x10_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2010_2x10-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2012_2x10_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2015_2x10_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2018_2x10-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2021_2x10-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2200_2x11_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2210_2x11-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2212_2x11_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2215_2x11_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2218_2x11-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2221_2x11-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2400_2x12_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2410_2x12-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43045-2412_2x12_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2415_2x12_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2418_2x12-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43045-2421_2x12-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0200_1x02_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0210_1x02-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0210_1x02-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0215_1x02_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0221_1x02_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0224_1x02-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0300_1x03_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0310_1x03-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0310_1x03-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0315_1x03_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0321_1x03_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0324_1x03-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0400_1x04_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0410_1x04-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0410_1x04-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0415_1x04_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0421_1x04_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0424_1x04-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0500_1x05_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0510_1x05-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0510_1x05-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0515_1x05_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0521_1x05_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0524_1x05-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0600_1x06_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0610_1x06-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0610_1x06-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0615_1x06_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0621_1x06_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0624_1x06-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0700_1x07_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0710_1x07-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0710_1x07-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0715_1x07_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0721_1x07_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0724_1x07-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0800_1x08_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0810_1x08-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0810_1x08-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0815_1x08_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0821_1x08_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0824_1x08-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0900_1x09_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0910_1x09-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-0910_1x09-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-0915_1x09_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0921_1x09_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-0924_1x09-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1000_1x10_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-1010_1x10-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-1010_1x10-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-1015_1x10_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1021_1x10_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1024_1x10-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1100_1x11_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-1110_1x11-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-1110_1x11-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-1115_1x11_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1121_1x11_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1124_1x11-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1200_1x12_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-1210_1x12-1MP_P3.00mm_Horizontal +Connector_Molex:Molex_Micro-Fit_3.0_43650-1210_1x12-1MP_P3.00mm_Horizontal_PnP +Connector_Molex:Molex_Micro-Fit_3.0_43650-1215_1x12_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1221_1x12_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Fit_3.0_43650-1224_1x12-1MP_P3.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0270_1x02_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0370_1x03_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0470_1x04_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0570_1x05_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0670_1x06_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0770_1x07_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0870_1x08_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-0970_1x09_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-1070_1x10_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-1170_1x11_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-1270_1x12_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-1370_1x13_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-1470_1x14_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53253-1570_1x15_P2.00mm_Vertical +Connector_Molex:Molex_Micro-Latch_53254-0270_1x02_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-0370_1x03_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-0470_1x04_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-0570_1x05_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-0670_1x06_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-0770_1x07_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-0870_1x08_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-0970_1x09_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-1070_1x10_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-1170_1x11_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-1270_1x12_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-1370_1x13_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-1470_1x14_P2.00mm_Horizontal +Connector_Molex:Molex_Micro-Latch_53254-1570_1x15_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55932-0210_1x02_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0230_1x02_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0310_1x03_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0330_1x03_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0410_1x04_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0430_1x04_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0510_1x05_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0530_1x05_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0610_1x06_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0630_1x06_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0710_1x07_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0730_1x07_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0810_1x08_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0830_1x08_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0910_1x09_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-0930_1x09_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1010_1x10_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1030_1x10_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1110_1x11_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1130_1x11_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1210_1x12_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1230_1x12_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1310_1x13_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1330_1x13_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1410_1x14_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1430_1x14_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1510_1x15_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55932-1530_1x15_P2.00mm_Vertical +Connector_Molex:Molex_MicroClasp_55935-0210_1x02_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0230_1x02_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0310_1x03_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0330_1x03_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0410_1x04_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0430_1x04_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0510_1x05_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0530_1x05_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0610_1x06_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0630_1x06_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0710_1x07_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0730_1x07_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0810_1x08_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0830_1x08_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0910_1x09_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-0930_1x09_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1010_1x10_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1030_1x10_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1110_1x11_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1130_1x11_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1210_1x12_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1230_1x12_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1310_1x13_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1330_1x13_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1410_1x14_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1430_1x14_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1510_1x15_P2.00mm_Horizontal +Connector_Molex:Molex_MicroClasp_55935-1530_1x15_P2.00mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5566-02A2_2x01_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-02A_2x01_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-04A2_2x02_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-04A_2x02_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-06A2_2x03_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-06A_2x03_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-08A2_2x04_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-08A_2x04_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-10A2_2x05_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-10A_2x05_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-12A2_2x06_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-12A_2x06_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-14A2_2x07_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-14A_2x07_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-16A2_2x08_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-16A_2x08_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-18A2_2x09_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-18A_2x09_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-20A2_2x10_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-20A_2x10_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-22A2_2x11_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-22A_2x11_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-24A2_2x12_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5566-24A_2x12_P4.20mm_Vertical +Connector_Molex:Molex_Mini-Fit_Jr_5569-02A1_2x01_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-02A2_2x01_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-04A1_2x02_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-04A2_2x02_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-06A1_2x03_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-06A2_2x03_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-08A1_2x04_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-08A2_2x04_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-10A1_2x05_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-10A2_2x05_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-12A1_2x06_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-12A2_2x06_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-14A1_2x07_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-14A2_2x07_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-16A1_2x08_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-16A2_2x08_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-18A1_2x09_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-18A2_2x09_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-20A1_2x10_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-20A2_2x10_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-22A1_2x11_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-22A2_2x11_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-24A1_2x12_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Jr_5569-24A2_2x12_P4.20mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Sr_42819-22XX_1x02_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_42819-22XX_1x02_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42819-32XX_1x03_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_42819-32XX_1x03_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42819-42XX_1x04_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_42819-42XX_1x04_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42819-52XX_1x05_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_42819-52XX_1x05_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42819-62XX_1x06_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_42819-62XX_1x06_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42820-22XX_1x02_P10.00mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Sr_42820-22XX_1x02_P10.00mm_Horizontal_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42820-32XX_1x03_P10.00mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Sr_42820-32XX_1x03_P10.00mm_Horizontal_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42820-42XX_1x04_P10.00mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Sr_42820-42XX_1x04_P10.00mm_Horizontal_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42820-52XX_1x05_P10.00mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Sr_42820-52XX_1x05_P10.00mm_Horizontal_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_42820-62XX_1x06_P10.00mm_Horizontal +Connector_Molex:Molex_Mini-Fit_Sr_42820-62XX_1x06_P10.00mm_Horizontal_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx06_2x03_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx06_2x03_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx08_2x04_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx08_2x04_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx10_2x05_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx10_2x05_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx12_2x06_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx12_2x06_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx14_2x07_P10.00mm_Vertical +Connector_Molex:Molex_Mini-Fit_Sr_43915-xx14_2x07_P10.00mm_Vertical_ThermalVias +Connector_Molex:Molex_Nano-Fit_105309-xx02_1x02_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105309-xx03_1x03_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105309-xx04_1x04_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105309-xx05_1x05_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105309-xx06_1x06_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105309-xx07_1x07_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105309-xx08_1x08_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105310-xx04_2x02_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105310-xx06_2x03_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105310-xx08_2x04_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105310-xx10_2x05_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105310-xx12_2x06_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105310-xx14_2x07_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105310-xx16_2x08_P2.50mm_Vertical +Connector_Molex:Molex_Nano-Fit_105313-xx02_1x02_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105313-xx03_1x03_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105313-xx04_1x04_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105313-xx05_1x05_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105313-xx06_1x06_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105313-xx07_1x07_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105313-xx08_1x08_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105314-xx04_2x02_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105314-xx06_2x03_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105314-xx08_2x04_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105314-xx10_2x05_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105314-xx12_2x06_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105314-xx14_2x07_P2.50mm_Horizontal +Connector_Molex:Molex_Nano-Fit_105314-xx16_2x08_P2.50mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0270_1x02-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0370_1x03-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0470_1x04-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0570_1x05-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0670_1x06-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0770_1x07-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0870_1x08-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-0970_1x09-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-1070_1x10-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-1270_1x12-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-1470_1x14-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-1570_1x15-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-1870_1x18-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Panelmate_53780-3070_1x30-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0207_1x02-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0307_1x03-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0407_1x04-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0507_1x05-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0607_1x06-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0707_1x07-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0807_1x08-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-0907_1x09-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-1007_1x10-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-1107_1x11-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-1207_1x12-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-1307_1x13-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-1407_1x14-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_202396-1507_1x15-1MP_P1.00mm_Horizontal +Connector_Molex:Molex_Pico-Clasp_501331-0207_1x02-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-0307_1x03-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-0407_1x04-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-0507_1x05-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-0607_1x06-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-0707_1x07-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-0807_1x08-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-0907_1x09-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-1007_1x10-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-1107_1x11-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-1207_1x12-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-1307_1x13-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-1407_1x14-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-Clasp_501331-1507_1x15-1MP_P1.00mm_Vertical +Connector_Molex:Molex_Pico-EZmate_78171-0002_1x02-1MP_P1.20mm_Vertical +Connector_Molex:Molex_Pico-EZmate_78171-0003_1x03-1MP_P1.20mm_Vertical +Connector_Molex:Molex_Pico-EZmate_78171-0004_1x04-1MP_P1.20mm_Vertical +Connector_Molex:Molex_Pico-EZmate_78171-0005_1x05-1MP_P1.20mm_Vertical +Connector_Molex:Molex_Pico-EZmate_Slim_202656-0021_1x02-1MP_P1.20mm_Vertical +Connector_Molex:Molex_Pico-Lock_205338-0002_1x02-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_Pico-Lock_205338-0004_1x04-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_Pico-Lock_205338-0006_1x06-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_Pico-Lock_205338-0008_1x08-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_Pico-Lock_205338-0010_1x10-1MP_P2.00mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0291_1x02-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0391_1x03-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0491_1x04-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0591_1x05-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0691_1x06-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0791_1x07-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0891_1x08-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-0991_1x09-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-1091_1x10-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-1191_1x11-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-Lock_504050-1291_1x12-1MP_P1.50mm_Horizontal +Connector_Molex:Molex_Pico-SPOX_87437-1443_1x14-P1.5mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0210_1x02_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0310_1x03_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0410_1x04_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0510_1x05_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0610_1x06_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0710_1x07_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0810_1x08_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-0910_1x09_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-1010_1x10_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-1110_1x11_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-1210_1x12_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-1310_1x13_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-1410_1x14_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53047-1510_1x15_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53048-0210_1x02_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-0310_1x03_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-0410_1x04_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-0510_1x05_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-0610_1x06_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-0710_1x07_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-0810_1x08_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-0910_1x09_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-1010_1x10_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-1110_1x11_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-1210_1x12_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-1310_1x13_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-1410_1x14_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53048-1510_1x15_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0271_1x02-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0371_1x03-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0471_1x04-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0571_1x05-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0671_1x06-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0771_1x07-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0871_1x08-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-0971_1x09-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-1071_1x10-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-1171_1x11-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-1271_1x12-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-1371_1x13-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-1471_1x14-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-1571_1x15-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53261-1771_1x17-1MP_P1.25mm_Horizontal +Connector_Molex:Molex_PicoBlade_53398-0271_1x02-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-0371_1x03-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-0571_1x05-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-0671_1x06-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-0771_1x07-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-0871_1x08-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-0971_1x09-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-1071_1x10-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-1171_1x11-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-1271_1x12-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-1371_1x13-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-1471_1x14-1MP_P1.25mm_Vertical +Connector_Molex:Molex_PicoBlade_53398-1571_1x15-1MP_P1.25mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0004_2x02_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0006_2x03_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0008_2x04_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0010_2x05_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0012_2x06_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0014_2x07_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0016_2x08_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0018_2x09_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0020_2x10_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0022_2x11_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0024_2x12_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90325-0026_2x13_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0004_2x02_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0006_2x03_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0008_2x04_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0010_2x05_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0012_2x06_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0014_2x07_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0016_2x08_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0018_2x09_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0020_2x10_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0022_2x11_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0024_2x12_P1.27mm_Vertical +Connector_Molex:Molex_Picoflex_90814-0026_2x13_P1.27mm_Vertical +Connector_Molex:Molex_Sabre_43160-0102_1x02_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-0102_1x02_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-0103_1x03_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-0103_1x03_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-0104_1x04_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-0104_1x04_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-0105_1x05_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-0105_1x05_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-0106_1x06_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-0106_1x06_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-1102_1x02_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_43160-1102_1x02_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_43160-1103_1x03_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_43160-1103_1x03_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_43160-1104_1x04_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_43160-1104_1x04_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_43160-1105_1x05_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_43160-1105_1x05_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_43160-1106_1x06_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_43160-1106_1x06_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_43160-2102_1x02_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-2102_1x02_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-2103_1x03_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-2103_1x03_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-2104_1x04_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-2104_1x04_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-2105_1x05_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-2105_1x05_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_43160-2106_1x06_P7.49mm_Vertical +Connector_Molex:Molex_Sabre_43160-2106_1x06_P7.49mm_Vertical_ThermalVias +Connector_Molex:Molex_Sabre_46007-1102_1x02_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_46007-1102_1x02_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_46007-1103_1x03_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_46007-1103_1x03_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_46007-1104_1x04_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_46007-1104_1x04_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_46007-1105_1x05_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_46007-1105_1x05_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_Sabre_46007-1106_1x06_P7.49mm_Horizontal +Connector_Molex:Molex_Sabre_46007-1106_1x06_P7.49mm_Horizontal_ThermalVias +Connector_Molex:Molex_SlimStack_501920-3001_2x15_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_501920-4001_2x20_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_501920-5001_2x25_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_502426-0810_2x04_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-1410_2x07_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-2010_2x10_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-2210_2x11_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-2410_2x12_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-2610_2x13_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-3010_2x15_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-3210_2x16_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-3410_2x17_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-4010_2x20_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-4410_2x22_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-5010_2x25_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-6010_2x30_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-6410_2x32_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502426-8010_2x40_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-0820_2x04_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-1410_2x07_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-2010_2x10_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-2210_2x11_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-2410_2x12_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-2610_2x13_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-3010_2x15_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-3210_2x16_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-3410_2x17_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-4010_2x20_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-4410_2x22_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-5010_2x25_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-6010_2x30_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-6410_2x32_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_502430-8010_2x40_P0.40mm_Vertical +Connector_Molex:Molex_SlimStack_52991-0208_2x10_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_52991-0308_2x15_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_52991-0408_2x20_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_52991-0508_2x25_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_52991-0608_2x30_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_52991-0708_2x35_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_52991-0808_2x40_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_53748-0208_2x10_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_53748-0308_2x15_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_53748-0408_2x20_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_53748-0608_2x30_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_53748-0708_2x35_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_53748-0808_2x40_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0164_2x08_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0204_2x10_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0224_2x11_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0244_2x12_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0304_2x15_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0344_2x17_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0404_2x20_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0504_2x25_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0604_2x30_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_54722-0804_2x40_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0161_2x08_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0201_2x10_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0221_2x11_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0241_2x12_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0301_2x15_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0341_2x17_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0401_2x20_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0501_2x25_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0601_2x30_P0.50mm_Vertical +Connector_Molex:Molex_SlimStack_55560-0801_2x40_P0.50mm_Vertical +Connector_Molex:Molex_SL_171971-0002_1x02_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0003_1x03_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0004_1x04_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0005_1x05_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0006_1x06_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0007_1x07_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0008_1x08_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0009_1x09_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0010_1x10_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0011_1x11_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0012_1x12_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0013_1x13_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0014_1x14_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0015_1x15_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0016_1x16_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0017_1x17_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0018_1x18_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0019_1x19_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0020_1x20_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0021_1x21_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0022_1x22_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0023_1x23_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0024_1x24_P2.54mm_Vertical +Connector_Molex:Molex_SL_171971-0025_1x25_P2.54mm_Vertical +Connector_Molex:Molex_SPOX_5267-02A_1x02_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-03A_1x03_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-04A_1x04_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-05A_1x05_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-06A_1x06_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-07A_1x07_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-08A_1x08_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-09A_1x09_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-10A_1x10_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-11A_1x11_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-12A_1x12_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-13A_1x13_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-14A_1x14_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5267-15A_1x15_P2.50mm_Vertical +Connector_Molex:Molex_SPOX_5268-02A_1x02_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-03A_1x03_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-04A_1x04_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-05A_1x05_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-06A_1x06_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-07A_1x07_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-08A_1x08_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-09A_1x09_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-10A_1x10_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-11A_1x11_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-12A_1x12_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-13A_1x13_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-14A_1x14_P2.50mm_Horizontal +Connector_Molex:Molex_SPOX_5268-15A_1x15_P2.50mm_Horizontal +Connector_PCBEdge:4UCON_10156_2x40_P1.27mm_Socket_Horizontal +Connector_PCBEdge:BUS_AT +Connector_PCBEdge:BUS_PCI +Connector_PCBEdge:BUS_PCIexpress_x1 +Connector_PCBEdge:BUS_PCIexpress_x16 +Connector_PCBEdge:BUS_PCIexpress_x4 +Connector_PCBEdge:BUS_PCIexpress_x8 +Connector_PCBEdge:BUS_PCI_Express_Mini +Connector_PCBEdge:BUS_PCI_Express_Mini_Dual +Connector_PCBEdge:BUS_PCI_Express_Mini_Full +Connector_PCBEdge:BUS_PCI_Express_Mini_Half +Connector_PCBEdge:JAE_MM60-EZH039-Bx_BUS_PCI_Express_Holder +Connector_PCBEdge:JAE_MM60-EZH059-Bx_BUS_PCI_Express_Holder +Connector_PCBEdge:molex_EDGELOCK_2-CKT +Connector_PCBEdge:molex_EDGELOCK_4-CKT +Connector_PCBEdge:molex_EDGELOCK_6-CKT +Connector_PCBEdge:molex_EDGELOCK_8-CKT +Connector_PCBEdge:Samtec_MECF-05-01-L-DV-WT_2x05_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-01-L-DV_2x05_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-01-NP-L-DV-WT_2x05_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-01-NP-L-DV_2x05_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-02-L-DV-WT_2x05_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-02-L-DV_2x05_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-02-NP-L-DV-WT_2x05_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-02-NP-L-DV_2x05_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-05-0_-L-DV_2x05_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-05-0_-NP-L-DV_2x05_P1.27mm_Edge +Connector_PCBEdge:Samtec_MECF-08-01-L-DV-WT_2x08_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-01-L-DV_2x08_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-01-NP-L-DV-WT_2x08_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-01-NP-L-DV_2x08_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-02-L-DV-WT_2x08_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-02-L-DV_2x08_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-02-NP-L-DV-WT_2x08_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-02-NP-L-DV_2x08_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-08-0_-L-DV_2x08_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-08-0_-NP-L-DV_2x08_P1.27mm_Edge +Connector_PCBEdge:Samtec_MECF-20-01-L-DV-WT_2x20_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-01-L-DV_2x20_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-01-NP-L-DV-WT_2x20_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-01-NP-L-DV_2x20_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-02-L-DV-WT_2x20_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-02-L-DV_2x20_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-02-NP-L-DV-WT_2x20_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-02-NP-L-DV_2x20_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-20-0_-L-DV_2x20_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-20-0_-NP-L-DV_2x20_P1.27mm_Edge +Connector_PCBEdge:Samtec_MECF-30-01-L-DV-WT_2x30_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-01-L-DV_2x30_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-01-NP-L-DV-WT_2x30_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-01-NP-L-DV_2x30_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-02-L-DV-WT_2x30_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-02-L-DV_2x30_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-02-NP-L-DV-WT_2x30_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-02-NP-L-DV_2x30_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-30-0_-L-DV_2x30_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-30-0_-NP-L-DV_2x30_P1.27mm_Edge +Connector_PCBEdge:Samtec_MECF-40-01-L-DV-WT_2x40_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-01-L-DV_2x40_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-01-NP-L-DV-WT_2x40_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-01-NP-L-DV_2x40_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-02-L-DV-WT_2x40_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-02-L-DV_2x40_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-02-NP-L-DV-WT_2x40_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-02-NP-L-DV_2x40_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-40-0_-L-DV_2x40_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-40-0_-NP-L-DV_2x40_P1.27mm_Edge +Connector_PCBEdge:Samtec_MECF-50-01-L-DV-WT_2x50_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-01-L-DV_2x50_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-01-NP-L-DV-WT_2x50_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-01-NP-L-DV_2x50_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-02-L-DV-WT_2x50_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-02-L-DV_2x50_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-02-NP-L-DV-WT_2x50_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-02-NP-L-DV_2x50_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-50-0_-L-DV_2x50_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-50-0_-NP-L-DV_2x50_P1.27mm_Edge +Connector_PCBEdge:Samtec_MECF-60-01-L-DV-WT_2x60_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-01-L-DV_2x60_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-01-NP-L-DV-WT_2x60_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-01-NP-L-DV_2x60_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-02-L-DV-WT_2x60_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-02-L-DV_2x60_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-02-NP-L-DV-WT_2x60_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-02-NP-L-DV_2x60_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-60-0_-L-DV_2x60_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-60-0_-NP-L-DV_2x60_P1.27mm_Edge +Connector_PCBEdge:Samtec_MECF-70-01-L-DV-WT_2x70_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-01-L-DV_2x70_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-01-NP-L-DV-WT_2x70_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-01-NP-L-DV_2x70_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-02-L-DV-WT_2x70_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-02-L-DV_2x70_P1.27mm_Polarized_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-02-NP-L-DV-WT_2x70_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-02-NP-L-DV_2x70_P1.27mm_Socket_Horizontal +Connector_PCBEdge:Samtec_MECF-70-0_-L-DV_2x70_P1.27mm_Polarized_Edge +Connector_PCBEdge:Samtec_MECF-70-0_-NP-L-DV_2x70_P1.27mm_Edge +Connector_PCBEdge:SODIMM-200_1.8V_Card_edge +Connector_PCBEdge:SODIMM-200_2.5V_Card_edge +Connector_PCBEdge:SODIMM-260_DDR4_H4.0-5.2_OrientationStd_Socket +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_10-G-7,62_1x10_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_10-G_1x10_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_11-G-7,62_1x11_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_11-G_1x11_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_12-G-7,62_1x12_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_12-G_1x12_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_2-G-7,62_1x02_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_2-G_1x02_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_3-G-7,62_1x03_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_3-G_1x03_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_4-G-7,62_1x04_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_4-G_1x04_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_5-G-7,62_1x05_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_5-G_1x05_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_6-G-7,62_1x06_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_6-G_1x06_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_7-G-7,62_1x07_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_7-G_1x07_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_8-G-7,62_1x08_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_8-G_1x08_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_9-G-7,62_1x09_P7.62mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBA_2,5_9-G_1x09_P7.50mm_Horizontal +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_10-G-7,62_1x10_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_10-G_1x10_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_11-G-7,62_1x11_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_11-G_1x11_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_12-G-7,62_1x12_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_12-G_1x12_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_2-G-7,62_1x02_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_2-G_1x02_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_3-G-7,62_1x03_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_3-G_1x03_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_4-G-7,62_1x04_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_4-G_1x04_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_5-G-7,62_1x05_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_5-G_1x05_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_6-G-7,62_1x06_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_6-G_1x06_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_7-G-7,62_1x07_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_7-G_1x07_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_8-G-7,62_1x08_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_8-G_1x08_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_9-G-7,62_1x09_P7.62mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBVA_2,5_9-G_1x09_P7.50mm_Vertical +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_10-GF-7,62_1x10_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_10-GF-7,62_1x10_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_11-GF-7,62_1x11_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_11-GF-7,62_1x11_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_12-GF-7,62_1x12_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_12-GF-7,62_1x12_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_2-GF-7,62_1x02_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_2-GF-7,62_1x02_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_3-GF-7,62_1x03_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_3-GF-7,62_1x03_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_4-GF-7,62_1x04_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_4-GF-7,62_1x04_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_5-GF-7,62_1x05_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_5-GF-7,62_1x05_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_6-GF-7,62_1x06_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_6-GF-7,62_1x06_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_7-GF-7,62_1x07_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_7-GF-7,62_1x07_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_8-GF-7,62_1x08_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_8-GF-7,62_1x08_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_9-GF-7,62_1x09_P7.62mm_Vertical_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTBV_2,5_9-GF-7,62_1x09_P7.62mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_10-GF-7,62_1x10_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_10-GF-7,62_1x10_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_11-GF-7,62_1x11_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_11-GF-7,62_1x11_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_12-GF-7,62_1x12_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_12-GF-7,62_1x12_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_2-GF-7,62_1x02_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_2-GF-7,62_1x02_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_3-GF-7,62_1x03_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_3-GF-7,62_1x03_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_4-GF-7,62_1x04_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_4-GF-7,62_1x04_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_5-GF-7,62_1x05_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_5-GF-7,62_1x05_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_6-GF-7,62_1x06_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_6-GF-7,62_1x06_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_7-GF-7,62_1x07_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_7-GF-7,62_1x07_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_8-GF-7,62_1x08_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_8-GF-7,62_1x08_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_9-GF-7,62_1x09_P7.62mm_Horizontal_ThreadedFlange +Connector_Phoenix_GMSTB:PhoenixContact_GMSTB_2,5_9-GF-7,62_1x09_P7.62mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_10-G-3.5_1x10_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_10-G-3.81_1x10_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_10-GF-3.5_1x10_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_10-GF-3.5_1x10_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_10-GF-3.81_1x10_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_10-GF-3.81_1x10_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_11-G-3.5_1x11_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_11-G-3.81_1x11_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_11-GF-3.5_1x11_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_11-GF-3.5_1x11_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_11-GF-3.81_1x11_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_11-GF-3.81_1x11_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_12-G-3.5_1x12_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_12-G-3.81_1x12_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_12-GF-3.5_1x12_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_12-GF-3.5_1x12_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_12-GF-3.81_1x12_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_12-GF-3.81_1x12_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_13-G-3.5_1x13_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_13-G-3.81_1x13_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_13-GF-3.5_1x13_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_13-GF-3.5_1x13_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_13-GF-3.81_1x13_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_13-GF-3.81_1x13_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_14-G-3.5_1x14_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_14-G-3.81_1x14_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_14-GF-3.5_1x14_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_14-GF-3.5_1x14_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_14-GF-3.81_1x14_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_14-GF-3.81_1x14_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_15-G-3.5_1x15_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_15-G-3.81_1x15_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_15-GF-3.5_1x15_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_15-GF-3.5_1x15_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_15-GF-3.81_1x15_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_15-GF-3.81_1x15_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_16-G-3.5_1x16_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_16-G-3.81_1x16_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_16-GF-3.5_1x16_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_16-GF-3.5_1x16_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_16-GF-3.81_1x16_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_16-GF-3.81_1x16_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_2-G-3.5_1x02_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_2-G-3.81_1x02_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_2-GF-3.5_1x02_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_2-GF-3.5_1x02_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_2-GF-3.81_1x02_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_2-GF-3.81_1x02_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_3-G-3.5_1x03_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_3-G-3.81_1x03_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_3-GF-3.5_1x03_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_3-GF-3.5_1x03_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_3-GF-3.81_1x03_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_3-GF-3.81_1x03_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_4-G-3.5_1x04_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_4-G-3.81_1x04_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_4-GF-3.5_1x04_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_4-GF-3.5_1x04_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_4-GF-3.81_1x04_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_4-GF-3.81_1x04_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_5-G-3.5_1x05_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_5-G-3.81_1x05_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_5-GF-3.5_1x05_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_5-GF-3.5_1x05_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_5-GF-3.81_1x05_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_5-GF-3.81_1x05_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_6-G-3.5_1x06_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_6-G-3.81_1x06_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_6-GF-3.5_1x06_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_6-GF-3.5_1x06_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_6-GF-3.81_1x06_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_6-GF-3.81_1x06_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_7-G-3.5_1x07_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_7-G-3.81_1x07_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_7-GF-3.5_1x07_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_7-GF-3.5_1x07_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_7-GF-3.81_1x07_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_7-GF-3.81_1x07_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_8-G-3.5_1x08_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_8-G-3.81_1x08_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_8-GF-3.5_1x08_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_8-GF-3.5_1x08_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_8-GF-3.81_1x08_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_8-GF-3.81_1x08_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_9-G-3.5_1x09_P3.50mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_9-G-3.81_1x09_P3.81mm_Vertical +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_9-GF-3.5_1x09_P3.50mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_9-GF-3.5_1x09_P3.50mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_9-GF-3.81_1x09_P3.81mm_Vertical_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MCV_1,5_9-GF-3.81_1x09_P3.81mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_10-G-3.5_1x10_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_10-G-3.81_1x10_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_10-GF-3.5_1x10_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_10-GF-3.5_1x10_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_10-GF-3.81_1x10_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_10-GF-3.81_1x10_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_11-G-3.5_1x11_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_11-G-3.81_1x11_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_11-GF-3.5_1x11_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_11-GF-3.5_1x11_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_11-GF-3.81_1x11_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_11-GF-3.81_1x11_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_12-G-3.5_1x12_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_12-G-3.81_1x12_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_12-GF-3.5_1x12_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_12-GF-3.5_1x12_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_12-GF-3.81_1x12_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_12-GF-3.81_1x12_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_13-G-3.5_1x13_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_13-G-3.81_1x13_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_13-GF-3.5_1x13_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_13-GF-3.5_1x13_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_13-GF-3.81_1x13_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_13-GF-3.81_1x13_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_14-G-3.5_1x14_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_14-G-3.81_1x14_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_14-GF-3.5_1x14_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_14-GF-3.5_1x14_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_14-GF-3.81_1x14_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_14-GF-3.81_1x14_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_15-G-3.5_1x15_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_15-G-3.81_1x15_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_15-GF-3.5_1x15_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_15-GF-3.5_1x15_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_15-GF-3.81_1x15_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_15-GF-3.81_1x15_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_16-G-3.5_1x16_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_16-G-3.81_1x16_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_16-GF-3.5_1x16_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_16-GF-3.5_1x16_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_16-GF-3.81_1x16_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_16-GF-3.81_1x16_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_2-G-3.5_1x02_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_2-G-3.81_1x02_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_2-GF-3.5_1x02_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_2-GF-3.5_1x02_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_2-GF-3.81_1x02_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_2-GF-3.81_1x02_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_3-G-3.5_1x03_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_3-G-3.81_1x03_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_3-GF-3.5_1x03_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_3-GF-3.5_1x03_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_3-GF-3.81_1x03_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_3-GF-3.81_1x03_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_4-G-3.5_1x04_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_4-G-3.81_1x04_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_4-GF-3.5_1x04_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_4-GF-3.5_1x04_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_4-GF-3.81_1x04_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_4-GF-3.81_1x04_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_5-G-3.5_1x05_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_5-G-3.81_1x05_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_5-GF-3.5_1x05_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_5-GF-3.5_1x05_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_5-GF-3.81_1x05_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_5-GF-3.81_1x05_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_6-G-3.5_1x06_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_6-G-3.81_1x06_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_6-GF-3.5_1x06_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_6-GF-3.5_1x06_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_6-GF-3.81_1x06_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_6-GF-3.81_1x06_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_7-G-3.5_1x07_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_7-G-3.81_1x07_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_7-GF-3.5_1x07_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_7-GF-3.5_1x07_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_7-GF-3.81_1x07_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_7-GF-3.81_1x07_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_8-G-3.5_1x08_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_8-G-3.81_1x08_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_8-GF-3.5_1x08_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_8-GF-3.5_1x08_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_8-GF-3.81_1x08_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_8-GF-3.81_1x08_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_9-G-3.5_1x09_P3.50mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_9-G-3.81_1x09_P3.81mm_Horizontal +Connector_Phoenix_MC:PhoenixContact_MC_1,5_9-GF-3.5_1x09_P3.50mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_9-GF-3.5_1x09_P3.50mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC:PhoenixContact_MC_1,5_9-GF-3.81_1x09_P3.81mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC:PhoenixContact_MC_1,5_9-GF-3.81_1x09_P3.81mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_10-G-5.08_1x10_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_10-GF-5.08_1x10_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_10-GF-5.08_1x10_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_11-G-5.08_1x11_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_11-GF-5.08_1x11_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_11-GF-5.08_1x11_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_12-G-5.08_1x12_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_12-GF-5.08_1x12_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_12-GF-5.08_1x12_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_2-G-5.08_1x02_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_2-GF-5.08_1x02_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_2-GF-5.08_1x02_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_3-G-5.08_1x03_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_3-GF-5.08_1x03_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_3-GF-5.08_1x03_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_4-G-5.08_1x04_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_4-GF-5.08_1x04_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_4-GF-5.08_1x04_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_5-G-5.08_1x05_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_5-GF-5.08_1x05_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_5-GF-5.08_1x05_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_6-G-5.08_1x06_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_6-GF-5.08_1x06_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_6-GF-5.08_1x06_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_7-G-5.08_1x07_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_7-GF-5.08_1x07_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_7-GF-5.08_1x07_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_8-G-5.08_1x08_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_8-GF-5.08_1x08_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_8-GF-5.08_1x08_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_9-G-5.08_1x09_P5.08mm_Vertical +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_9-GF-5.08_1x09_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MCV_1,5_9-GF-5.08_1x09_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_10-G-5.08_1x10_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_10-GF-5.08_1x10_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_10-GF-5.08_1x10_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_11-G-5.08_1x11_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_11-GF-5.08_1x11_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_11-GF-5.08_1x11_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_12-G-5.08_1x12_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_12-GF-5.08_1x12_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_12-GF-5.08_1x12_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_2-G-5.08_1x02_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_2-GF-5.08_1x02_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_2-GF-5.08_1x02_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_3-G-5.08_1x03_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_3-GF-5.08_1x03_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_3-GF-5.08_1x03_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_4-G-5.08_1x04_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_4-GF-5.08_1x04_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_4-GF-5.08_1x04_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_5-G-5.08_1x05_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_5-GF-5.08_1x05_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_5-GF-5.08_1x05_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_6-G-5.08_1x06_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_6-GF-5.08_1x06_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_6-GF-5.08_1x06_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_7-G-5.08_1x07_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_7-GF-5.08_1x07_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_7-GF-5.08_1x07_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_8-G-5.08_1x08_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_8-GF-5.08_1x08_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_8-GF-5.08_1x08_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_9-G-5.08_1x09_P5.08mm_Horizontal +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_9-GF-5.08_1x09_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MC_HighVoltage:PhoenixContact_MC_1,5_9-GF-5.08_1x09_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_10-G-5,08_1x10_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_10-G_1x10_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_11-G-5,08_1x11_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_11-G_1x11_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_12-G-5,08_1x12_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_12-G_1x12_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_13-G-5,08_1x13_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_13-G_1x13_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_14-G-5,08_1x14_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_14-G_1x14_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_15-G-5,08_1x15_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_15-G_1x15_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_16-G-5,08_1x16_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_16-G_1x16_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_2-G-5,08_1x02_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_2-G_1x02_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_3-G-5,08_1x03_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_3-G_1x03_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_4-G-5,08_1x04_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_4-G_1x04_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_5-G-5,08_1x05_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_5-G_1x05_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_6-G-5,08_1x06_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_6-G_1x06_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_7-G-5,08_1x07_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_7-G_1x07_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_8-G-5,08_1x08_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_8-G_1x08_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_9-G-5,08_1x09_P5.08mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBA_2,5_9-G_1x09_P5.00mm_Horizontal +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_10-G-5,08_1x10_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_10-G_1x10_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_11-G-5,08_1x11_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_11-G_1x11_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_12-G-5,08_1x12_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_12-G_1x12_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_13-G-5,08_1x13_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_13-G_1x13_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_14-G-5,08_1x14_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_14-G_1x14_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_15-G-5,08_1x15_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_15-G_1x15_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_16-G-5,08_1x16_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_16-G_1x16_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_2-G-5,08_1x02_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_2-G_1x02_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_3-G-5,08_1x03_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_3-G_1x03_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_4-G-5,08_1x04_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_4-G_1x04_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_5-G-5,08_1x05_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_5-G_1x05_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_6-G-5,08_1x06_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_6-G_1x06_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_7-G-5,08_1x07_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_7-G_1x07_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_8-G-5,08_1x08_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_8-G_1x08_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_9-G-5,08_1x09_P5.08mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBVA_2,5_9-G_1x09_P5.00mm_Vertical +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_10-GF-5,08_1x10_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_10-GF-5,08_1x10_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_10-GF_1x10_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_10-GF_1x10_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_11-GF-5,08_1x11_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_11-GF-5,08_1x11_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_11-GF_1x11_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_11-GF_1x11_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_12-GF-5,08_1x12_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_12-GF-5,08_1x12_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_12-GF_1x12_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_12-GF_1x12_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_13-GF-5,08_1x13_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_13-GF-5,08_1x13_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_13-GF_1x13_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_13-GF_1x13_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_14-GF-5,08_1x14_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_14-GF-5,08_1x14_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_14-GF_1x14_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_14-GF_1x14_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_15-GF-5,08_1x15_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_15-GF-5,08_1x15_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_15-GF_1x15_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_15-GF_1x15_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_16-GF-5,08_1x16_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_16-GF-5,08_1x16_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_16-GF_1x16_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_16-GF_1x16_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_2-GF-5,08_1x02_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_2-GF-5,08_1x02_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_2-GF_1x02_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_2-GF_1x02_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_3-GF-5,08_1x03_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_3-GF-5,08_1x03_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_3-GF_1x03_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_3-GF_1x03_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_4-GF-5,08_1x04_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_4-GF-5,08_1x04_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_4-GF_1x04_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_4-GF_1x04_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_5-GF-5,08_1x05_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_5-GF-5,08_1x05_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_5-GF_1x05_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_5-GF_1x05_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_6-GF-5,08_1x06_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_6-GF-5,08_1x06_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_6-GF_1x06_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_6-GF_1x06_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_7-GF-5,08_1x07_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_7-GF-5,08_1x07_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_7-GF_1x07_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_7-GF_1x07_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_8-GF-5,08_1x08_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_8-GF-5,08_1x08_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_8-GF_1x08_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_8-GF_1x08_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_9-GF-5,08_1x09_P5.08mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_9-GF-5,08_1x09_P5.08mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_9-GF_1x09_P5.00mm_Vertical_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTBV_2,5_9-GF_1x09_P5.00mm_Vertical_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_10-GF-5,08_1x10_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_10-GF-5,08_1x10_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_10-GF_1x10_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_10-GF_1x10_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_11-GF-5,08_1x11_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_11-GF-5,08_1x11_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_11-GF_1x11_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_11-GF_1x11_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_12-GF-5,08_1x12_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_12-GF-5,08_1x12_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_12-GF_1x12_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_12-GF_1x12_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_13-GF-5,08_1x13_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_13-GF-5,08_1x13_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_13-GF_1x13_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_13-GF_1x13_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_14-GF-5,08_1x14_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_14-GF-5,08_1x14_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_14-GF_1x14_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_14-GF_1x14_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_15-GF-5,08_1x15_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_15-GF-5,08_1x15_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_15-GF_1x15_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_15-GF_1x15_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_16-GF-5,08_1x16_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_16-GF-5,08_1x16_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_16-GF_1x16_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_16-GF_1x16_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_2-GF-5,08_1x02_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_2-GF-5,08_1x02_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_2-GF_1x02_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_2-GF_1x02_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_3-GF-5,08_1x03_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_3-GF-5,08_1x03_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_3-GF_1x03_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_3-GF_1x03_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_4-GF-5,08_1x04_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_4-GF-5,08_1x04_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_4-GF_1x04_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_4-GF_1x04_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_5-GF-5,08_1x05_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_5-GF-5,08_1x05_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_5-GF_1x05_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_5-GF_1x05_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_6-GF-5,08_1x06_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_6-GF-5,08_1x06_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_6-GF_1x06_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_6-GF_1x06_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_7-GF-5,08_1x07_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_7-GF-5,08_1x07_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_7-GF_1x07_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_7-GF_1x07_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_8-GF-5,08_1x08_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_8-GF-5,08_1x08_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_8-GF_1x08_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_8-GF_1x08_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_9-GF-5,08_1x09_P5.08mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_9-GF-5,08_1x09_P5.08mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_9-GF_1x09_P5.00mm_Horizontal_ThreadedFlange +Connector_Phoenix_MSTB:PhoenixContact_MSTB_2,5_9-GF_1x09_P5.00mm_Horizontal_ThreadedFlange_MountHole +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_10-H-3.5_1x10_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_11-H-3.5_1x11_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_12-H-3.5_1x12_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_2-H-3.5_1x02_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_3-H-3.5_1x03_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_4-H-3.5_1x04_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_5-H-3.5_1x05_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_6-H-3.5_1x06_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_7-H-3.5_1x07_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_8-H-3.5_1x08_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_1.5_9-H-3.5_1x09_P3.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_1-H-5.0_1x01_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_10-H-5.0-EX_1x10_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_10-H-5.0_1x10_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_10-V-5.0-EX_1x10_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_11-H-5.0-EX_1x11_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_11-H-5.0_1x11_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_11-V-5.0-EX_1x11_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_12-H-5.0-EX_1x12_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_12-H-5.0_1x12_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_12-V-5.0-EX_1x12_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_2-H-5.0-EX_1x02_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_2-H-5.0_1x02_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_2-V-5.0-EX_1x02_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_3-H-5.0-EX_1x03_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_3-H-5.0_1x03_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_3-V-5.0-EX_1x03_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_4-H-5.0-EX_1x04_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_4-H-5.0_1x04_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_4-V-5.0-EX_1x04_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_5-H-5.0-EX_1x05_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_5-H-5.0_1x05_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_5-V-5.0-EX_1x05_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_6-H-5.0-EX_1x06_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_6-H-5.0_1x06_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_6-V-5.0-EX_1x06_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_7-H-5.0-EX_1x07_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_7-H-5.0_1x07_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_7-V-5.0-EX_1x07_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_8-H-5.0-EX_1x08_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_8-H-5.0_1x08_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_8-V-5.0-EX_1x08_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_9-H-5.0-EX_1x09_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_9-H-5.0_1x09_P5.0mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_2.5_9-V-5.0-EX_1x09_P5.0mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_1-H-7.5_1x01_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_1-V-7.5_1x01_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_10-H-7.5-ZB_1x10_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_10-V-7.5-ZB_1x10_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_11-H-7.5-ZB_1x11_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_11-V-7.5-ZB_1x11_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_12-H-7.5-ZB_1x12_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_12-V-7.5-ZB_1x12_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_2-H-7.5-ZB_1x02_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_2-V-7.5_1x02_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_3-H-7.5-ZB_1x03_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_3-H-7.5_1x03_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_3-V-7.5-ZB_1x03_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_4-H-7.5-ZB_1x04_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_5-H-7.5-ZB_1x05_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_5-V-7.5-ZB_1x05_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_6-H-7.5-ZB_1x06_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_6-V-7.5-ZB_1x06_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_7-H-7.5-ZB_1x07_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_7-V-7.5-ZB_1x07_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_8-H-7.5-ZB_1x08_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_8-V-7.5-ZB_1x08_P7.5mm_Vertical +Connector_Phoenix_SPT:PhoenixContact_SPT_5_9-H-7.5-ZB_1x09_P7.5mm_Horizontal +Connector_Phoenix_SPT:PhoenixContact_SPT_5_9-V-7.5-ZB_1x09_P7.5mm_Vertical +Connector_Pin:Pin_D0.7mm_L6.5mm_W1.8mm_FlatFork +Connector_Pin:Pin_D0.9mm_L10.0mm_W2.4mm_FlatFork +Connector_Pin:Pin_D1.0mm_L10.0mm +Connector_Pin:Pin_D1.0mm_L10.0mm_LooseFit +Connector_Pin:Pin_D1.1mm_L10.2mm_W3.5mm_Flat +Connector_Pin:Pin_D1.1mm_L8.5mm_W2.5mm_FlatFork +Connector_Pin:Pin_D1.2mm_L10.2mm_W2.9mm_FlatFork +Connector_Pin:Pin_D1.2mm_L11.3mm_W3.0mm_Flat +Connector_Pin:Pin_D1.3mm_L10.0mm_W3.5mm_Flat +Connector_Pin:Pin_D1.3mm_L11.0mm +Connector_Pin:Pin_D1.3mm_L11.0mm_LooseFit +Connector_Pin:Pin_D1.3mm_L11.3mm_W2.8mm_Flat +Connector_Pin:Pin_D1.4mm_L8.5mm_W2.8mm_FlatFork +Connector_PinHeader_1.00mm:PinHeader_1x01_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x01_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x02_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x02_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x02_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x02_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x03_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x03_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x03_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x03_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x04_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x04_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x04_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x04_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x05_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x05_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x06_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x06_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x06_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x06_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x07_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x07_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x07_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x07_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x08_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x08_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x08_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x08_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x09_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x09_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x09_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x09_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x10_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x10_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x10_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x10_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x11_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x11_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x11_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x11_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x12_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x12_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x12_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x12_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x13_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x13_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x13_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x13_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x14_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x14_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x14_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x14_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x15_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x15_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x15_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x15_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x16_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x16_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x16_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x16_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x17_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x17_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x17_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x17_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x18_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x18_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x18_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x18_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x19_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x19_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x19_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x19_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x20_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x20_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x20_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x20_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x21_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x21_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x21_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x21_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x22_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x22_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x22_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x22_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x23_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x23_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x23_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x23_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x24_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x24_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x24_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x24_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x25_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x25_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x25_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x25_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x26_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x26_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x26_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x26_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x27_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x27_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x27_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x27_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x28_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x28_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x28_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x28_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x29_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x29_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x29_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x29_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x30_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x30_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x30_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x30_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x31_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x31_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x31_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x31_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x32_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x32_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x32_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x32_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x33_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x33_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x33_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x33_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x34_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x34_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x34_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x34_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x35_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x35_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x35_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x35_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x36_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x36_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x36_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x36_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x37_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x37_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x37_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x37_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x38_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x38_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x38_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x38_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x39_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x39_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x39_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x39_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_1x40_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_1x40_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_1x40_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.00mm:PinHeader_1x40_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.00mm:PinHeader_2x01_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x01_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x01_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x02_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x02_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x02_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x03_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x03_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x03_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x04_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x04_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x04_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x05_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x05_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x05_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x06_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x06_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x06_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x07_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x07_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x07_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x08_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x08_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x08_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x09_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x09_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x09_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x10_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x10_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x10_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x11_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x11_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x11_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x12_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x12_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x12_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x13_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x13_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x13_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x14_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x14_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x14_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x15_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x15_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x15_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x16_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x16_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x16_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x17_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x17_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x17_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x18_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x18_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x18_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x19_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x19_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x19_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x20_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x20_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x20_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x21_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x21_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x21_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x22_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x22_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x22_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x23_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x23_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x23_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x24_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x24_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x24_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x25_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x25_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x25_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x26_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x26_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x26_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x27_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x27_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x27_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x28_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x28_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x28_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x29_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x29_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x29_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x30_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x30_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x30_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x31_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x31_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x31_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x32_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x32_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x32_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x33_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x33_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x33_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x34_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x34_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x34_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x35_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x35_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x35_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x36_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x36_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x36_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x37_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x37_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x37_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x38_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x38_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x38_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x39_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x39_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x39_P1.00mm_Vertical_SMD +Connector_PinHeader_1.00mm:PinHeader_2x40_P1.00mm_Horizontal +Connector_PinHeader_1.00mm:PinHeader_2x40_P1.00mm_Vertical +Connector_PinHeader_1.00mm:PinHeader_2x40_P1.00mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_1x01_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x01_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x02_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x02_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x02_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x02_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x03_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x03_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x03_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x03_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x04_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x04_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x04_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x04_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x05_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x05_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x06_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x06_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x06_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x06_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x07_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x07_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x07_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x07_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x08_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x08_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x08_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x08_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x09_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x09_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x09_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x09_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x10_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x10_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x10_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x10_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x11_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x11_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x11_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x11_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x12_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x12_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x12_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x12_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x13_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x13_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x13_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x13_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x14_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x14_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x14_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x14_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x15_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x15_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x15_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x15_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x16_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x16_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x16_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x16_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x17_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x17_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x17_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x17_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x18_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x18_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x18_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x18_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x19_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x19_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x19_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x19_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x20_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x20_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x20_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x20_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x21_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x21_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x21_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x21_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x22_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x22_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x22_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x22_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x23_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x23_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x23_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x23_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x24_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x24_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x24_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x24_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x25_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x25_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x25_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x25_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x26_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x26_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x26_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x26_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x27_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x27_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x27_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x27_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x28_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x28_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x28_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x28_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x29_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x29_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x29_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x29_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x30_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x30_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x30_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x30_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x31_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x31_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x31_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x31_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x32_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x32_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x32_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x32_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x33_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x33_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x33_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x33_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x34_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x34_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x34_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x34_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x35_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x35_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x35_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x35_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x36_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x36_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x36_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x36_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x37_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x37_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x37_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x37_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x38_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x38_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x38_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x38_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x39_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x39_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x39_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x39_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_1x40_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_1x40_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_1x40_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinHeader_1.27mm:PinHeader_1x40_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinHeader_1.27mm:PinHeader_2x01_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x01_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x01_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x02_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x02_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x02_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x03_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x03_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x03_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x04_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x04_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x04_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x05_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x05_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x05_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x06_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x06_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x06_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x07_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x07_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x07_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x08_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x08_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x08_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x09_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x09_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x09_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x10_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x10_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x10_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x11_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x11_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x11_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x12_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x12_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x12_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x13_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x13_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x13_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x14_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x14_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x14_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x15_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x15_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x15_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x16_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x16_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x16_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x17_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x17_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x17_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x18_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x18_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x18_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x19_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x19_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x19_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x20_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x20_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x20_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x21_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x21_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x21_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x22_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x22_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x22_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x23_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x23_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x23_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x24_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x24_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x24_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x25_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x25_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x25_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x26_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x26_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x26_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x27_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x27_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x27_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x28_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x28_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x28_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x29_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x29_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x29_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x30_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x30_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x30_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x31_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x31_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x31_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x32_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x32_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x32_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x33_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x33_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x33_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x34_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x34_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x34_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x35_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x35_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x35_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x36_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x36_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x36_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x37_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x37_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x37_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x38_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x38_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x38_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x39_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x39_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x39_P1.27mm_Vertical_SMD +Connector_PinHeader_1.27mm:PinHeader_2x40_P1.27mm_Horizontal +Connector_PinHeader_1.27mm:PinHeader_2x40_P1.27mm_Vertical +Connector_PinHeader_1.27mm:PinHeader_2x40_P1.27mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_1x01_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x01_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x02_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x02_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x02_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x02_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x03_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x03_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x03_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x03_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x04_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x04_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x04_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x04_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x05_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x05_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x06_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x06_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x06_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x06_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x07_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x07_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x07_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x07_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x08_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x08_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x08_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x08_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x09_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x09_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x09_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x09_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x10_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x10_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x10_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x10_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x11_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x11_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x11_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x11_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x12_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x12_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x12_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x12_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x13_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x13_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x13_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x13_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x14_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x14_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x14_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x14_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x15_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x15_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x15_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x15_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x16_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x16_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x16_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x16_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x17_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x17_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x17_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x17_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x18_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x18_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x18_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x18_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x19_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x19_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x19_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x19_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x20_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x20_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x20_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x20_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x21_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x21_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x21_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x21_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x22_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x22_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x22_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x22_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x23_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x23_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x23_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x23_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x24_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x24_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x24_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x24_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x25_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x25_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x25_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x25_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x26_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x26_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x26_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x26_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x27_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x27_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x27_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x27_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x28_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x28_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x28_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x28_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x29_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x29_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x29_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x29_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x30_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x30_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x30_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x30_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x31_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x31_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x31_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x31_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x32_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x32_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x32_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x32_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x33_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x33_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x33_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x33_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x34_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x34_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x34_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x34_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x35_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x35_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x35_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x35_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x36_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x36_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x36_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x36_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x37_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x37_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x37_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x37_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x38_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x38_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x38_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x38_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x39_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x39_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x39_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x39_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_1x40_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_1x40_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_1x40_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.00mm:PinHeader_1x40_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.00mm:PinHeader_2x01_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x01_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x01_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x02_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x02_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x02_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x03_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x03_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x03_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x04_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x04_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x04_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x05_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x05_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x05_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x06_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x06_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x06_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x07_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x07_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x07_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x08_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x08_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x08_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x09_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x09_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x09_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x10_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x10_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x10_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x11_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x11_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x11_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x12_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x12_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x12_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x13_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x13_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x13_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x14_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x14_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x14_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x15_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x15_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x15_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x16_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x16_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x16_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x17_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x17_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x17_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x18_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x18_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x18_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x19_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x19_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x19_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x20_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x20_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x20_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x21_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x21_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x21_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x22_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x22_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x22_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x23_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x23_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x23_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x24_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x24_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x24_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x25_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x25_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x25_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x26_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x26_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x26_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x27_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x27_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x27_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x28_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x28_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x28_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x29_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x29_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x29_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x30_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x30_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x30_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x31_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x31_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x31_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x32_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x32_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x32_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x33_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x33_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x33_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x34_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x34_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x34_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x35_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x35_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x35_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x36_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x36_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x36_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x37_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x37_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x37_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x38_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x38_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x38_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x39_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x39_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x39_P2.00mm_Vertical_SMD +Connector_PinHeader_2.00mm:PinHeader_2x40_P2.00mm_Horizontal +Connector_PinHeader_2.00mm:PinHeader_2x40_P2.00mm_Vertical +Connector_PinHeader_2.00mm:PinHeader_2x40_P2.00mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_1x01_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x01_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x07_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x07_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x07_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x07_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x09_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x09_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x09_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x09_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x11_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x11_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x11_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x11_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x12_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x12_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x12_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x12_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x13_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x13_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x13_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x13_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x14_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x14_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x14_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x14_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x15_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x15_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x15_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x15_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x16_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x16_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x16_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x16_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x17_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x17_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x17_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x17_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x18_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x18_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x18_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x18_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x19_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x19_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x19_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x19_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x20_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x20_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x20_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x20_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x21_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x21_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x21_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x21_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x22_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x22_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x22_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x22_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x23_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x23_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x23_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x23_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x24_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x24_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x24_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x24_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x25_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x25_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x25_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x25_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x26_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x26_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x26_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x26_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x27_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x27_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x27_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x27_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x28_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x28_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x28_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x28_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x29_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x29_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x29_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x29_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x30_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x30_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x30_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x30_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x31_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x31_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x31_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x31_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x32_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x32_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x32_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x32_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x33_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x33_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x33_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x33_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x34_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x34_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x34_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x34_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x35_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x35_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x35_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x35_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x36_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x36_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x36_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x36_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x37_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x37_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x37_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x37_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x38_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x38_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x38_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x38_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x39_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x39_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x39_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x39_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_1x40_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_1x40_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_1x40_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinHeader_2.54mm:PinHeader_1x40_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinHeader_2.54mm:PinHeader_2x01_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x01_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x01_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x05_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x05_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x05_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x06_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x06_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x06_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x07_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x07_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x07_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x08_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x08_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x08_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x09_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x09_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x09_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x10_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x10_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x10_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x11_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x11_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x11_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x12_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x12_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x12_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x13_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x13_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x13_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x14_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x14_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x14_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x15_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x15_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x15_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x16_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x16_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x16_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x17_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x17_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x17_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x18_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x18_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x18_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x19_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x19_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x19_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x21_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x21_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x21_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x22_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x22_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x22_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x23_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x23_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x23_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x24_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x24_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x24_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x25_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x25_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x25_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x26_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x26_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x26_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x27_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x27_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x27_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x28_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x28_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x28_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x29_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x29_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x29_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x30_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x30_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x30_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x31_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x31_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x31_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x32_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x32_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x32_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x33_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x33_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x33_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x34_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x34_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x34_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x35_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x35_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x35_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x36_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x36_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x36_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x37_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x37_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x37_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x38_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x38_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x38_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x39_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x39_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x39_P2.54mm_Vertical_SMD +Connector_PinHeader_2.54mm:PinHeader_2x40_P2.54mm_Horizontal +Connector_PinHeader_2.54mm:PinHeader_2x40_P2.54mm_Vertical +Connector_PinHeader_2.54mm:PinHeader_2x40_P2.54mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_1x02_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x02_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x02_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x03_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x03_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x03_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x04_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x04_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x04_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x05_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x05_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x05_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x06_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x06_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x06_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x07_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x07_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x07_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x08_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x08_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x08_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x09_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x09_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x09_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x10_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x10_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x10_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x11_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x11_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x11_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x12_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x12_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x12_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x13_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x13_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x13_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x14_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x14_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x14_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x15_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x15_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x15_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x16_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x16_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x16_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x17_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x17_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x17_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x18_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x18_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x18_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x19_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x19_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x19_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x20_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x20_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x20_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x21_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x21_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x21_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x22_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x22_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x22_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x23_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x23_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x23_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x24_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x24_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x24_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x25_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x25_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x25_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x26_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x26_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x26_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x27_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x27_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x27_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x28_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x28_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x28_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x29_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x29_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x29_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x30_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x30_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x30_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x31_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x31_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x31_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x32_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x32_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x32_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x33_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x33_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x33_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x34_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x34_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x34_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x35_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x35_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x35_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x36_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x36_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x36_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x37_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x37_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x37_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x38_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x38_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x38_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x39_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x39_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x39_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_1x40_P1.00mm_Vertical +Connector_PinSocket_1.00mm:PinSocket_1x40_P1.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.00mm:PinSocket_1x40_P1.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.00mm:PinSocket_2x02_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x03_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x04_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x05_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x06_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x07_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x08_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x09_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x10_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x11_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x12_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x13_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x14_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x15_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x16_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x17_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x18_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x19_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x20_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x21_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x22_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x23_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x24_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x25_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x26_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x27_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x28_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x29_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x30_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x31_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x32_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x33_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x34_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x35_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x36_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x37_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x38_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x39_P1.00mm_Vertical_SMD +Connector_PinSocket_1.00mm:PinSocket_2x40_P1.00mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_1x01_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x02_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x02_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x02_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x03_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x03_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x03_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x04_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x04_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x04_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x05_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x05_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x05_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x06_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x06_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x06_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x07_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x07_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x07_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x08_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x08_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x08_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x09_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x09_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x09_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x10_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x10_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x10_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x11_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x11_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x11_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x12_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x12_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x12_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x13_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x13_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x13_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x14_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x14_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x14_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x15_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x15_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x15_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x16_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x16_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x16_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x17_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x17_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x17_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x18_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x18_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x18_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x19_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x19_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x19_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x20_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x20_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x20_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x21_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x21_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x21_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x22_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x22_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x22_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x23_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x23_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x23_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x24_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x24_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x24_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x25_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x25_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x25_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x26_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x26_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x26_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x27_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x27_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x27_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x28_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x28_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x28_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x29_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x29_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x29_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x30_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x30_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x30_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x31_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x31_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x31_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x32_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x32_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x32_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x33_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x33_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x33_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x34_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x34_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x34_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x35_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x35_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x35_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x36_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x36_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x36_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x37_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x37_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x37_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x38_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x38_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x38_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x39_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x39_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x39_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_1x40_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_1x40_P1.27mm_Vertical_SMD_Pin1Left +Connector_PinSocket_1.27mm:PinSocket_1x40_P1.27mm_Vertical_SMD_Pin1Right +Connector_PinSocket_1.27mm:PinSocket_2x01_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x01_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x02_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x02_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x03_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x03_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x03_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x04_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x04_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x04_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x05_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x05_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x05_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x06_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x06_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x06_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x07_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x07_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x07_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x08_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x08_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x08_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x09_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x09_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x09_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x10_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x10_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x10_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x11_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x11_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x11_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x12_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x12_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x12_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x13_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x13_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x13_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x14_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x14_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x14_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x15_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x15_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x15_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x16_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x16_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x16_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x17_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x17_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x17_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x18_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x18_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x18_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x19_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x19_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x19_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x20_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x20_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x20_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x21_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x21_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x21_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x22_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x22_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x22_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x23_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x23_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x23_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x24_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x24_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x24_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x25_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x25_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x25_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x26_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x26_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x26_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x27_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x27_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x27_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x28_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x28_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x28_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x29_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x29_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x29_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x30_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x30_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x30_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x31_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x31_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x31_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x32_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x32_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x32_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x33_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x33_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x33_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x34_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x34_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x34_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x35_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x35_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x35_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x36_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x36_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x36_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x37_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x37_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x37_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x38_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x38_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x38_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x39_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x39_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x39_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x40_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x40_P1.27mm_Vertical +Connector_PinSocket_1.27mm:PinSocket_2x40_P1.27mm_Vertical_SMD +Connector_PinSocket_1.27mm:PinSocket_2x41_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x42_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x43_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x44_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x45_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x46_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x47_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x48_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x49_P1.27mm_Horizontal +Connector_PinSocket_1.27mm:PinSocket_2x50_P1.27mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x01_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x01_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x02_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x02_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x02_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x02_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x03_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x03_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x03_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x03_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x04_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x04_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x04_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x04_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x05_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x05_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x05_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x05_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x06_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x06_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x06_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x06_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x07_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x07_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x07_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x07_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x08_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x08_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x08_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x08_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x09_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x09_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x09_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x09_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x10_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x10_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x10_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x10_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x11_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x11_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x11_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x11_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x12_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x12_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x12_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x12_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x13_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x13_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x13_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x13_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x14_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x14_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x14_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x14_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x15_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x15_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x15_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x15_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x16_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x16_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x16_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x16_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x17_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x17_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x17_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x17_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x18_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x18_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x18_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x18_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x19_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x19_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x19_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x19_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x20_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x20_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x20_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x20_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x21_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x21_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x21_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x21_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x22_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x22_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x22_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x22_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x23_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x23_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x23_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x23_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x24_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x24_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x24_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x24_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x25_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x25_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x25_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x25_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x26_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x26_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x26_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x26_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x27_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x27_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x27_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x27_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x28_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x28_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x28_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x28_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x29_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x29_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x29_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x29_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x30_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x30_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x30_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x30_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x31_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x31_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x31_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x31_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x32_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x32_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x32_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x32_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x33_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x33_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x33_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x33_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x34_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x34_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x34_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x34_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x35_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x35_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x35_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x35_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x36_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x36_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x36_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x36_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x37_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x37_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x37_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x37_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x38_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x38_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x38_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x38_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x39_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x39_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x39_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x39_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_1x40_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_1x40_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_1x40_P2.00mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.00mm:PinSocket_1x40_P2.00mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.00mm:PinSocket_2x01_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x01_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x01_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x02_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x02_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x02_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x03_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x03_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x03_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x04_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x04_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x04_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x05_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x05_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x05_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x06_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x06_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x06_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x07_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x07_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x07_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x08_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x08_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x08_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x09_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x09_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x09_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x10_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x10_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x10_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x11_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x11_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x11_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x12_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x12_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x12_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x13_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x13_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x13_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x14_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x14_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x14_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x15_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x15_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x15_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x16_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x16_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x16_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x17_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x17_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x17_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x18_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x18_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x18_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x19_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x19_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x19_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x20_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x20_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x20_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x21_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x21_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x21_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x22_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x22_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x22_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x23_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x23_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x23_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x24_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x24_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x24_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x25_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x25_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x25_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x26_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x26_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x26_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x27_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x27_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x27_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x28_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x28_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x28_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x29_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x29_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x29_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x30_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x30_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x30_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x31_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x31_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x31_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x32_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x32_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x32_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x33_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x33_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x33_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x34_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x34_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x34_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x35_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x35_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x35_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x36_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x36_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x36_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x37_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x37_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x37_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x38_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x38_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x38_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x39_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x39_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x39_P2.00mm_Vertical_SMD +Connector_PinSocket_2.00mm:PinSocket_2x40_P2.00mm_Horizontal +Connector_PinSocket_2.00mm:PinSocket_2x40_P2.00mm_Vertical +Connector_PinSocket_2.00mm:PinSocket_2x40_P2.00mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_1x01_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x01_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x03_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x03_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x03_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x03_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x04_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x04_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x04_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x04_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x05_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x05_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x05_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x05_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x06_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x06_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x06_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x06_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x07_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x07_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x07_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x07_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x08_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x08_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x08_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x08_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x09_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x09_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x09_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x09_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x10_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x10_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x10_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x10_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x11_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x11_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x11_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x11_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x12_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x12_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x12_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x12_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x13_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x13_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x13_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x13_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x14_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x14_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x14_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x14_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x15_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x15_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x15_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x15_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x16_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x16_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x16_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x16_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x17_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x17_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x17_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x17_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x18_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x18_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x18_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x18_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x19_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x19_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x19_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x19_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x20_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x20_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x20_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x20_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x21_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x21_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x21_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x21_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x22_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x22_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x22_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x22_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x23_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x23_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x23_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x23_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x24_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x24_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x24_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x24_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x25_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x25_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x25_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x25_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x26_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x26_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x26_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x26_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x27_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x27_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x27_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x27_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x28_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x28_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x28_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x28_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x29_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x29_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x29_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x29_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x30_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x30_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x30_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x30_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x31_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x31_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x31_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x31_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x32_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x32_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x32_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x32_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x33_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x33_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x33_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x33_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x34_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x34_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x34_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x34_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x35_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x35_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x35_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x35_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x36_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x36_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x36_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x36_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x37_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x37_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x37_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x37_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x38_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x38_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x38_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x38_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x39_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x39_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x39_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x39_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_1x40_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_1x40_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_1x40_P2.54mm_Vertical_SMD_Pin1Left +Connector_PinSocket_2.54mm:PinSocket_1x40_P2.54mm_Vertical_SMD_Pin1Right +Connector_PinSocket_2.54mm:PinSocket_2x01_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x01_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x01_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x02_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x02_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x02_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x03_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x03_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x03_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x04_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x04_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x04_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x05_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x05_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x05_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x06_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x06_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x06_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x07_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x07_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x07_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x08_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x08_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x08_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x09_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x09_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x09_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x10_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x10_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x10_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x11_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x11_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x11_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x12_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x12_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x12_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x13_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x13_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x13_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x14_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x14_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x14_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x15_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x15_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x15_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x16_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x16_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x16_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x17_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x17_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x17_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x18_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x18_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x18_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x19_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x19_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x19_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x20_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x20_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x20_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x21_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x21_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x21_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x22_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x22_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x22_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x23_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x23_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x23_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x24_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x24_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x24_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x25_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x25_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x25_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x26_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x26_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x26_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x27_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x27_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x27_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x28_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x28_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x28_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x29_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x29_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x29_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x30_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x30_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x30_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x31_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x31_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x31_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x32_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x32_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x32_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x33_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x33_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x33_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x34_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x34_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x34_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x35_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x35_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x35_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x36_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x36_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x36_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x37_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x37_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x37_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x38_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x38_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x38_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x39_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x39_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x39_P2.54mm_Vertical_SMD +Connector_PinSocket_2.54mm:PinSocket_2x40_P2.54mm_Horizontal +Connector_PinSocket_2.54mm:PinSocket_2x40_P2.54mm_Vertical +Connector_PinSocket_2.54mm:PinSocket_2x40_P2.54mm_Vertical_SMD +Connector_RJ:RJ12_Amphenol_54601-x06_Horizontal +Connector_RJ:RJ14_Connfly_DS1133-S4_Horizontal +Connector_RJ:RJ25_Wayconn_MJEA-660X1_Horizontal +Connector_RJ:RJ45_Abracon_ARJP11A-MA_Horizontal +Connector_RJ:RJ45_Amphenol_54602-x08_Horizontal +Connector_RJ:RJ45_Amphenol_RJHSE5380-08 +Connector_RJ:RJ45_Amphenol_RJHSE5380 +Connector_RJ:RJ45_Amphenol_RJHSE538X-02 +Connector_RJ:RJ45_Amphenol_RJHSE538X-04 +Connector_RJ:RJ45_Amphenol_RJHSE538X +Connector_RJ:RJ45_Amphenol_RJMG1BD3B8K1ANR +Connector_RJ:RJ45_Bel_SI-60062-F +Connector_RJ:RJ45_BEL_SS74301-00x_Vertical +Connector_RJ:RJ45_Bel_V895-1001-AW_Vertical +Connector_RJ:RJ45_Cetus_J1B1211CCD_Horizontal +Connector_RJ:RJ45_Connfly_DS1128-09-S8xx-S_Horizontal +Connector_RJ:RJ45_HALO_HFJ11-x2450E-LxxRL_Horizontal +Connector_RJ:RJ45_HALO_HFJ11-x2450ERL_Horizontal +Connector_RJ:RJ45_HALO_HFJ11-x2450HRL_Horizontal +Connector_RJ:RJ45_Hanrun_HR911105A_Horizontal +Connector_RJ:RJ45_Kycon_G7LX-A88S7-BP-xx_Horizontal +Connector_RJ:RJ45_Molex_0855135013_Vertical +Connector_RJ:RJ45_Molex_9346520x_Horizontal +Connector_RJ:RJ45_Ninigi_GE +Connector_RJ:RJ45_OST_PJ012-8P8CX_Vertical +Connector_RJ:RJ45_Plug_Metz_AJP92A8813 +Connector_RJ:RJ45_Pulse_JK00177NL_Horizontal +Connector_RJ:RJ45_Pulse_JK0654219NL_Horizontal +Connector_RJ:RJ45_Pulse_JXD6-0001NL_Horizontal +Connector_RJ:RJ45_RCH_RC01937 +Connector_RJ:RJ45_UDE_RB1-125B8G1A +Connector_RJ:RJ45_Wuerth_74980111211_Horizontal +Connector_RJ:RJ45_Wuerth_7499010001A_Horizontal +Connector_RJ:RJ45_Wuerth_7499010121A_Horizontal +Connector_RJ:RJ45_Wuerth_7499010211A_Horizontal +Connector_RJ:RJ45_Wuerth_7499111446_Horizontal +Connector_RJ:RJ45_Wuerth_7499151120_Horizontal +Connector_RJ:RJ9_Evercom_5301-440xxx_Horizontal +Connector_Samtec:Samtec_FMC_ASP-134486-01_10x40_P1.27mm_Vertical +Connector_Samtec:Samtec_FMC_ASP-134602-01_10x40_P1.27mm_Vertical +Connector_Samtec:Samtec_FMC_ASP-134604-01_4x40_Vertical +Connector_Samtec:Samtec_LSHM-105-xx.x-x-DV-N_2x05_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-105-xx.x-x-DV-S_2x05-1SH_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-110-xx.x-x-DV-N_2x10_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-110-xx.x-x-DV-S_2x10-1SH_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-120-xx.x-x-DV-N_2x20_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-120-xx.x-x-DV-S_2x20-1SH_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-130-xx.x-x-DV-N_2x30_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-130-xx.x-x-DV-S_2x30-1SH_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-140-xx.x-x-DV-N_2x40_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-140-xx.x-x-DV-S_2x40-1SH_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-150-xx.x-x-DV-N_2x50_P0.50mm_Vertical +Connector_Samtec:Samtec_LSHM-150-xx.x-x-DV-S_2x50-1SH_P0.50mm_Vertical +Connector_Samtec_HLE_SMD:Samtec_HLE-102-02-xxx-DV-BE-LC_2x02_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-102-02-xxx-DV-BE_2x02_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-102-02-xxx-DV-LC_2x02_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-102-02-xxx-DV_2x02_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-103-02-xxx-DV-BE-LC_2x03_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-103-02-xxx-DV-BE_2x03_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-103-02-xxx-DV-LC_2x03_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-103-02-xxx-DV_2x03_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-104-02-xxx-DV-A_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-104-02-xxx-DV-BE-A_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-104-02-xxx-DV-BE-LC_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-104-02-xxx-DV-BE_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-104-02-xxx-DV-LC_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-104-02-xxx-DV_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-105-02-xxx-DV-A_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-105-02-xxx-DV-BE-A_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-105-02-xxx-DV-BE-LC_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-105-02-xxx-DV-BE_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-105-02-xxx-DV-LC_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-105-02-xxx-DV_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-106-02-xxx-DV-A_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-106-02-xxx-DV-BE-A_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-106-02-xxx-DV-BE-LC_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-106-02-xxx-DV-BE_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-106-02-xxx-DV-LC_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-106-02-xxx-DV_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-107-02-xxx-DV-A_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-107-02-xxx-DV-BE-A_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-107-02-xxx-DV-BE-LC_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-107-02-xxx-DV-BE_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-107-02-xxx-DV-LC_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-107-02-xxx-DV_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-108-02-xxx-DV-A_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-108-02-xxx-DV-BE-A_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-108-02-xxx-DV-BE-LC_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-108-02-xxx-DV-BE_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-108-02-xxx-DV-LC_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-108-02-xxx-DV_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-109-02-xxx-DV-A_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-109-02-xxx-DV-BE-A_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-109-02-xxx-DV-BE-LC_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-109-02-xxx-DV-BE_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-109-02-xxx-DV-LC_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-109-02-xxx-DV_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-110-02-xxx-DV-A_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-110-02-xxx-DV-BE-A_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-110-02-xxx-DV-BE-LC_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-110-02-xxx-DV-BE_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-110-02-xxx-DV-LC_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-110-02-xxx-DV_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-111-02-xxx-DV-A_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-111-02-xxx-DV-BE-A_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-111-02-xxx-DV-BE-LC_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-111-02-xxx-DV-BE_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-111-02-xxx-DV-LC_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-111-02-xxx-DV_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-112-02-xxx-DV-A_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-112-02-xxx-DV-BE-A_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-112-02-xxx-DV-BE-LC_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-112-02-xxx-DV-BE_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-112-02-xxx-DV-LC_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-112-02-xxx-DV_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-113-02-xxx-DV-A_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-113-02-xxx-DV-BE-A_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-113-02-xxx-DV-BE-LC_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-113-02-xxx-DV-BE_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-113-02-xxx-DV-LC_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-113-02-xxx-DV_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-114-02-xxx-DV-A_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-114-02-xxx-DV-BE-A_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-114-02-xxx-DV-BE-LC_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-114-02-xxx-DV-BE_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-114-02-xxx-DV-LC_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-114-02-xxx-DV_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-115-02-xxx-DV-A_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-115-02-xxx-DV-BE-A_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-115-02-xxx-DV-BE-LC_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-115-02-xxx-DV-BE_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-115-02-xxx-DV-LC_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-115-02-xxx-DV_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-116-02-xxx-DV-A_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-116-02-xxx-DV-BE-A_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-116-02-xxx-DV-BE-LC_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-116-02-xxx-DV-BE_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-116-02-xxx-DV-LC_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-116-02-xxx-DV_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-117-02-xxx-DV-A_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-117-02-xxx-DV-BE-A_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-117-02-xxx-DV-BE-LC_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-117-02-xxx-DV-BE_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-117-02-xxx-DV-LC_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-117-02-xxx-DV_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-118-02-xxx-DV-A_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-118-02-xxx-DV-BE-A_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-118-02-xxx-DV-BE-LC_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-118-02-xxx-DV-BE_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-118-02-xxx-DV-LC_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-118-02-xxx-DV_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-119-02-xxx-DV-A_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-119-02-xxx-DV-BE-A_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-119-02-xxx-DV-BE-LC_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-119-02-xxx-DV-BE_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-119-02-xxx-DV-LC_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-119-02-xxx-DV_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-120-02-xxx-DV-A_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-120-02-xxx-DV-BE-A_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-120-02-xxx-DV-BE-LC_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-120-02-xxx-DV-BE_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-120-02-xxx-DV-LC_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-120-02-xxx-DV_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-121-02-xxx-DV-A_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-121-02-xxx-DV-BE-A_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-121-02-xxx-DV-BE-LC_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-121-02-xxx-DV-BE_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-121-02-xxx-DV-LC_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-121-02-xxx-DV_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-122-02-xxx-DV-A_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-122-02-xxx-DV-BE-A_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-122-02-xxx-DV-BE-LC_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-122-02-xxx-DV-BE_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-122-02-xxx-DV-LC_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-122-02-xxx-DV_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-123-02-xxx-DV-A_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-123-02-xxx-DV-BE-A_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-123-02-xxx-DV-BE-LC_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-123-02-xxx-DV-BE_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-123-02-xxx-DV-LC_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-123-02-xxx-DV_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-124-02-xxx-DV-A_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-124-02-xxx-DV-BE-A_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-124-02-xxx-DV-BE-LC_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-124-02-xxx-DV-BE_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-124-02-xxx-DV-LC_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-124-02-xxx-DV_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-125-02-xxx-DV-A_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-125-02-xxx-DV-BE-A_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-125-02-xxx-DV-BE-LC_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-125-02-xxx-DV-BE_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-125-02-xxx-DV-LC_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-125-02-xxx-DV_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-126-02-xxx-DV-A_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-126-02-xxx-DV-BE-A_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-126-02-xxx-DV-BE-LC_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-126-02-xxx-DV-BE_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-126-02-xxx-DV-LC_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-126-02-xxx-DV_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-127-02-xxx-DV-A_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-127-02-xxx-DV-BE-A_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-127-02-xxx-DV-BE-LC_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-127-02-xxx-DV-BE_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-127-02-xxx-DV-LC_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-127-02-xxx-DV_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-128-02-xxx-DV-A_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-128-02-xxx-DV-BE-A_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-128-02-xxx-DV-BE-LC_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-128-02-xxx-DV-BE_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-128-02-xxx-DV-LC_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-128-02-xxx-DV_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-129-02-xxx-DV-A_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-129-02-xxx-DV-BE-A_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-129-02-xxx-DV-BE-LC_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-129-02-xxx-DV-BE_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-129-02-xxx-DV-LC_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-129-02-xxx-DV_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-130-02-xxx-DV-A_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-130-02-xxx-DV-BE-A_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-130-02-xxx-DV-BE-LC_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-130-02-xxx-DV-BE_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-130-02-xxx-DV-LC_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-130-02-xxx-DV_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-131-02-xxx-DV-A_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-131-02-xxx-DV-BE-A_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-131-02-xxx-DV-BE-LC_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-131-02-xxx-DV-BE_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-131-02-xxx-DV-LC_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-131-02-xxx-DV_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-132-02-xxx-DV-A_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-132-02-xxx-DV-BE-A_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-132-02-xxx-DV-BE-LC_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-132-02-xxx-DV-BE_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-132-02-xxx-DV-LC_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-132-02-xxx-DV_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-133-02-xxx-DV-A_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-133-02-xxx-DV-BE-A_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-133-02-xxx-DV-BE-LC_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-133-02-xxx-DV-BE_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-133-02-xxx-DV-LC_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-133-02-xxx-DV_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-134-02-xxx-DV-A_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-134-02-xxx-DV-BE-A_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-134-02-xxx-DV-BE-LC_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-134-02-xxx-DV-BE_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-134-02-xxx-DV-LC_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-134-02-xxx-DV_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-135-02-xxx-DV-A_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-135-02-xxx-DV-BE-A_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-135-02-xxx-DV-BE-LC_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-135-02-xxx-DV-BE_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-135-02-xxx-DV-LC_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-135-02-xxx-DV_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-136-02-xxx-DV-A_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-136-02-xxx-DV-BE-A_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-136-02-xxx-DV-BE-LC_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-136-02-xxx-DV-BE_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-136-02-xxx-DV-LC_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-136-02-xxx-DV_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-137-02-xxx-DV-A_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-137-02-xxx-DV-BE-A_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-137-02-xxx-DV-BE-LC_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-137-02-xxx-DV-BE_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-137-02-xxx-DV-LC_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-137-02-xxx-DV_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-138-02-xxx-DV-A_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-138-02-xxx-DV-BE-A_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-138-02-xxx-DV-BE-LC_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-138-02-xxx-DV-BE_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-138-02-xxx-DV-LC_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-138-02-xxx-DV_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-139-02-xxx-DV-A_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-139-02-xxx-DV-BE-A_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-139-02-xxx-DV-BE-LC_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-139-02-xxx-DV-BE_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-139-02-xxx-DV-LC_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-139-02-xxx-DV_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-140-02-xxx-DV-A_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-140-02-xxx-DV-BE-A_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-140-02-xxx-DV-BE-LC_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-140-02-xxx-DV-BE_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-140-02-xxx-DV-LC_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-140-02-xxx-DV_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-141-02-xxx-DV-A_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-141-02-xxx-DV-BE-A_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-141-02-xxx-DV-BE-LC_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-141-02-xxx-DV-BE_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-141-02-xxx-DV-LC_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-141-02-xxx-DV_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-142-02-xxx-DV-A_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-142-02-xxx-DV-BE-A_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-142-02-xxx-DV-BE-LC_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-142-02-xxx-DV-BE_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-142-02-xxx-DV-LC_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-142-02-xxx-DV_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-143-02-xxx-DV-A_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-143-02-xxx-DV-BE-A_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-143-02-xxx-DV-BE-LC_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-143-02-xxx-DV-BE_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-143-02-xxx-DV-LC_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-143-02-xxx-DV_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-144-02-xxx-DV-A_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-144-02-xxx-DV-BE-A_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-144-02-xxx-DV-BE-LC_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-144-02-xxx-DV-BE_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-144-02-xxx-DV-LC_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-144-02-xxx-DV_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-145-02-xxx-DV-A_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-145-02-xxx-DV-BE-A_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-145-02-xxx-DV-BE-LC_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-145-02-xxx-DV-BE_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-145-02-xxx-DV-LC_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-145-02-xxx-DV_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-146-02-xxx-DV-A_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-146-02-xxx-DV-BE-A_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-146-02-xxx-DV-BE-LC_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-146-02-xxx-DV-BE_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-146-02-xxx-DV-LC_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-146-02-xxx-DV_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-147-02-xxx-DV-A_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-147-02-xxx-DV-BE-A_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-147-02-xxx-DV-BE-LC_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-147-02-xxx-DV-BE_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-147-02-xxx-DV-LC_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-147-02-xxx-DV_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-148-02-xxx-DV-A_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-148-02-xxx-DV-BE-A_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-148-02-xxx-DV-BE-LC_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-148-02-xxx-DV-BE_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-148-02-xxx-DV-LC_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-148-02-xxx-DV_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-149-02-xxx-DV-A_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-149-02-xxx-DV-BE-A_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-149-02-xxx-DV-BE-LC_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-149-02-xxx-DV-BE_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-149-02-xxx-DV-LC_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-149-02-xxx-DV_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-150-02-xxx-DV-A_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-150-02-xxx-DV-BE-A_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-150-02-xxx-DV-BE-LC_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-150-02-xxx-DV-BE_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-150-02-xxx-DV-LC_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_SMD:Samtec_HLE-150-02-xxx-DV_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-104-02-xx-DV-PE-LC_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-104-02-xx-DV-PE_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-104-02-xx-DV-TE_2x04_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-105-02-xx-DV-PE-LC_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-105-02-xx-DV-PE_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-105-02-xx-DV-TE_2x05_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-106-02-xx-DV-PE-LC_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-106-02-xx-DV-PE_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-106-02-xx-DV-TE_2x06_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-107-02-xx-DV-PE-LC_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-107-02-xx-DV-PE_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-107-02-xx-DV-TE_2x07_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-108-02-xx-DV-PE-LC_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-108-02-xx-DV-PE_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-108-02-xx-DV-TE_2x08_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-109-02-xx-DV-PE-LC_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-109-02-xx-DV-PE_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-109-02-xx-DV-TE_2x09_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-110-02-xx-DV-PE-LC_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-110-02-xx-DV-PE_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-110-02-xx-DV-TE_2x10_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-111-02-xx-DV-PE-LC_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-111-02-xx-DV-PE_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-111-02-xx-DV-TE_2x11_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-112-02-xx-DV-PE-LC_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-112-02-xx-DV-PE_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-112-02-xx-DV-TE_2x12_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-113-02-xx-DV-PE-LC_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-113-02-xx-DV-PE_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-113-02-xx-DV-TE_2x13_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-114-02-xx-DV-PE-LC_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-114-02-xx-DV-PE_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-114-02-xx-DV-TE_2x14_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-115-02-xx-DV-PE-LC_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-115-02-xx-DV-PE_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-115-02-xx-DV-TE_2x15_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-116-02-xx-DV-PE-LC_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-116-02-xx-DV-PE_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-116-02-xx-DV-TE_2x16_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-117-02-xx-DV-PE-LC_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-117-02-xx-DV-PE_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-117-02-xx-DV-TE_2x17_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-118-02-xx-DV-PE-LC_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-118-02-xx-DV-PE_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-118-02-xx-DV-TE_2x18_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-119-02-xx-DV-PE-LC_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-119-02-xx-DV-PE_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-119-02-xx-DV-TE_2x19_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-120-02-xx-DV-PE-LC_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-120-02-xx-DV-PE_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-120-02-xx-DV-TE_2x20_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-121-02-xx-DV-PE-LC_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-121-02-xx-DV-PE_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-121-02-xx-DV-TE_2x21_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-122-02-xx-DV-PE-LC_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-122-02-xx-DV-PE_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-122-02-xx-DV-TE_2x22_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-123-02-xx-DV-PE-LC_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-123-02-xx-DV-PE_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-123-02-xx-DV-TE_2x23_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-124-02-xx-DV-PE-LC_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-124-02-xx-DV-PE_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-124-02-xx-DV-TE_2x24_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-125-02-xx-DV-PE-LC_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-125-02-xx-DV-PE_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-125-02-xx-DV-TE_2x25_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-126-02-xx-DV-PE-LC_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-126-02-xx-DV-PE_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-126-02-xx-DV-TE_2x26_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-127-02-xx-DV-PE-LC_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-127-02-xx-DV-PE_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-127-02-xx-DV-TE_2x27_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-128-02-xx-DV-PE-LC_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-128-02-xx-DV-PE_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-128-02-xx-DV-TE_2x28_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-129-02-xx-DV-PE-LC_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-129-02-xx-DV-PE_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-129-02-xx-DV-TE_2x29_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-130-02-xx-DV-PE-LC_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-130-02-xx-DV-PE_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-130-02-xx-DV-TE_2x30_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-131-02-xx-DV-PE-LC_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-131-02-xx-DV-PE_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-131-02-xx-DV-TE_2x31_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-132-02-xx-DV-PE-LC_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-132-02-xx-DV-PE_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-132-02-xx-DV-TE_2x32_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-133-02-xx-DV-PE-LC_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-133-02-xx-DV-PE_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-133-02-xx-DV-TE_2x33_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-134-02-xx-DV-PE-LC_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-134-02-xx-DV-PE_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-134-02-xx-DV-TE_2x34_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-135-02-xx-DV-PE-LC_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-135-02-xx-DV-PE_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-135-02-xx-DV-TE_2x35_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-136-02-xx-DV-PE-LC_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-136-02-xx-DV-PE_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-136-02-xx-DV-TE_2x36_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-137-02-xx-DV-PE-LC_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-137-02-xx-DV-PE_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-137-02-xx-DV-TE_2x37_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-138-02-xx-DV-PE-LC_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-138-02-xx-DV-PE_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-138-02-xx-DV-TE_2x38_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-139-02-xx-DV-PE-LC_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-139-02-xx-DV-PE_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-139-02-xx-DV-TE_2x39_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-140-02-xx-DV-PE-LC_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-140-02-xx-DV-PE_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-140-02-xx-DV-TE_2x40_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-141-02-xx-DV-PE-LC_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-141-02-xx-DV-PE_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-141-02-xx-DV-TE_2x41_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-142-02-xx-DV-PE-LC_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-142-02-xx-DV-PE_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-142-02-xx-DV-TE_2x42_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-143-02-xx-DV-PE-LC_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-143-02-xx-DV-PE_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-143-02-xx-DV-TE_2x43_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-144-02-xx-DV-PE-LC_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-144-02-xx-DV-PE_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-144-02-xx-DV-TE_2x44_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-145-02-xx-DV-PE-LC_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-145-02-xx-DV-PE_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-145-02-xx-DV-TE_2x45_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-146-02-xx-DV-PE-LC_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-146-02-xx-DV-PE_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-146-02-xx-DV-TE_2x46_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-147-02-xx-DV-PE-LC_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-147-02-xx-DV-PE_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-147-02-xx-DV-TE_2x47_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-148-02-xx-DV-PE-LC_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-148-02-xx-DV-PE_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-148-02-xx-DV-TE_2x48_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-149-02-xx-DV-PE-LC_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-149-02-xx-DV-PE_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-149-02-xx-DV-TE_2x49_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-150-02-xx-DV-PE-LC_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-150-02-xx-DV-PE_2x50_P2.54mm_Horizontal +Connector_Samtec_HLE_THT:Samtec_HLE-150-02-xx-DV-TE_2x50_P2.54mm_Horizontal +Connector_Samtec_HPM_THT:Samtec_HPM-01-01-x-S_Straight_1x01_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-01-05-x-S_Straight_1x01_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-02-01-x-S_Straight_1x02_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-02-05-x-S_Straight_1x02_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-03-01-x-S_Straight_1x03_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-03-05-x-S_Straight_1x03_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-04-01-x-S_Straight_1x04_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-04-05-x-S_Straight_1x04_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-05-01-x-S_Straight_1x05_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-05-05-x-S_Straight_1x05_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-06-01-x-S_Straight_1x06_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-06-05-x-S_Straight_1x06_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-07-01-x-S_Straight_1x07_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-07-05-x-S_Straight_1x07_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-08-01-x-S_Straight_1x08_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-08-05-x-S_Straight_1x08_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-09-01-x-S_Straight_1x09_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-09-05-x-S_Straight_1x09_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-10-01-x-S_Straight_1x10_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-10-05-x-S_Straight_1x10_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-11-01-x-S_Straight_1x11_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-11-05-x-S_Straight_1x11_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-12-01-x-S_Straight_1x12_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-12-05-x-S_Straight_1x12_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-13-01-x-S_Straight_1x13_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-13-05-x-S_Straight_1x13_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-14-01-x-S_Straight_1x14_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-14-05-x-S_Straight_1x14_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-15-01-x-S_Straight_1x15_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-15-05-x-S_Straight_1x15_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-16-01-x-S_Straight_1x16_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-16-05-x-S_Straight_1x16_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-17-01-x-S_Straight_1x17_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-17-05-x-S_Straight_1x17_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-18-01-x-S_Straight_1x18_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-18-05-x-S_Straight_1x18_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-19-01-x-S_Straight_1x19_Pitch5.08mm +Connector_Samtec_HPM_THT:Samtec_HPM-19-05-x-S_Straight_1x19_Pitch5.08mm +Connector_Samtec_HSEC8:Samtec_HSEC8-109-01-X-DV-A-BL_2x09_P0.8mm_Pol04_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-109-01-X-DV-A-WT_2x09_P0.8mm_Pol04_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-109-X-X-DV-BL_2x09_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-109-X-X-DV_2x09_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-109-X-X-DV_2x09_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-110-01-X-DV-A-BL_2x10_P0.8mm_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-110-01-X-DV-A-WT_2x10_P0.8mm_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-110-01-X-DV-A_2x10_P0.8mm_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-110-01-X-DV_2x10_P0.8mm_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-110-03-X-DV-A-WT_2x10_P0.8mm_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-110-03-X-DV-A_2x10_P0.8mm_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-110-03-X-DV_2x10_P0.8mm_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-110-X-X-DV-BL_2x10_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-110-X-X-DV_2x10_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-110-X-X-DV_2x10_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-01-X-DV-A-BL_2x100_P0.8mm_Pol32_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-01-X-DV-A-WT_2x100_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-01-X-DV-A_2x100_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-01-X-DV_2x100_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-03-X-DV-A-WT_2x100_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-03-X-DV-A_2x100_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-03-X-DV_2x100_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-X-X-DV-BL_2x100_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-X-X-DV_2x100_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-1100-X-X-DV_2x100_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-113-01-X-DV-A-BL_2x13_P0.8mm_Pol06_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-113-01-X-DV-A-WT_2x13_P0.8mm_Pol06_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-113-01-X-DV-A_2x13_P0.8mm_Pol06_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-113-01-X-DV_2x13_P0.8mm_Pol06_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-113-X-X-DV-BL_2x13_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-113-X-X-DV_2x13_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-113-X-X-DV_2x13_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-120-01-X-DV-A-BL_2x20_P0.8mm_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-120-01-X-DV-A-WT_2x20_P0.8mm_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-120-01-X-DV-A_2x20_P0.8mm_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-120-01-X-DV_2x20_P0.8mm_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-120-03-X-DV-A-WT_2x20_P0.8mm_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-120-03-X-DV-A_2x20_P0.8mm_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-120-03-X-DV_2x20_P0.8mm_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-120-X-X-DV-BL_2x20_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-120-X-X-DV_2x20_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-120-X-X-DV_2x20_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-125-01-X-DV-A-BL_2x25_P0.8mm_Pol06_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-125-01-X-DV-A-WT_2x25_P0.8mm_Pol06_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-125-01-X-DV-A_2x25_P0.8mm_Pol06_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-125-01-X-DV_2x25_P0.8mm_Pol06_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-125-X-X-DV-BL_2x25_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-125-X-X-DV_2x25_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-125-X-X-DV_2x25_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-130-01-X-DV-A-BL_2x30_P0.8mm_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-130-01-X-DV-A-WT_2x30_P0.8mm_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-130-01-X-DV-A_2x30_P0.8mm_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-130-01-X-DV_2x30_P0.8mm_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-130-03-X-DV-A-WT_2x30_P0.8mm_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-130-03-X-DV-A_2x30_P0.8mm_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-130-03-X-DV_2x30_P0.8mm_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-130-X-X-DV-BL_2x30_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-130-X-X-DV_2x30_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-130-X-X-DV_2x30_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-137-01-X-DV-A-BL_2x37_P0.8mm_Pol21_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-137-01-X-DV-A-WT_2x37_P0.8mm_Pol21_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-137-01-X-DV-A_2x37_P0.8mm_Pol21_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-137-01-X-DV_2x37_P0.8mm_Pol21_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-137-X-X-DV-BL_2x37_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-137-X-X-DV_2x37_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-137-X-X-DV_2x37_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-140-01-X-DV-A-BL_2x40_P0.8mm_Pol22_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-140-01-X-DV-A-WT_2x40_P0.8mm_Pol22_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-140-01-X-DV-A_2x40_P0.8mm_Pol22_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-140-01-X-DV_2x40_P0.8mm_Pol22_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-140-03-X-DV-A-WT_2x40_P0.8mm_Pol22_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-140-03-X-DV-A_2x40_P0.8mm_Pol22_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-140-03-X-DV_2x40_P0.8mm_Pol22_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-140-X-X-DV-BL_2x40_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-140-X-X-DV_2x40_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-140-X-X-DV_2x40_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-149-01-X-DV-A-BL_2x49_P0.8mm_Pol27_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-149-01-X-DV-A-WT_2x49_P0.8mm_Pol27_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-149-01-X-DV-A_2x49_P0.8mm_Pol27_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-149-01-X-DV_2x49_P0.8mm_Pol27_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-149-X-X-DV-BL_2x49_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-149-X-X-DV_2x49_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-149-X-X-DV_2x49_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-150-01-X-DV-A-BL_2x50_P0.8mm_Pol27_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-150-01-X-DV-A-WT_2x50_P0.8mm_Pol27_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-150-01-X-DV-A_2x50_P0.8mm_Pol27_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-150-01-X-DV_2x50_P0.8mm_Pol27_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-150-03-X-DV-A-WT_2x50_P0.8mm_Pol27_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-150-03-X-DV-A_2x50_P0.8mm_Pol27_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-150-03-X-DV_2x50_P0.8mm_Pol27_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-150-X-X-DV-BL_2x50_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-150-X-X-DV_2x50_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-150-X-X-DV_2x50_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-160-01-X-DV-A-BL_2x60_P0.8mm_Pol32_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-160-01-X-DV-A-WT_2x60_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-160-01-X-DV-A_2x60_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-160-01-X-DV_2x60_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-160-03-X-DV-A-WT_2x60_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-160-03-X-DV-A_2x60_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-160-03-X-DV_2x60_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-160-X-X-DV-BL_2x60_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-160-X-X-DV_2x60_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-160-X-X-DV_2x60_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-170-01-X-DV-A-BL_2x70_P0.8mm_Pol32_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-170-01-X-DV-A-WT_2x70_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-170-01-X-DV-A_2x70_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-170-01-X-DV_2x70_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-170-03-X-DV-A-WT_2x70_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-170-03-X-DV-A_2x70_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-170-03-X-DV_2x70_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-170-X-X-DV-BL_2x70_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-170-X-X-DV_2x70_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-170-X-X-DV_2x70_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-180-01-X-DV-A-BL_2x80_P0.8mm_Pol32_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-180-01-X-DV-A-WT_2x80_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-180-01-X-DV-A_2x80_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-180-01-X-DV_2x80_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-180-03-X-DV-A-WT_2x80_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-180-03-X-DV-A_2x80_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-180-03-X-DV_2x80_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-180-X-X-DV-BL_2x80_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-180-X-X-DV_2x80_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-180-X-X-DV_2x80_P0.8mm_Wing_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-190-01-X-DV-A-BL_2x90_P0.8mm_Pol32_Socket_WeldTabs_BoardLocks +Connector_Samtec_HSEC8:Samtec_HSEC8-190-01-X-DV-A-WT_2x90_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-190-01-X-DV-A_2x90_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-190-01-X-DV_2x90_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-190-03-X-DV-A-WT_2x90_P0.8mm_Pol32_Socket_WeldTabs +Connector_Samtec_HSEC8:Samtec_HSEC8-190-03-X-DV-A_2x90_P0.8mm_Pol32_Socket_AlignmentPins +Connector_Samtec_HSEC8:Samtec_HSEC8-190-03-X-DV_2x90_P0.8mm_Pol32_Socket +Connector_Samtec_HSEC8:Samtec_HSEC8-190-X-X-DV-BL_2x90_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-190-X-X-DV_2x90_P0.8mm_Edge +Connector_Samtec_HSEC8:Samtec_HSEC8-190-X-X-DV_2x90_P0.8mm_Wing_Edge +Connector_Samtec_MicroMate:Samtec_T1M-02-X-S-RA_1x02-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-02-X-S-V_1x02-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-02-X-SH-L_1x02-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-02-X-SV-L_1x02-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-03-X-S-RA_1x03-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-03-X-S-V_1x03-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-03-X-SH-L_1x03-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-03-X-SV-L_1x03-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-04-X-S-RA_1x04-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-04-X-S-V_1x04-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-04-X-SH-L_1x04-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-04-X-SV-L_1x04-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-05-X-S-RA_1x05-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-05-X-S-V_1x05-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-05-X-SH-L_1x05-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-05-X-SV-L_1x05-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-06-X-S-RA_1x06-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-06-X-S-V_1x06-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-06-X-SH-L_1x06-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-06-X-SV-L_1x06-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-07-X-S-RA_1x07-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-07-X-S-V_1x07-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-07-X-SH-L_1x07-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-07-X-SV-L_1x07-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-08-X-S-RA_1x08-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-08-X-S-V_1x08-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-08-X-SH-L_1x08-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-08-X-SV-L_1x08-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-09-X-S-RA_1x09-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-09-X-S-V_1x09-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-09-X-SH-L_1x09-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-09-X-SV-L_1x09-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-10-X-S-RA_1x10-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-10-X-S-V_1x10-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-10-X-SH-L_1x10-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-10-X-SV-L_1x10-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-11-X-S-RA_1x11-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-11-X-S-V_1x11-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-11-X-SH-L_1x11-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-11-X-SV-L_1x11-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-12-X-S-RA_1x12-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-12-X-S-V_1x12-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-12-X-SH-L_1x12-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-12-X-SV-L_1x12-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-13-X-S-RA_1x13-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-13-X-S-V_1x13-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-13-X-SH-L_1x13-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-13-X-SV-L_1x13-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-14-X-S-RA_1x14-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-14-X-S-V_1x14-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-14-X-SH-L_1x14-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-14-X-SV-L_1x14-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-15-X-S-RA_1x15-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-15-X-S-V_1x15-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-15-X-SH-L_1x15-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-15-X-SV-L_1x15-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-16-X-S-RA_1x16-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-16-X-S-V_1x16-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-16-X-SH-L_1x16-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-16-X-SV-L_1x16-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-17-X-S-RA_1x17-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-17-X-S-V_1x17-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-17-X-SH-L_1x17-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-17-X-SV-L_1x17-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-18-X-S-RA_1x18-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-18-X-S-V_1x18-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-18-X-SH-L_1x18-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-18-X-SV-L_1x18-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-19-X-S-RA_1x19-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-19-X-S-V_1x19-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-19-X-SH-L_1x19-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-19-X-SV-L_1x19-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroMate:Samtec_T1M-20-X-S-RA_1x20-1MP_P1.0mm_Terminal_Horizontal +Connector_Samtec_MicroMate:Samtec_T1M-20-X-S-V_1x20-1MP_P1.0mm_Terminal_Vertical +Connector_Samtec_MicroMate:Samtec_T1M-20-X-SH-L_1x20-1MP_P1.0mm_Terminal_Horizontal_Latch +Connector_Samtec_MicroMate:Samtec_T1M-20-X-SV-L_1x20-1MP_P1.0mm_Terminal_Vertical_Latch +Connector_Samtec_MicroPower:Samtec_UMPS-02-XX.X-X-V-S-W_1x02-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-02-XX.X-X-V-S_1x02_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-03-XX.X-X-V-S-W_1x03-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-03-XX.X-X-V-S_1x03_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-04-XX.X-X-V-S-W_1x04-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-04-XX.X-X-V-S_1x04_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-05-XX.X-X-V-S-W_1x05-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-05-XX.X-X-V-S_1x05_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-06-XX.X-X-V-S-W_1x06-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-06-XX.X-X-V-S_1x06_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-07-XX.X-X-V-S-W_1x07-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-07-XX.X-X-V-S_1x07_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-08-XX.X-X-V-S-W_1x08-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-08-XX.X-X-V-S_1x08_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-09-XX.X-X-V-S-W_1x09-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-09-XX.X-X-V-S_1x09_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPS-10-XX.X-X-V-S-W_1x10-1MP_P2.0mm_Socket_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPS-10-XX.X-X-V-S_1x10_P2.0mm_Socket +Connector_Samtec_MicroPower:Samtec_UMPT-02-XX.X-X-V-S-W_1x02-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-02-XX.X-X-V-S_1x02_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-03-XX.X-X-V-S-W_1x03-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-03-XX.X-X-V-S_1x03_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-04-XX.X-X-V-S-W_1x04-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-04-XX.X-X-V-S_1x04_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-05-XX.X-X-V-S-W_1x05-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-05-XX.X-X-V-S_1x05_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-06-XX.X-X-V-S-W_1x06-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-06-XX.X-X-V-S_1x06_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-07-XX.X-X-V-S-W_1x07-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-07-XX.X-X-V-S_1x07_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-08-XX.X-X-V-S-W_1x08-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-08-XX.X-X-V-S_1x08_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-09-XX.X-X-V-S-W_1x09-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-09-XX.X-X-V-S_1x09_P2.0mm_Terminal +Connector_Samtec_MicroPower:Samtec_UMPT-10-XX.X-X-V-S-W_1x10-1MP_P2.0mm_Terminal_WeldTab +Connector_Samtec_MicroPower:Samtec_UMPT-10-XX.X-X-V-S_1x10_P2.0mm_Terminal +Connector_SATA_SAS:SAS-mini_TEConnectivity_1888174_Vertical +Connector_SATA_SAS:SATA_Amphenol_10029364-001LF_Horizontal +Connector_Stocko:Stocko_MKS_1651-6-0-202_1x2_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1652-6-0-202_1x2_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1653-6-0-303_1x3_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1654-6-0-404_1x4_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1655-6-0-505_1x5_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1656-6-0-606_1x6_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1657-6-0-707_1x7_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1658-6-0-808_1x8_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1659-6-0-909_1x9_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1660-6-0-1010_1x10_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1661-6-0-1111_1x11_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1662-6-0-1212_1x12_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1663-6-0-1313_1x13_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1664-6-0-1414_1x14_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1665-6-0-1515_1x15_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1666-6-0-1616_1x16_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1667-6-0-1717_1x17_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1668-6-0-1818_1x18_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1669-6-0-1919_1x19_P2.50mm_Vertical +Connector_Stocko:Stocko_MKS_1670-6-0-2020_1x20_P2.50mm_Vertical +Connector_TE-Connectivity:TE_1-826576-3_1x13_P3.96mm_Vertical +Connector_TE-Connectivity:TE_1-826576-5_1x15_P3.96mm_Vertical +Connector_TE-Connectivity:TE_1-826576-6_1x16_P3.96mm_Vertical +Connector_TE-Connectivity:TE_1-826576-7_1x17_P3.96mm_Vertical +Connector_TE-Connectivity:TE_1-826576-8_1x18_P3.96mm_Vertical +Connector_TE-Connectivity:TE_2-826576-0_1x20_P3.96mm_Vertical +Connector_TE-Connectivity:TE_2834006-1_1x01_P4.0mm_Horizontal +Connector_TE-Connectivity:TE_2834006-2_1x02_P4.0mm_Horizontal +Connector_TE-Connectivity:TE_2834006-3_1x03_P4.0mm_Horizontal +Connector_TE-Connectivity:TE_2834006-4_1x04_P4.0mm_Horizontal +Connector_TE-Connectivity:TE_2834006-5_1x05_P4.0mm_Horizontal +Connector_TE-Connectivity:TE_3-826576-6_1x36_P3.96mm_Vertical +Connector_TE-Connectivity:TE_440054-2_1x02_P2.00mm_Vertical +Connector_TE-Connectivity:TE_440055-2_1x02_P2.00mm_Horizontal +Connector_TE-Connectivity:TE_5767171-1_2x19_P0.635mm_Vertical +Connector_TE-Connectivity:TE_826576-2_1x02_P3.96mm_Vertical +Connector_TE-Connectivity:TE_826576-3_1x03_P3.96mm_Vertical +Connector_TE-Connectivity:TE_826576-5_1x05_P3.96mm_Vertical +Connector_TE-Connectivity:TE_826576-6_1x06_P3.96mm_Vertical +Connector_TE-Connectivity:TE_826576-7_1x07_P3.96mm_Vertical +Connector_TE-Connectivity:TE_826576-8_1x08_P3.96mm_Vertical +Connector_TE-Connectivity:TE_826576-9_1x09_P3.96mm_Vertical +Connector_TE-Connectivity:TE_AMPSEAL_1-776087-x_3Rows_23_P0.4mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770182-x_3x03_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770186-x_3x04_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770190-x_3x05_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770621-x_2x06_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770858-x_2x05_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770866-x_1x02_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770870-x_1x03_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770874-x_2x02_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770875-x_2x03_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770966-x_1x02_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770967-x_1x03_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770968-x_2x02_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770969-x_2x03_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770970-x_2x04_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770971-x_2x05_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770972-x_2x06_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770973-x_2x07_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-770974-x_2x08_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794067-x_2x07_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794068-x_2x08_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794069-x_2x09_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794070-x_2x10_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794071-x_2x11_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794072-x_2x12_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794073-x_2x04_P4.14mm_Vertical +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794105-x_2x09_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794106-x_2x10_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794107-x_2x11_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794108-x_2x12_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_1-794374-x_1x01_P4.14mm_Horizontal +Connector_TE-Connectivity:TE_MATE-N-LOK_350211-1_1x04_P5.08mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_1-215079-0_2x05_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_1-215079-2_2x06_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_1-215079-4_2x07_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_1-215079-6_2x08_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_1-215079-8_2x09_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_2-215079-0_2x10_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_215079-4_2x02_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_215079-6_2x03_P1.27mm_Vertical +Connector_TE-Connectivity:TE_Micro-MaTch_215079-8_2x04_P1.27mm_Vertical +Connector_TE-Connectivity:TE_T4041037031-000_M8_03_Socket_Straight +Connector_TE-Connectivity:TE_T4041037041-000_M8_04_Socket_Straight +Connector_USB:USB3_A_Molex_48393-001 +Connector_USB:USB3_A_Molex_48406-0001_Horizontal_Stacked +Connector_USB:USB3_A_Plug_Wuerth_692112030100_Horizontal +Connector_USB:USB3_A_Receptacle_Wuerth_692122030100 +Connector_USB:USB3_Micro-B_Connfly_DS1104-01 +Connector_USB:USB_A_CNCTech_1001-011-01101_Horizontal +Connector_USB:USB_A_Connfly_DS1095 +Connector_USB:USB_A_Connfly_DS1098_Horizontal +Connector_USB:USB_A_CUI_UJ2-ADH-TH_Horizontal_Stacked +Connector_USB:USB_A_Kycon_KUSBX-AS1N-B_Horizontal +Connector_USB:USB_A_Molex_105057_Vertical +Connector_USB:USB_A_Molex_48037-2200_Horizontal +Connector_USB:USB_A_Molex_67643_Horizontal +Connector_USB:USB_A_Receptacle_GCT_USB1046 +Connector_USB:USB_A_Receptacle_XKB_U231-091N-4BLRA00-S +Connector_USB:USB_A_Stewart_SS-52100-001_Horizontal +Connector_USB:USB_A_TE_292303-7_Horizontal +Connector_USB:USB_A_Wuerth_614004134726_Horizontal +Connector_USB:USB_A_Wuerth_61400826021_Horizontal_Stacked +Connector_USB:USB_B_Amphenol_MUSB-D511_Vertical_Rugged +Connector_USB:USB_B_Lumberg_2411_02_Horizontal +Connector_USB:USB_B_OST_USB-B1HSxx_Horizontal +Connector_USB:USB_B_TE_5787834_Vertical +Connector_USB:USB_C_Plug_JAE_DX07P024AJ1 +Connector_USB:USB_C_Plug_Molex_105444 +Connector_USB:USB_C_Plug_ShenzhenJingTuoJin_918-118A2021Y40002_Vertical +Connector_USB:USB_C_Receptacle_Amphenol_12401548E4-2A +Connector_USB:USB_C_Receptacle_Amphenol_12401548E4-2A_CircularHoles +Connector_USB:USB_C_Receptacle_Amphenol_12401610E4-2A +Connector_USB:USB_C_Receptacle_Amphenol_12401610E4-2A_CircularHoles +Connector_USB:USB_C_Receptacle_Amphenol_12401948E412A +Connector_USB:USB_C_Receptacle_Amphenol_124019772112A +Connector_USB:USB_C_Receptacle_CNCTech_C-ARA1-AK51X +Connector_USB:USB_C_Receptacle_G-Switch_GT-USB-7010ASV +Connector_USB:USB_C_Receptacle_G-Switch_GT-USB-7025 +Connector_USB:USB_C_Receptacle_G-Switch_GT-USB-7051x +Connector_USB:USB_C_Receptacle_GCT_USB4085 +Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal +Connector_USB:USB_C_Receptacle_GCT_USB4110 +Connector_USB:USB_C_Receptacle_GCT_USB4115-03-C +Connector_USB:USB_C_Receptacle_GCT_USB4125-xx-x-0190_6P_TopMnt_Horizontal +Connector_USB:USB_C_Receptacle_GCT_USB4125-xx-x_6P_TopMnt_Horizontal +Connector_USB:USB_C_Receptacle_GCT_USB4135-GF-A_6P_TopMnt_Horizontal +Connector_USB:USB_C_Receptacle_HCTL_HC-TYPE-C-16P-01A +Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12 +Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-17 +Connector_USB:USB_C_Receptacle_JAE_DX07S016JA1R1500 +Connector_USB:USB_C_Receptacle_JAE_DX07S024WJ1R350 +Connector_USB:USB_C_Receptacle_JAE_DX07S024WJ3R400 +Connector_USB:USB_C_Receptacle_Molex_105450-0101 +Connector_USB:USB_C_Receptacle_Palconn_UTC16-G +Connector_USB:USB_C_Receptacle_XKB_U262-16XN-4BVC11 +Connector_USB:USB_Micro-AB_Molex_47590-0001 +Connector_USB:USB_Micro-B_Amphenol_10103594-0001LF_Horizontal +Connector_USB:USB_Micro-B_Amphenol_10104110_Horizontal +Connector_USB:USB_Micro-B_Amphenol_10118193-0001LF_Horizontal +Connector_USB:USB_Micro-B_Amphenol_10118193-0002LF_Horizontal +Connector_USB:USB_Micro-B_Amphenol_10118194-0001LF_Horizontal +Connector_USB:USB_Micro-B_Amphenol_10118194_Horizontal +Connector_USB:USB_Micro-B_GCT_USB3076-30-A +Connector_USB:USB_Micro-B_Molex-105017-0001 +Connector_USB:USB_Micro-B_Molex-105133-0001 +Connector_USB:USB_Micro-B_Molex-105133-0031 +Connector_USB:USB_Micro-B_Molex_47346-0001 +Connector_USB:USB_Micro-B_Technik_TWP-4002D-H3 +Connector_USB:USB_Micro-B_Wuerth_614105150721_Vertical +Connector_USB:USB_Micro-B_Wuerth_614105150721_Vertical_CircularHoles +Connector_USB:USB_Micro-B_Wuerth_629105150521 +Connector_USB:USB_Micro-B_Wuerth_629105150521_CircularHoles +Connector_USB:USB_Micro-B_XKB_U254-051T-4BH83-F1S +Connector_USB:USB_Mini-B_AdamTech_MUSB-B5-S-VT-TSMT-1_SMD_Vertical +Connector_USB:USB_Mini-B_Lumberg_2486_01_Horizontal +Connector_USB:USB_Mini-B_Tensility_54-00023_Vertical +Connector_USB:USB_Mini-B_Tensility_54-00023_Vertical_CircularHoles +Connector_USB:USB_Mini-B_Wuerth_65100516121_Horizontal +Connector_Video:DVI-D_Molex_74320-4004_Horizontal +Connector_Video:DVI-I_Molex_74320-1004_Horizontal +Connector_Video:HDMI_A_Amphenol_10029449-x01xLF_Horizontal +Connector_Video:HDMI_A_Contact_Technology_HDMI-19APL2_Horizontal +Connector_Video:HDMI_A_Kycon_KDMIX-SL1-NS-WS-B15_VerticalRightAngle +Connector_Video:HDMI_A_Molex_208658-1001_Horizontal +Connector_Video:HDMI_Micro-D_Molex_46765-0x01 +Connector_Video:HDMI_Micro-D_Molex_46765-1x01 +Connector_Video:HDMI_Micro-D_Molex_46765-2x0x +Connector_Wago:Wago_734-132_1x02_P3.50mm_Vertical +Connector_Wago:Wago_734-133_1x03_P3.50mm_Vertical +Connector_Wago:Wago_734-134_1x04_P3.50mm_Vertical +Connector_Wago:Wago_734-135_1x05_P3.50mm_Vertical +Connector_Wago:Wago_734-136_1x06_P3.50mm_Vertical +Connector_Wago:Wago_734-137_1x07_P3.50mm_Vertical +Connector_Wago:Wago_734-138_1x08_P3.50mm_Vertical +Connector_Wago:Wago_734-139_1x09_P3.50mm_Vertical +Connector_Wago:Wago_734-140_1x10_P3.50mm_Vertical +Connector_Wago:Wago_734-141_1x11_P3.50mm_Vertical +Connector_Wago:Wago_734-142_1x12_P3.50mm_Vertical +Connector_Wago:Wago_734-143_1x13_P3.50mm_Vertical +Connector_Wago:Wago_734-144_1x14_P3.50mm_Vertical +Connector_Wago:Wago_734-146_1x16_P3.50mm_Vertical +Connector_Wago:Wago_734-148_1x18_P3.50mm_Vertical +Connector_Wago:Wago_734-150_1x20_P3.50mm_Vertical +Connector_Wago:Wago_734-154_1x24_P3.50mm_Vertical +Connector_Wago:Wago_734-162_1x02_P3.50mm_Horizontal +Connector_Wago:Wago_734-163_1x03_P3.50mm_Horizontal +Connector_Wago:Wago_734-164_1x04_P3.50mm_Horizontal +Connector_Wago:Wago_734-165_1x05_P3.50mm_Horizontal +Connector_Wago:Wago_734-166_1x06_P3.50mm_Horizontal +Connector_Wago:Wago_734-167_1x07_P3.50mm_Horizontal +Connector_Wago:Wago_734-168_1x08_P3.50mm_Horizontal +Connector_Wago:Wago_734-169_1x09_P3.50mm_Horizontal +Connector_Wago:Wago_734-170_1x10_P3.50mm_Horizontal +Connector_Wago:Wago_734-171_1x11_P3.50mm_Horizontal +Connector_Wago:Wago_734-172_1x12_P3.50mm_Horizontal +Connector_Wago:Wago_734-173_1x13_P3.50mm_Horizontal +Connector_Wago:Wago_734-174_1x14_P3.50mm_Horizontal +Connector_Wago:Wago_734-176_1x16_P3.50mm_Horizontal +Connector_Wago:Wago_734-178_1x18_P3.50mm_Horizontal +Connector_Wago:Wago_734-180_1x20_P3.50mm_Horizontal +Connector_Wago:Wago_734-184_1x24_P3.50mm_Horizontal +Connector_Wire:SolderWire-0.127sqmm_1x01_D0.48mm_OD1mm +Connector_Wire:SolderWire-0.127sqmm_1x01_D0.48mm_OD1mm_Relief +Connector_Wire:SolderWire-0.127sqmm_1x01_D0.48mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.127sqmm_1x02_P3.7mm_D0.48mm_OD1mm +Connector_Wire:SolderWire-0.127sqmm_1x02_P3.7mm_D0.48mm_OD1mm_Relief +Connector_Wire:SolderWire-0.127sqmm_1x02_P3.7mm_D0.48mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.127sqmm_1x03_P3.7mm_D0.48mm_OD1mm +Connector_Wire:SolderWire-0.127sqmm_1x03_P3.7mm_D0.48mm_OD1mm_Relief +Connector_Wire:SolderWire-0.127sqmm_1x03_P3.7mm_D0.48mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.127sqmm_1x04_P3.7mm_D0.48mm_OD1mm +Connector_Wire:SolderWire-0.127sqmm_1x04_P3.7mm_D0.48mm_OD1mm_Relief +Connector_Wire:SolderWire-0.127sqmm_1x04_P3.7mm_D0.48mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.127sqmm_1x05_P3.7mm_D0.48mm_OD1mm +Connector_Wire:SolderWire-0.127sqmm_1x05_P3.7mm_D0.48mm_OD1mm_Relief +Connector_Wire:SolderWire-0.127sqmm_1x05_P3.7mm_D0.48mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.127sqmm_1x06_P3.7mm_D0.48mm_OD1mm +Connector_Wire:SolderWire-0.127sqmm_1x06_P3.7mm_D0.48mm_OD1mm_Relief +Connector_Wire:SolderWire-0.127sqmm_1x06_P3.7mm_D0.48mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.15sqmm_1x01_D0.5mm_OD1.5mm +Connector_Wire:SolderWire-0.15sqmm_1x01_D0.5mm_OD1.5mm_Relief +Connector_Wire:SolderWire-0.15sqmm_1x01_D0.5mm_OD1.5mm_Relief2x +Connector_Wire:SolderWire-0.15sqmm_1x02_P4mm_D0.5mm_OD1.5mm +Connector_Wire:SolderWire-0.15sqmm_1x02_P4mm_D0.5mm_OD1.5mm_Relief +Connector_Wire:SolderWire-0.15sqmm_1x02_P4mm_D0.5mm_OD1.5mm_Relief2x +Connector_Wire:SolderWire-0.15sqmm_1x03_P4mm_D0.5mm_OD1.5mm +Connector_Wire:SolderWire-0.15sqmm_1x03_P4mm_D0.5mm_OD1.5mm_Relief +Connector_Wire:SolderWire-0.15sqmm_1x03_P4mm_D0.5mm_OD1.5mm_Relief2x +Connector_Wire:SolderWire-0.15sqmm_1x04_P4mm_D0.5mm_OD1.5mm +Connector_Wire:SolderWire-0.15sqmm_1x04_P4mm_D0.5mm_OD1.5mm_Relief +Connector_Wire:SolderWire-0.15sqmm_1x04_P4mm_D0.5mm_OD1.5mm_Relief2x +Connector_Wire:SolderWire-0.15sqmm_1x05_P4mm_D0.5mm_OD1.5mm +Connector_Wire:SolderWire-0.15sqmm_1x05_P4mm_D0.5mm_OD1.5mm_Relief +Connector_Wire:SolderWire-0.15sqmm_1x05_P4mm_D0.5mm_OD1.5mm_Relief2x +Connector_Wire:SolderWire-0.15sqmm_1x06_P4mm_D0.5mm_OD1.5mm +Connector_Wire:SolderWire-0.15sqmm_1x06_P4mm_D0.5mm_OD1.5mm_Relief +Connector_Wire:SolderWire-0.15sqmm_1x06_P4mm_D0.5mm_OD1.5mm_Relief2x +Connector_Wire:SolderWire-0.1sqmm_1x01_D0.4mm_OD1mm +Connector_Wire:SolderWire-0.1sqmm_1x01_D0.4mm_OD1mm_Relief +Connector_Wire:SolderWire-0.1sqmm_1x01_D0.4mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.1sqmm_1x02_P3.6mm_D0.4mm_OD1mm +Connector_Wire:SolderWire-0.1sqmm_1x02_P3.6mm_D0.4mm_OD1mm_Relief +Connector_Wire:SolderWire-0.1sqmm_1x02_P3.6mm_D0.4mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.1sqmm_1x03_P3.6mm_D0.4mm_OD1mm +Connector_Wire:SolderWire-0.1sqmm_1x03_P3.6mm_D0.4mm_OD1mm_Relief +Connector_Wire:SolderWire-0.1sqmm_1x03_P3.6mm_D0.4mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.1sqmm_1x04_P3.6mm_D0.4mm_OD1mm +Connector_Wire:SolderWire-0.1sqmm_1x04_P3.6mm_D0.4mm_OD1mm_Relief +Connector_Wire:SolderWire-0.1sqmm_1x04_P3.6mm_D0.4mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.1sqmm_1x05_P3.6mm_D0.4mm_OD1mm +Connector_Wire:SolderWire-0.1sqmm_1x05_P3.6mm_D0.4mm_OD1mm_Relief +Connector_Wire:SolderWire-0.1sqmm_1x05_P3.6mm_D0.4mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.1sqmm_1x06_P3.6mm_D0.4mm_OD1mm +Connector_Wire:SolderWire-0.1sqmm_1x06_P3.6mm_D0.4mm_OD1mm_Relief +Connector_Wire:SolderWire-0.1sqmm_1x06_P3.6mm_D0.4mm_OD1mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x01_D0.65mm_OD1.7mm +Connector_Wire:SolderWire-0.25sqmm_1x01_D0.65mm_OD1.7mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x01_D0.65mm_OD1.7mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x01_D0.65mm_OD2mm +Connector_Wire:SolderWire-0.25sqmm_1x01_D0.65mm_OD2mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x01_D0.65mm_OD2mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x02_P4.2mm_D0.65mm_OD1.7mm +Connector_Wire:SolderWire-0.25sqmm_1x02_P4.2mm_D0.65mm_OD1.7mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x02_P4.2mm_D0.65mm_OD1.7mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x02_P4.5mm_D0.65mm_OD2mm +Connector_Wire:SolderWire-0.25sqmm_1x02_P4.5mm_D0.65mm_OD2mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x02_P4.5mm_D0.65mm_OD2mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x03_P4.2mm_D0.65mm_OD1.7mm +Connector_Wire:SolderWire-0.25sqmm_1x03_P4.2mm_D0.65mm_OD1.7mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x03_P4.2mm_D0.65mm_OD1.7mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x03_P4.5mm_D0.65mm_OD2mm +Connector_Wire:SolderWire-0.25sqmm_1x03_P4.5mm_D0.65mm_OD2mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x03_P4.5mm_D0.65mm_OD2mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x04_P4.2mm_D0.65mm_OD1.7mm +Connector_Wire:SolderWire-0.25sqmm_1x04_P4.2mm_D0.65mm_OD1.7mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x04_P4.2mm_D0.65mm_OD1.7mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x04_P4.5mm_D0.65mm_OD2mm +Connector_Wire:SolderWire-0.25sqmm_1x04_P4.5mm_D0.65mm_OD2mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x04_P4.5mm_D0.65mm_OD2mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x05_P4.2mm_D0.65mm_OD1.7mm +Connector_Wire:SolderWire-0.25sqmm_1x05_P4.2mm_D0.65mm_OD1.7mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x05_P4.2mm_D0.65mm_OD1.7mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x05_P4.5mm_D0.65mm_OD2mm +Connector_Wire:SolderWire-0.25sqmm_1x05_P4.5mm_D0.65mm_OD2mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x05_P4.5mm_D0.65mm_OD2mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x06_P4.2mm_D0.65mm_OD1.7mm +Connector_Wire:SolderWire-0.25sqmm_1x06_P4.2mm_D0.65mm_OD1.7mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x06_P4.2mm_D0.65mm_OD1.7mm_Relief2x +Connector_Wire:SolderWire-0.25sqmm_1x06_P4.5mm_D0.65mm_OD2mm +Connector_Wire:SolderWire-0.25sqmm_1x06_P4.5mm_D0.65mm_OD2mm_Relief +Connector_Wire:SolderWire-0.25sqmm_1x06_P4.5mm_D0.65mm_OD2mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x01_D0.9mm_OD2.1mm +Connector_Wire:SolderWire-0.5sqmm_1x01_D0.9mm_OD2.1mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x01_D0.9mm_OD2.1mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x01_D0.9mm_OD2.3mm +Connector_Wire:SolderWire-0.5sqmm_1x01_D0.9mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x01_D0.9mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x02_P4.6mm_D0.9mm_OD2.1mm +Connector_Wire:SolderWire-0.5sqmm_1x02_P4.6mm_D0.9mm_OD2.1mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x02_P4.6mm_D0.9mm_OD2.1mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x02_P4.8mm_D0.9mm_OD2.3mm +Connector_Wire:SolderWire-0.5sqmm_1x02_P4.8mm_D0.9mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x02_P4.8mm_D0.9mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x03_P4.6mm_D0.9mm_OD2.1mm +Connector_Wire:SolderWire-0.5sqmm_1x03_P4.6mm_D0.9mm_OD2.1mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x03_P4.6mm_D0.9mm_OD2.1mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x03_P4.8mm_D0.9mm_OD2.3mm +Connector_Wire:SolderWire-0.5sqmm_1x03_P4.8mm_D0.9mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x03_P4.8mm_D0.9mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x04_P4.6mm_D0.9mm_OD2.1mm +Connector_Wire:SolderWire-0.5sqmm_1x04_P4.6mm_D0.9mm_OD2.1mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x04_P4.6mm_D0.9mm_OD2.1mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x04_P4.8mm_D0.9mm_OD2.3mm +Connector_Wire:SolderWire-0.5sqmm_1x04_P4.8mm_D0.9mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x04_P4.8mm_D0.9mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x05_P4.6mm_D0.9mm_OD2.1mm +Connector_Wire:SolderWire-0.5sqmm_1x05_P4.6mm_D0.9mm_OD2.1mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x05_P4.6mm_D0.9mm_OD2.1mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x05_P4.8mm_D0.9mm_OD2.3mm +Connector_Wire:SolderWire-0.5sqmm_1x05_P4.8mm_D0.9mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x05_P4.8mm_D0.9mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x06_P4.6mm_D0.9mm_OD2.1mm +Connector_Wire:SolderWire-0.5sqmm_1x06_P4.6mm_D0.9mm_OD2.1mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x06_P4.6mm_D0.9mm_OD2.1mm_Relief2x +Connector_Wire:SolderWire-0.5sqmm_1x06_P4.8mm_D0.9mm_OD2.3mm +Connector_Wire:SolderWire-0.5sqmm_1x06_P4.8mm_D0.9mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.5sqmm_1x06_P4.8mm_D0.9mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x01_D1.25mm_OD2.3mm +Connector_Wire:SolderWire-0.75sqmm_1x01_D1.25mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x01_D1.25mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x01_D1.25mm_OD3.5mm +Connector_Wire:SolderWire-0.75sqmm_1x01_D1.25mm_OD3.5mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x01_D1.25mm_OD3.5mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x02_P4.8mm_D1.25mm_OD2.3mm +Connector_Wire:SolderWire-0.75sqmm_1x02_P4.8mm_D1.25mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x02_P4.8mm_D1.25mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x02_P7mm_D1.25mm_OD3.5mm +Connector_Wire:SolderWire-0.75sqmm_1x02_P7mm_D1.25mm_OD3.5mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x02_P7mm_D1.25mm_OD3.5mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x03_P4.8mm_D1.25mm_OD2.3mm +Connector_Wire:SolderWire-0.75sqmm_1x03_P4.8mm_D1.25mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x03_P4.8mm_D1.25mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x03_P7mm_D1.25mm_OD3.5mm +Connector_Wire:SolderWire-0.75sqmm_1x03_P7mm_D1.25mm_OD3.5mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x03_P7mm_D1.25mm_OD3.5mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x04_P4.8mm_D1.25mm_OD2.3mm +Connector_Wire:SolderWire-0.75sqmm_1x04_P4.8mm_D1.25mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x04_P4.8mm_D1.25mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x04_P7mm_D1.25mm_OD3.5mm +Connector_Wire:SolderWire-0.75sqmm_1x04_P7mm_D1.25mm_OD3.5mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x04_P7mm_D1.25mm_OD3.5mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x05_P4.8mm_D1.25mm_OD2.3mm +Connector_Wire:SolderWire-0.75sqmm_1x05_P4.8mm_D1.25mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x05_P4.8mm_D1.25mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x05_P7mm_D1.25mm_OD3.5mm +Connector_Wire:SolderWire-0.75sqmm_1x05_P7mm_D1.25mm_OD3.5mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x05_P7mm_D1.25mm_OD3.5mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x06_P4.8mm_D1.25mm_OD2.3mm +Connector_Wire:SolderWire-0.75sqmm_1x06_P4.8mm_D1.25mm_OD2.3mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x06_P4.8mm_D1.25mm_OD2.3mm_Relief2x +Connector_Wire:SolderWire-0.75sqmm_1x06_P7mm_D1.25mm_OD3.5mm +Connector_Wire:SolderWire-0.75sqmm_1x06_P7mm_D1.25mm_OD3.5mm_Relief +Connector_Wire:SolderWire-0.75sqmm_1x06_P7mm_D1.25mm_OD3.5mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x01_D1.7mm_OD3.9mm +Connector_Wire:SolderWire-1.5sqmm_1x01_D1.7mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x01_D1.7mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x01_D1.7mm_OD3mm +Connector_Wire:SolderWire-1.5sqmm_1x01_D1.7mm_OD3mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x01_D1.7mm_OD3mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x02_P6mm_D1.7mm_OD3mm +Connector_Wire:SolderWire-1.5sqmm_1x02_P6mm_D1.7mm_OD3mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x02_P6mm_D1.7mm_OD3mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x02_P7.8mm_D1.7mm_OD3.9mm +Connector_Wire:SolderWire-1.5sqmm_1x02_P7.8mm_D1.7mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x02_P7.8mm_D1.7mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x03_P6mm_D1.7mm_OD3mm +Connector_Wire:SolderWire-1.5sqmm_1x03_P6mm_D1.7mm_OD3mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x03_P6mm_D1.7mm_OD3mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x03_P7.8mm_D1.7mm_OD3.9mm +Connector_Wire:SolderWire-1.5sqmm_1x03_P7.8mm_D1.7mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x03_P7.8mm_D1.7mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x04_P6mm_D1.7mm_OD3mm +Connector_Wire:SolderWire-1.5sqmm_1x04_P6mm_D1.7mm_OD3mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x04_P6mm_D1.7mm_OD3mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x04_P7.8mm_D1.7mm_OD3.9mm +Connector_Wire:SolderWire-1.5sqmm_1x04_P7.8mm_D1.7mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x04_P7.8mm_D1.7mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x05_P6mm_D1.7mm_OD3mm +Connector_Wire:SolderWire-1.5sqmm_1x05_P6mm_D1.7mm_OD3mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x05_P6mm_D1.7mm_OD3mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x05_P7.8mm_D1.7mm_OD3.9mm +Connector_Wire:SolderWire-1.5sqmm_1x05_P7.8mm_D1.7mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x05_P7.8mm_D1.7mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x06_P6mm_D1.7mm_OD3mm +Connector_Wire:SolderWire-1.5sqmm_1x06_P6mm_D1.7mm_OD3mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x06_P6mm_D1.7mm_OD3mm_Relief2x +Connector_Wire:SolderWire-1.5sqmm_1x06_P7.8mm_D1.7mm_OD3.9mm +Connector_Wire:SolderWire-1.5sqmm_1x06_P7.8mm_D1.7mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1.5sqmm_1x06_P7.8mm_D1.7mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x01_D1.4mm_OD2.7mm +Connector_Wire:SolderWire-1sqmm_1x01_D1.4mm_OD2.7mm_Relief +Connector_Wire:SolderWire-1sqmm_1x01_D1.4mm_OD2.7mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x01_D1.4mm_OD3.9mm +Connector_Wire:SolderWire-1sqmm_1x01_D1.4mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1sqmm_1x01_D1.4mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x02_P5.4mm_D1.4mm_OD2.7mm +Connector_Wire:SolderWire-1sqmm_1x02_P5.4mm_D1.4mm_OD2.7mm_Relief +Connector_Wire:SolderWire-1sqmm_1x02_P5.4mm_D1.4mm_OD2.7mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x02_P7.8mm_D1.4mm_OD3.9mm +Connector_Wire:SolderWire-1sqmm_1x02_P7.8mm_D1.4mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1sqmm_1x02_P7.8mm_D1.4mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x03_P5.4mm_D1.4mm_OD2.7mm +Connector_Wire:SolderWire-1sqmm_1x03_P5.4mm_D1.4mm_OD2.7mm_Relief +Connector_Wire:SolderWire-1sqmm_1x03_P5.4mm_D1.4mm_OD2.7mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x03_P7.8mm_D1.4mm_OD3.9mm +Connector_Wire:SolderWire-1sqmm_1x03_P7.8mm_D1.4mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1sqmm_1x03_P7.8mm_D1.4mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x04_P5.4mm_D1.4mm_OD2.7mm +Connector_Wire:SolderWire-1sqmm_1x04_P5.4mm_D1.4mm_OD2.7mm_Relief +Connector_Wire:SolderWire-1sqmm_1x04_P5.4mm_D1.4mm_OD2.7mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x04_P7.8mm_D1.4mm_OD3.9mm +Connector_Wire:SolderWire-1sqmm_1x04_P7.8mm_D1.4mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1sqmm_1x04_P7.8mm_D1.4mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x05_P5.4mm_D1.4mm_OD2.7mm +Connector_Wire:SolderWire-1sqmm_1x05_P5.4mm_D1.4mm_OD2.7mm_Relief +Connector_Wire:SolderWire-1sqmm_1x05_P5.4mm_D1.4mm_OD2.7mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x05_P7.8mm_D1.4mm_OD3.9mm +Connector_Wire:SolderWire-1sqmm_1x05_P7.8mm_D1.4mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1sqmm_1x05_P7.8mm_D1.4mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x06_P5.4mm_D1.4mm_OD2.7mm +Connector_Wire:SolderWire-1sqmm_1x06_P5.4mm_D1.4mm_OD2.7mm_Relief +Connector_Wire:SolderWire-1sqmm_1x06_P5.4mm_D1.4mm_OD2.7mm_Relief2x +Connector_Wire:SolderWire-1sqmm_1x06_P7.8mm_D1.4mm_OD3.9mm +Connector_Wire:SolderWire-1sqmm_1x06_P7.8mm_D1.4mm_OD3.9mm_Relief +Connector_Wire:SolderWire-1sqmm_1x06_P7.8mm_D1.4mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x01_D2.4mm_OD3.6mm +Connector_Wire:SolderWire-2.5sqmm_1x01_D2.4mm_OD3.6mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x01_D2.4mm_OD3.6mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x01_D2.4mm_OD4.4mm +Connector_Wire:SolderWire-2.5sqmm_1x01_D2.4mm_OD4.4mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x01_D2.4mm_OD4.4mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x02_P7.2mm_D2.4mm_OD3.6mm +Connector_Wire:SolderWire-2.5sqmm_1x02_P7.2mm_D2.4mm_OD3.6mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x02_P7.2mm_D2.4mm_OD3.6mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x02_P8.8mm_D2.4mm_OD4.4mm +Connector_Wire:SolderWire-2.5sqmm_1x02_P8.8mm_D2.4mm_OD4.4mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x02_P8.8mm_D2.4mm_OD4.4mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x03_P7.2mm_D2.4mm_OD3.6mm +Connector_Wire:SolderWire-2.5sqmm_1x03_P7.2mm_D2.4mm_OD3.6mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x03_P7.2mm_D2.4mm_OD3.6mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x03_P8.8mm_D2.4mm_OD4.4mm +Connector_Wire:SolderWire-2.5sqmm_1x03_P8.8mm_D2.4mm_OD4.4mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x03_P8.8mm_D2.4mm_OD4.4mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x04_P7.2mm_D2.4mm_OD3.6mm +Connector_Wire:SolderWire-2.5sqmm_1x04_P7.2mm_D2.4mm_OD3.6mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x04_P7.2mm_D2.4mm_OD3.6mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x04_P8.8mm_D2.4mm_OD4.4mm +Connector_Wire:SolderWire-2.5sqmm_1x04_P8.8mm_D2.4mm_OD4.4mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x04_P8.8mm_D2.4mm_OD4.4mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x05_P7.2mm_D2.4mm_OD3.6mm +Connector_Wire:SolderWire-2.5sqmm_1x05_P7.2mm_D2.4mm_OD3.6mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x05_P7.2mm_D2.4mm_OD3.6mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x05_P8.8mm_D2.4mm_OD4.4mm +Connector_Wire:SolderWire-2.5sqmm_1x05_P8.8mm_D2.4mm_OD4.4mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x05_P8.8mm_D2.4mm_OD4.4mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x06_P7.2mm_D2.4mm_OD3.6mm +Connector_Wire:SolderWire-2.5sqmm_1x06_P7.2mm_D2.4mm_OD3.6mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x06_P7.2mm_D2.4mm_OD3.6mm_Relief2x +Connector_Wire:SolderWire-2.5sqmm_1x06_P8.8mm_D2.4mm_OD4.4mm +Connector_Wire:SolderWire-2.5sqmm_1x06_P8.8mm_D2.4mm_OD4.4mm_Relief +Connector_Wire:SolderWire-2.5sqmm_1x06_P8.8mm_D2.4mm_OD4.4mm_Relief2x +Connector_Wire:SolderWire-2sqmm_1x01_D2mm_OD3.9mm +Connector_Wire:SolderWire-2sqmm_1x01_D2mm_OD3.9mm_Relief +Connector_Wire:SolderWire-2sqmm_1x01_D2mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-2sqmm_1x02_P7.8mm_D2mm_OD3.9mm +Connector_Wire:SolderWire-2sqmm_1x02_P7.8mm_D2mm_OD3.9mm_Relief +Connector_Wire:SolderWire-2sqmm_1x02_P7.8mm_D2mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-2sqmm_1x03_P7.8mm_D2mm_OD3.9mm +Connector_Wire:SolderWire-2sqmm_1x03_P7.8mm_D2mm_OD3.9mm_Relief +Connector_Wire:SolderWire-2sqmm_1x03_P7.8mm_D2mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-2sqmm_1x04_P7.8mm_D2mm_OD3.9mm +Connector_Wire:SolderWire-2sqmm_1x04_P7.8mm_D2mm_OD3.9mm_Relief +Connector_Wire:SolderWire-2sqmm_1x04_P7.8mm_D2mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-2sqmm_1x05_P7.8mm_D2mm_OD3.9mm +Connector_Wire:SolderWire-2sqmm_1x05_P7.8mm_D2mm_OD3.9mm_Relief +Connector_Wire:SolderWire-2sqmm_1x05_P7.8mm_D2mm_OD3.9mm_Relief2x +Connector_Wire:SolderWire-2sqmm_1x06_P7.8mm_D2mm_OD3.9mm +Connector_Wire:SolderWire-2sqmm_1x06_P7.8mm_D2mm_OD3.9mm_Relief +Connector_Wire:SolderWire-2sqmm_1x06_P7.8mm_D2mm_OD3.9mm_Relief2x +Connector_Wire:SolderWirePad_1x01_SMD_1x2mm +Connector_Wire:SolderWirePad_1x01_SMD_5x10mm +Connector_Wuerth:Wuerth_WR-PHD_610004243021_SMD_2x02_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610006243021_SMD_2x03_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610008243021_SMD_2x04_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610010243021_SMD_2x05_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610012243021_SMD_2x06_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610016243021_SMD_2x08_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610018243021_SMD_2x09_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610020243021_SMD_2x10_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610022243021_SMD_2x11_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610024243021_SMD_2x12_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610026243021_SMD_2x13_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610032243021_SMD_2x16_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_610034243021_SMD_2x17_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613004216921_Large_2x02_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61300425721_Standard_2x02_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613006216921_Large_2x03_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61300625721_Standard_2x03_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613008216921_Large_2x04_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61300825721_Standard_2x04_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613010216921_Large_2x05_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61301025721_Standard_2x05_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613012216921_Large_2x06_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61301225721_Standard_2x06_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613016216921_Large_2x08_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61301625721_Standard_2x08_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613018216921_Large_2x09_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613020216921_Large_2x10_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61302025721_Standard_2x10_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613022216921_Large_2x11_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613024216921_Large_2x12_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61302425721_Standard_2x12_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613026216921_Large_2x13_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61302625721_Standard_2x13_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613032216921_Large_2x16_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61303225721_Standard_2x16_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_613034216921_Large_2x17_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-PHD_61303425721_Standard_2x17_P2.54mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800211622_1x02_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800311622_1x03_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800411622_1x04_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800511622_1x05_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800611622_1x06_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800711622_1x07_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800811622_1x08_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64800911622_1x09_P1.50mm_Vertical +Connector_Wuerth:Wuerth_WR-WTB_64801011622_1x10_P1.50mm_Vertical +Converter_ACDC:Converter_ACDC_CUI_PBO-3-Sxx_THT_Vertical +Converter_ACDC:Converter_ACDC_Hahn_HS-400xx_THT +Converter_ACDC:Converter_ACDC_Hi-Link_HLK-10Mxx +Converter_ACDC:Converter_ACDC_Hi-Link_HLK-12MxxA +Converter_ACDC:Converter_ACDC_Hi-Link_HLK-20Mxx +Converter_ACDC:Converter_ACDC_Hi-Link_HLK-2Mxx +Converter_ACDC:Converter_ACDC_Hi-Link_HLK-30Mxx +Converter_ACDC:Converter_ACDC_Hi-Link_HLK-5Mxx +Converter_ACDC:Converter_ACDC_Hi-Link_HLK-PMxx +Converter_ACDC:Converter_ACDC_MeanWell_IRM-02-xx_SMD +Converter_ACDC:Converter_ACDC_MeanWell_IRM-02-xx_THT +Converter_ACDC:Converter_ACDC_MeanWell_IRM-03-xx_SMD +Converter_ACDC:Converter_ACDC_MeanWell_IRM-03-xx_THT +Converter_ACDC:Converter_ACDC_MeanWell_IRM-05-xx_THT +Converter_ACDC:Converter_ACDC_MeanWell_IRM-10-xx_THT +Converter_ACDC:Converter_ACDC_MeanWell_IRM-20-xx_THT +Converter_ACDC:Converter_ACDC_MeanWell_IRM-60-xx_THT +Converter_ACDC:Converter_ACDC_MeanWell_MFM-10-xx_THT +Converter_ACDC:Converter_ACDC_MeanWell_MFM-15-xx_THT +Converter_ACDC:Converter_ACDC_Murata_BAC05SxxDC_THT +Converter_ACDC:Converter_ACDC_RECOM_RAC01-xxSGB_THT +Converter_ACDC:Converter_ACDC_RECOM_RAC04-xxSGx_THT +Converter_ACDC:Converter_ACDC_RECOM_RAC05-xxSK_THT +Converter_ACDC:Converter_ACDC_Recom_RAC20-xxDK_THT +Converter_ACDC:Converter_ACDC_Recom_RAC20-xxSK_THT +Converter_ACDC:Converter_ACDC_TRACO_TMF_051xx_THT +Converter_ACDC:Converter_ACDC_TRACO_TMF_101xx_THT +Converter_ACDC:Converter_ACDC_TRACO_TMF_201xx_THT +Converter_ACDC:Converter_ACDC_TRACO_TMF_301xx_THT +Converter_ACDC:Converter_ACDC_TRACO_TMG-15_THT +Converter_ACDC:Converter_ACDC_TRACO_TMLM-04_THT +Converter_ACDC:Converter_ACDC_TRACO_TMLM-05_THT +Converter_ACDC:Converter_ACDC_TRACO_TMLM-10-20_THT +Converter_ACDC:Converter_ACDC_TRACO_TPP-15-1xx-D_THT +Converter_ACDC:Converter_ACDC_Vigortronix_VTX-214-010-xxx_THT +Converter_ACDC:Converter_ACDC_Vigortronix_VTX-214-015-1xx_THT +Converter_ACDC:Converter_ACDC_ZETTLER_ZPI03Sxx00WC_THT +Converter_DCDC:Converter_DCDC_Artesyn_ATA_SMD +Converter_DCDC:Converter_DCDC_Bothhand_CFUDxxxx_THT +Converter_DCDC:Converter_DCDC_Bothhand_CFUSxxxxEH_THT +Converter_DCDC:Converter_DCDC_Bothhand_CFUSxxxx_THT +Converter_DCDC:Converter_DCDC_Cincon_EC5BExx_Dual_THT +Converter_DCDC:Converter_DCDC_Cincon_EC5BExx_Single_THT +Converter_DCDC:Converter_DCDC_Cincon_EC6Cxx_Dual-Triple_THT +Converter_DCDC:Converter_DCDC_Cincon_EC6Cxx_Single_THT +Converter_DCDC:Converter_DCDC_Cyntec_MUN12AD01-SH +Converter_DCDC:Converter_DCDC_Cyntec_MUN12AD03-SH +Converter_DCDC:Converter_DCDC_MeanWell_NID30_THT +Converter_DCDC:Converter_DCDC_MeanWell_NID60_THT +Converter_DCDC:Converter_DCDC_MeanWell_NSD10_THT +Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxx3C_THT +Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxDC_THT +Converter_DCDC:Converter_DCDC_Murata_CRE1xxxxxxSC_THT +Converter_DCDC:Converter_DCDC_Murata_MEE1SxxxxSC_THT +Converter_DCDC:Converter_DCDC_Murata_MEE3SxxxxSC_THT +Converter_DCDC:Converter_DCDC_muRata_MEJ1DxxxxSC_THT +Converter_DCDC:Converter_DCDC_muRata_MEJ1SxxxxSC_THT +Converter_DCDC:Converter_DCDC_Murata_MGJ2DxxxxxxSC_THT +Converter_DCDC:Converter_DCDC_Murata_MGJ3 +Converter_DCDC:Converter_DCDC_Murata_MYRxP +Converter_DCDC:Converter_DCDC_Murata_NCS1SxxxxSC_THT +Converter_DCDC:Converter_DCDC_Murata_NMAxxxxDC_THT +Converter_DCDC:Converter_DCDC_Murata_NMAxxxxSC_THT +Converter_DCDC:Converter_DCDC_Murata_NXExSxxxxMC_SMD +Converter_DCDC:Converter_DCDC_Murata_OKI-78SR_Horizontal +Converter_DCDC:Converter_DCDC_Murata_OKI-78SR_Vertical +Converter_DCDC:Converter_DCDC_RECOM_R-78B-2.0_THT +Converter_DCDC:Converter_DCDC_RECOM_R-78E-0.5_THT +Converter_DCDC:Converter_DCDC_RECOM_R-78HB-0.5L_THT +Converter_DCDC:Converter_DCDC_RECOM_R-78HB-0.5_THT +Converter_DCDC:Converter_DCDC_RECOM_R-78S-0.1_THT +Converter_DCDC:Converter_DCDC_RECOM_R5xxxDA_THT +Converter_DCDC:Converter_DCDC_RECOM_R5xxxPA_THT +Converter_DCDC:Converter_DCDC_RECOM_RCD-24_THT +Converter_DCDC:Converter_DCDC_RECOM_RPA60-xxxxSFW +Converter_DCDC:Converter_DCDC_RECOM_RPMx.x-x.0 +Converter_DCDC:Converter_DCDC_Silvertel_Ag54xx +Converter_DCDC:Converter_DCDC_Silvertel_Ag5810 +Converter_DCDC:Converter_DCDC_Silvertel_Ag99xxLP_THT +Converter_DCDC:Converter_DCDC_TRACO_TBA1-xxxxE_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TBA1-xxxxE_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TBA2-xxxx_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TBA2-xxxx_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TDN_5-xxxxWISM_SMD +Converter_DCDC:Converter_DCDC_TRACO_TDN_5-xxxxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_TDU1-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TEA1-xxxxE_THT +Converter_DCDC:Converter_DCDC_TRACO_TEA1-xxxxHI_THT +Converter_DCDC:Converter_DCDC_TRACO_TEA1-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TEC3-24xxUI_THT +Converter_DCDC:Converter_DCDC_TRACO_TEL12-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN10-110xxWIRH_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN10-xxxx_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN10-xxxx_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN10-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN20-110xxWIRH_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN20-xxxx-N4_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN20-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TEN40-110xxWIRH_THT +Converter_DCDC:Converter_DCDC_TRACO_THB10-xxxx_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_THB10-xxxx_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_THD_15-xxxxWIN_THT +Converter_DCDC:Converter_DCDC_TRACO_THN30-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_THR40-72xxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_TMA-05xxD_12xxD_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TMA-05xxS_12xxS_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TMA-15xxD_24xxD_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TMA-15xxS_24xxS_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TME_03xxS_05xxS_12xxS_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TME_24xxS_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR-1-xxxx_Dual_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR-1-xxxx_Single_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR-1SM_SMD +Converter_DCDC:Converter_DCDC_TRACO_TMR-2xxxxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TMR4-xxxxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_TMU3-05xx_12xx_THT +Converter_DCDC:Converter_DCDC_TRACO_TMU3-24xx_THT +Converter_DCDC:Converter_DCDC_TRACO_TOS06-05SIL_THT +Converter_DCDC:Converter_DCDC_TRACO_TOS06-12SIL_THT +Converter_DCDC:Converter_DCDC_TRACO_TRA3-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TRI1-xxxx_THT +Converter_DCDC:Converter_DCDC_TRACO_TSR-1_THT +Converter_DCDC:Converter_DCDC_TRACO_TSR0.6-48xxWI_TSR0.6-48xxxWI_THT +Converter_DCDC:Converter_DCDC_TRACO_TSR1-xxxxE_THT +Converter_DCDC:Converter_DCDC_TRACO_TSR2-24xxN_TSR2-24xxxN_THT +Converter_DCDC:Converter_DCDC_TRACO_TSR2-xxxx_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IA48xxD_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IA48xxS_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IAxxxxD_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IAxxxxS_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IHxxxxDH_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IHxxxxD_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IHxxxxSH_THT +Converter_DCDC:Converter_DCDC_XP_POWER-IHxxxxS_THT +Converter_DCDC:Converter_DCDC_XP_POWER-ISU02_SMD +Converter_DCDC:Converter_DCDC_XP_POWER-ITQxxxxS-H_THT +Converter_DCDC:Converter_DCDC_XP_POWER-ITXxxxxSA_THT +Converter_DCDC:Converter_DCDC_XP_POWER-ITxxxxxS_THT +Converter_DCDC:Converter_DCDC_XP_POWER_JTDxxxxxxx_THT +Converter_DCDC:Converter_DCDC_XP_POWER_JTExxxxDxx_THT +Crystal:Crystal_AT310_D3.0mm_L10.0mm_Horizontal +Crystal:Crystal_AT310_D3.0mm_L10.0mm_Horizontal_1EP_style1 +Crystal:Crystal_AT310_D3.0mm_L10.0mm_Horizontal_1EP_style2 +Crystal:Crystal_AT310_D3.0mm_L10.0mm_Vertical +Crystal:Crystal_C26-LF_D2.1mm_L6.5mm_Horizontal +Crystal:Crystal_C26-LF_D2.1mm_L6.5mm_Horizontal_1EP_style1 +Crystal:Crystal_C26-LF_D2.1mm_L6.5mm_Horizontal_1EP_style2 +Crystal:Crystal_C26-LF_D2.1mm_L6.5mm_Vertical +Crystal:Crystal_C38-LF_D3.0mm_L8.0mm_Horizontal +Crystal:Crystal_C38-LF_D3.0mm_L8.0mm_Horizontal_1EP_style1 +Crystal:Crystal_C38-LF_D3.0mm_L8.0mm_Horizontal_1EP_style2 +Crystal:Crystal_C38-LF_D3.0mm_L8.0mm_Vertical +Crystal:Crystal_DS10_D1.0mm_L4.3mm_Horizontal +Crystal:Crystal_DS10_D1.0mm_L4.3mm_Horizontal_1EP_style1 +Crystal:Crystal_DS10_D1.0mm_L4.3mm_Horizontal_1EP_style2 +Crystal:Crystal_DS10_D1.0mm_L4.3mm_Vertical +Crystal:Crystal_DS15_D1.5mm_L5.0mm_Horizontal +Crystal:Crystal_DS15_D1.5mm_L5.0mm_Horizontal_1EP_style1 +Crystal:Crystal_DS15_D1.5mm_L5.0mm_Horizontal_1EP_style2 +Crystal:Crystal_DS15_D1.5mm_L5.0mm_Vertical +Crystal:Crystal_DS26_D2.0mm_L6.0mm_Horizontal +Crystal:Crystal_DS26_D2.0mm_L6.0mm_Horizontal_1EP_style1 +Crystal:Crystal_DS26_D2.0mm_L6.0mm_Horizontal_1EP_style2 +Crystal:Crystal_DS26_D2.0mm_L6.0mm_Vertical +Crystal:Crystal_HC18-U_Horizontal +Crystal:Crystal_HC18-U_Horizontal_1EP_style1 +Crystal:Crystal_HC18-U_Horizontal_1EP_style2 +Crystal:Crystal_HC18-U_Vertical +Crystal:Crystal_HC33-U_Horizontal +Crystal:Crystal_HC33-U_Horizontal_1EP_style1 +Crystal:Crystal_HC33-U_Horizontal_1EP_style2 +Crystal:Crystal_HC33-U_Vertical +Crystal:Crystal_HC35-U +Crystal:Crystal_HC49-4H_Vertical +Crystal:Crystal_HC49-U-3Pin_Vertical +Crystal:Crystal_HC49-U_Horizontal +Crystal:Crystal_HC49-U_Horizontal_1EP_style1 +Crystal:Crystal_HC49-U_Horizontal_1EP_style2 +Crystal:Crystal_HC49-U_Vertical +Crystal:Crystal_HC50_Horizontal +Crystal:Crystal_HC50_Horizontal_1EP_style1 +Crystal:Crystal_HC50_Horizontal_1EP_style2 +Crystal:Crystal_HC50_Vertical +Crystal:Crystal_HC51-U_Vertical +Crystal:Crystal_HC51_Horizontal +Crystal:Crystal_HC51_Horizontal_1EP_style1 +Crystal:Crystal_HC51_Horizontal_1EP_style2 +Crystal:Crystal_HC52-6mm_Horizontal +Crystal:Crystal_HC52-6mm_Horizontal_1EP_style1 +Crystal:Crystal_HC52-6mm_Horizontal_1EP_style2 +Crystal:Crystal_HC52-6mm_Vertical +Crystal:Crystal_HC52-8mm_Horizontal +Crystal:Crystal_HC52-8mm_Horizontal_1EP_style1 +Crystal:Crystal_HC52-8mm_Horizontal_1EP_style2 +Crystal:Crystal_HC52-8mm_Vertical +Crystal:Crystal_HC52-U-3Pin_Vertical +Crystal:Crystal_HC52-U_Horizontal +Crystal:Crystal_HC52-U_Horizontal_1EP_style1 +Crystal:Crystal_HC52-U_Horizontal_1EP_style2 +Crystal:Crystal_HC52-U_Vertical +Crystal:Crystal_Round_D1.0mm_Vertical +Crystal:Crystal_Round_D1.5mm_Vertical +Crystal:Crystal_Round_D2.0mm_Vertical +Crystal:Crystal_Round_D3.0mm_Vertical +Crystal:Crystal_SMD_0603-2Pin_6.0x3.5mm +Crystal:Crystal_SMD_0603-2Pin_6.0x3.5mm_HandSoldering +Crystal:Crystal_SMD_0603-4Pin_6.0x3.5mm +Crystal:Crystal_SMD_0603-4Pin_6.0x3.5mm_HandSoldering +Crystal:Crystal_SMD_1210-4Pin_1.2x1.0mm +Crystal:Crystal_SMD_2012-2Pin_2.0x1.2mm +Crystal:Crystal_SMD_2012-2Pin_2.0x1.2mm_HandSoldering +Crystal:Crystal_SMD_2016-4Pin_2.0x1.6mm +Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm +Crystal:Crystal_SMD_3215-2Pin_3.2x1.5mm +Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm_HandSoldering +Crystal:Crystal_SMD_5032-2Pin_5.0x3.2mm +Crystal:Crystal_SMD_5032-2Pin_5.0x3.2mm_HandSoldering +Crystal:Crystal_SMD_5032-4Pin_5.0x3.2mm +Crystal:Crystal_SMD_7050-2Pin_7.0x5.0mm +Crystal:Crystal_SMD_7050-2Pin_7.0x5.0mm_HandSoldering +Crystal:Crystal_SMD_7050-4Pin_7.0x5.0mm +Crystal:Crystal_SMD_Abracon_ABM10-4Pin_2.5x2.0mm +Crystal:Crystal_SMD_Abracon_ABM3-2Pin_5.0x3.2mm +Crystal:Crystal_SMD_Abracon_ABM3-2Pin_5.0x3.2mm_HandSoldering +Crystal:Crystal_SMD_Abracon_ABM3B-4Pin_5.0x3.2mm +Crystal:Crystal_SMD_Abracon_ABM3C-4Pin_5.0x3.2mm +Crystal:Crystal_SMD_Abracon_ABM7-2Pin_6.0x3.5mm +Crystal:Crystal_SMD_Abracon_ABM8AIG-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_Abracon_ABM8G-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_Abracon_ABS25-4Pin_8.0x3.8mm +Crystal:Crystal_SMD_ECS_CSM3X-2Pin_7.6x4.1mm +Crystal:Crystal_SMD_EuroQuartz_EQ161-2Pin_3.2x1.5mm +Crystal:Crystal_SMD_EuroQuartz_EQ161-2Pin_3.2x1.5mm_HandSoldering +Crystal:Crystal_SMD_EuroQuartz_MJ-4Pin_5.0x3.2mm +Crystal:Crystal_SMD_EuroQuartz_MJ-4Pin_5.0x3.2mm_HandSoldering +Crystal:Crystal_SMD_EuroQuartz_MQ-4Pin_7.0x5.0mm +Crystal:Crystal_SMD_EuroQuartz_MQ-4Pin_7.0x5.0mm_HandSoldering +Crystal:Crystal_SMD_EuroQuartz_MQ2-2Pin_7.0x5.0mm +Crystal:Crystal_SMD_EuroQuartz_MQ2-2Pin_7.0x5.0mm_HandSoldering +Crystal:Crystal_SMD_EuroQuartz_MT-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_EuroQuartz_MT-4Pin_3.2x2.5mm_HandSoldering +Crystal:Crystal_SMD_EuroQuartz_X22-4Pin_2.5x2.0mm +Crystal:Crystal_SMD_EuroQuartz_X22-4Pin_2.5x2.0mm_HandSoldering +Crystal:Crystal_SMD_FOX_FE-2Pin_7.5x5.0mm +Crystal:Crystal_SMD_FOX_FE-2Pin_7.5x5.0mm_HandSoldering +Crystal:Crystal_SMD_FOX_FQ7050-2Pin_7.0x5.0mm +Crystal:Crystal_SMD_FOX_FQ7050-2Pin_7.0x5.0mm_HandSoldering +Crystal:Crystal_SMD_FOX_FQ7050-4Pin_7.0x5.0mm +Crystal:Crystal_SMD_FrontierElectronics_FM206 +Crystal:Crystal_SMD_G8-2Pin_3.2x1.5mm +Crystal:Crystal_SMD_G8-2Pin_3.2x1.5mm_HandSoldering +Crystal:Crystal_SMD_HC49-SD +Crystal:Crystal_SMD_HC49-SD_HandSoldering +Crystal:Crystal_SMD_MicroCrystal_CC1V-T1A-2Pin_8.0x3.7mm +Crystal:Crystal_SMD_MicroCrystal_CC1V-T1A-2Pin_8.0x3.7mm_HandSoldering +Crystal:Crystal_SMD_MicroCrystal_CC4V-T1A-2Pin_5.0x1.9mm +Crystal:Crystal_SMD_MicroCrystal_CC4V-T1A-2Pin_5.0x1.9mm_HandSoldering +Crystal:Crystal_SMD_MicroCrystal_CC5V-T1A-2Pin_4.1x1.5mm +Crystal:Crystal_SMD_MicroCrystal_CC5V-T1A-2Pin_4.1x1.5mm_HandSoldering +Crystal:Crystal_SMD_MicroCrystal_CC7V-T1A-2Pin_3.2x1.5mm +Crystal:Crystal_SMD_MicroCrystal_CC7V-T1A-2Pin_3.2x1.5mm_HandSoldering +Crystal:Crystal_SMD_MicroCrystal_CC8V-T1A-2Pin_2.0x1.2mm +Crystal:Crystal_SMD_MicroCrystal_CC8V-T1A-2Pin_2.0x1.2mm_HandSoldering +Crystal:Crystal_SMD_MicroCrystal_CM9V-T1A-2Pin_1.6x1.0mm +Crystal:Crystal_SMD_MicroCrystal_CM9V-T1A-2Pin_1.6x1.0mm_HandSoldering +Crystal:Crystal_SMD_MicroCrystal_MS1V-T1K +Crystal:Crystal_SMD_MicroCrystal_MS3V-T1R +Crystal:Crystal_SMD_Qantek_QC5CB-2Pin_5x3.2mm +Crystal:Crystal_SMD_SeikoEpson_FA128-4Pin_2.0x1.6mm +Crystal:Crystal_SMD_SeikoEpson_FA238-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_SeikoEpson_FA238-4Pin_3.2x2.5mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_FA238V-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_SeikoEpson_FA238V-4Pin_3.2x2.5mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MA406-4Pin_11.7x4.0mm +Crystal:Crystal_SMD_SeikoEpson_MA406-4Pin_11.7x4.0mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MA505-2Pin_12.7x5.1mm +Crystal:Crystal_SMD_SeikoEpson_MA505-2Pin_12.7x5.1mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MA506-4Pin_12.7x5.1mm +Crystal:Crystal_SMD_SeikoEpson_MA506-4Pin_12.7x5.1mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MC146-4Pin_6.7x1.5mm +Crystal:Crystal_SMD_SeikoEpson_MC146-4Pin_6.7x1.5mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MC156-4Pin_7.1x2.5mm +Crystal:Crystal_SMD_SeikoEpson_MC156-4Pin_7.1x2.5mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MC306-4Pin_8.0x3.2mm +Crystal:Crystal_SMD_SeikoEpson_MC306-4Pin_8.0x3.2mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MC405-2Pin_9.6x4.1mm +Crystal:Crystal_SMD_SeikoEpson_MC405-2Pin_9.6x4.1mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_MC406-4Pin_9.6x4.1mm +Crystal:Crystal_SMD_SeikoEpson_MC406-4Pin_9.6x4.1mm_HandSoldering +Crystal:Crystal_SMD_SeikoEpson_TSX3225-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_SeikoEpson_TSX3225-4Pin_3.2x2.5mm_HandSoldering +Crystal:Crystal_SMD_TXC_7A-2Pin_5x3.2mm +Crystal:Crystal_SMD_TXC_7M-4Pin_3.2x2.5mm +Crystal:Crystal_SMD_TXC_7M-4Pin_3.2x2.5mm_HandSoldering +Crystal:Crystal_SMD_TXC_9HT11-2Pin_2.0x1.2mm +Crystal:Crystal_SMD_TXC_9HT11-2Pin_2.0x1.2mm_HandSoldering +Crystal:Crystal_SMD_TXC_AX_8045-2Pin_8.0x4.5mm +Crystal:Resonator-2Pin_W10.0mm_H5.0mm +Crystal:Resonator-2Pin_W6.0mm_H3.0mm +Crystal:Resonator-2Pin_W7.0mm_H2.5mm +Crystal:Resonator-2Pin_W8.0mm_H3.5mm +Crystal:Resonator-3Pin_W10.0mm_H5.0mm +Crystal:Resonator-3Pin_W6.0mm_H3.0mm +Crystal:Resonator-3Pin_W7.0mm_H2.5mm +Crystal:Resonator-3Pin_W8.0mm_H3.5mm +Crystal:Resonator_Murata_CSTLSxxxG-3Pin_W8.0mm_H3.0mm +Crystal:Resonator_Murata_CSTLSxxxX-3Pin_W5.5mm_H3.0mm +Crystal:Resonator_Murata_DSN6-3Pin_W7.0mm_H2.5mm +Crystal:Resonator_Murata_DSS6-3Pin_W7.0mm_H2.5mm +Crystal:Resonator_SMD-3Pin_7.2x3.0mm +Crystal:Resonator_SMD-3Pin_7.2x3.0mm_HandSoldering +Crystal:Resonator_SMD_Murata_CDSCB-2Pin_4.5x2.0mm +Crystal:Resonator_SMD_Murata_CDSCB-2Pin_4.5x2.0mm_HandSoldering +Crystal:Resonator_SMD_Murata_CSTCR_4.5x2x1.15mm +Crystal:Resonator_SMD_Murata_CSTxExxV-3Pin_3.0x1.1mm +Crystal:Resonator_SMD_Murata_CSTxExxV-3Pin_3.0x1.1mm_HandSoldering +Crystal:Resonator_SMD_Murata_SFECV-3Pin_6.9x2.9mm +Crystal:Resonator_SMD_Murata_SFECV-3Pin_6.9x2.9mm_HandSoldering +Crystal:Resonator_SMD_Murata_SFSKA-3Pin_7.9x3.8mm +Crystal:Resonator_SMD_Murata_SFSKA-3Pin_7.9x3.8mm_HandSoldering +Crystal:Resonator_SMD_Murata_TPSKA-3Pin_7.9x3.8mm +Crystal:Resonator_SMD_Murata_TPSKA-3Pin_7.9x3.8mm_HandSoldering +Diode_SMD:Diode_Bridge_Bourns_CD-DF4xxS +Diode_SMD:Diode_Bridge_Diotec_ABS +Diode_SMD:Diode_Bridge_Diotec_MicroDil_3.0x3.0x1.8mm +Diode_SMD:Diode_Bridge_Diotec_SO-DIL-Slim +Diode_SMD:Diode_Bridge_OnSemi_SDIP-4L +Diode_SMD:Diode_Bridge_Vishay_DFS +Diode_SMD:Diode_Bridge_Vishay_DFSFlat +Diode_SMD:Diode_Bridge_Vishay_MBLS +Diode_SMD:D_01005_0402Metric +Diode_SMD:D_01005_0402Metric_Pad0.57x0.30mm_HandSolder +Diode_SMD:D_0201_0603Metric +Diode_SMD:D_0201_0603Metric_Pad0.64x0.40mm_HandSolder +Diode_SMD:D_0402_1005Metric +Diode_SMD:D_0402_1005Metric_Pad0.77x0.64mm_HandSolder +Diode_SMD:D_0603_1608Metric +Diode_SMD:D_0603_1608Metric_Pad1.05x0.95mm_HandSolder +Diode_SMD:D_0805_2012Metric +Diode_SMD:D_0805_2012Metric_Pad1.15x1.40mm_HandSolder +Diode_SMD:D_1206_3216Metric +Diode_SMD:D_1206_3216Metric_Pad1.42x1.75mm_HandSolder +Diode_SMD:D_1210_3225Metric +Diode_SMD:D_1210_3225Metric_Pad1.42x2.65mm_HandSolder +Diode_SMD:D_1812_4532Metric +Diode_SMD:D_1812_4532Metric_Pad1.30x3.40mm_HandSolder +Diode_SMD:D_2010_5025Metric +Diode_SMD:D_2010_5025Metric_Pad1.52x2.65mm_HandSolder +Diode_SMD:D_2114_3652Metric +Diode_SMD:D_2114_3652Metric_Pad1.85x3.75mm_HandSolder +Diode_SMD:D_2512_6332Metric +Diode_SMD:D_2512_6332Metric_Pad1.52x3.35mm_HandSolder +Diode_SMD:D_3220_8050Metric +Diode_SMD:D_3220_8050Metric_Pad2.65x5.15mm_HandSolder +Diode_SMD:D_MELF-RM10_Universal_Handsoldering +Diode_SMD:D_MELF +Diode_SMD:D_MELF_Handsoldering +Diode_SMD:D_MicroMELF +Diode_SMD:D_MicroMELF_Handsoldering +Diode_SMD:D_MicroSMP_AK +Diode_SMD:D_MicroSMP_KA +Diode_SMD:D_MiniMELF +Diode_SMD:D_MiniMELF_Handsoldering +Diode_SMD:D_PowerDI-123 +Diode_SMD:D_PowerDI-5 +Diode_SMD:D_Powermite2_AK +Diode_SMD:D_Powermite2_KA +Diode_SMD:D_Powermite3 +Diode_SMD:D_Powermite_AK +Diode_SMD:D_Powermite_KA +Diode_SMD:D_QFN_3.3x3.3mm_P0.65mm +Diode_SMD:D_SC-80 +Diode_SMD:D_SC-80_HandSoldering +Diode_SMD:D_SMA-SMB_Universal_Handsoldering +Diode_SMD:D_SMA +Diode_SMD:D_SMA_Handsoldering +Diode_SMD:D_SMB-SMC_Universal_Handsoldering +Diode_SMD:D_SMB +Diode_SMD:D_SMB_Handsoldering +Diode_SMD:D_SMB_Modified +Diode_SMD:D_SMC-RM10_Universal_Handsoldering +Diode_SMD:D_SMC +Diode_SMD:D_SMC_Handsoldering +Diode_SMD:D_SMF +Diode_SMD:D_SMP_DO-220AA +Diode_SMD:D_SOD-110 +Diode_SMD:D_SOD-123 +Diode_SMD:D_SOD-123F +Diode_SMD:D_SOD-128 +Diode_SMD:D_SOD-323 +Diode_SMD:D_SOD-323F +Diode_SMD:D_SOD-323_HandSoldering +Diode_SMD:D_SOD-523 +Diode_SMD:D_SOD-882 +Diode_SMD:D_SOD-882D +Diode_SMD:D_SOD-923 +Diode_SMD:D_TUMD2 +Diode_SMD:Infineon_SG-WLL-2-3_0.58x0.28_P0.36mm +Diode_SMD:Littelfuse_PolyZen-LS +Diode_SMD:Nexperia_CFP3_SOD-123W +Diode_SMD:Nexperia_DSN0603-2_0.6x0.3mm_P0.4mm +Diode_SMD:Nexperia_DSN1608-2_1.6x0.8mm +Diode_SMD:OnSemi_751EP_SOIC-4_3.9x4.725mm_P2.54mm +Diode_SMD:ST_D_SMC +Diode_SMD:ST_QFN-2L_1.6x1.0mm +Diode_SMD:Vishay_SMPA +Diode_THT:Diode_Bridge_15.1x15.1x6.3mm_P10.9mm +Diode_THT:Diode_Bridge_15.2x15.2x6.3mm_P10.9mm +Diode_THT:Diode_Bridge_15.7x15.7x6.3mm_P10.8mm +Diode_THT:Diode_Bridge_16.7x16.7x6.3mm_P10.8mm +Diode_THT:Diode_Bridge_19.0x19.0x6.8mm_P12.7mm +Diode_THT:Diode_Bridge_19.0x3.5x10.0mm_P5.0mm +Diode_THT:Diode_Bridge_28.6x28.6x7.3mm_P18.0mm_P11.6mm +Diode_THT:Diode_Bridge_32.0x5.6x17.0mm_P10.0mm_P7.5mm +Diode_THT:Diode_Bridge_Comchip_SCVB-L +Diode_THT:Diode_Bridge_DIGITRON_KBPC_T +Diode_THT:Diode_Bridge_DIP-4_W5.08mm_P2.54mm +Diode_THT:Diode_Bridge_DIP-4_W7.62mm_P5.08mm +Diode_THT:Diode_Bridge_GeneSiC_KBPC_T +Diode_THT:Diode_Bridge_GeneSiC_KBPC_W +Diode_THT:Diode_Bridge_IXYS_GUFP +Diode_THT:Diode_Bridge_Round_D8.9mm +Diode_THT:Diode_Bridge_Round_D9.0mm +Diode_THT:Diode_Bridge_Round_D9.8mm +Diode_THT:Diode_Bridge_Vishay_GBL +Diode_THT:Diode_Bridge_Vishay_GBU +Diode_THT:Diode_Bridge_Vishay_KBL +Diode_THT:Diode_Bridge_Vishay_KBPC1 +Diode_THT:Diode_Bridge_Vishay_KBPC6 +Diode_THT:Diode_Bridge_Vishay_KBPM +Diode_THT:Diode_Bridge_Vishay_KBU +Diode_THT:D_5KPW_P12.70mm_Horizontal +Diode_THT:D_5KPW_P7.62mm_Vertical_AnodeUp +Diode_THT:D_5KPW_P7.62mm_Vertical_KathodeUp +Diode_THT:D_5KP_P10.16mm_Horizontal +Diode_THT:D_5KP_P12.70mm_Horizontal +Diode_THT:D_5KP_P7.62mm_Vertical_AnodeUp +Diode_THT:D_5KP_P7.62mm_Vertical_KathodeUp +Diode_THT:D_5W_P10.16mm_Horizontal +Diode_THT:D_5W_P12.70mm_Horizontal +Diode_THT:D_5W_P5.08mm_Vertical_AnodeUp +Diode_THT:D_5W_P5.08mm_Vertical_KathodeUp +Diode_THT:D_A-405_P10.16mm_Horizontal +Diode_THT:D_A-405_P12.70mm_Horizontal +Diode_THT:D_A-405_P2.54mm_Vertical_AnodeUp +Diode_THT:D_A-405_P2.54mm_Vertical_KathodeUp +Diode_THT:D_A-405_P5.08mm_Vertical_AnodeUp +Diode_THT:D_A-405_P5.08mm_Vertical_KathodeUp +Diode_THT:D_A-405_P7.62mm_Horizontal +Diode_THT:D_DO-15_P10.16mm_Horizontal +Diode_THT:D_DO-15_P12.70mm_Horizontal +Diode_THT:D_DO-15_P15.24mm_Horizontal +Diode_THT:D_DO-15_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-15_P2.54mm_Vertical_KathodeUp +Diode_THT:D_DO-15_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-15_P3.81mm_Vertical_KathodeUp +Diode_THT:D_DO-15_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-15_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-201AD_P12.70mm_Horizontal +Diode_THT:D_DO-201AD_P15.24mm_Horizontal +Diode_THT:D_DO-201AD_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-201AD_P3.81mm_Vertical_KathodeUp +Diode_THT:D_DO-201AD_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-201AD_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-201AE_P12.70mm_Horizontal +Diode_THT:D_DO-201AE_P15.24mm_Horizontal +Diode_THT:D_DO-201AE_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-201AE_P3.81mm_Vertical_KathodeUp +Diode_THT:D_DO-201AE_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-201AE_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-201_P12.70mm_Horizontal +Diode_THT:D_DO-201_P15.24mm_Horizontal +Diode_THT:D_DO-201_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-201_P3.81mm_Vertical_KathodeUp +Diode_THT:D_DO-201_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-201_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-247_Horizontal_TabDown +Diode_THT:D_DO-247_Horizontal_TabUp +Diode_THT:D_DO-247_Vertical +Diode_THT:D_DO-27_P12.70mm_Horizontal +Diode_THT:D_DO-27_P15.24mm_Horizontal +Diode_THT:D_DO-27_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-27_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-34_SOD68_P10.16mm_Horizontal +Diode_THT:D_DO-34_SOD68_P12.70mm_Horizontal +Diode_THT:D_DO-34_SOD68_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-34_SOD68_P2.54mm_Vertical_KathodeUp +Diode_THT:D_DO-34_SOD68_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-34_SOD68_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-34_SOD68_P7.62mm_Horizontal +Diode_THT:D_DO-35_SOD27_P10.16mm_Horizontal +Diode_THT:D_DO-35_SOD27_P12.70mm_Horizontal +Diode_THT:D_DO-35_SOD27_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-35_SOD27_P2.54mm_Vertical_KathodeUp +Diode_THT:D_DO-35_SOD27_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-35_SOD27_P3.81mm_Vertical_KathodeUp +Diode_THT:D_DO-35_SOD27_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-35_SOD27_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal +Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal +Diode_THT:D_DO-41_SOD81_P12.70mm_Horizontal +Diode_THT:D_DO-41_SOD81_P2.54mm_Vertical_AnodeUp +Diode_THT:D_DO-41_SOD81_P2.54mm_Vertical_KathodeUp +Diode_THT:D_DO-41_SOD81_P3.81mm_Vertical_AnodeUp +Diode_THT:D_DO-41_SOD81_P3.81mm_Vertical_KathodeUp +Diode_THT:D_DO-41_SOD81_P5.08mm_Vertical_AnodeUp +Diode_THT:D_DO-41_SOD81_P5.08mm_Vertical_KathodeUp +Diode_THT:D_DO-41_SOD81_P7.62mm_Horizontal +Diode_THT:D_P600_R-6_P12.70mm_Horizontal +Diode_THT:D_P600_R-6_P20.00mm_Horizontal +Diode_THT:D_P600_R-6_P7.62mm_Vertical_AnodeUp +Diode_THT:D_P600_R-6_P7.62mm_Vertical_KathodeUp +Diode_THT:D_T-1_P10.16mm_Horizontal +Diode_THT:D_T-1_P12.70mm_Horizontal +Diode_THT:D_T-1_P2.54mm_Vertical_AnodeUp +Diode_THT:D_T-1_P2.54mm_Vertical_KathodeUp +Diode_THT:D_T-1_P5.08mm_Horizontal +Display:Adafruit_SSD1306 +Display:Adafruit_SSD1306_No_Mounting_Holes +Display:AG12864E +Display:CR2013-MI2120 +Display:EA-eDIP128B-XXX +Display:EA_DOGL128-6 +Display:EA_DOGM128-6 +Display:EA_DOGS104X-A +Display:EA_DOGXL160-7 +Display:EA_DOGXL160-7_Backlight +Display:EA_eDIP160-XXX +Display:EA_eDIP240-XXX +Display:EA_eDIP320X-XXX +Display:EA_eDIPTFT32-XXX +Display:EA_eDIPTFT43-ATC +Display:EA_eDIPTFT43-XXX +Display:EA_eDIPTFT57-XXX +Display:EA_eDIPTFT70-ATC +Display:EA_eDIPTFT70-XXX +Display:EA_T123X-I2C +Display:ER-OLED0.42-1W_Folded +Display:ERM19264 +Display:HDSM-441B_HDSM-443B +Display:HDSM-541B_HDSM-543B +Display:HDSP-4830 +Display:HDSP-4832 +Display:HDSP-4836 +Display:HDSP-4840 +Display:HDSP-4850 +Display:HDSP-48xx +Display:HLCP-J100 +Display:HY1602E +Display:LCD-016N002L +Display:LM16255 +Display:NHD-0420H1Z +Display:NHD-C0220BiZ-FSRGB +Display:NHD-C0220BiZ +Display:NHD-C12832A1Z-FSRGB +Display:OLED-128O064D +Display:RC1602A +Display:WC1602A +Display_7Segment:7SEGMENT-LED__HDSM531_HDSM533_SMD +Display_7Segment:7SegmentLED_LTS6760_LTS6780 +Display_7Segment:AD-121F2 +Display_7Segment:AFF_2x7SEG-DIGIT_10mm +Display_7Segment:CA56-12CGKWA +Display_7Segment:CA56-12EWA +Display_7Segment:CA56-12SEKWA +Display_7Segment:CA56-12SRWA +Display_7Segment:CA56-12SURKWA +Display_7Segment:CA56-12SYKWA +Display_7Segment:CC56-12GWA +Display_7Segment:CC56-12YWA +Display_7Segment:D1X8K +Display_7Segment:DA04-11CGKWA +Display_7Segment:DA04-11SEKWA +Display_7Segment:DA04-11SURKWA +Display_7Segment:DA04-11SYKWA +Display_7Segment:DA56-11CGKWA +Display_7Segment:DA56-11SEKWA +Display_7Segment:DA56-11SURKWA +Display_7Segment:DA56-11SYKWA +Display_7Segment:DE113-XX-XX +Display_7Segment:DE114-RS-20 +Display_7Segment:DE119-XX-XX +Display_7Segment:DE122-XX-XX +Display_7Segment:DE152-XX-XX +Display_7Segment:DE170-XX-XX +Display_7Segment:ELD_426XXXX +Display_7Segment:HDSP-7401 +Display_7Segment:HDSP-7507 +Display_7Segment:HDSP-7801 +Display_7Segment:HDSP-7807 +Display_7Segment:HDSP-A151 +Display_7Segment:HDSP-A401 +Display_7Segment:KCSC02-105 +Display_7Segment:KCSC02-106 +Display_7Segment:KCSC02-107 +Display_7Segment:KCSC02-123 +Display_7Segment:KCSC02-136 +Display_7Segment:LTC-4627Jx +Display_7Segment:MAN3410A +Display_7Segment:MAN3420A +Display_7Segment:MAN3610A +Display_7Segment:MAN3620A +Display_7Segment:MAN3630A +Display_7Segment:MAN3810A +Display_7Segment:MAN3820A +Display_7Segment:MAN71A +Display_7Segment:MAN72A +Display_7Segment:MAN73A +Display_7Segment:SA15-11xxx +Display_7Segment:SBC18-11SURKCGKWA +Display_7Segment:Sx39-1xxxxx +Ferrite_THT:LairdTech_28C0236-0JW-10 +Fiducial:Fiducial_0.5mm_Mask1.5mm +Fiducial:Fiducial_0.5mm_Mask1mm +Fiducial:Fiducial_0.75mm_Mask1.5mm +Fiducial:Fiducial_0.75mm_Mask2.25mm +Fiducial:Fiducial_1.5mm_Mask3mm +Fiducial:Fiducial_1.5mm_Mask4.5mm +Fiducial:Fiducial_1mm_Mask2mm +Fiducial:Fiducial_1mm_Mask3mm +Filter:Filter_1109-5_1.1x0.9mm +Filter:Filter_1411-5_1.4x1.1mm +Filter:Filter_Bourns_SRF0905_6.0x9.2mm +Filter:Filter_FILTERCON_1FPxx +Filter:Filter_KEMET_PZB300_24.0x12.5mm_P10.0mm +Filter:Filter_Mini-Circuits_FV1206-1 +Filter:Filter_Mini-Circuits_FV1206-4 +Filter:Filter_Mini-Circuits_FV1206-5 +Filter:Filter_Mini-Circuits_FV1206-6 +Filter:Filter_Mini-Circuits_FV1206-7 +Filter:Filter_Mini-Circuits_FV1206 +Filter:Filter_Murata_BNX025 +Filter:Filter_Murata_BNX025_ThermalVias +Filter:Filter_Murata_SFECF-6 +Filter:Filter_Murata_SFECF-6_HandSoldering +Filter:Filter_SAW-6_3.8x3.8mm +Filter:Filter_SAW-8_3.8x3.8mm +Filter:Filter_SAW_Epcos_DCC6C_3x3mm +Filter:Filter_Schaffner_FN405 +Filter:Filter_Schaffner_FN406 +Fuse:FuseHolder_Blade_ATO_Littelfuse_FLR_178.6165 +Fuse:Fuseholder_Blade_ATO_Littelfuse_Pudenz_2_Pin +Fuse:Fuseholder_Blade_Mini_Keystone_3568 +Fuse:Fuseholder_Clip-5x20mm_Bel_FC-203-22_Lateral_P17.80x5.00mm_D1.17mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Eaton_1A5601-01_Inline_P20.80x6.76mm_D1.70mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3512P_Inline_P23.62x7.27mm_D1.02x2.41x1.02x1.57mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3512_Inline_P23.62x7.27mm_D1.02x1.57mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3517_Inline_P23.11x6.76mm_D1.70mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Keystone_3518P_Inline_P23.11x6.76mm_D2.44x1.70mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_100_Inline_P20.50x4.60mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_111_Inline_P20.00x5.00mm_D1.05mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_111_Lateral_P18.80x5.00mm_D1.17mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_445-030_Inline_P20.50x5.20mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_519_Inline_P20.60x5.00mm_D1.00mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_520_Inline_P20.50x5.80mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Littelfuse_521_Lateral_P17.00x5.00mm_D1.30mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Schurter_CQM_Inline_P20.60x5.00mm_D1.00mm_Horizontal +Fuse:Fuseholder_Clip-5x20mm_Schurter_OG_Lateral_P15.00x5.00mm_D1.3mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102071_Inline_P34.70x7.60mm_D2.00mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102_122_Inline_P34.21x7.62mm_D1.98mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_102_Inline_P34.21x7.62mm_D2.54mm_Horizontal +Fuse:Fuseholder_Clip-6.3x32mm_Littelfuse_122_Inline_P34.21x7.62mm_D2.54mm_Horizontal +Fuse:Fuseholder_Cylinder-5x20mm_Bulgin_FX0456_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Bulgin_FX0457_Horizontal_Closed +Fuse:Fuseholder_Cylinder-5x20mm_EATON_H15-V-1_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_EATON_HBV_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_EATON_HBW_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_0031_8201_Horizontal_Open +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FAB_0031-355x_Horizontal_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FPG4_Vertical_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_FUP_0031.2510_Horizontal_Closed +Fuse:Fuseholder_Cylinder-5x20mm_Schurter_OGN-SMD_Horizontal_Open +Fuse:Fuseholder_Cylinder-5x20mm_Stelvio-Kontek_PTF78_Horizontal_Open +Fuse:Fuseholder_Cylinder-5x20mm_Wuerth_696103101002-SMD_Horizontal_Open +Fuse:Fuseholder_Cylinder-6.3x32mm_Schurter_0031-8002_Horizontal_Open +Fuse:Fuseholder_Cylinder-6.3x32mm_Schurter_FUP_0031.2520_Horizontal_Closed +Fuse:Fuseholder_Keystone_3555-2 +Fuse:Fuseholder_Littelfuse_100_series_5x20mm +Fuse:Fuseholder_Littelfuse_100_series_5x25mm +Fuse:Fuseholder_Littelfuse_100_series_5x30mm +Fuse:Fuseholder_Littelfuse_445_030_series_5x20mm +Fuse:Fuseholder_Littelfuse_445_030_series_5x25mm +Fuse:Fuseholder_Littelfuse_445_030_series_5x30mm +Fuse:Fuseholder_Littelfuse_Nano2_154x +Fuse:Fuseholder_Littelfuse_Nano2_157x +Fuse:Fuseholder_TR5_Littelfuse_No560_No460 +Fuse:Fuse_0402_1005Metric +Fuse:Fuse_0402_1005Metric_Pad0.77x0.64mm_HandSolder +Fuse:Fuse_0603_1608Metric +Fuse:Fuse_0603_1608Metric_Pad1.05x0.95mm_HandSolder +Fuse:Fuse_0805_2012Metric +Fuse:Fuse_0805_2012Metric_Pad1.15x1.40mm_HandSolder +Fuse:Fuse_1206_3216Metric +Fuse:Fuse_1206_3216Metric_Pad1.42x1.75mm_HandSolder +Fuse:Fuse_1210_3225Metric +Fuse:Fuse_1210_3225Metric_Pad1.42x2.65mm_HandSolder +Fuse:Fuse_1812_4532Metric +Fuse:Fuse_1812_4532Metric_Pad1.30x3.40mm_HandSolder +Fuse:Fuse_2010_5025Metric +Fuse:Fuse_2010_5025Metric_Pad1.52x2.65mm_HandSolder +Fuse:Fuse_2512_6332Metric +Fuse:Fuse_2512_6332Metric_Pad1.52x3.35mm_HandSolder +Fuse:Fuse_2920_7451Metric +Fuse:Fuse_2920_7451Metric_Pad2.10x5.45mm_HandSolder +Fuse:Fuse_BelFuse_0ZRE0005FF_L8.3mm_W3.8mm +Fuse:Fuse_BelFuse_0ZRE0008FF_L8.3mm_W3.8mm +Fuse:Fuse_BelFuse_0ZRE0012FF_L8.3mm_W3.8mm +Fuse:Fuse_BelFuse_0ZRE0016FF_L9.9mm_W3.8mm +Fuse:Fuse_BelFuse_0ZRE0025FF_L9.6mm_W3.8mm +Fuse:Fuse_BelFuse_0ZRE0033FF_L11.4mm_W3.8mm +Fuse:Fuse_BelFuse_0ZRE0040FF_L11.5mm_W3.8mm +Fuse:Fuse_BelFuse_0ZRE0055FF_L14.0mm_W4.1mm +Fuse:Fuse_BelFuse_0ZRE0075FF_L11.5mm_W4.8mm +Fuse:Fuse_BelFuse_0ZRE0100FF_L18.7mm_W5.1mm +Fuse:Fuse_BelFuse_0ZRE0125FF_L21.2mm_W5.3mm +Fuse:Fuse_BelFuse_0ZRE0150FF_L23.4mm_W5.3mm +Fuse:Fuse_BelFuse_0ZRE0200FF_L24.9mm_W6.1mm +Fuse:Fuse_Blade_ATO_directSolder +Fuse:Fuse_Blade_Mini_directSolder +Fuse:Fuse_Bourns_MF-RG1000 +Fuse:Fuse_Bourns_MF-RG1100 +Fuse:Fuse_Bourns_MF-RG300 +Fuse:Fuse_Bourns_MF-RG400 +Fuse:Fuse_Bourns_MF-RG500 +Fuse:Fuse_Bourns_MF-RG600 +Fuse:Fuse_Bourns_MF-RG650 +Fuse:Fuse_Bourns_MF-RG700 +Fuse:Fuse_Bourns_MF-RG800 +Fuse:Fuse_Bourns_MF-RG900 +Fuse:Fuse_Bourns_MF-RHT050 +Fuse:Fuse_Bourns_MF-RHT070 +Fuse:Fuse_Bourns_MF-RHT100 +Fuse:Fuse_Bourns_MF-RHT1000 +Fuse:Fuse_Bourns_MF-RHT1100 +Fuse:Fuse_Bourns_MF-RHT1300 +Fuse:Fuse_Bourns_MF-RHT200 +Fuse:Fuse_Bourns_MF-RHT300 +Fuse:Fuse_Bourns_MF-RHT400 +Fuse:Fuse_Bourns_MF-RHT500 +Fuse:Fuse_Bourns_MF-RHT550 +Fuse:Fuse_Bourns_MF-RHT600 +Fuse:Fuse_Bourns_MF-RHT650 +Fuse:Fuse_Bourns_MF-RHT700 +Fuse:Fuse_Bourns_MF-RHT750 +Fuse:Fuse_Bourns_MF-RHT800 +Fuse:Fuse_Bourns_MF-RHT900 +Fuse:Fuse_Bourns_MF-SM_7.98x5.44mm +Fuse:Fuse_Bourns_MF-SM_9.5x6.71mm +Fuse:Fuse_Bourns_TBU-CA +Fuse:Fuse_Littelfuse-LVR100 +Fuse:Fuse_Littelfuse-LVR125 +Fuse:Fuse_Littelfuse-LVR200 +Fuse:Fuse_Littelfuse-NANO2-451_453 +Fuse:Fuse_Littelfuse-NANO2-462 +Fuse:Fuse_Littelfuse-NANO2-885 +Fuse:Fuse_Littelfuse_372_D8.50mm +Fuse:Fuse_Littelfuse_395Series +Fuse:Fuse_Schurter_UMT250 +Fuse:Fuse_Schurter_UMZ250 +Fuse:Fuse_SunFuse-6HP +Heatsink:Heatsink_125x35x50mm_3xFixationM3 +Heatsink:Heatsink_35x26mm_1xFixation3mm_Fischer-SK486-35 +Heatsink:Heatsink_38x38mm_SpringFixation +Heatsink:Heatsink_62x40mm_2xFixation3mm +Heatsink:Heatsink_AAVID_573300D00010G_TO-263 +Heatsink:Heatsink_AAVID_576802B03900G +Heatsink:Heatsink_AAVID_590302B03600G +Heatsink:Heatsink_AAVID_TV5G_TO220_Horizontal +Heatsink:Heatsink_Fischer_FK224xx2201_25x8.3mm +Heatsink:Heatsink_Fischer_FK24413D2PAK_26x13mm +Heatsink:Heatsink_Fischer_FK24413DPAK_23x13mm +Heatsink:Heatsink_Fischer_SK104-STC-STIC_35x13mm_2xDrill2.5mm +Heatsink:Heatsink_Fischer_SK104-STCB_35x13mm__2xDrill3.5mm_ScrewM3 +Heatsink:Heatsink_Fischer_SK129-STS_42x25mm_2xDrill2.5mm +Heatsink:Heatsink_SheetType_50x7mm_2Fixations +Heatsink:Heatsink_Stonecold_HS-130_30x12mm_2xFixation2.5mm +Heatsink:Heatsink_Stonecold_HS-132_32x14mm_2xFixation1.5mm +Heatsink:Heatsink_Stonecold_HS-S01_13.21x6.35mm +Heatsink:Heatsink_Stonecold_HS-S02_13.21x9.53mm +Heatsink:Heatsink_Stonecold_HS-S03_13.21x12.7mm +Inductor_SMD:L_01005_0402Metric +Inductor_SMD:L_01005_0402Metric_Pad0.57x0.30mm_HandSolder +Inductor_SMD:L_0201_0603Metric +Inductor_SMD:L_0201_0603Metric_Pad0.64x0.40mm_HandSolder +Inductor_SMD:L_0402_1005Metric +Inductor_SMD:L_0402_1005Metric_Pad0.77x0.64mm_HandSolder +Inductor_SMD:L_0603_1608Metric +Inductor_SMD:L_0603_1608Metric_Pad1.05x0.95mm_HandSolder +Inductor_SMD:L_0805_2012Metric +Inductor_SMD:L_0805_2012Metric_Pad1.05x1.20mm_HandSolder +Inductor_SMD:L_0805_2012Metric_Pad1.15x1.40mm_HandSolder +Inductor_SMD:L_10.4x10.4_H4.8 +Inductor_SMD:L_1008_2520Metric +Inductor_SMD:L_1008_2520Metric_Pad1.43x2.20mm_HandSolder +Inductor_SMD:L_1206_3216Metric +Inductor_SMD:L_1206_3216Metric_Pad1.22x1.90mm_HandSolder +Inductor_SMD:L_1206_3216Metric_Pad1.42x1.75mm_HandSolder +Inductor_SMD:L_1210_3225Metric +Inductor_SMD:L_1210_3225Metric_Pad1.42x2.65mm_HandSolder +Inductor_SMD:L_12x12mm_H4.5mm +Inductor_SMD:L_12x12mm_H6mm +Inductor_SMD:L_12x12mm_H8mm +Inductor_SMD:L_1806_4516Metric +Inductor_SMD:L_1806_4516Metric_Pad1.45x1.90mm_HandSolder +Inductor_SMD:L_1812_4532Metric +Inductor_SMD:L_1812_4532Metric_Pad1.30x3.40mm_HandSolder +Inductor_SMD:L_2010_5025Metric +Inductor_SMD:L_2010_5025Metric_Pad1.52x2.65mm_HandSolder +Inductor_SMD:L_2512_6332Metric +Inductor_SMD:L_2512_6332Metric_Pad1.52x3.35mm_HandSolder +Inductor_SMD:L_6.3x6.3_H3 +Inductor_SMD:L_7.3x7.3_H3.5 +Inductor_SMD:L_7.3x7.3_H4.5 +Inductor_SMD:L_Abracon_ASPI-0425 +Inductor_SMD:L_Abracon_ASPI-0630LR +Inductor_SMD:L_Abracon_ASPI-3012S +Inductor_SMD:L_Abracon_ASPI-4030S +Inductor_SMD:L_Abracon_ASPIAIG-F4020 +Inductor_SMD:L_APV_ANR252010 +Inductor_SMD:L_APV_ANR252012 +Inductor_SMD:L_APV_ANR3010 +Inductor_SMD:L_APV_ANR3012 +Inductor_SMD:L_APV_ANR3015 +Inductor_SMD:L_APV_ANR4010 +Inductor_SMD:L_APV_ANR4012 +Inductor_SMD:L_APV_ANR4018 +Inductor_SMD:L_APV_ANR4020 +Inductor_SMD:L_APV_ANR4026 +Inductor_SMD:L_APV_ANR4030 +Inductor_SMD:L_APV_ANR5012 +Inductor_SMD:L_APV_ANR5020 +Inductor_SMD:L_APV_ANR5040 +Inductor_SMD:L_APV_ANR5045 +Inductor_SMD:L_APV_ANR6020 +Inductor_SMD:L_APV_ANR6028 +Inductor_SMD:L_APV_ANR6045 +Inductor_SMD:L_APV_ANR8040 +Inductor_SMD:L_APV_ANR8065 +Inductor_SMD:L_APV_APH0412 +Inductor_SMD:L_APV_APH0420 +Inductor_SMD:L_APV_APH0518 +Inductor_SMD:L_APV_APH0530 +Inductor_SMD:L_APV_APH0615 +Inductor_SMD:L_APV_APH0618 +Inductor_SMD:L_APV_APH0620 +Inductor_SMD:L_APV_APH0624 +Inductor_SMD:L_APV_APH0630 +Inductor_SMD:L_APV_APH0640 +Inductor_SMD:L_APV_APH0650 +Inductor_SMD:L_APV_APH0840 +Inductor_SMD:L_APV_APH0850 +Inductor_SMD:L_APV_APH1030 +Inductor_SMD:L_APV_APH1040 +Inductor_SMD:L_APV_APH1050 +Inductor_SMD:L_APV_APH1240 +Inductor_SMD:L_APV_APH1250 +Inductor_SMD:L_APV_APH1260 +Inductor_SMD:L_APV_APH1265 +Inductor_SMD:L_APV_APH1770 +Inductor_SMD:L_APV_APH2213 +Inductor_SMD:L_AVX_LMLP07A7 +Inductor_SMD:L_Bourns-SRN1060 +Inductor_SMD:L_Bourns-SRN4018 +Inductor_SMD:L_Bourns-SRN6028 +Inductor_SMD:L_Bourns-SRN8040_8x8.15mm +Inductor_SMD:L_Bourns-SRR1005 +Inductor_SMD:L_Bourns-SRU1028_10.0x10.0mm +Inductor_SMD:L_Bourns-SRU8028_8.0x8.0mm +Inductor_SMD:L_Bourns-SRU8043 +Inductor_SMD:L_Bourns_SDR0604 +Inductor_SMD:L_Bourns_SDR1806 +Inductor_SMD:L_Bourns_SRF1260 +Inductor_SMD:L_Bourns_SRN6045TA +Inductor_SMD:L_Bourns_SRN8040TA +Inductor_SMD:L_Bourns_SRP1038C_10.0x10.0mm +Inductor_SMD:L_Bourns_SRP1050WA +Inductor_SMD:L_Bourns_SRP1245A +Inductor_SMD:L_Bourns_SRP1770TA_16.9x16.9mm +Inductor_SMD:L_Bourns_SRP2313AA +Inductor_SMD:L_Bourns_SRP5030T +Inductor_SMD:L_Bourns_SRP6060FA +Inductor_SMD:L_Bourns_SRP7028A_7.3x6.6mm +Inductor_SMD:L_Bourns_SRR1208_12.7x12.7mm +Inductor_SMD:L_Bourns_SRR1210A +Inductor_SMD:L_Bourns_SRR1260 +Inductor_SMD:L_Bourns_SRU5016_5.2x5.2mm +Inductor_SMD:L_Cenker_CKCS201610 +Inductor_SMD:L_Cenker_CKCS252010 +Inductor_SMD:L_Cenker_CKCS252012 +Inductor_SMD:L_Cenker_CKCS3012 +Inductor_SMD:L_Cenker_CKCS3015 +Inductor_SMD:L_Cenker_CKCS4018 +Inductor_SMD:L_Cenker_CKCS4020 +Inductor_SMD:L_Cenker_CKCS4030 +Inductor_SMD:L_Cenker_CKCS5020 +Inductor_SMD:L_Cenker_CKCS5040 +Inductor_SMD:L_Cenker_CKCS6020 +Inductor_SMD:L_Cenker_CKCS6028 +Inductor_SMD:L_Cenker_CKCS6045 +Inductor_SMD:L_Cenker_CKCS8040 +Inductor_SMD:L_Cenker_CKCS8060 +Inductor_SMD:L_Cenker_CKCS8080 +Inductor_SMD:L_Changjiang_FNR252010S +Inductor_SMD:L_Changjiang_FNR252012S +Inductor_SMD:L_Changjiang_FNR3010S +Inductor_SMD:L_Changjiang_FNR3012S +Inductor_SMD:L_Changjiang_FNR3015S +Inductor_SMD:L_Changjiang_FNR3021S +Inductor_SMD:L_Changjiang_FNR4010S +Inductor_SMD:L_Changjiang_FNR4012S +Inductor_SMD:L_Changjiang_FNR4015S +Inductor_SMD:L_Changjiang_FNR4018S +Inductor_SMD:L_Changjiang_FNR4020S +Inductor_SMD:L_Changjiang_FNR4026S +Inductor_SMD:L_Changjiang_FNR4030S +Inductor_SMD:L_Changjiang_FNR5012S +Inductor_SMD:L_Changjiang_FNR5015S +Inductor_SMD:L_Changjiang_FNR5020S +Inductor_SMD:L_Changjiang_FNR5030S +Inductor_SMD:L_Changjiang_FNR5040S +Inductor_SMD:L_Changjiang_FNR5045S +Inductor_SMD:L_Changjiang_FNR6020S +Inductor_SMD:L_Changjiang_FNR6028S +Inductor_SMD:L_Changjiang_FNR6040S +Inductor_SMD:L_Changjiang_FNR6045S +Inductor_SMD:L_Changjiang_FNR8040S +Inductor_SMD:L_Changjiang_FNR8050S +Inductor_SMD:L_Changjiang_FNR8065S +Inductor_SMD:L_Changjiang_FXL0412 +Inductor_SMD:L_Changjiang_FXL0420 +Inductor_SMD:L_Changjiang_FXL0518 +Inductor_SMD:L_Changjiang_FXL0530 +Inductor_SMD:L_Changjiang_FXL0615 +Inductor_SMD:L_Changjiang_FXL0618 +Inductor_SMD:L_Changjiang_FXL0624 +Inductor_SMD:L_Changjiang_FXL0630 +Inductor_SMD:L_Changjiang_FXL0640 +Inductor_SMD:L_Changjiang_FXL0650 +Inductor_SMD:L_Changjiang_FXL0840 +Inductor_SMD:L_Changjiang_FXL1030 +Inductor_SMD:L_Changjiang_FXL1040 +Inductor_SMD:L_Changjiang_FXL1050 +Inductor_SMD:L_Changjiang_FXL1340 +Inductor_SMD:L_Changjiang_FXL1350 +Inductor_SMD:L_Changjiang_FXL1360 +Inductor_SMD:L_Changjiang_FXL1365 +Inductor_SMD:L_Changjiang_FXL1770 +Inductor_SMD:L_Changjiang_FXL2213 +Inductor_SMD:L_Chilisin_BMRA00040415 +Inductor_SMD:L_Chilisin_BMRA00040420 +Inductor_SMD:L_Chilisin_BMRA00050520 +Inductor_SMD:L_Chilisin_BMRA00050530 +Inductor_SMD:L_Chilisin_BMRB00050512 +Inductor_SMD:L_Chilisin_BMRB00050518-B +Inductor_SMD:L_Chilisin_BMRB00050518 +Inductor_SMD:L_Chilisin_BMRB00060612 +Inductor_SMD:L_Chilisin_BMRB00060618 +Inductor_SMD:L_Chilisin_BMRB00060624 +Inductor_SMD:L_Chilisin_BMRB00060650 +Inductor_SMD:L_Chilisin_BMRF00101040 +Inductor_SMD:L_Chilisin_BMRF00131350 +Inductor_SMD:L_Chilisin_BMRF00131360 +Inductor_SMD:L_Chilisin_BMRF00171770 +Inductor_SMD:L_Chilisin_BMRG00101030 +Inductor_SMD:L_Chilisin_BMRG00131360 +Inductor_SMD:L_Chilisin_BMRx00040412 +Inductor_SMD:L_Chilisin_BMRx00050512-B +Inductor_SMD:L_Chilisin_BMRx00050515 +Inductor_SMD:L_Chilisin_BMRx00060615 +Inductor_SMD:L_Chilisin_BMRx00060630 +Inductor_SMD:L_Coilcraft_1515SQ-47N +Inductor_SMD:L_Coilcraft_1515SQ-68N +Inductor_SMD:L_Coilcraft_1515SQ-82N +Inductor_SMD:L_Coilcraft_2222SQ-111 +Inductor_SMD:L_Coilcraft_2222SQ-131 +Inductor_SMD:L_Coilcraft_2222SQ-161 +Inductor_SMD:L_Coilcraft_2222SQ-181 +Inductor_SMD:L_Coilcraft_2222SQ-221 +Inductor_SMD:L_Coilcraft_2222SQ-271 +Inductor_SMD:L_Coilcraft_2222SQ-301 +Inductor_SMD:L_Coilcraft_2222SQ-90N +Inductor_SMD:L_Coilcraft_2929SQ-331 +Inductor_SMD:L_Coilcraft_2929SQ-361 +Inductor_SMD:L_Coilcraft_2929SQ-391 +Inductor_SMD:L_Coilcraft_2929SQ-431 +Inductor_SMD:L_Coilcraft_2929SQ-501 +Inductor_SMD:L_Coilcraft_LPS3010 +Inductor_SMD:L_Coilcraft_LPS3314 +Inductor_SMD:L_Coilcraft_LPS4018 +Inductor_SMD:L_Coilcraft_LPS4414 +Inductor_SMD:L_Coilcraft_LPS5030 +Inductor_SMD:L_Coilcraft_MOS6020-XXX +Inductor_SMD:L_Coilcraft_MSS1038-XXX +Inductor_SMD:L_Coilcraft_MSS1038T-XXX +Inductor_SMD:L_Coilcraft_MSS1048-XXX +Inductor_SMD:L_Coilcraft_MSS1048T-XXX +Inductor_SMD:L_Coilcraft_MSS1210-XXX +Inductor_SMD:L_Coilcraft_MSS1210H-XXX +Inductor_SMD:L_Coilcraft_MSS1246-XXX +Inductor_SMD:L_Coilcraft_MSS1246H-XXX +Inductor_SMD:L_Coilcraft_MSS1246T-XXX +Inductor_SMD:L_Coilcraft_MSS1260-XXX +Inductor_SMD:L_Coilcraft_MSS1260H-XXX +Inductor_SMD:L_Coilcraft_MSS1260T-XXX +Inductor_SMD:L_Coilcraft_MSS1278-XXX +Inductor_SMD:L_Coilcraft_MSS1278H-XXX +Inductor_SMD:L_Coilcraft_MSS1278T-XXX +Inductor_SMD:L_Coilcraft_MSS1514V-XXX +Inductor_SMD:L_Coilcraft_MSS1583-XXX +Inductor_SMD:L_Coilcraft_MSS1812T-XXX +Inductor_SMD:L_Coilcraft_MSS7348-XXX +Inductor_SMD:L_Coilcraft_XAL1010-XXX +Inductor_SMD:L_Coilcraft_XAL1030-XXX +Inductor_SMD:L_Coilcraft_XAL1060-XXX +Inductor_SMD:L_Coilcraft_XAL1350-XXX +Inductor_SMD:L_Coilcraft_XAL1510-103 +Inductor_SMD:L_Coilcraft_XAL1510-153 +Inductor_SMD:L_Coilcraft_XAL1510-223 +Inductor_SMD:L_Coilcraft_XAL1510-333 +Inductor_SMD:L_Coilcraft_XAL1510-472 +Inductor_SMD:L_Coilcraft_XAL1510-682 +Inductor_SMD:L_Coilcraft_XAL1510-822 +Inductor_SMD:L_Coilcraft_XAL1513-153 +Inductor_SMD:L_Coilcraft_XAL1580-102 +Inductor_SMD:L_Coilcraft_XAL1580-132 +Inductor_SMD:L_Coilcraft_XAL1580-182 +Inductor_SMD:L_Coilcraft_XAL1580-202 +Inductor_SMD:L_Coilcraft_XAL1580-302 +Inductor_SMD:L_Coilcraft_XAL1580-401 +Inductor_SMD:L_Coilcraft_XAL1580-452 +Inductor_SMD:L_Coilcraft_XAL1580-532 +Inductor_SMD:L_Coilcraft_XAL1580-612 +Inductor_SMD:L_Coilcraft_XAL1580-741 +Inductor_SMD:L_Coilcraft_XAL4020-XXX +Inductor_SMD:L_Coilcraft_XAL4030-XXX +Inductor_SMD:L_Coilcraft_XAL4040-XXX +Inductor_SMD:L_Coilcraft_XAL5020-XXX +Inductor_SMD:L_Coilcraft_XAL5030-XXX +Inductor_SMD:L_Coilcraft_XAL5050-XXX +Inductor_SMD:L_Coilcraft_XAL6020-XXX +Inductor_SMD:L_Coilcraft_XAL6030-XXX +Inductor_SMD:L_Coilcraft_XAL6060-XXX +Inductor_SMD:L_Coilcraft_XAL7020-102 +Inductor_SMD:L_Coilcraft_XAL7020-122 +Inductor_SMD:L_Coilcraft_XAL7020-151 +Inductor_SMD:L_Coilcraft_XAL7020-152 +Inductor_SMD:L_Coilcraft_XAL7020-222 +Inductor_SMD:L_Coilcraft_XAL7020-271 +Inductor_SMD:L_Coilcraft_XAL7020-331 +Inductor_SMD:L_Coilcraft_XAL7020-471 +Inductor_SMD:L_Coilcraft_XAL7020-681 +Inductor_SMD:L_Coilcraft_XAL7030-102 +Inductor_SMD:L_Coilcraft_XAL7030-103 +Inductor_SMD:L_Coilcraft_XAL7030-152 +Inductor_SMD:L_Coilcraft_XAL7030-161 +Inductor_SMD:L_Coilcraft_XAL7030-222 +Inductor_SMD:L_Coilcraft_XAL7030-272 +Inductor_SMD:L_Coilcraft_XAL7030-301 +Inductor_SMD:L_Coilcraft_XAL7030-332 +Inductor_SMD:L_Coilcraft_XAL7030-472 +Inductor_SMD:L_Coilcraft_XAL7030-562 +Inductor_SMD:L_Coilcraft_XAL7030-601 +Inductor_SMD:L_Coilcraft_XAL7030-682 +Inductor_SMD:L_Coilcraft_XAL7030-822 +Inductor_SMD:L_Coilcraft_XAL7050-XXX +Inductor_SMD:L_Coilcraft_XAL7070-XXX +Inductor_SMD:L_Coilcraft_XAL8050-223 +Inductor_SMD:L_Coilcraft_XAL8080-XXX +Inductor_SMD:L_Coilcraft_XFL2010 +Inductor_SMD:L_Coilcraft_XxL4020 +Inductor_SMD:L_Coilcraft_XxL4030 +Inductor_SMD:L_Coilcraft_XxL4040 +Inductor_SMD:L_CommonModeChoke_Coilcraft_0603USB +Inductor_SMD:L_CommonModeChoke_Coilcraft_0805USB +Inductor_SMD:L_CommonModeChoke_Coilcraft_1812CAN +Inductor_SMD:L_CommonModeChoke_Murata_DLW5BTMxxxSQ2x_5x5mm +Inductor_SMD:L_CommonModeChoke_TDK_ACM2520-2P +Inductor_SMD:L_CommonModeChoke_TDK_ACM2520-3P +Inductor_SMD:L_CommonModeChoke_TDK_ACM7060 +Inductor_SMD:L_CommonModeChoke_Wuerth_WE-SL5 +Inductor_SMD:L_CommonMode_Delevan_4222 +Inductor_SMD:L_CommonMode_Wuerth_WE-SL2 +Inductor_SMD:L_CommonMode_Wurth_WE-CNSW-1206 +Inductor_SMD:L_Eaton_MCL2012V1 +Inductor_SMD:L_Fastron_PISN +Inductor_SMD:L_Fastron_PISN_Handsoldering +Inductor_SMD:L_Fastron_PISR +Inductor_SMD:L_Fastron_PISR_Handsoldering +Inductor_SMD:L_Ferrocore_DLG-0302 +Inductor_SMD:L_Ferrocore_DLG-0403 +Inductor_SMD:L_Ferrocore_DLG-0504 +Inductor_SMD:L_Ferrocore_DLG-0703 +Inductor_SMD:L_Ferrocore_DLG-0705 +Inductor_SMD:L_Ferrocore_DLG-1004 +Inductor_SMD:L_Ferrocore_DLG-1005 +Inductor_SMD:L_KOHERelec_MDA5030 +Inductor_SMD:L_KOHERelec_MDA7030 +Inductor_SMD:L_Murata_DEM35xxC +Inductor_SMD:L_Murata_DFE201610P +Inductor_SMD:L_Murata_LQH2MCNxxxx02_2.0x1.6mm +Inductor_SMD:L_Murata_LQH55DN_5.7x5.0mm +Inductor_SMD:L_Neosid_Air-Coil_SML_1turn_HDM0131A +Inductor_SMD:L_Neosid_Air-Coil_SML_2turn_HAM0231A +Inductor_SMD:L_Neosid_Air-Coil_SML_2turn_HDM0231A +Inductor_SMD:L_Neosid_Air-Coil_SML_3turn_HAM0331A +Inductor_SMD:L_Neosid_Air-Coil_SML_3turn_HDM0331A +Inductor_SMD:L_Neosid_Air-Coil_SML_4turn_HAM0431A +Inductor_SMD:L_Neosid_Air-Coil_SML_4turn_HDM0431A +Inductor_SMD:L_Neosid_Air-Coil_SML_5turn_HAM0531A +Inductor_SMD:L_Neosid_Air-Coil_SML_5turn_HDM0531A +Inductor_SMD:L_Neosid_Air-Coil_SML_6-10turn_HAM0631A-HAM1031A +Inductor_SMD:L_Neosid_Air-Coil_SML_6-10turn_HDM0431A-HDM1031A +Inductor_SMD:L_Neosid_Air-Coil_SML_6turn_HAM0631A +Inductor_SMD:L_Neosid_MicroCoil_Ms36-L +Inductor_SMD:L_Neosid_Ms42 +Inductor_SMD:L_Neosid_Ms50 +Inductor_SMD:L_Neosid_Ms50T +Inductor_SMD:L_Neosid_Ms85 +Inductor_SMD:L_Neosid_Ms85T +Inductor_SMD:L_Neosid_Ms95 +Inductor_SMD:L_Neosid_Ms95a +Inductor_SMD:L_Neosid_Ms95T +Inductor_SMD:L_Neosid_SM-NE127 +Inductor_SMD:L_Neosid_SM-NE127_HandSoldering +Inductor_SMD:L_Neosid_SM-NE150 +Inductor_SMD:L_Neosid_SM-NE95H +Inductor_SMD:L_Neosid_SM-PIC0512H +Inductor_SMD:L_Neosid_SM-PIC0602H +Inductor_SMD:L_Neosid_SM-PIC0612H +Inductor_SMD:L_Neosid_SM-PIC1004H +Inductor_SMD:L_Neosid_SMS-ME3010 +Inductor_SMD:L_Neosid_SMS-ME3015 +Inductor_SMD:L_Neosid_SMs42 +Inductor_SMD:L_Neosid_SMs50 +Inductor_SMD:L_Neosid_SMs85 +Inductor_SMD:L_Neosid_SMs95_SMs95p +Inductor_SMD:L_Pulse_P059x +Inductor_SMD:L_Pulse_PA4320 +Inductor_SMD:L_Pulse_PA4332 +Inductor_SMD:L_Pulse_PA4340 +Inductor_SMD:L_Pulse_PA4341 +Inductor_SMD:L_Pulse_PA4344 +Inductor_SMD:L_Pulse_PA4349 +Inductor_SMD:L_Pulse_PA5402 +Inductor_SMD:L_Sagami_CER1242B +Inductor_SMD:L_Sagami_CER1257B +Inductor_SMD:L_Sagami_CER1277B +Inductor_SMD:L_Sagami_CWR1242C +Inductor_SMD:L_Sagami_CWR1257C +Inductor_SMD:L_Sagami_CWR1277C +Inductor_SMD:L_SigTra_SC3316F +Inductor_SMD:L_SOREDE_SNR.1050_10x10x5mm +Inductor_SMD:L_Sumida_CDMC6D28_7.25x6.5mm +Inductor_SMD:L_Sumida_CR75 +Inductor_SMD:L_Sunlord_MWSA0402S +Inductor_SMD:L_Sunlord_MWSA0412S +Inductor_SMD:L_Sunlord_MWSA0503S +Inductor_SMD:L_Sunlord_MWSA0518S +Inductor_SMD:L_Sunlord_MWSA0602S +Inductor_SMD:L_Sunlord_MWSA0603S +Inductor_SMD:L_Sunlord_MWSA0604S +Inductor_SMD:L_Sunlord_MWSA0605S +Inductor_SMD:L_Sunlord_MWSA0615S +Inductor_SMD:L_Sunlord_MWSA0618S +Inductor_SMD:L_Sunlord_MWSA0624S +Inductor_SMD:L_Sunlord_MWSA0804S +Inductor_SMD:L_Sunlord_MWSA1003S +Inductor_SMD:L_Sunlord_MWSA1004S +Inductor_SMD:L_Sunlord_MWSA1005S +Inductor_SMD:L_Sunlord_MWSA1204S-100 +Inductor_SMD:L_Sunlord_MWSA1204S-150 +Inductor_SMD:L_Sunlord_MWSA1204S-1R0 +Inductor_SMD:L_Sunlord_MWSA1204S-1R5 +Inductor_SMD:L_Sunlord_MWSA1204S-220 +Inductor_SMD:L_Sunlord_MWSA1204S-2R2 +Inductor_SMD:L_Sunlord_MWSA1204S-3R3 +Inductor_SMD:L_Sunlord_MWSA1204S-4R7 +Inductor_SMD:L_Sunlord_MWSA1204S-6R8 +Inductor_SMD:L_Sunlord_MWSA1204S-R22 +Inductor_SMD:L_Sunlord_MWSA1204S-R47 +Inductor_SMD:L_Sunlord_MWSA1204S-R68 +Inductor_SMD:L_Sunlord_MWSA1204S-R82 +Inductor_SMD:L_Sunlord_MWSA1205S-100 +Inductor_SMD:L_Sunlord_MWSA1205S-150 +Inductor_SMD:L_Sunlord_MWSA1205S-1R0 +Inductor_SMD:L_Sunlord_MWSA1205S-1R5 +Inductor_SMD:L_Sunlord_MWSA1205S-220 +Inductor_SMD:L_Sunlord_MWSA1205S-2R2 +Inductor_SMD:L_Sunlord_MWSA1205S-330 +Inductor_SMD:L_Sunlord_MWSA1205S-3R3 +Inductor_SMD:L_Sunlord_MWSA1205S-470 +Inductor_SMD:L_Sunlord_MWSA1205S-4R7 +Inductor_SMD:L_Sunlord_MWSA1205S-6R8 +Inductor_SMD:L_Sunlord_MWSA1205S-R22 +Inductor_SMD:L_Sunlord_MWSA1205S-R36 +Inductor_SMD:L_Sunlord_MWSA1205S-R50 +Inductor_SMD:L_Sunlord_MWSA1205S-R68 +Inductor_SMD:L_Sunlord_MWSA1205S-R82 +Inductor_SMD:L_Sunlord_MWSA1206S-100 +Inductor_SMD:L_Sunlord_MWSA1206S-101 +Inductor_SMD:L_Sunlord_MWSA1206S-120 +Inductor_SMD:L_Sunlord_MWSA1206S-121 +Inductor_SMD:L_Sunlord_MWSA1206S-150 +Inductor_SMD:L_Sunlord_MWSA1206S-151 +Inductor_SMD:L_Sunlord_MWSA1206S-180 +Inductor_SMD:L_Sunlord_MWSA1206S-1R5 +Inductor_SMD:L_Sunlord_MWSA1206S-220 +Inductor_SMD:L_Sunlord_MWSA1206S-270 +Inductor_SMD:L_Sunlord_MWSA1206S-2R2 +Inductor_SMD:L_Sunlord_MWSA1206S-330 +Inductor_SMD:L_Sunlord_MWSA1206S-3R3 +Inductor_SMD:L_Sunlord_MWSA1206S-470 +Inductor_SMD:L_Sunlord_MWSA1206S-4R7 +Inductor_SMD:L_Sunlord_MWSA1206S-5R6 +Inductor_SMD:L_Sunlord_MWSA1206S-680 +Inductor_SMD:L_Sunlord_MWSA1206S-6R8 +Inductor_SMD:L_Sunlord_MWSA1206S-8R2 +Inductor_SMD:L_Sunlord_MWSA1206S-R68 +Inductor_SMD:L_Sunlord_MWSA1265S +Inductor_SMD:L_Sunlord_MWSA1707S +Inductor_SMD:L_Sunlord_MWSA2213S +Inductor_SMD:L_Sunlord_SWPA252010S +Inductor_SMD:L_Sunlord_SWPA252012S +Inductor_SMD:L_Sunlord_SWPA3010S +Inductor_SMD:L_Sunlord_SWPA3012S +Inductor_SMD:L_Sunlord_SWPA3015S +Inductor_SMD:L_Sunlord_SWPA4010S +Inductor_SMD:L_Sunlord_SWPA4012S +Inductor_SMD:L_Sunlord_SWPA4018S +Inductor_SMD:L_Sunlord_SWPA4020S +Inductor_SMD:L_Sunlord_SWPA4026S +Inductor_SMD:L_Sunlord_SWPA4030S +Inductor_SMD:L_Sunlord_SWPA5012S +Inductor_SMD:L_Sunlord_SWPA5020S +Inductor_SMD:L_Sunlord_SWPA5040S +Inductor_SMD:L_Sunlord_SWPA6020S +Inductor_SMD:L_Sunlord_SWPA6028S +Inductor_SMD:L_Sunlord_SWPA6040S +Inductor_SMD:L_Sunlord_SWPA6045S +Inductor_SMD:L_Sunlord_SWPA8040S +Inductor_SMD:L_Sunlord_SWRB1204S +Inductor_SMD:L_Sunlord_SWRB1205S +Inductor_SMD:L_Sunlord_SWRB1207S +Inductor_SMD:L_SXN_SMDRI124 +Inductor_SMD:L_SXN_SMDRI125 +Inductor_SMD:L_SXN_SMDRI127 +Inductor_SMD:L_SXN_SMDRI62 +Inductor_SMD:L_SXN_SMDRI64 +Inductor_SMD:L_SXN_SMDRI73 +Inductor_SMD:L_SXN_SMDRI74 +Inductor_SMD:L_TaiTech_TMPC1265_13.5x12.5mm +Inductor_SMD:L_Taiyo-Yuden_BK_Array_1206_3216Metric +Inductor_SMD:L_Taiyo-Yuden_MD-1616 +Inductor_SMD:L_Taiyo-Yuden_MD-2020 +Inductor_SMD:L_Taiyo-Yuden_MD-3030 +Inductor_SMD:L_Taiyo-Yuden_MD-4040 +Inductor_SMD:L_Taiyo-Yuden_MD-5050 +Inductor_SMD:L_Taiyo-Yuden_NR-10050_9.8x10.0mm +Inductor_SMD:L_Taiyo-Yuden_NR-10050_9.8x10.0mm_HandSoldering +Inductor_SMD:L_Taiyo-Yuden_NR-20xx +Inductor_SMD:L_Taiyo-Yuden_NR-20xx_HandSoldering +Inductor_SMD:L_Taiyo-Yuden_NR-24xx +Inductor_SMD:L_Taiyo-Yuden_NR-24xx_HandSoldering +Inductor_SMD:L_Taiyo-Yuden_NR-30xx +Inductor_SMD:L_Taiyo-Yuden_NR-30xx_HandSoldering +Inductor_SMD:L_Taiyo-Yuden_NR-40xx +Inductor_SMD:L_Taiyo-Yuden_NR-40xx_HandSoldering +Inductor_SMD:L_Taiyo-Yuden_NR-50xx +Inductor_SMD:L_Taiyo-Yuden_NR-50xx_HandSoldering +Inductor_SMD:L_Taiyo-Yuden_NR-60xx +Inductor_SMD:L_Taiyo-Yuden_NR-60xx_HandSoldering +Inductor_SMD:L_Taiyo-Yuden_NR-80xx +Inductor_SMD:L_Taiyo-Yuden_NR-80xx_HandSoldering +Inductor_SMD:L_TDK_MLZ1608 +Inductor_SMD:L_TDK_MLZ2012_h0.85mm +Inductor_SMD:L_TDK_MLZ2012_h1.25mm +Inductor_SMD:L_TDK_NLV25_2.5x2.0mm +Inductor_SMD:L_TDK_NLV32_3.2x2.5mm +Inductor_SMD:L_TDK_SLF10145 +Inductor_SMD:L_TDK_SLF10165 +Inductor_SMD:L_TDK_SLF12555 +Inductor_SMD:L_TDK_SLF12565 +Inductor_SMD:L_TDK_SLF12575 +Inductor_SMD:L_TDK_SLF6025 +Inductor_SMD:L_TDK_SLF6028 +Inductor_SMD:L_TDK_SLF6045 +Inductor_SMD:L_TDK_SLF7032 +Inductor_SMD:L_TDK_SLF7045 +Inductor_SMD:L_TDK_SLF7055 +Inductor_SMD:L_TDK_VLF10040 +Inductor_SMD:L_TDK_VLP8040 +Inductor_SMD:L_TDK_VLS6045EX_VLS6045AF +Inductor_SMD:L_TracoPower_TCK-047_5.2x5.8mm +Inductor_SMD:L_TracoPower_TCK-141 +Inductor_SMD:L_Vishay_IFSC-1515AH_4x4x1.8mm +Inductor_SMD:L_Vishay_IHLP-1212 +Inductor_SMD:L_Vishay_IHLP-1616 +Inductor_SMD:L_Vishay_IHLP-2020 +Inductor_SMD:L_Vishay_IHLP-2525 +Inductor_SMD:L_Vishay_IHLP-4040 +Inductor_SMD:L_Vishay_IHLP-5050 +Inductor_SMD:L_Vishay_IHLP-6767 +Inductor_SMD:L_Vishay_IHSM-3825 +Inductor_SMD:L_Vishay_IHSM-4825 +Inductor_SMD:L_Vishay_IHSM-5832 +Inductor_SMD:L_Vishay_IHSM-7832 +Inductor_SMD:L_Walsin_WLFM201209x +Inductor_SMD:L_Walsin_WLFM201609x +Inductor_SMD:L_Walsin_WLFM252009x +Inductor_SMD:L_Wuerth_HCF-2013 +Inductor_SMD:L_Wuerth_HCF-2815 +Inductor_SMD:L_Wuerth_HCF-2818 +Inductor_SMD:L_Wuerth_HCI-1030 +Inductor_SMD:L_Wuerth_HCI-1040 +Inductor_SMD:L_Wuerth_HCI-1050 +Inductor_SMD:L_Wuerth_HCI-1335 +Inductor_SMD:L_Wuerth_HCI-1350 +Inductor_SMD:L_Wuerth_HCI-1365 +Inductor_SMD:L_Wuerth_HCI-1890 +Inductor_SMD:L_Wuerth_HCI-2212 +Inductor_SMD:L_Wuerth_HCI-5040 +Inductor_SMD:L_Wuerth_HCI-7030 +Inductor_SMD:L_Wuerth_HCI-7040 +Inductor_SMD:L_Wuerth_HCI-7050 +Inductor_SMD:L_Wuerth_HCM-1050 +Inductor_SMD:L_Wuerth_HCM-1052 +Inductor_SMD:L_Wuerth_HCM-1070 +Inductor_SMD:L_Wuerth_HCM-1078 +Inductor_SMD:L_Wuerth_HCM-1190 +Inductor_SMD:L_Wuerth_HCM-1240 +Inductor_SMD:L_Wuerth_HCM-1350 +Inductor_SMD:L_Wuerth_HCM-1390 +Inductor_SMD:L_Wuerth_HCM-7050 +Inductor_SMD:L_Wuerth_HCM-7070 +Inductor_SMD:L_Wuerth_MAPI-1610 +Inductor_SMD:L_Wuerth_MAPI-2010 +Inductor_SMD:L_Wuerth_MAPI-2506 +Inductor_SMD:L_Wuerth_MAPI-2508 +Inductor_SMD:L_Wuerth_MAPI-2510 +Inductor_SMD:L_Wuerth_MAPI-2512 +Inductor_SMD:L_Wuerth_MAPI-3010 +Inductor_SMD:L_Wuerth_MAPI-3012 +Inductor_SMD:L_Wuerth_MAPI-3015 +Inductor_SMD:L_Wuerth_MAPI-3020 +Inductor_SMD:L_Wuerth_MAPI-4020 +Inductor_SMD:L_Wuerth_MAPI-4030 +Inductor_SMD:L_Wuerth_WE-DD-Typ-L-Typ-XL-Typ-XXL +Inductor_SMD:L_Wuerth_WE-DD-Typ-M-Typ-S +Inductor_SMD:L_Wuerth_WE-GF-1210 +Inductor_SMD:L_Wuerth_WE-PD-Typ-7345 +Inductor_SMD:L_Wuerth_WE-PD-Typ-LS +Inductor_SMD:L_Wuerth_WE-PD-Typ-LS_Handsoldering +Inductor_SMD:L_Wuerth_WE-PD-Typ-M-Typ-S +Inductor_SMD:L_Wuerth_WE-PD-Typ-M-Typ-S_Handsoldering +Inductor_SMD:L_Wuerth_WE-PD2-Typ-L +Inductor_SMD:L_Wuerth_WE-PD2-Typ-MS +Inductor_SMD:L_Wuerth_WE-PD2-Typ-XL +Inductor_SMD:L_Wuerth_WE-PD4-Typ-X +Inductor_SMD:L_Wuerth_WE-PDF +Inductor_SMD:L_Wuerth_WE-PDF_Handsoldering +Inductor_SMD:L_Wuerth_WE-TPC-3816 +Inductor_SMD:L_Wuerth_WE-XHMI-8080 +Inductor_SMD:L_Wurth_WE-CAIR-5910 +Inductor_SMD_Wurth:L_Wurth_WE-LQSH-2010 +Inductor_SMD_Wurth:L_Wurth_WE-LQSH-2512 +Inductor_SMD_Wurth:L_Wurth_WE-LQSH-3012 +Inductor_SMD_Wurth:L_Wurth_WE-LQSH-4020 +Inductor_THT:Choke_EPCOS_B82722A +Inductor_THT:Choke_Schaffner_RN102-04-14.0x14.0mm +Inductor_THT:Choke_Schaffner_RN112-04-17.7x17.1mm +Inductor_THT:Choke_Schaffner_RN114-04-22.5x21.5mm +Inductor_THT:Choke_Schaffner_RN116-04-22.5x21.5mm +Inductor_THT:Choke_Schaffner_RN122-04-28.0x27.0mm +Inductor_THT:Choke_Schaffner_RN142-04-33.1x32.5mm +Inductor_THT:Choke_Schaffner_RN143-04-33.1x32.5mm +Inductor_THT:Choke_Schaffner_RN152-04-43.0x41.8mm +Inductor_THT:Choke_Schaffner_RN202-04-8.8x18.2mm +Inductor_THT:Choke_Schaffner_RN204-04-9.0x14.0mm +Inductor_THT:Choke_Schaffner_RN212-04-12.5x18.0mm +Inductor_THT:Choke_Schaffner_RN214-04-15.5x23.0mm +Inductor_THT:Choke_Schaffner_RN216-04-15.5x23.0mm +Inductor_THT:Choke_Schaffner_RN218-04-12.5x18.0mm +Inductor_THT:Choke_Schaffner_RN222-04-18.0x31.0mm +Inductor_THT:Choke_Schaffner_RN232-04-18.0x31.0mm +Inductor_THT:Choke_Schaffner_RN242-04-18.0x31.0mm +Inductor_THT:L_Axial_L11.0mm_D4.5mm_P15.24mm_Horizontal_Fastron_MECC +Inductor_THT:L_Axial_L11.0mm_D4.5mm_P5.08mm_Vertical_Fastron_MECC +Inductor_THT:L_Axial_L11.0mm_D4.5mm_P7.62mm_Vertical_Fastron_MECC +Inductor_THT:L_Axial_L12.0mm_D5.0mm_P15.24mm_Horizontal_Fastron_MISC +Inductor_THT:L_Axial_L12.0mm_D5.0mm_P5.08mm_Vertical_Fastron_MISC +Inductor_THT:L_Axial_L12.0mm_D5.0mm_P7.62mm_Vertical_Fastron_MISC +Inductor_THT:L_Axial_L12.8mm_D5.8mm_P20.32mm_Horizontal_Fastron_HBCC +Inductor_THT:L_Axial_L12.8mm_D5.8mm_P25.40mm_Horizontal_Fastron_HBCC +Inductor_THT:L_Axial_L12.8mm_D5.8mm_P5.08mm_Vertical_Fastron_HBCC +Inductor_THT:L_Axial_L12.8mm_D5.8mm_P7.62mm_Vertical_Fastron_HBCC +Inductor_THT:L_Axial_L13.0mm_D4.5mm_P15.24mm_Horizontal_Fastron_HCCC +Inductor_THT:L_Axial_L13.0mm_D4.5mm_P5.08mm_Vertical_Fastron_HCCC +Inductor_THT:L_Axial_L13.0mm_D4.5mm_P7.62mm_Vertical_Fastron_HCCC +Inductor_THT:L_Axial_L14.0mm_D4.5mm_P15.24mm_Horizontal_Fastron_LACC +Inductor_THT:L_Axial_L14.0mm_D4.5mm_P5.08mm_Vertical_Fastron_LACC +Inductor_THT:L_Axial_L14.0mm_D4.5mm_P7.62mm_Vertical_Fastron_LACC +Inductor_THT:L_Axial_L14.5mm_D5.8mm_P20.32mm_Horizontal_Fastron_HBCC +Inductor_THT:L_Axial_L14.5mm_D5.8mm_P25.40mm_Horizontal_Fastron_HBCC +Inductor_THT:L_Axial_L14.5mm_D5.8mm_P5.08mm_Vertical_Fastron_HBCC +Inductor_THT:L_Axial_L14.5mm_D5.8mm_P7.62mm_Vertical_Fastron_HBCC +Inductor_THT:L_Axial_L16.0mm_D6.3mm_P20.32mm_Horizontal_Fastron_VHBCC +Inductor_THT:L_Axial_L16.0mm_D6.3mm_P25.40mm_Horizontal_Fastron_VHBCC +Inductor_THT:L_Axial_L16.0mm_D6.3mm_P5.08mm_Vertical_Fastron_VHBCC +Inductor_THT:L_Axial_L16.0mm_D6.3mm_P7.62mm_Vertical_Fastron_VHBCC +Inductor_THT:L_Axial_L16.0mm_D7.5mm_P20.32mm_Horizontal_Fastron_XHBCC +Inductor_THT:L_Axial_L16.0mm_D7.5mm_P25.40mm_Horizontal_Fastron_XHBCC +Inductor_THT:L_Axial_L16.0mm_D7.5mm_P5.08mm_Vertical_Fastron_XHBCC +Inductor_THT:L_Axial_L16.0mm_D7.5mm_P7.62mm_Vertical_Fastron_XHBCC +Inductor_THT:L_Axial_L16.0mm_D9.5mm_P20.32mm_Horizontal_Vishay_IM-10-37 +Inductor_THT:L_Axial_L16.0mm_D9.5mm_P5.08mm_Vertical_Vishay_IM-10-37 +Inductor_THT:L_Axial_L17.5mm_D12.0mm_P20.32mm_Horizontal_Vishay_IM-10-46 +Inductor_THT:L_Axial_L17.5mm_D12.0mm_P7.62mm_Vertical_Vishay_IM-10-46 +Inductor_THT:L_Axial_L20.0mm_D8.0mm_P25.40mm_Horizontal +Inductor_THT:L_Axial_L20.0mm_D8.0mm_P5.08mm_Vertical +Inductor_THT:L_Axial_L20.0mm_D8.0mm_P7.62mm_Vertical +Inductor_THT:L_Axial_L20.3mm_D12.1mm_P28.50mm_Horizontal_Vishay_IHA-101 +Inductor_THT:L_Axial_L20.3mm_D12.1mm_P7.62mm_Vertical_Vishay_IHA-101 +Inductor_THT:L_Axial_L20.3mm_D12.7mm_P25.40mm_Horizontal_Vishay_IHA-201 +Inductor_THT:L_Axial_L20.3mm_D12.7mm_P7.62mm_Vertical_Vishay_IHA-201 +Inductor_THT:L_Axial_L23.4mm_D12.7mm_P32.00mm_Horizontal_Vishay_IHA-203 +Inductor_THT:L_Axial_L23.4mm_D12.7mm_P7.62mm_Vertical_Vishay_IHA-203 +Inductor_THT:L_Axial_L24.0mm_D7.1mm_P30.48mm_Horizontal_Vishay_IM-10-28 +Inductor_THT:L_Axial_L24.0mm_D7.1mm_P5.08mm_Vertical_Vishay_IM-10-28 +Inductor_THT:L_Axial_L24.0mm_D7.5mm_P27.94mm_Horizontal_Fastron_MESC +Inductor_THT:L_Axial_L24.0mm_D7.5mm_P5.08mm_Vertical_Fastron_MESC +Inductor_THT:L_Axial_L24.0mm_D7.5mm_P7.62mm_Vertical_Fastron_MESC +Inductor_THT:L_Axial_L26.0mm_D10.0mm_P30.48mm_Horizontal_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D10.0mm_P5.08mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D10.0mm_P7.62mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D11.0mm_P30.48mm_Horizontal_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D11.0mm_P5.08mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D11.0mm_P7.62mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D9.0mm_P30.48mm_Horizontal_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D9.0mm_P5.08mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L26.0mm_D9.0mm_P7.62mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L26.7mm_D12.1mm_P35.00mm_Horizontal_Vishay_IHA-103 +Inductor_THT:L_Axial_L26.7mm_D12.1mm_P7.62mm_Vertical_Vishay_IHA-103 +Inductor_THT:L_Axial_L26.7mm_D14.0mm_P35.00mm_Horizontal_Vishay_IHA-104 +Inductor_THT:L_Axial_L26.7mm_D14.0mm_P7.62mm_Vertical_Vishay_IHA-104 +Inductor_THT:L_Axial_L29.9mm_D14.0mm_P38.00mm_Horizontal_Vishay_IHA-105 +Inductor_THT:L_Axial_L29.9mm_D14.0mm_P7.62mm_Vertical_Vishay_IHA-105 +Inductor_THT:L_Axial_L30.0mm_D8.0mm_P35.56mm_Horizontal_Fastron_77A +Inductor_THT:L_Axial_L30.0mm_D8.0mm_P5.08mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L30.0mm_D8.0mm_P7.62mm_Vertical_Fastron_77A +Inductor_THT:L_Axial_L5.0mm_D3.6mm_P10.00mm_Horizontal_Murata_BL01RN1A2A2 +Inductor_THT:L_Axial_L5.3mm_D2.2mm_P10.16mm_Horizontal_Vishay_IM-1 +Inductor_THT:L_Axial_L5.3mm_D2.2mm_P2.54mm_Vertical_Vishay_IM-1 +Inductor_THT:L_Axial_L5.3mm_D2.2mm_P7.62mm_Horizontal_Vishay_IM-1 +Inductor_THT:L_Axial_L6.6mm_D2.7mm_P10.16mm_Horizontal_Vishay_IM-2 +Inductor_THT:L_Axial_L6.6mm_D2.7mm_P2.54mm_Vertical_Vishay_IM-2 +Inductor_THT:L_Axial_L7.0mm_D3.3mm_P10.16mm_Horizontal_Fastron_MICC +Inductor_THT:L_Axial_L7.0mm_D3.3mm_P12.70mm_Horizontal_Fastron_MICC +Inductor_THT:L_Axial_L7.0mm_D3.3mm_P2.54mm_Vertical_Fastron_MICC +Inductor_THT:L_Axial_L7.0mm_D3.3mm_P5.08mm_Vertical_Fastron_MICC +Inductor_THT:L_Axial_L9.5mm_D4.0mm_P12.70mm_Horizontal_Fastron_SMCC +Inductor_THT:L_Axial_L9.5mm_D4.0mm_P15.24mm_Horizontal_Fastron_SMCC +Inductor_THT:L_Axial_L9.5mm_D4.0mm_P2.54mm_Vertical_Fastron_SMCC +Inductor_THT:L_Axial_L9.5mm_D4.0mm_P5.08mm_Vertical_Fastron_SMCC +Inductor_THT:L_CommonMode_PulseElectronics_PH9455x105NL_1 +Inductor_THT:L_CommonMode_PulseElectronics_PH9455x155NL_1 +Inductor_THT:L_CommonMode_PulseElectronics_PH9455x205NL_1 +Inductor_THT:L_CommonMode_PulseElectronics_PH9455x405NL_1 +Inductor_THT:L_CommonMode_PulseElectronics_PH9455x705NL_1 +Inductor_THT:L_CommonMode_PulseElectronics_PH9455xxx6NL_2 +Inductor_THT:L_CommonMode_TDK_B82746S4143A040 +Inductor_THT:L_CommonMode_TDK_B82746S6702A040 +Inductor_THT:L_CommonMode_TDK_B82747E6163A040 +Inductor_THT:L_CommonMode_TDK_B82747E6203A040 +Inductor_THT:L_CommonMode_TDK_B82747E6253A040 +Inductor_THT:L_CommonMode_TDK_B82747E6353A040 +Inductor_THT:L_CommonMode_TDK_B82767S4123N030 +Inductor_THT:L_CommonMode_TDK_B82767S4193N030 +Inductor_THT:L_CommonMode_TDK_B82767S4263N030 +Inductor_THT:L_CommonMode_Toroid_Vertical_L19.3mm_W10.8mm_Px6.35mm_Py15.24mm_Bourns_8100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L21.0mm_W10.0mm_Px5.08mm_Py12.70mm_Murata_5100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L24.0mm_W16.3mm_Px10.16mm_Py20.32mm_Murata_5200 +Inductor_THT:L_CommonMode_Toroid_Vertical_L30.5mm_W15.2mm_Px10.16mm_Py20.32mm_Bourns_8100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L34.3mm_W20.3mm_Px15.24mm_Py22.86mm_Bourns_8100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L36.8mm_W20.3mm_Px15.24mm_Py22.86mm_Bourns_8100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L38.1mm_W20.3mm_Px15.24mm_Py22.86mm_Bourns_8100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L39.4mm_W20.3mm_Px15.24mm_Py22.86mm_Bourns_8100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L41.9mm_W20.3mm_Px15.24mm_Py22.86mm_Bourns_8100 +Inductor_THT:L_CommonMode_Toroid_Vertical_L43.2mm_W22.9mm_Px17.78mm_Py30.48mm_Bourns_8100 +Inductor_THT:L_CommonMode_VAC_T60405-S6123-X140 +Inductor_THT:L_CommonMode_VAC_T60405-S6123-X240 +Inductor_THT:L_CommonMode_VAC_T60405-S6123-X402 +Inductor_THT:L_CommonMode_Wuerth_WE-CMB-L +Inductor_THT:L_CommonMode_Wuerth_WE-CMB-M +Inductor_THT:L_CommonMode_Wuerth_WE-CMB-S +Inductor_THT:L_CommonMode_Wuerth_WE-CMB-XL +Inductor_THT:L_CommonMode_Wuerth_WE-CMB-XS +Inductor_THT:L_CommonMode_Wuerth_WE-CMB-XXL +Inductor_THT:L_Mount_Lodestone_VTM120 +Inductor_THT:L_Mount_Lodestone_VTM160 +Inductor_THT:L_Mount_Lodestone_VTM254 +Inductor_THT:L_Mount_Lodestone_VTM280 +Inductor_THT:L_Mount_Lodestone_VTM950-6 +Inductor_THT:L_Radial_D10.0mm_P5.00mm_Fastron_07M +Inductor_THT:L_Radial_D10.0mm_P5.00mm_Fastron_07P +Inductor_THT:L_Radial_D10.0mm_P5.00mm_Neosid_SD12k_style3 +Inductor_THT:L_Radial_D10.0mm_P5.00mm_Neosid_SD12_style3 +Inductor_THT:L_Radial_D10.5mm_P4.00x5.00mm_Murata_1200RS +Inductor_THT:L_Radial_D10.5mm_P5.00mm_Abacron_AISR-01 +Inductor_THT:L_Radial_D12.0mm_P10.00mm_Neosid_SD12k_style1 +Inductor_THT:L_Radial_D12.0mm_P10.00mm_Neosid_SD12_style1 +Inductor_THT:L_Radial_D12.0mm_P5.00mm_Fastron_11P +Inductor_THT:L_Radial_D12.0mm_P5.00mm_Neosid_SD12k_style2 +Inductor_THT:L_Radial_D12.0mm_P5.00mm_Neosid_SD12_style2 +Inductor_THT:L_Radial_D12.0mm_P6.00mm_Murata_1900R +Inductor_THT:L_Radial_D12.5mm_P7.00mm_Fastron_09HCP +Inductor_THT:L_Radial_D12.5mm_P9.00mm_Fastron_09HCP +Inductor_THT:L_Radial_D13.5mm_P7.00mm_Fastron_09HCP +Inductor_THT:L_Radial_D14.2mm_P10.00mm_Neosid_SD14 +Inductor_THT:L_Radial_D16.0mm_P10.00mm_Panasonic_15E-L +Inductor_THT:L_Radial_D16.8mm_P11.43mm_Vishay_IHB-1 +Inductor_THT:L_Radial_D16.8mm_P12.07mm_Vishay_IHB-1 +Inductor_THT:L_Radial_D16.8mm_P12.70mm_Vishay_IHB-1 +Inductor_THT:L_Radial_D18.0mm_P10.00mm +Inductor_THT:L_Radial_D21.0mm_P14.61mm_Vishay_IHB-2 +Inductor_THT:L_Radial_D21.0mm_P15.00mm_Vishay_IHB-2 +Inductor_THT:L_Radial_D21.0mm_P15.24mm_Vishay_IHB-2 +Inductor_THT:L_Radial_D21.0mm_P15.75mm_Vishay_IHB-2 +Inductor_THT:L_Radial_D21.0mm_P19.00mm +Inductor_THT:L_Radial_D24.0mm_P24.00mm +Inductor_THT:L_Radial_D24.4mm_P22.90mm_Murata_1400series +Inductor_THT:L_Radial_D24.4mm_P23.10mm_Murata_1400series +Inductor_THT:L_Radial_D24.4mm_P23.40mm_Murata_1400series +Inductor_THT:L_Radial_D24.4mm_P23.70mm_Murata_1400series +Inductor_THT:L_Radial_D24.4mm_P23.90mm_Murata_1400series +Inductor_THT:L_Radial_D27.9mm_P18.29mm_Vishay_IHB-3 +Inductor_THT:L_Radial_D27.9mm_P19.05mm_Vishay_IHB-3 +Inductor_THT:L_Radial_D27.9mm_P20.07mm_Vishay_IHB-3 +Inductor_THT:L_Radial_D28.0mm_P29.20mm +Inductor_THT:L_Radial_D29.8mm_P28.30mm_Murata_1400series +Inductor_THT:L_Radial_D29.8mm_P28.50mm_Murata_1400series +Inductor_THT:L_Radial_D29.8mm_P28.80mm_Murata_1400series +Inductor_THT:L_Radial_D29.8mm_P29.00mm_Murata_1400series +Inductor_THT:L_Radial_D29.8mm_P29.30mm_Murata_1400series +Inductor_THT:L_Radial_D40.6mm_P26.16mm_Vishay_IHB-5 +Inductor_THT:L_Radial_D40.6mm_P27.18mm_Vishay_IHB-4 +Inductor_THT:L_Radial_D40.6mm_P27.94mm_Vishay_IHB-4 +Inductor_THT:L_Radial_D40.6mm_P27.94mm_Vishay_IHB-5 +Inductor_THT:L_Radial_D40.6mm_P28.70mm_Vishay_IHB-5 +Inductor_THT:L_Radial_D50.8mm_P33.27mm_Vishay_IHB-6 +Inductor_THT:L_Radial_D50.8mm_P34.29mm_Vishay_IHB-6 +Inductor_THT:L_Radial_D50.8mm_P35.81mm_Vishay_IHB-6 +Inductor_THT:L_Radial_D50.8mm_P36.32mm_Vishay_IHB-6 +Inductor_THT:L_Radial_D50.8mm_P38.86mm_Vishay_IHB-6 +Inductor_THT:L_Radial_D6.0mm_P4.00mm +Inductor_THT:L_Radial_D7.0mm_P3.00mm +Inductor_THT:L_Radial_D7.2mm_P3.00mm_Murata_1700 +Inductor_THT:L_Radial_D7.5mm_P3.50mm_Fastron_07P +Inductor_THT:L_Radial_D7.5mm_P5.00mm_Fastron_07P +Inductor_THT:L_Radial_D7.8mm_P5.00mm_Fastron_07HCP +Inductor_THT:L_Radial_D8.7mm_P5.00mm_Fastron_07HCP +Inductor_THT:L_Radial_D9.5mm_P5.00mm_Fastron_07HVP +Inductor_THT:L_Radial_L10.2mm_W10.2mm_Px7.62mm_Py7.62mm_Pulse_LP-30 +Inductor_THT:L_Radial_L11.5mm_W11.5mm_Px6.00mm_Py6.00mm_Neosid_NE-CPB-11EN_Drill1.3mm +Inductor_THT:L_Radial_L11.5mm_W11.5mm_Px6.00mm_Py6.00mm_Neosid_NE-CPB-11EN_Drill1.5mm +Inductor_THT:L_Radial_L11.5mm_W11.5mm_Px6.00mm_Py6.00mm_Neosid_NE-CPB-11EN_Drill1.7mm +Inductor_THT:L_Radial_L11.5mm_W11.5mm_Px6.00mm_Py6.00mm_Neosid_NE-CPB-11EN_Drill1.8mm +Inductor_THT:L_Radial_L12.6mm_W12.6mm_Px9.52mm_Py9.52mm_Pulse_LP-37 +Inductor_THT:L_Radial_L16.1mm_W16.1mm_Px7.62mm_Py12.70mm_Pulse_LP-44 +Inductor_THT:L_Radial_L7.5mm_W4.6mm_P5.00mm_Neosid_SD75 +Inductor_THT:L_Radial_L8.0mm_W8.0mm_P5.00mm_Neosid_NE-CPB-07E +Inductor_THT:L_Radial_L8.0mm_W8.0mm_P5.00mm_Neosid_SD8 +Inductor_THT:L_Radial_L9.1mm_W9.1mm_Px6.35mm_Py6.35mm_Pulse_LP-25 +Inductor_THT:L_SELF1408 +Inductor_THT:L_SELF1418 +Inductor_THT:L_Toroid_Horizontal_D11.2mm_P17.00mm_Diameter12-5mm_Amidon-T44 +Inductor_THT:L_Toroid_Horizontal_D12.7mm_P20.00mm_Diameter14-5mm_Amidon-T50 +Inductor_THT:L_Toroid_Horizontal_D16.8mm_P14.70mm_Vishay_TJ3 +Inductor_THT:L_Toroid_Horizontal_D16.8mm_P14.70mm_Vishay_TJ3_BigPads +Inductor_THT:L_Toroid_Horizontal_D17.3mm_P15.24mm_Bourns_2000 +Inductor_THT:L_Toroid_Horizontal_D21.8mm_P19.10mm_Bourns_2100 +Inductor_THT:L_Toroid_Horizontal_D21.8mm_P19.60mm_Bourns_2100 +Inductor_THT:L_Toroid_Horizontal_D22.4mm_P19.80mm_Vishay_TJ4 +Inductor_THT:L_Toroid_Horizontal_D24.1mm_P21.80mm_Bourns_2200 +Inductor_THT:L_Toroid_Horizontal_D24.1mm_P23.10mm_Bourns_2200 +Inductor_THT:L_Toroid_Horizontal_D25.4mm_P22.90mm_Vishay_TJ5 +Inductor_THT:L_Toroid_Horizontal_D25.4mm_P22.90mm_Vishay_TJ5_BigPads +Inductor_THT:L_Toroid_Horizontal_D26.0mm_P5.08mm +Inductor_THT:L_Toroid_Horizontal_D28.0mm_P25.10mm_Bourns_2200 +Inductor_THT:L_Toroid_Horizontal_D28.0mm_P26.67mm_Bourns_2200 +Inductor_THT:L_Toroid_Horizontal_D3.2mm_P6.40mm_Diameter3-5mm_Amidon-T12 +Inductor_THT:L_Toroid_Horizontal_D32.5mm_P28.90mm_Bourns_2300 +Inductor_THT:L_Toroid_Horizontal_D32.5mm_P30.00mm_Bourns_2300 +Inductor_THT:L_Toroid_Horizontal_D35.1mm_P31.00mm_Vishay_TJ6 +Inductor_THT:L_Toroid_Horizontal_D4.1mm_P8.00mm_Diameter4-5mm_Amidon-T16 +Inductor_THT:L_Toroid_Horizontal_D40.0mm_P48.26mm +Inductor_THT:L_Toroid_Horizontal_D41.9mm_P37.60mm_Vishay_TJ7 +Inductor_THT:L_Toroid_Horizontal_D49.3mm_P44.60mm_Vishay_TJ8 +Inductor_THT:L_Toroid_Horizontal_D5.1mm_P9.00mm_Diameter6-5mm_Amidon-T20 +Inductor_THT:L_Toroid_Horizontal_D6.5mm_P10.00mm_Diameter7-5mm_Amidon-T25 +Inductor_THT:L_Toroid_Horizontal_D69.1mm_P63.20mm_Vishay_TJ9 +Inductor_THT:L_Toroid_Horizontal_D7.8mm_P13.00mm_Diameter9-5mm_Amidon-T30 +Inductor_THT:L_Toroid_Horizontal_D9.5mm_P15.00mm_Diameter10-5mm_Amidon-T37 +Inductor_THT:L_Toroid_Vertical_L10.0mm_W5.0mm_P5.08mm +Inductor_THT:L_Toroid_Vertical_L13.0mm_W6.5mm_P5.60mm +Inductor_THT:L_Toroid_Vertical_L14.0mm_W5.6mm_P5.30mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L14.0mm_W6.3mm_P4.57mm_Pulse_A +Inductor_THT:L_Toroid_Vertical_L14.7mm_W8.6mm_P5.58mm_Pulse_KM-1 +Inductor_THT:L_Toroid_Vertical_L16.0mm_W8.0mm_P7.62mm +Inductor_THT:L_Toroid_Vertical_L16.3mm_W7.1mm_P7.11mm_Pulse_H +Inductor_THT:L_Toroid_Vertical_L16.4mm_W7.6mm_P6.60mm_Vishay_TJ3 +Inductor_THT:L_Toroid_Vertical_L16.5mm_W11.4mm_P7.62mm_Pulse_KM-2 +Inductor_THT:L_Toroid_Vertical_L16.8mm_W9.2mm_P7.10mm_Vishay_TJ3 +Inductor_THT:L_Toroid_Vertical_L16.8mm_W9.2mm_P7.10mm_Vishay_TJ3_BigPads +Inductor_THT:L_Toroid_Vertical_L17.8mm_W8.1mm_P7.62mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L17.8mm_W9.7mm_P7.11mm_Pulse_B +Inductor_THT:L_Toroid_Vertical_L19.1mm_W8.1mm_P7.10mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L21.6mm_W11.4mm_P7.62mm_Pulse_KM-3 +Inductor_THT:L_Toroid_Vertical_L21.6mm_W8.4mm_P8.38mm_Pulse_G +Inductor_THT:L_Toroid_Vertical_L21.6mm_W9.1mm_P8.40mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L21.6mm_W9.5mm_P7.11mm_Pulse_C +Inductor_THT:L_Toroid_Vertical_L22.4mm_W10.2mm_P7.90mm_Vishay_TJ4 +Inductor_THT:L_Toroid_Vertical_L24.6mm_W15.5mm_P11.44mm_Pulse_KM-4 +Inductor_THT:L_Toroid_Vertical_L25.4mm_W14.7mm_P12.20mm_Vishay_TJ5 +Inductor_THT:L_Toroid_Vertical_L25.4mm_W14.7mm_P12.20mm_Vishay_TJ5_BigPads +Inductor_THT:L_Toroid_Vertical_L26.7mm_W14.0mm_P10.16mm_Pulse_D +Inductor_THT:L_Toroid_Vertical_L28.6mm_W14.3mm_P11.43mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L31.8mm_W15.9mm_P13.50mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L33.0mm_W17.8mm_P12.70mm_Pulse_KM-5 +Inductor_THT:L_Toroid_Vertical_L35.1mm_W21.1mm_P18.50mm_Vishay_TJ6 +Inductor_THT:L_Toroid_Vertical_L35.6mm_W17.8mm_P12.70mm_Pulse_E +Inductor_THT:L_Toroid_Vertical_L41.9mm_W17.8mm_P12.70mm_Pulse_F +Inductor_THT:L_Toroid_Vertical_L41.9mm_W19.1mm_P15.80mm_Vishay_TJ7 +Inductor_THT:L_Toroid_Vertical_L46.0mm_W19.1mm_P21.80mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L48.8mm_W25.4mm_P20.80mm_Vishay_TJ8 +Inductor_THT:L_Toroid_Vertical_L54.0mm_W23.8mm_P20.10mm_Bourns_5700 +Inductor_THT:L_Toroid_Vertical_L67.6mm_W36.1mm_P31.80mm_Vishay_TJ9 +Inductor_THT_Wurth:L_Wurth_WE-HCFT-2012_LeadDiameter1.2mm +Inductor_THT_Wurth:L_Wurth_WE-HCFT-2012_LeadDiameter1.5mm +Inductor_THT_Wurth:L_Wurth_WE-HCFT-2504 +Inductor_THT_Wurth:L_Wurth_WE-HCFT-3521 +Inductor_THT_Wurth:L_Wurth_WE-HCFT-3533_LeadDiameter1.8mm +Inductor_THT_Wurth:L_Wurth_WE-HCFT-3533_LeadDiameter2.0mm +Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter0.8mm +Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter1.3mm +Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter1.5mm +Inductor_THT_Wurth:L_Wurth_WE-HCFT-3540_LeadDiameter2.0mm +Jumper:SolderJumper-2_P1.3mm_Bridged2Bar_Pad1.0x1.5mm +Jumper:SolderJumper-2_P1.3mm_Bridged2Bar_RoundedPad1.0x1.5mm +Jumper:SolderJumper-2_P1.3mm_Bridged_Pad1.0x1.5mm +Jumper:SolderJumper-2_P1.3mm_Bridged_RoundedPad1.0x1.5mm +Jumper:SolderJumper-2_P1.3mm_Open_Pad1.0x1.5mm +Jumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mm +Jumper:SolderJumper-2_P1.3mm_Open_TrianglePad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Bridged12_RoundedPad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged12_RoundedPad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar12_Pad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar12_Pad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar12_RoundedPad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Bridged2Bar12_RoundedPad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Open_Pad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Open_Pad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P1.3mm_Open_RoundedPad1.0x1.5mm +Jumper:SolderJumper-3_P1.3mm_Open_RoundedPad1.0x1.5mm_NumberLabels +Jumper:SolderJumper-3_P2.0mm_Open_TrianglePad1.0x1.5mm +Jumper:SolderJumper-3_P2.0mm_Open_TrianglePad1.0x1.5mm_NumberLabels +LED_SMD:LED-APA102-2020 +LED_SMD:LED-L1T2_LUMILEDS +LED_SMD:LED_0201_0603Metric +LED_SMD:LED_0201_0603Metric_Pad0.64x0.40mm_HandSolder +LED_SMD:LED_0402_1005Metric +LED_SMD:LED_0402_1005Metric_Pad0.77x0.64mm_HandSolder +LED_SMD:LED_0603_1608Metric +LED_SMD:LED_0603_1608Metric_Pad1.05x0.95mm_HandSolder +LED_SMD:LED_0805_2012Metric +LED_SMD:LED_0805_2012Metric_Pad1.15x1.40mm_HandSolder +LED_SMD:LED_1206_3216Metric +LED_SMD:LED_1206_3216Metric_Pad1.42x1.75mm_HandSolder +LED_SMD:LED_1206_3216Metric_ReverseMount_Hole1.8x2.4mm +LED_SMD:LED_1210_3225Metric +LED_SMD:LED_1210_3225Metric_Pad1.42x2.65mm_HandSolder +LED_SMD:LED_1812_4532Metric +LED_SMD:LED_1812_4532Metric_Pad1.30x3.40mm_HandSolder +LED_SMD:LED_1W_3W_R8 +LED_SMD:LED_2010_5025Metric +LED_SMD:LED_2010_5025Metric_Pad1.52x2.65mm_HandSolder +LED_SMD:LED_2512_6332Metric +LED_SMD:LED_2512_6332Metric_Pad1.52x3.35mm_HandSolder +LED_SMD:LED_ASMB-KTF0-0A306 +LED_SMD:LED_Avago_PLCC4_3.2x2.8mm_CW +LED_SMD:LED_Avago_PLCC6_3x2.8mm +LED_SMD:LED_Cree-PLCC4_2x2mm_CW +LED_SMD:LED_Cree-PLCC4_3.2x2.8mm_CCW +LED_SMD:LED_Cree-PLCC4_5x5mm_CW +LED_SMD:LED_Cree-PLCC6_4.7x1.5mm +LED_SMD:LED_Cree-XB +LED_SMD:LED_Cree-XH +LED_SMD:LED_Cree-XHP35 +LED_SMD:LED_Cree-XHP50_12V +LED_SMD:LED_Cree-XHP50_6V +LED_SMD:LED_Cree-XHP70_12V +LED_SMD:LED_Cree-XHP70_6V +LED_SMD:LED_Cree-XP-G +LED_SMD:LED_Cree-XP +LED_SMD:LED_Cree-XQ +LED_SMD:LED_Cree-XQ_HandSoldering +LED_SMD:LED_CSP_Samsung_LH181B_2.36x2.36mm +LED_SMD:LED_Dialight_591 +LED_SMD:LED_Everlight-SMD3528_3.5x2.8mm_67-21ST +LED_SMD:LED_Inolux_IN-P55TATRGB_PLCC6_5.0x5.5mm_P1.8mm +LED_SMD:LED_Inolux_IN-PI554FCH_PLCC4_5.0x5.0mm_P3.2mm +LED_SMD:LED_Kingbright_AAA3528ESGCT +LED_SMD:LED_Kingbright_APA1606_1.6x0.6mm_Horizontal +LED_SMD:LED_Kingbright_APDA3020VBCD +LED_SMD:LED_Kingbright_APFA3010_3x1.5mm_Horizontal +LED_SMD:LED_Kingbright_APHBM2012_2x1.25mm +LED_SMD:LED_Kingbright_KPA-3010_3x2x1mm +LED_SMD:LED_Kingbright_KPBD-3224 +LED_SMD:LED_LiteOn_LTST-C19HE1WT +LED_SMD:LED_LiteOn_LTST-C235KGKRKT +LED_SMD:LED_LiteOn_LTST-C295K_1.6x0.8mm +LED_SMD:LED_LiteOn_LTST-E563C_PLCC4_5.0x5.0mm_P3.2mm +LED_SMD:LED_LiteOn_LTST-E563C_PLCC4_5.0x5.0mm_P3.2mm_HandSoldering +LED_SMD:LED_LiteOn_LTST-S326 +LED_SMD:LED_Lumex_SML-LX0303SIUPGUSB +LED_SMD:LED_Lumex_SML-LX0404SIUPGUSB +LED_SMD:LED_Luminus_MP-3030-1100_3.0x3.0mm +LED_SMD:LED_miniPLCC_2315 +LED_SMD:LED_miniPLCC_2315_Handsoldering +LED_SMD:LED_OPSCO_SK6812_PLCC4_5.0x5.0mm_P3.1mm +LED_SMD:LED_Osram_Lx_P47F_D2mm_ReverseMount +LED_SMD:LED_PLCC-2_3.4x3.0mm_AK +LED_SMD:LED_PLCC-2_3.4x3.0mm_KA +LED_SMD:LED_PLCC-2_3x2mm_AK +LED_SMD:LED_PLCC-2_3x2mm_KA +LED_SMD:LED_PLCC_2835 +LED_SMD:LED_PLCC_2835_Handsoldering +LED_SMD:LED_RGB_1210 +LED_SMD:LED_RGB_5050-6 +LED_SMD:LED_RGB_Cree-PLCC-6_6x5mm_P2.1mm +LED_SMD:LED_RGB_Everlight_EASV3015RGBA0_Horizontal +LED_SMD:LED_RGB_Getian_GT-P6PRGB4303 +LED_SMD:LED_RGB_Lumex_SML-LXT0805SIUGUBW +LED_SMD:LED_RGB_PLCC-6 +LED_SMD:LED_RGB_Wuerth-PLCC4_3.2x2.8mm_150141M173100 +LED_SMD:LED_RGB_Wuerth_150080M153000 +LED_SMD:LED_ROHM_SMLVN6 +LED_SMD:LED_SK6805_PLCC4_2.4x2.7mm_P1.3mm +LED_SMD:LED_SK6812MINI_PLCC4_3.5x3.5mm_P1.75mm +LED_SMD:LED_SK6812_EC15_1.5x1.5mm +LED_SMD:LED_SK6812_PLCC4_5.0x5.0mm_P3.2mm +LED_SMD:LED_WS2812B-2020_PLCC4_2.0x2.0mm +LED_SMD:LED_WS2812B-Mini_PLCC4_3.5x3.5mm +LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm +LED_SMD:LED_WS2812_PLCC6_5.0x5.0mm_P1.6mm +LED_SMD:LED_Wurth_150044M155260 +LED_SMD:LED_Yuji_5730 +LED_THT:LED_BL-FL7680RGB +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O1.27mm_Z1.6mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O1.27mm_Z4.9mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O1.27mm_Z8.2mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O3.81mm_Z1.6mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O3.81mm_Z4.9mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O3.81mm_Z8.2mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O6.35mm_Z1.6mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O6.35mm_Z4.9mm +LED_THT:LED_D1.8mm_W1.8mm_H2.4mm_Horizontal_O6.35mm_Z8.2mm +LED_THT:LED_D1.8mm_W3.3mm_H2.4mm +LED_THT:LED_D10.0mm-3 +LED_THT:LED_D10.0mm +LED_THT:LED_D2.0mm_W4.0mm_H2.8mm_FlatTop +LED_THT:LED_D2.0mm_W4.8mm_H2.5mm_FlatTop +LED_THT:LED_D20.0mm +LED_THT:LED_D3.0mm-3 +LED_THT:LED_D3.0mm +LED_THT:LED_D3.0mm_Clear +LED_THT:LED_D3.0mm_FlatTop +LED_THT:LED_D3.0mm_Horizontal_O1.27mm_Z10.0mm +LED_THT:LED_D3.0mm_Horizontal_O1.27mm_Z2.0mm +LED_THT:LED_D3.0mm_Horizontal_O1.27mm_Z2.0mm_Clear +LED_THT:LED_D3.0mm_Horizontal_O1.27mm_Z2.0mm_IRBlack +LED_THT:LED_D3.0mm_Horizontal_O1.27mm_Z2.0mm_IRGrey +LED_THT:LED_D3.0mm_Horizontal_O1.27mm_Z6.0mm +LED_THT:LED_D3.0mm_Horizontal_O3.81mm_Z10.0mm +LED_THT:LED_D3.0mm_Horizontal_O3.81mm_Z2.0mm +LED_THT:LED_D3.0mm_Horizontal_O3.81mm_Z6.0mm +LED_THT:LED_D3.0mm_Horizontal_O6.35mm_Z10.0mm +LED_THT:LED_D3.0mm_Horizontal_O6.35mm_Z2.0mm +LED_THT:LED_D3.0mm_Horizontal_O6.35mm_Z6.0mm +LED_THT:LED_D3.0mm_IRBlack +LED_THT:LED_D3.0mm_IRGrey +LED_THT:LED_D4.0mm +LED_THT:LED_D5.0mm-3 +LED_THT:LED_D5.0mm-3_Horizontal_O3.81mm_Z3.0mm +LED_THT:LED_D5.0mm-4_RGB +LED_THT:LED_D5.0mm-4_RGB_Staggered_Pins +LED_THT:LED_D5.0mm-4_RGB_Wide_Pins +LED_THT:LED_D5.0mm +LED_THT:LED_D5.0mm_Clear +LED_THT:LED_D5.0mm_FlatTop +LED_THT:LED_D5.0mm_Horizontal_O1.27mm_Z15.0mm +LED_THT:LED_D5.0mm_Horizontal_O1.27mm_Z3.0mm +LED_THT:LED_D5.0mm_Horizontal_O1.27mm_Z3.0mm_Clear +LED_THT:LED_D5.0mm_Horizontal_O1.27mm_Z3.0mm_IRBlack +LED_THT:LED_D5.0mm_Horizontal_O1.27mm_Z3.0mm_IRGrey +LED_THT:LED_D5.0mm_Horizontal_O1.27mm_Z9.0mm +LED_THT:LED_D5.0mm_Horizontal_O3.81mm_Z15.0mm +LED_THT:LED_D5.0mm_Horizontal_O3.81mm_Z3.0mm +LED_THT:LED_D5.0mm_Horizontal_O3.81mm_Z9.0mm +LED_THT:LED_D5.0mm_Horizontal_O6.35mm_Z15.0mm +LED_THT:LED_D5.0mm_Horizontal_O6.35mm_Z3.0mm +LED_THT:LED_D5.0mm_Horizontal_O6.35mm_Z9.0mm +LED_THT:LED_D5.0mm_IRBlack +LED_THT:LED_D5.0mm_IRGrey +LED_THT:LED_D8.0mm-3 +LED_THT:LED_D8.0mm +LED_THT:LED_Oval_W5.2mm_H3.8mm +LED_THT:LED_Rectangular_W3.0mm_H2.0mm +LED_THT:LED_Rectangular_W3.9mm_H1.8mm +LED_THT:LED_Rectangular_W3.9mm_H1.9mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm-3Pins +LED_THT:LED_Rectangular_W5.0mm_H2.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O1.27mm_Z1.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O1.27mm_Z3.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O1.27mm_Z5.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O3.81mm_Z1.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O3.81mm_Z3.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O3.81mm_Z5.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O6.35mm_Z1.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O6.35mm_Z3.0mm +LED_THT:LED_Rectangular_W5.0mm_H2.0mm_Horizontal_O6.35mm_Z5.0mm +LED_THT:LED_Rectangular_W5.0mm_H5.0mm +LED_THT:LED_Rectangular_W7.62mm_H4.55mm_P5.08mm_R3 +LED_THT:LED_SideEmitter_Rectangular_W4.5mm_H1.6mm +LED_THT:LED_VCCLite_5381H1_6.35x6.35mm +LED_THT:LED_VCCLite_5381H3_6.35x6.35mm +LED_THT:LED_VCCLite_5381H5_6.35x6.35mm +LED_THT:LED_VCCLite_5381H7_6.35x6.35mm +Module:A20_OLINUXINO_LIME2 +Module:Adafruit_Feather +Module:Adafruit_Feather_32u4_FONA +Module:Adafruit_Feather_32u4_FONA_WithMountingHoles +Module:Adafruit_Feather_32u4_RFM +Module:Adafruit_Feather_32u4_RFM_WithMountingHoles +Module:Adafruit_Feather_M0_RFM +Module:Adafruit_Feather_M0_RFM_WithMountingHoles +Module:Adafruit_Feather_M0_Wifi +Module:Adafruit_Feather_M0_Wifi_WithMountingHoles +Module:Adafruit_Feather_WICED +Module:Adafruit_Feather_WICED_WithMountingHoles +Module:Adafruit_Feather_WithMountingHoles +Module:Adafruit_HUZZAH_ESP8266_breakout +Module:Adafruit_HUZZAH_ESP8266_breakout_WithMountingHoles +Module:Arduino_Nano +Module:Arduino_Nano_WithMountingHoles +Module:Arduino_UNO_R2 +Module:Arduino_UNO_R2_WithMountingHoles +Module:Arduino_UNO_R3 +Module:Arduino_UNO_R3_WithMountingHoles +Module:BeagleBoard_PocketBeagle +Module:Carambola2 +Module:Electrosmith_Daisy_Seed +Module:Flipper_Zero_Angled +Module:Flipper_Zero_Straight +Module:Google_Coral_SMT_TPU_Module +Module:Maple_Mini +Module:Olimex_MOD-WIFI-ESP8266-DEV +Module:Onion_Omega2+ +Module:Onion_Omega2S +Module:Pololu_Breakout-16_15.2x20.3mm +Module:RaspberryPi_Pico_Common_SMD +Module:RaspberryPi_Pico_Common_THT +Module:RaspberryPi_Pico_Common_Unspecified +Module:RaspberryPi_Pico_SMD +Module:RaspberryPi_Pico_SMD_HandSolder +Module:RaspberryPi_Pico_W_SMD +Module:RaspberryPi_Pico_W_SMD_HandSolder +Module:Raspberry_Pi_Zero_Socketed_THT_FaceDown_MountingHoles +Module:Sipeed-M1 +Module:Sipeed-M1W +Module:ST_Morpho_Connector_144_STLink +Module:ST_Morpho_Connector_144_STLink_MountingHoles +Module:Texas_EUK_R-PDSS-T7_THT +Module:Texas_EUS_R-PDSS-T5_THT +Module:Texas_EUW_R-PDSS-T7_THT +Motors:Vybronics_VZ30C1T8219732L +MountingEquipment:DINRailAdapter_3xM3_PhoenixContact_1201578 +MountingHole:MountingHole_2.1mm +MountingHole:MountingHole_2.2mm_M2 +MountingHole:MountingHole_2.2mm_M2_DIN965 +MountingHole:MountingHole_2.2mm_M2_DIN965_Pad +MountingHole:MountingHole_2.2mm_M2_DIN965_Pad_TopBottom +MountingHole:MountingHole_2.2mm_M2_DIN965_Pad_TopOnly +MountingHole:MountingHole_2.2mm_M2_ISO14580 +MountingHole:MountingHole_2.2mm_M2_ISO14580_Pad +MountingHole:MountingHole_2.2mm_M2_ISO14580_Pad_TopBottom +MountingHole:MountingHole_2.2mm_M2_ISO14580_Pad_TopOnly +MountingHole:MountingHole_2.2mm_M2_ISO7380 +MountingHole:MountingHole_2.2mm_M2_ISO7380_Pad +MountingHole:MountingHole_2.2mm_M2_ISO7380_Pad_TopBottom +MountingHole:MountingHole_2.2mm_M2_ISO7380_Pad_TopOnly +MountingHole:MountingHole_2.2mm_M2_Pad +MountingHole:MountingHole_2.2mm_M2_Pad_TopBottom +MountingHole:MountingHole_2.2mm_M2_Pad_TopOnly +MountingHole:MountingHole_2.2mm_M2_Pad_Via +MountingHole:MountingHole_2.5mm +MountingHole:MountingHole_2.5mm_Pad +MountingHole:MountingHole_2.5mm_Pad_TopBottom +MountingHole:MountingHole_2.5mm_Pad_TopOnly +MountingHole:MountingHole_2.5mm_Pad_Via +MountingHole:MountingHole_2.7mm +MountingHole:MountingHole_2.7mm_M2.5 +MountingHole:MountingHole_2.7mm_M2.5_DIN965 +MountingHole:MountingHole_2.7mm_M2.5_DIN965_Pad +MountingHole:MountingHole_2.7mm_M2.5_DIN965_Pad_TopBottom +MountingHole:MountingHole_2.7mm_M2.5_DIN965_Pad_TopOnly +MountingHole:MountingHole_2.7mm_M2.5_ISO14580 +MountingHole:MountingHole_2.7mm_M2.5_ISO14580_Pad +MountingHole:MountingHole_2.7mm_M2.5_ISO14580_Pad_TopBottom +MountingHole:MountingHole_2.7mm_M2.5_ISO14580_Pad_TopOnly +MountingHole:MountingHole_2.7mm_M2.5_ISO7380 +MountingHole:MountingHole_2.7mm_M2.5_ISO7380_Pad +MountingHole:MountingHole_2.7mm_M2.5_ISO7380_Pad_TopBottom +MountingHole:MountingHole_2.7mm_M2.5_ISO7380_Pad_TopOnly +MountingHole:MountingHole_2.7mm_M2.5_Pad +MountingHole:MountingHole_2.7mm_M2.5_Pad_TopBottom +MountingHole:MountingHole_2.7mm_M2.5_Pad_TopOnly +MountingHole:MountingHole_2.7mm_M2.5_Pad_Via +MountingHole:MountingHole_2.7mm_Pad +MountingHole:MountingHole_2.7mm_Pad_TopBottom +MountingHole:MountingHole_2.7mm_Pad_TopOnly +MountingHole:MountingHole_2.7mm_Pad_Via +MountingHole:MountingHole_2mm +MountingHole:MountingHole_3.2mm_M3 +MountingHole:MountingHole_3.2mm_M3_DIN965 +MountingHole:MountingHole_3.2mm_M3_DIN965_Pad +MountingHole:MountingHole_3.2mm_M3_DIN965_Pad_TopBottom +MountingHole:MountingHole_3.2mm_M3_DIN965_Pad_TopOnly +MountingHole:MountingHole_3.2mm_M3_ISO14580 +MountingHole:MountingHole_3.2mm_M3_ISO14580_Pad +MountingHole:MountingHole_3.2mm_M3_ISO14580_Pad_TopBottom +MountingHole:MountingHole_3.2mm_M3_ISO14580_Pad_TopOnly +MountingHole:MountingHole_3.2mm_M3_ISO7380 +MountingHole:MountingHole_3.2mm_M3_ISO7380_Pad +MountingHole:MountingHole_3.2mm_M3_ISO7380_Pad_TopBottom +MountingHole:MountingHole_3.2mm_M3_ISO7380_Pad_TopOnly +MountingHole:MountingHole_3.2mm_M3_Pad +MountingHole:MountingHole_3.2mm_M3_Pad_TopBottom +MountingHole:MountingHole_3.2mm_M3_Pad_TopOnly +MountingHole:MountingHole_3.2mm_M3_Pad_Via +MountingHole:MountingHole_3.5mm +MountingHole:MountingHole_3.5mm_Pad +MountingHole:MountingHole_3.5mm_Pad_TopBottom +MountingHole:MountingHole_3.5mm_Pad_TopOnly +MountingHole:MountingHole_3.5mm_Pad_Via +MountingHole:MountingHole_3.7mm +MountingHole:MountingHole_3.7mm_Pad +MountingHole:MountingHole_3.7mm_Pad_TopBottom +MountingHole:MountingHole_3.7mm_Pad_TopOnly +MountingHole:MountingHole_3.7mm_Pad_Via +MountingHole:MountingHole_3mm +MountingHole:MountingHole_3mm_Pad +MountingHole:MountingHole_3mm_Pad_TopBottom +MountingHole:MountingHole_3mm_Pad_TopOnly +MountingHole:MountingHole_3mm_Pad_Via +MountingHole:MountingHole_4.3mm_M4 +MountingHole:MountingHole_4.3mm_M4_DIN965 +MountingHole:MountingHole_4.3mm_M4_DIN965_Pad +MountingHole:MountingHole_4.3mm_M4_DIN965_Pad_TopBottom +MountingHole:MountingHole_4.3mm_M4_DIN965_Pad_TopOnly +MountingHole:MountingHole_4.3mm_M4_ISO14580 +MountingHole:MountingHole_4.3mm_M4_ISO14580_Pad +MountingHole:MountingHole_4.3mm_M4_ISO14580_Pad_TopBottom +MountingHole:MountingHole_4.3mm_M4_ISO14580_Pad_TopOnly +MountingHole:MountingHole_4.3mm_M4_ISO7380 +MountingHole:MountingHole_4.3mm_M4_ISO7380_Pad +MountingHole:MountingHole_4.3mm_M4_ISO7380_Pad_TopBottom +MountingHole:MountingHole_4.3mm_M4_ISO7380_Pad_TopOnly +MountingHole:MountingHole_4.3mm_M4_Pad +MountingHole:MountingHole_4.3mm_M4_Pad_TopBottom +MountingHole:MountingHole_4.3mm_M4_Pad_TopOnly +MountingHole:MountingHole_4.3mm_M4_Pad_Via +MountingHole:MountingHole_4.3x6.2mm_M4_Pad +MountingHole:MountingHole_4.3x6.2mm_M4_Pad_Via +MountingHole:MountingHole_4.5mm +MountingHole:MountingHole_4.5mm_Pad +MountingHole:MountingHole_4.5mm_Pad_TopBottom +MountingHole:MountingHole_4.5mm_Pad_TopOnly +MountingHole:MountingHole_4.5mm_Pad_Via +MountingHole:MountingHole_4mm +MountingHole:MountingHole_4mm_Pad +MountingHole:MountingHole_4mm_Pad_TopBottom +MountingHole:MountingHole_4mm_Pad_TopOnly +MountingHole:MountingHole_4mm_Pad_Via +MountingHole:MountingHole_5.3mm_M5 +MountingHole:MountingHole_5.3mm_M5_DIN965 +MountingHole:MountingHole_5.3mm_M5_DIN965_Pad +MountingHole:MountingHole_5.3mm_M5_DIN965_Pad_TopBottom +MountingHole:MountingHole_5.3mm_M5_DIN965_Pad_TopOnly +MountingHole:MountingHole_5.3mm_M5_ISO14580 +MountingHole:MountingHole_5.3mm_M5_ISO14580_Pad +MountingHole:MountingHole_5.3mm_M5_ISO14580_Pad_TopBottom +MountingHole:MountingHole_5.3mm_M5_ISO14580_Pad_TopOnly +MountingHole:MountingHole_5.3mm_M5_ISO7380 +MountingHole:MountingHole_5.3mm_M5_ISO7380_Pad +MountingHole:MountingHole_5.3mm_M5_ISO7380_Pad_TopBottom +MountingHole:MountingHole_5.3mm_M5_ISO7380_Pad_TopOnly +MountingHole:MountingHole_5.3mm_M5_Pad +MountingHole:MountingHole_5.3mm_M5_Pad_TopBottom +MountingHole:MountingHole_5.3mm_M5_Pad_TopOnly +MountingHole:MountingHole_5.3mm_M5_Pad_Via +MountingHole:MountingHole_5.5mm +MountingHole:MountingHole_5.5mm_Pad +MountingHole:MountingHole_5.5mm_Pad_TopBottom +MountingHole:MountingHole_5.5mm_Pad_TopOnly +MountingHole:MountingHole_5.5mm_Pad_Via +MountingHole:MountingHole_5mm +MountingHole:MountingHole_5mm_Pad +MountingHole:MountingHole_5mm_Pad_TopBottom +MountingHole:MountingHole_5mm_Pad_TopOnly +MountingHole:MountingHole_5mm_Pad_Via +MountingHole:MountingHole_6.4mm_M6 +MountingHole:MountingHole_6.4mm_M6_DIN965 +MountingHole:MountingHole_6.4mm_M6_DIN965_Pad +MountingHole:MountingHole_6.4mm_M6_DIN965_Pad_TopBottom +MountingHole:MountingHole_6.4mm_M6_DIN965_Pad_TopOnly +MountingHole:MountingHole_6.4mm_M6_ISO14580 +MountingHole:MountingHole_6.4mm_M6_ISO14580_Pad +MountingHole:MountingHole_6.4mm_M6_ISO14580_Pad_TopBottom +MountingHole:MountingHole_6.4mm_M6_ISO14580_Pad_TopOnly +MountingHole:MountingHole_6.4mm_M6_ISO7380 +MountingHole:MountingHole_6.4mm_M6_ISO7380_Pad +MountingHole:MountingHole_6.4mm_M6_ISO7380_Pad_TopBottom +MountingHole:MountingHole_6.4mm_M6_ISO7380_Pad_TopOnly +MountingHole:MountingHole_6.4mm_M6_Pad +MountingHole:MountingHole_6.4mm_M6_Pad_TopBottom +MountingHole:MountingHole_6.4mm_M6_Pad_TopOnly +MountingHole:MountingHole_6.4mm_M6_Pad_Via +MountingHole:MountingHole_6.5mm +MountingHole:MountingHole_6.5mm_Pad +MountingHole:MountingHole_6.5mm_Pad_TopBottom +MountingHole:MountingHole_6.5mm_Pad_TopOnly +MountingHole:MountingHole_6.5mm_Pad_Via +MountingHole:MountingHole_6mm +MountingHole:MountingHole_6mm_Pad +MountingHole:MountingHole_6mm_Pad_TopBottom +MountingHole:MountingHole_6mm_Pad_TopOnly +MountingHole:MountingHole_6mm_Pad_Via +MountingHole:MountingHole_8.4mm_M8 +MountingHole:MountingHole_8.4mm_M8_Pad +MountingHole:MountingHole_8.4mm_M8_Pad_TopBottom +MountingHole:MountingHole_8.4mm_M8_Pad_TopOnly +MountingHole:MountingHole_8.4mm_M8_Pad_Via +MountingHole:ToolingHole_1.152mm +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H10mm_9771100360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H11mm_9771110360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H12mm_9771120360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H13mm_9771130360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H14mm_9771140360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H15mm_9771150360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H5mm_9771050360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H6mm_9771060360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H7mm_9771070360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H8mm_9771080360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSE-ExternalM3_H9mm_9771090360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H10mm_9774100482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H1mm_9774010482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H2mm_9774020482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H3mm_9774030482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H4mm_9774040482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H5mm_9774050482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H6mm_9774060482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H7mm_9774070482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H8mm_9774080482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-4.5mm_H9mm_9774090482 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H1.5mm_9774015633 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H1mm_9774010633 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H2.5mm_9774025633 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H2.5mm_ThreadDepth1.5mm_97730256332 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H2.5mm_ThreadDepth1.5mm_NoNPTH_97730256330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H2mm_9774020633 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H3.5mm_ThreadDepth2mm_97730356332 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H3.5mm_ThreadDepth2mm_97730356334 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H3.5mm_ThreadDepth2mm_NoNPTH_97730356330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H3mm_9774030633 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H3mm_ThreadDepth1.8mm_97730306332 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H3mm_ThreadDepth1.8mm_NoNPTH_97730306330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H4.5mm_ThreadDepth2mm_97730456332 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H4.5mm_ThreadDepth2mm_97730456334 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H4.5mm_ThreadDepth2mm_NoNPTH_97730456330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H4mm_ThreadDepth2mm_97730406332 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H4mm_ThreadDepth2mm_97730406334 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H4mm_ThreadDepth2mm_NoNPTH_97730406330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H5mm_ThreadDepth2mm_97730506332 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H5mm_ThreadDepth2mm_97730506334 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H5mm_ThreadDepth2mm_NoNPTH_97730506330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H6mm_ThreadDepth2mm_97730606332 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H6mm_ThreadDepth2mm_97730606334 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M1.6_H6mm_ThreadDepth2mm_NoNPTH_97730606330 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H1.5mm_9774015243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H1mm_9774010243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H2.5mm_9774025243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H2mm_9774020243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H3.5mm_9774035243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H3mm_9774030243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H4.5mm_9774045243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H4mm_9774040243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H5mm_9774050243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H6mm_9774060243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H7mm_9774070243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M2_H8mm_9774080243 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H1.5mm_9774015360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H10mm_9774100360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H11mm_9774110360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H12mm_9774120360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H13mm_9774130360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H14mm_9774140360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H15mm_9774150360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H1mm_9774010360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H2.5mm_9774025360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H2mm_9774020360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H3mm_9774030360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H4mm_9774040360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H5mm_9774050360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H6mm_9774060360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H7mm_9774070360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H8mm_9774080360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSI-M3_H9mm_9774090360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H10.6mm_ReverseMount_9775106960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H11.6mm_ReverseMount_9775116960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H2.6mm_ReverseMount_9775026960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H3.1mm_ReverseMount_9775031960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H3.6mm_ReverseMount_9775036960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H4.1mm_ReverseMount_9775041960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H4.6mm_ReverseMount_9775046960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H5.1mm_ReverseMount_9775051960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H5.6mm_ReverseMount_9775056960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H6.6mm_ReverseMount_9775066960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H7.6mm_ReverseMount_9775076960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H8.6mm_ReverseMount_9775086960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-3.2mm_H9.6mm_ReverseMount_9775096960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H10.6mm_ReverseMount_9775106360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H11.6mm_ReverseMount_9775116360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H2.6mm_ReverseMount_9775026360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H3.1mm_ReverseMount_9775031360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H3.6mm_ReverseMount_9775036360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H4.1mm_ReverseMount_9775041360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H4.6mm_ReverseMount_9775046360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H5.1mm_ReverseMount_9775051360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H5.6mm_ReverseMount_9775056360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H6.6mm_ReverseMount_9775066360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H7.6mm_ReverseMount_9775076360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H8.6mm_ReverseMount_9775086360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSR-M3_H9.6mm_ReverseMount_9775096360 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H10mm_SnapRivet_9776100960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H2.5mm_SnapRivet_9776025960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H2mm_SnapRivet_9776020960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H3mm_SnapRivet_9776030960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H4mm_SnapRivet_9776040960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H5mm_SnapRivet_9776050960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H6mm_SnapRivet_9776060960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H7mm_SnapRivet_9776070960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H8mm_SnapRivet_9776080960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMSSR-3.3mm_H9mm_SnapRivet_9776090960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H1.5mm_9774015943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H1mm_9774010943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H2.5mm_9774025943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H2mm_9774020943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H3.5mm_9774035943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H3mm_9774030943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H4.5mm_9774045943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H4mm_9774040943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H5mm_9774050943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H6mm_9774060943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H7mm_9774070943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.25mm_H8mm_9774080943 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H1.5mm_9774015951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H10mm_9774100951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H1mm_9774010951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H2.5mm_9774025951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H2mm_9774020951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H3mm_9774030951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H4mm_9774040951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H5.5mm_9774055951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H5mm_9774050951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H6.5mm_9774065951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H6mm_9774060951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H7mm_9774070951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H8mm_9774080951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-2.7mm_H9mm_9774090951 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H1.5mm_9774015960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H10mm_9774100960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H11mm_9774110960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H12mm_9774120960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H13mm_9774130960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H14mm_9774140960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H15mm_9774150960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H1mm_9774010960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H2.5mm_9774025960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H2mm_9774020960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H3mm_9774030960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H4mm_9774040960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H5mm_9774050960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H6mm_9774060960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H7mm_9774070960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H8mm_9774080960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-3.3mm_H9mm_9774090960 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H10mm_9774100982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H1mm_9774010982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H2mm_9774020982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H3mm_9774030982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H4mm_9774040982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H5mm_9774050982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H6mm_9774060982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H7mm_9774070982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H8mm_9774080982 +Mounting_Wuerth:Mounting_Wuerth_WA-SMST-4.5mm_H9mm_9774090982 +Mounting_Wuerth:Mounting_Wuerth_WP-SMRA-D3.3mm_L7mm_7466300_Horizontal +Mounting_Wuerth:Mounting_Wuerth_WP-SMRA-M3_L7mm_7466303_Horizontal +NetTie:NetTie-2_SMD_Pad0.5mm +NetTie:NetTie-2_SMD_Pad2.0mm +NetTie:NetTie-2_THT_Pad0.3mm +NetTie:NetTie-2_THT_Pad1.0mm +NetTie:NetTie-3_SMD_Pad0.5mm +NetTie:NetTie-3_SMD_Pad2.0mm +NetTie:NetTie-3_THT_Pad0.3mm +NetTie:NetTie-3_THT_Pad1.0mm +NetTie:NetTie-4_SMD_Pad0.5mm +NetTie:NetTie-4_SMD_Pad2.0mm +NetTie:NetTie-4_THT_Pad0.3mm +NetTie:NetTie-4_THT_Pad1.0mm +OptoDevice:ADNS-9800 +OptoDevice:AGILENT_HFBR-152x +OptoDevice:AGILENT_HFBR-252x +OptoDevice:AMS_TSL2550_SMD +OptoDevice:AMS_TSL25911FN +OptoDevice:Broadcom_AFBR-16xxZ_Horizontal +OptoDevice:Broadcom_AFBR-16xxZ_Tilted +OptoDevice:Broadcom_AFBR-16xxZ_Vertical +OptoDevice:Broadcom_APDS-9160-003 +OptoDevice:Broadcom_APDS-9301 +OptoDevice:Broadcom_DFN-6_2x2mm_P0.65mm +OptoDevice:Broadcom_LGA-8_2x2mm_P0.53mm +OptoDevice:Broadcom_LGA-8_2x2mm_P0.5mm +OptoDevice:Everlight_IRM-H6xxT +OptoDevice:Everlight_ITR1201SR10AR +OptoDevice:Everlight_ITR8307 +OptoDevice:Everlight_ITR8307F43 +OptoDevice:Everlight_ITR8307_Reverse +OptoDevice:Everlight_ITR9608-F +OptoDevice:Finder_34.81 +OptoDevice:Hamamatsu_C12880 +OptoDevice:Hamamatsu_S13360-30CS +OptoDevice:Kingbright_KPS-3227 +OptoDevice:Kingbright_KPS-5130 +OptoDevice:Kingbright_KRC011_Horizontal +OptoDevice:Kingbright_KRC011_Vertical +OptoDevice:Kodenshi_LG206D +OptoDevice:Kodenshi_LG206L +OptoDevice:Kodenshi_SG105 +OptoDevice:Kodenshi_SG105F +OptoDevice:Kodenshi_SG105_Reverse +OptoDevice:LaserDiode_TO18-D5.6-3 +OptoDevice:LaserDiode_TO3.3-D3.3-3 +OptoDevice:LaserDiode_TO38ICut-3 +OptoDevice:LaserDiode_TO5-D9-3 +OptoDevice:LaserDiode_TO56-3 +OptoDevice:Lightpipe_Bivar_RLP1-400-650 +OptoDevice:Lightpipe_Bivar_SLP3-150-100-F +OptoDevice:Lightpipe_Bivar_SLP3-150-100-R +OptoDevice:Lightpipe_Bivar_SLP3-150-150-F +OptoDevice:Lightpipe_Bivar_SLP3-150-150-R +OptoDevice:Lightpipe_Bivar_SLP3-150-200-R +OptoDevice:Lightpipe_Bivar_SLP3-150-250-F +OptoDevice:Lightpipe_Bivar_SLP3-150-250-R +OptoDevice:Lightpipe_Bivar_SLP3-150-300-F +OptoDevice:Lightpipe_Bivar_SLP3-150-300-R +OptoDevice:Lightpipe_Bivar_SLP3-150-450-R +OptoDevice:Lightpipe_Dialight_515-1064F +OptoDevice:Lightpipe_LPF-C012303S +OptoDevice:Lightpipe_LPF-C013301S +OptoDevice:Lightpipe_Mentor_1275.x00x +OptoDevice:Lightpipe_Mentor_1276.1004 +OptoDevice:Lightpipe_Mentor_1276.2004 +OptoDevice:Lite-On_LTR-303ALS-01 +OptoDevice:Luna_NSL-32 +OptoDevice:Maxim_OLGA-14_3.3x5.6mm_P0.8mm +OptoDevice:OnSemi_CASE100AQ +OptoDevice:OnSemi_CASE100CY +OptoDevice:ONSemi_QSE15x +OptoDevice:Osram_BP104-SMD +OptoDevice:Osram_BPW34S-SMD +OptoDevice:Osram_BPW82 +OptoDevice:Osram_DIL2_4.3x4.65mm_P5.08mm +OptoDevice:Osram_LPT80A +OptoDevice:Osram_SFH205 +OptoDevice:Osram_SFH2201 +OptoDevice:Osram_SFH225 +OptoDevice:Osram_SFH2430 +OptoDevice:Osram_SFH2440 +OptoDevice:Osram_SFH3710 +OptoDevice:Osram_SFH9x0x +OptoDevice:Osram_SMD-SmartDIL +OptoDevice:Panasonic_APV-AQY_SSOP-4_4.45x2.65mm_P1.27mm +OptoDevice:PerkinElmer_VTL5C +OptoDevice:PerkinElmer_VTL5Cx2 +OptoDevice:Renesas_DFN-6_1.5x1.6mm_P0.5mm +OptoDevice:Rohm_RPR-0720 +OptoDevice:R_LDR_10x8.5mm_P7.6mm_Vertical +OptoDevice:R_LDR_11x9.4mm_P8.2mm_Vertical +OptoDevice:R_LDR_12x10.8mm_P9.0mm_Vertical +OptoDevice:R_LDR_4.9x4.2mm_P2.54mm_Vertical +OptoDevice:R_LDR_5.0x4.1mm_P3mm_Vertical +OptoDevice:R_LDR_5.1x4.3mm_P3.4mm_Vertical +OptoDevice:R_LDR_5.2x5.2mm_P3.5mm_Horizontal +OptoDevice:R_LDR_7x6mm_P5.1mm_Vertical +OptoDevice:R_LDR_D13.8mm_P9.0mm_Vertical +OptoDevice:R_LDR_D20mm_P17.5mm_Vertical +OptoDevice:R_LDR_D6.4mm_P3.4mm_Vertical +OptoDevice:Sharp_GP2S700HCP +OptoDevice:Sharp_GP2Y0A41SK0F +OptoDevice:Sharp_IS471F +OptoDevice:Sharp_IS485 +OptoDevice:Siemens_SFH900 +OptoDevice:ST_VL53L0X +OptoDevice:Toshiba_TORX170_TORX173_TORX193_TORX194 +OptoDevice:Toshiba_TOTX170_TOTX173_TOTX193_TOTX194 +OptoDevice:Vishay_CAST-3Pin +OptoDevice:Vishay_CNY70 +OptoDevice:Vishay_MINICAST-3Pin +OptoDevice:Vishay_MINIMOLD-3Pin +OptoDevice:Vishay_MOLD-3Pin +OptoDevice:Vishay_TCRT5000 +Oscillator:Oscillator_DIP-14 +Oscillator:Oscillator_DIP-14_LargePads +Oscillator:Oscillator_DIP-8 +Oscillator:Oscillator_DIP-8_LargePads +Oscillator:Oscillator_OCXO_Morion_MV267 +Oscillator:Oscillator_OCXO_Morion_MV317 +Oscillator:Oscillator_SeikoEpson_SG-8002DB +Oscillator:Oscillator_SeikoEpson_SG-8002DC +Oscillator:Oscillator_SMD_Abracon_ABLNO +Oscillator:Oscillator_SMD_Abracon_ASCO-4Pin_1.6x1.2mm +Oscillator:Oscillator_SMD_Abracon_ASDMB-4Pin_2.5x2.0mm +Oscillator:Oscillator_SMD_Abracon_ASE-4Pin_3.2x2.5mm +Oscillator:Oscillator_SMD_Abracon_ASE-4Pin_3.2x2.5mm_HandSoldering +Oscillator:Oscillator_SMD_Abracon_ASV-4Pin_7.0x5.1mm +Oscillator:Oscillator_SMD_Abracon_ASV-4Pin_7.0x5.1mm_HandSoldering +Oscillator:Oscillator_SMD_Diodes_FN-4Pin_7.0x5.0mm +Oscillator:Oscillator_SMD_ECS_2520MV-xxx-xx-4Pin_2.5x2.0mm +Oscillator:Oscillator_SMD_EuroQuartz_XO32-4Pin_3.2x2.5mm +Oscillator:Oscillator_SMD_EuroQuartz_XO32-4Pin_3.2x2.5mm_HandSoldering +Oscillator:Oscillator_SMD_EuroQuartz_XO53-4Pin_5.0x3.2mm +Oscillator:Oscillator_SMD_EuroQuartz_XO53-4Pin_5.0x3.2mm_HandSoldering +Oscillator:Oscillator_SMD_EuroQuartz_XO91-4Pin_7.0x5.0mm +Oscillator:Oscillator_SMD_EuroQuartz_XO91-4Pin_7.0x5.0mm_HandSoldering +Oscillator:Oscillator_SMD_Fordahl_DFAS1-6Pin_14.8x9.1mm +Oscillator:Oscillator_SMD_Fordahl_DFAS11-4Pin_7.0x5.0mm +Oscillator:Oscillator_SMD_Fordahl_DFAS11-4Pin_7.0x5.0mm_HandSoldering +Oscillator:Oscillator_SMD_Fordahl_DFAS15-4Pin_5.0x3.2mm +Oscillator:Oscillator_SMD_Fordahl_DFAS15-4Pin_5.0x3.2mm_HandSoldering +Oscillator:Oscillator_SMD_Fordahl_DFAS2-4Pin_7.3x5.1mm +Oscillator:Oscillator_SMD_Fordahl_DFAS2-4Pin_7.3x5.1mm_HandSoldering +Oscillator:Oscillator_SMD_Fordahl_DFAS3-4Pin_9.1x7.2mm +Oscillator:Oscillator_SMD_Fordahl_DFAS3-4Pin_9.1x7.2mm_HandSoldering +Oscillator:Oscillator_SMD_Fordahl_DFAS7-4Pin_19.9x12.9mm +Oscillator:Oscillator_SMD_Fordahl_DFAS7-4Pin_19.9x12.9mm_HandSoldering +Oscillator:Oscillator_SMD_Fox_FT5H_5.0x3.2mm +Oscillator:Oscillator_SMD_IDT_JS6-6_5.0x3.2mm_P1.27mm +Oscillator:Oscillator_SMD_IDT_JU6-6_7.0x5.0mm_P2.54mm +Oscillator:Oscillator_SMD_IQD_IQXO70-4Pin_7.5x5.0mm +Oscillator:Oscillator_SMD_IQD_IQXO70-4Pin_7.5x5.0mm_HandSoldering +Oscillator:Oscillator_SMD_Kyocera_2520-6Pin_2.5x2.0mm +Oscillator:Oscillator_SMD_Kyocera_KC2520Z-4Pin_2.5x2.0mm +Oscillator:Oscillator_SMD_OCXO_ConnorWinfield_OH300 +Oscillator:Oscillator_SMD_SeikoEpson_SG210-4Pin_2.5x2.0mm +Oscillator:Oscillator_SMD_SeikoEpson_SG210-4Pin_2.5x2.0mm_HandSoldering +Oscillator:Oscillator_SMD_SeikoEpson_SG3030CM +Oscillator:Oscillator_SMD_SeikoEpson_SG8002CA-4Pin_7.0x5.0mm +Oscillator:Oscillator_SMD_SeikoEpson_SG8002CA-4Pin_7.0x5.0mm_HandSoldering +Oscillator:Oscillator_SMD_SeikoEpson_SG8002CE-4Pin_3.2x2.5mm +Oscillator:Oscillator_SMD_SeikoEpson_SG8002CE-4Pin_3.2x2.5mm_HandSoldering +Oscillator:Oscillator_SMD_SeikoEpson_SG8002JA-4Pin_14.0x8.7mm +Oscillator:Oscillator_SMD_SeikoEpson_SG8002JA-4Pin_14.0x8.7mm_HandSoldering +Oscillator:Oscillator_SMD_SeikoEpson_SG8002JC-4Pin_10.5x5.0mm +Oscillator:Oscillator_SMD_SeikoEpson_SG8002JC-4Pin_10.5x5.0mm_HandSoldering +Oscillator:Oscillator_SMD_SeikoEpson_SG8002LB-4Pin_5.0x3.2mm +Oscillator:Oscillator_SMD_SeikoEpson_SG8002LB-4Pin_5.0x3.2mm_HandSoldering +Oscillator:Oscillator_SMD_SeikoEpson_TG2520SMN-xxx-xxxxxx-4Pin_2.5x2.0mm +Oscillator:Oscillator_SMD_SI570_SI571_HandSoldering +Oscillator:Oscillator_SMD_SI570_SI571_Standard +Oscillator:Oscillator_SMD_Silicon_Labs_LGA-6_2.5x3.2mm_P1.25mm +Oscillator:Oscillator_SMD_SiTime_PQFD-6L_3.2x2.5mm +Oscillator:Oscillator_SMD_SiTime_SiT9121-6Pin_3.2x2.5mm +Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_2.0x1.6mm +Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_2.5x2.0mm +Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_3.2x2.5mm +Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_5.0x3.2mm +Oscillator:Oscillator_SMD_SiT_PQFN-4Pin_7.0x5.0mm +Oscillator:Oscillator_SMD_TCXO_G158 +Oscillator:Oscillator_SMD_TXC_7C-4Pin_5.0x3.2mm +Oscillator:Oscillator_SMD_TXC_7C-4Pin_5.0x3.2mm_HandSoldering +Package_BGA:Alliance_TFBGA-36_6x8mm_Layout6x8_P0.75mm +Package_BGA:Alliance_TFBGA-54_8x8mm_Layout9x9_P0.8mm +Package_BGA:Analog_BGA-165_11.9x16mm_Layout11x15_P1.0mm +Package_BGA:Analog_BGA-209_9.5x16mm_Layout11x19_P0.8mm +Package_BGA:Analog_BGA-28_4x6.25mm_Layout4x7_P0.8mm +Package_BGA:Analog_BGA-49_6.25x6.25mm_Layout7x7_P0.8mm +Package_BGA:Analog_BGA-77_9x15mm_Layout7x11_P1.27mm +Package_BGA:BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD +Package_BGA:BGA-100_6.0x6.0mm_Layout11x11_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD +Package_BGA:BGA-1023_33.0x33.0mm_Layout32x32_P1.0mm +Package_BGA:BGA-1156_35.0x35.0mm_Layout34x34_P1.0mm +Package_BGA:BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD +Package_BGA:BGA-1295_37.5x37.5mm_Layout36x36_P1.0mm +Package_BGA:BGA-132_12x18mm_Layout11x17_P1.0mm +Package_BGA:BGA-144_13.0x13.0mm_Layout12x12_P1.0mm +Package_BGA:BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD +Package_BGA:BGA-152_14x18mm_Layout13x17_P0.5mm +Package_BGA:BGA-153_8.0x8.0mm_Layout15x15_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD +Package_BGA:BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD +Package_BGA:BGA-16_1.92x1.92mm_Layout4x4_P0.5mm +Package_BGA:BGA-196_15x15mm_Layout14x14_P1.0mm +Package_BGA:BGA-200_10x14.5mm_Layout12x22_P0.8x0.65mm +Package_BGA:BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD +Package_BGA:BGA-256_14.0x14.0mm_Layout16x16_P0.8mm_Ball0.45mm_Pad0.32mm_NSMD +Package_BGA:BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD +Package_BGA:BGA-25_6.35x6.35mm_Layout5x5_P1.27mm +Package_BGA:BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD +Package_BGA:BGA-324_15x15mm_Layout18x18_P0.8mm +Package_BGA:BGA-324_19.0x19.0mm_Layout18x18_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD +Package_BGA:BGA-352_35.0x35.0mm_Layout26x26_P1.27mm +Package_BGA:BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD +Package_BGA:BGA-400_21.0x21.0mm_Layout20x20_P1.0mm +Package_BGA:BGA-484_23.0x23.0mm_Layout22x22_P1.0mm +Package_BGA:BGA-48_8.0x9.0mm_Layout6x8_P0.8mm +Package_BGA:BGA-529_19x19mm_Layout23x23_P0.8mm +Package_BGA:BGA-624_21x21mm_Layout25x25_P0.8mm +Package_BGA:BGA-625_21.0x21.0mm_Layout25x25_P0.8mm +Package_BGA:BGA-64_9.0x9.0mm_Layout10x10_P0.8mm +Package_BGA:BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD +Package_BGA:BGA-676_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD +Package_BGA:BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD +Package_BGA:BGA-81_4.496x4.377mm_Layout9x9_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD +Package_BGA:BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm +Package_BGA:BGA-96_9.0x13.0mm_Layout2x3x16_P0.8mm +Package_BGA:BGA-9_1.6x1.6mm_Layout3x3_P0.5mm +Package_BGA:EPC_BGA-4_0.9x0.9mm_Layout2x2_P0.45mm +Package_BGA:FB-BGA-484_23.0x23.0mm_Layout22x22_P1.0mm +Package_BGA:FBGA-78_7.5x11mm_Layout2x3x13_P0.8mm +Package_BGA:Fujitsu_WLP-15_2.28x3.092mm_Layout3x5_P0.4mm +Package_BGA:Infineon_LFBGA-292_17x17mm_Layout20x20_P0.8mm +Package_BGA:Infineon_TFBGA-48_6x10mm_Layout6x8_P0.75mm +Package_BGA:Lattice_caBGA-381_17x17mm_Layout20x20_P0.8mm +Package_BGA:Lattice_caBGA-381_17x17mm_Layout20x20_P0.8mm_SMD +Package_BGA:Lattice_caBGA-756_27x27mm_Layout32x32_P0.8mm +Package_BGA:Lattice_iCE40_csBGA-132_8x8mm_Layout14x14_P0.5mm +Package_BGA:LFBGA-100_10x10mm_Layout10x10_P0.8mm +Package_BGA:LFBGA-144_10x10mm_Layout12x12_P0.8mm +Package_BGA:LFBGA-153_11.5x13mm_Layout14x14_P0.5mm +Package_BGA:LFBGA-169_12x16mm_Layout14x28_P0.5mm +Package_BGA:LFBGA-169_12x18mm_Layout14x28_P0.5mm +Package_BGA:LFBGA-169_14x18mm_Layout14x28_P0.5mm +Package_BGA:LFBGA-289_14x14mm_Layout17x17_P0.8mm +Package_BGA:LFBGA-400_16x16mm_Layout20x20_P0.8mm +Package_BGA:LFBGA-484_18x18mm_Layout22x22_P0.8mm +Package_BGA:Linear_BGA-133_15.0x15.0mm_Layout12x12_P1.27mm +Package_BGA:MAPBGA-272_9x9mm_Layout17x17_P0.5mm +Package_BGA:MAPBGA-289_14x14mm_Layout17x17_P0.8mm +Package_BGA:Maxim_WLP-12 +Package_BGA:Maxim_WLP-12_2.008x1.608mm_Layout4x3_P0.4mm +Package_BGA:Maxim_WLP-9_1.595x1.415_Layout3x3_P0.4mm_Ball0.27mm_Pad0.25mm_NSMD +Package_BGA:Microchip_TFBGA-196_11x11mm_Layout14x14_P0.75mm_SMD +Package_BGA:Micron_FBGA-78_7.5x10.6mm_Layout9x13_P0.8mm +Package_BGA:Micron_FBGA-78_8x10.5mm_Layout9x13_P0.8mm +Package_BGA:Micron_FBGA-78_9x10.5mm_Layout9x13_P0.8mm +Package_BGA:Micron_FBGA-96_7.5x13.5mm_Layout9x16_P0.8mm +Package_BGA:Micron_FBGA-96_8x14mm_Layout9x16_P0.8mm +Package_BGA:Micron_FBGA-96_9x14mm_Layout9x16_P0.8mm +Package_BGA:NXP_VFBGA-42_2.6x3mm_Layout6x7_P0.4mm +Package_BGA:ST_LFBGA-354_16x16mm_Layout19x19_P0.8mm +Package_BGA:ST_LFBGA-448_18x18mm_Layout22x22_P0.8mm +Package_BGA:ST_TFBGA-169_7x7mm_Layout13x13_P0.5mm +Package_BGA:ST_TFBGA-225_13x13mm_Layout15x15_P0.8mm +Package_BGA:ST_TFBGA-257_10x10mm_Layout19x19_P0.5mmP0.65mm +Package_BGA:ST_TFBGA-320_11x11mm_Layout21x21_P0.5mm +Package_BGA:ST_TFBGA-361_12x12mm_Layout23x23_P0.5mmP0.65mm +Package_BGA:ST_UFBGA-121_6x6mm_Layout11x11_P0.5mm +Package_BGA:ST_UFBGA-129_7x7mm_Layout13x13_P0.5mm +Package_BGA:ST_UFBGA-59_5x5mm_Layout8x8_P0.5mm +Package_BGA:ST_UFBGA-73_5x5mm_Layout9x9_P0.5mm +Package_BGA:ST_UFBGA-81_5x5mm_Layout9x9_P0.5mm +Package_BGA:ST_uTFBGA-36_3.6x3.6mm_Layout6x6_P0.5mm +Package_BGA:Texas_BGA-289_15x15mm_Layout17x17_P0.8mm +Package_BGA:Texas_DSBGA-10_1.36x1.86mm_Layout3x4_P0.5mm +Package_BGA:Texas_DSBGA-12_1.36x1.86mm_Layout3x4_P0.5mm +Package_BGA:Texas_DSBGA-16_2.39x2.39mm_Layout4x4_P0.5mm +Package_BGA:Texas_DSBGA-28_1.9x3mm_Layout4x7_P0.4mm +Package_BGA:Texas_DSBGA-49_3.33x3.488mm_Layout7x7_P0.4mm +Package_BGA:Texas_DSBGA-5_0.822x1.116mm_Layout2x1x2_P0.4mm +Package_BGA:Texas_DSBGA-5_0.8875x1.3875mm_Layout2x3_P0.5mm +Package_BGA:Texas_DSBGA-5_1.5855x1.6365mm_Layout3x2_P0.5mm +Package_BGA:Texas_DSBGA-64_3.415x3.535mm_Layout8x8_P0.4mm +Package_BGA:Texas_DSBGA-6_0.704x1.054mm_Layout2x3_P0.35mm +Package_BGA:Texas_DSBGA-6_0.757x1.01mm_Layout2x3_P0.35mm +Package_BGA:Texas_DSBGA-6_0.855x1.255mm_Layout2x3_P0.4mm_LevelB +Package_BGA:Texas_DSBGA-6_0.855x1.255mm_Layout2x3_P0.4mm_LevelC +Package_BGA:Texas_DSBGA-6_0.95x1.488mm_Layout2x3_P0.4mm +Package_BGA:Texas_DSBGA-6_0.9x1.4mm_Layout2x3_P0.5mm +Package_BGA:Texas_DSBGA-8_0.705x1.468mm_Layout2x4_P0.4mm +Package_BGA:Texas_DSBGA-8_0.9x1.9mm_Layout2x4_P0.5mm +Package_BGA:Texas_DSBGA-8_1.43x1.41mm_Layout3x3_P0.5mm +Package_BGA:Texas_DSBGA-8_1.5195x1.5195mm_Layout3x3_P0.5mm +Package_BGA:Texas_DSBGA-9_1.4715x1.4715mm_Layout3x3_P0.5mm +Package_BGA:Texas_DSBGA-9_1.62x1.58mm_Layout3x3_P0.5mm +Package_BGA:Texas_MicroStar_Junior_BGA-113_7x7mm_Layout12x12_P0.5mm +Package_BGA:Texas_MicroStar_Junior_BGA-12_2.0x2.5mm_Layout4x3_P0.5mm +Package_BGA:Texas_MicroStar_Junior_BGA-80_5.0x5.0mm_Layout9x9_P0.5mm +Package_BGA:Texas_PicoStar_BGA-4_0.758x0.758mm_Layout2x2_P0.4mm +Package_BGA:Texas_YFP0020_DSBGA-20_1.588x1.988mm_Layout4x5_P0.4mm +Package_BGA:TFBGA-100_5.5x5.5mm_Layout10x10_P0.5mm +Package_BGA:TFBGA-100_8x8mm_Layout10x10_P0.8mm +Package_BGA:TFBGA-100_9.0x9.0mm_Layout10x10_P0.8mm +Package_BGA:TFBGA-121_10x10mm_Layout11x11_P0.8mm +Package_BGA:TFBGA-169_9x9mm_Layout13x13_P0.65mm +Package_BGA:TFBGA-216_13x13mm_Layout15x15_P0.8mm +Package_BGA:TFBGA-225_10x10mm_Layout15x15_P0.65mm +Package_BGA:TFBGA-256_13x13mm_Layout16x16_P0.8mm +Package_BGA:TFBGA-265_14x14mm_Layout17x17_P0.8mm +Package_BGA:TFBGA-289_9x9mm_Layout17x17_P0.5mm +Package_BGA:TFBGA-324_12x12mm_Layout18x18_P0.65mm +Package_BGA:TFBGA-361_13x13mm_Layout19x19_P0.65mm +Package_BGA:TFBGA-48_6x10mm_Layout6x8_P0.75mm +Package_BGA:TFBGA-49_3x3mm_Layout7x7_P0.4mm +Package_BGA:TFBGA-576_16x16mm_Layout24x24_P0.65mm +Package_BGA:TFBGA-644_19x19mm_Layout28x28_P0.65mm +Package_BGA:TFBGA-64_5x5mm_Layout8x8_P0.5mm +Package_BGA:TFBGA-81_5x5mm_Layout9x9_P0.5mm +Package_BGA:UCBGA-36_2.5x2.5mm_Layout6x6_P0.4mm +Package_BGA:UCBGA-49_3x3mm_Layout7x7_P0.4mm +Package_BGA:UCBGA-81_4x4mm_Layout9x9_P0.4mm +Package_BGA:UFBGA-100_7x7mm_Layout12x12_P0.5mm +Package_BGA:UFBGA-132_7x7mm_Layout12x12_P0.5mm +Package_BGA:UFBGA-132_7x7mm_P0.5mm +Package_BGA:UFBGA-144_10x10mm_Layout12x12_P0.8mm +Package_BGA:UFBGA-144_7x7mm_Layout12x12_P0.5mm +Package_BGA:UFBGA-15_3.0x3.0mm_Layout4x4_P0.65mm +Package_BGA:UFBGA-169_7x7mm_Layout13x13_P0.5mm +Package_BGA:UFBGA-201_10x10mm_Layout15x15_P0.65mm +Package_BGA:UFBGA-32_4.0x4.0mm_Layout6x6_P0.5mm +Package_BGA:UFBGA-64_5x5mm_Layout8x8_P0.5mm +Package_BGA:VFBGA-100_7.0x7.0mm_Layout10x10_P0.65mm +Package_BGA:VFBGA-49_5.0x5.0mm_Layout7x7_P0.65mm +Package_BGA:VFBGA-86_6x6mm_Layout10x10_P0.55mm +Package_BGA:WLP-4_0.728x0.728mm_Layout2x2_P0.35mm +Package_BGA:WLP-4_0.83x0.83mm_P0.4mm +Package_BGA:WLP-4_0.86x0.86mm_P0.4mm +Package_BGA:WLP-9_1.468x1.448mm_Layout3x3_P0.4mm +Package_BGA:XBGA-121_10x10mm_Layout11x11_P0.8mm +Package_BGA:XFBGA-121_8x8mm_Layout11x11_P0.65mm +Package_BGA:XFBGA-36_3.5x3.5mm_Layout6x6_P0.5mm +Package_BGA:XFBGA-64_5.0x5.0mm_Layout8x8_P0.5mm +Package_BGA:Xilinx_CLG225 +Package_BGA:Xilinx_CLG400 +Package_BGA:Xilinx_CLG484_CLG485 +Package_BGA:Xilinx_CPG236 +Package_BGA:Xilinx_CPG238 +Package_BGA:Xilinx_CPGA196 +Package_BGA:Xilinx_CSG324 +Package_BGA:Xilinx_CSG325 +Package_BGA:Xilinx_CSGA225 +Package_BGA:Xilinx_CSGA324 +Package_BGA:Xilinx_FBG484 +Package_BGA:Xilinx_FBG676 +Package_BGA:Xilinx_FBG900 +Package_BGA:Xilinx_FFG1156 +Package_BGA:Xilinx_FFG1157_FFG1158 +Package_BGA:Xilinx_FFG1761 +Package_BGA:Xilinx_FFG1926_FFG1927_FFG1928_FFG1930 +Package_BGA:Xilinx_FFG676 +Package_BGA:Xilinx_FFG900_FFG901 +Package_BGA:Xilinx_FFV1761 +Package_BGA:Xilinx_FGG484 +Package_BGA:Xilinx_FGG676 +Package_BGA:Xilinx_FGGA484 +Package_BGA:Xilinx_FGGA676 +Package_BGA:Xilinx_FHG1761 +Package_BGA:Xilinx_FLG1925_FLG1926_FLG1928_FLG1930 +Package_BGA:Xilinx_FTG256 +Package_BGA:Xilinx_FTGB196 +Package_BGA:Xilinx_RB484 +Package_BGA:Xilinx_RB676 +Package_BGA:Xilinx_RF1156 +Package_BGA:Xilinx_RF1157_RF1158 +Package_BGA:Xilinx_RF1761 +Package_BGA:Xilinx_RF1930 +Package_BGA:Xilinx_RF676 +Package_BGA:Xilinx_RF900 +Package_BGA:Xilinx_RFG676 +Package_BGA:Xilinx_RS484 +Package_BGA:Xilinx_SBG484 +Package_BGA:Xilinx_SBG485 +Package_CSP:Analog_LFCSP-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm +Package_CSP:Analog_LFCSP-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm_ThermalVias +Package_CSP:Analog_LFCSP-16-1EP_4x4mm_P0.65mm_EP2.35x2.35mm +Package_CSP:Analog_LFCSP-16-1EP_4x4mm_P0.65mm_EP2.35x2.35mm_ThermalVias +Package_CSP:Analog_LFCSP-8-1EP_3x3mm_P0.5mm_EP1.53x1.85mm +Package_CSP:Analog_LFCSP-UQ-10_1.3x1.6mm_P0.4mm +Package_CSP:Anpec_WLCSP-20_1.76x2.03mm_Layout4x5_P0.4mm +Package_CSP:Dialog_WLCSP-34_4.54x1.66mm_Layout17x4_P0.25x0.34mm +Package_CSP:DiodesInc_GEA20_WLCSP-20_1.7x2.1mm_Layout4x5_P0.4mm +Package_CSP:Efinix_WLCSP-64_3.5353x3.3753mm_Layout8x8_P0.4mm +Package_CSP:Efinix_WLCSP-80_4.4567x3.5569mm_Layout10x8_P0.4mm +Package_CSP:LFCSP-10_2x2mm_P0.5mm +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.3x1.3mm +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.3x1.3mm_ThermalVias +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.5x1.5mm +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm_ThermalVias +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.7x1.7mm +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.7x1.7mm_ThermalVias +Package_CSP:LFCSP-16-1EP_3x3mm_P0.5mm_EP1.854x1.854mm +Package_CSP:LFCSP-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm +Package_CSP:LFCSP-16-1EP_4x4mm_P0.65mm_EP2.4x2.4mm +Package_CSP:LFCSP-16-1EP_4x4mm_P0.65mm_EP2.4x2.4mm_ThermalVias +Package_CSP:LFCSP-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm +Package_CSP:LFCSP-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm_ThermalVias +Package_CSP:LFCSP-16_3x3mm_P0.5mm +Package_CSP:LFCSP-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm +Package_CSP:LFCSP-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_CSP:LFCSP-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm +Package_CSP:LFCSP-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias +Package_CSP:LFCSP-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_CSP:LFCSP-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_CSP:LFCSP-24-1EP_4x4mm_P0.5mm_EP0.5x0.5mm +Package_CSP:LFCSP-24-1EP_4x4mm_P0.5mm_EP2.3x2.3mm +Package_CSP:LFCSP-24-1EP_4x4mm_P0.5mm_EP2.3x2.3mm_ThermalVias +Package_CSP:LFCSP-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm +Package_CSP:LFCSP-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias +Package_CSP:LFCSP-28-1EP_5x5mm_P0.5mm_EP3.14x3.14mm +Package_CSP:LFCSP-28-1EP_5x5mm_P0.5mm_EP3.14x3.14mm_ThermalVias +Package_CSP:LFCSP-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_CSP:LFCSP-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_CSP:LFCSP-32-1EP_5x5mm_P0.5mm_EP3.25x3.25mm +Package_CSP:LFCSP-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm +Package_CSP:LFCSP-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm_ThermalVias +Package_CSP:LFCSP-32-1EP_5x5mm_P0.5mm_EP3.6x3.6mm +Package_CSP:LFCSP-32-1EP_5x5mm_P0.5mm_EP3.6x3.6mm_ThermalVias +Package_CSP:LFCSP-40-1EP_6x6mm_P0.5mm_EP3.9x3.9mm +Package_CSP:LFCSP-40-1EP_6x6mm_P0.5mm_EP3.9x3.9mm_ThermalVias +Package_CSP:LFCSP-40-1EP_6x6mm_P0.5mm_EP4.65x4.65mm +Package_CSP:LFCSP-40-1EP_6x6mm_P0.5mm_EP4.65x4.65mm_ThermalVias +Package_CSP:LFCSP-40-1EP_6x6mm_P0.5mm_EP4.6x4.6mm +Package_CSP:LFCSP-40-1EP_6x6mm_P0.5mm_EP4.6x4.6mm_ThermalVias +Package_CSP:LFCSP-48-1EP_7x7mm_P0.5mm_EP4.1x4.1mm +Package_CSP:LFCSP-48-1EP_7x7mm_P0.5mm_EP4.1x4.1mm_ThermalVias +Package_CSP:LFCSP-56-1EP_8x8mm_P0.5mm_EP6.6x6.6mm +Package_CSP:LFCSP-56-1EP_8x8mm_P0.5mm_EP6.6x6.6mm_ThermalVias +Package_CSP:LFCSP-6-1EP_2x2mm_P0.65mm_EP1x1.6mm +Package_CSP:LFCSP-64-1EP_9x9mm_P0.5mm_EP5.21x5.21mm +Package_CSP:LFCSP-64-1EP_9x9mm_P0.5mm_EP5.21x5.21mm_ThermalVias +Package_CSP:LFCSP-72-1EP_10x10mm_P0.5mm_EP5.3x5.3mm +Package_CSP:LFCSP-72-1EP_10x10mm_P0.5mm_EP5.3x5.3mm_ThermalVias +Package_CSP:LFCSP-72-1EP_10x10mm_P0.5mm_EP6.15x6.15mm +Package_CSP:LFCSP-8-1EP_3x2mm_P0.5mm_EP1.6x1.65mm +Package_CSP:LFCSP-8-1EP_3x3mm_P0.5mm_EP1.45x1.74mm +Package_CSP:LFCSP-8-1EP_3x3mm_P0.5mm_EP1.6x2.34mm +Package_CSP:LFCSP-8-1EP_3x3mm_P0.5mm_EP1.6x2.34mm_ThermalVias +Package_CSP:LFCSP-8_2x2mm_P0.5mm +Package_CSP:LFCSP-VQ-24-1EP_4x4mm_P0.5mm_EP2.642x2.642mm +Package_CSP:LFCSP-VQ-48-1EP_7x7mm_P0.5mm +Package_CSP:LFCSP-WD-10-1EP_3x3mm_P0.5mm_EP1.64x2.38mm +Package_CSP:LFCSP-WD-10-1EP_3x3mm_P0.5mm_EP1.64x2.38mm_ThermalVias +Package_CSP:LFCSP-WD-8-1EP_3x3mm_P0.65mm_EP1.6x2.44mm +Package_CSP:LFCSP-WD-8-1EP_3x3mm_P0.65mm_EP1.6x2.44mm_ThermalVias +Package_CSP:Macronix_WLCSP-12_2.02x2.09mm_Layout4x4_P0.5mm +Package_CSP:Maxim_WLCSP-35_2.998x2.168mm_Layout7x5_P0.4mm +Package_CSP:Nexperia_WLCSP-15_2.37x1.17mm_Layout6x3_P0.4mmP0.8mm +Package_CSP:OnSemi_ODCSP36_BGA-36_6.13x6.13mm_Layout6x6_P1.0mm +Package_CSP:OnSemi_ODCSP36_BGA-36_6.13x6.13mm_Layout6x6_P1.0mm_ManualAssembly +Package_CSP:OnSemi_ODCSP8_BGA-8_3.16x3.16mm_Layout3x3_P1.26mm +Package_CSP:OnSemi_ODCSP8_BGA-8_3.16x3.16mm_Layout3x3_P1.26mm_ManualAssembly +Package_CSP:pSemi_CSP-16_1.64x2.04mm_P0.4mm +Package_CSP:pSemi_CSP-16_1.64x2.04mm_P0.4mm_Pad0.18mm +Package_CSP:ST_WLCSP-100_4.437x4.456mm_Layout10x10_P0.4mm +Package_CSP:ST_WLCSP-100_4.4x4.38mm_Layout10x10_P0.4mm_Offcenter +Package_CSP:ST_WLCSP-100_Die422 +Package_CSP:ST_WLCSP-100_Die446 +Package_CSP:ST_WLCSP-100_Die452 +Package_CSP:ST_WLCSP-100_Die461 +Package_CSP:ST_WLCSP-101_3.86x3.79mm_Layout11x19_P0.35mm_Stagger +Package_CSP:ST_WLCSP-104_Die437 +Package_CSP:ST_WLCSP-115_3.73x4.15mm_Layout11x21_P0.35mm_Stagger +Package_CSP:ST_WLCSP-115_4.63x4.15mm_Layout21x11_P0.4mm_Stagger +Package_CSP:ST_WLCSP-12_1.7x1.42mm_Layout4x6_P0.35mm_Stagger +Package_CSP:ST_WLCSP-132_4.57x4.37mm_Layout12x11_P0.35mm +Package_CSP:ST_WLCSP-143_Die419 +Package_CSP:ST_WLCSP-143_Die449 +Package_CSP:ST_WLCSP-144_Die470 +Package_CSP:ST_WLCSP-150_5.38x5.47mm_Layout13x23_P0.4mm_Stagger +Package_CSP:ST_WLCSP-156_4.96x4.64mm_Layout13x12_P0.35mm +Package_CSP:ST_WLCSP-168_Die434 +Package_CSP:ST_WLCSP-180_Die451 +Package_CSP:ST_WLCSP-18_1.86x2.14mm_Layout7x5_P0.4mm_Stagger +Package_CSP:ST_WLCSP-208_5.38x5.47mm_Layout26x16_P0.35mm_Stagger +Package_CSP:ST_WLCSP-208_5.8x5.6mm_Layout26x16_P0.35mm_Stagger +Package_CSP:ST_WLCSP-20_1.94x2.4mm_Layout4x5_P0.4mm +Package_CSP:ST_WLCSP-25_2.33x2.24mm_Layout5x5_P0.4mm +Package_CSP:ST_WLCSP-25_2.3x2.48mm_Layout5x5_P0.4mm +Package_CSP:ST_WLCSP-25_Die425 +Package_CSP:ST_WLCSP-25_Die444 +Package_CSP:ST_WLCSP-25_Die457 +Package_CSP:ST_WLCSP-27_2.34x2.55mm_Layout9x6_P0.4mm_Stagger +Package_CSP:ST_WLCSP-27_2.55x2.34mm_P0.40mm_Stagger +Package_CSP:ST_WLCSP-36_2.58x3.07mm_Layout6x6_P0.4mm +Package_CSP:ST_WLCSP-36_Die417 +Package_CSP:ST_WLCSP-36_Die440 +Package_CSP:ST_WLCSP-36_Die445 +Package_CSP:ST_WLCSP-36_Die458 +Package_CSP:ST_WLCSP-41_2.98x2.76mm_Layout13x7_P0.4mm_Stagger +Package_CSP:ST_WLCSP-42_2.82x2.93mm_P0.40mm_Stagger +Package_CSP:ST_WLCSP-42_2.93x2.82mm_Layout12x7_P0.4mm_Stagger +Package_CSP:ST_WLCSP-49_3.15x3.13mm_Layout7x7_P0.4mm +Package_CSP:ST_WLCSP-49_3.3x3.38mm_Layout7x7_P0.4mm_Offcenter +Package_CSP:ST_WLCSP-49_Die423 +Package_CSP:ST_WLCSP-49_Die431 +Package_CSP:ST_WLCSP-49_Die433 +Package_CSP:ST_WLCSP-49_Die435 +Package_CSP:ST_WLCSP-49_Die438 +Package_CSP:ST_WLCSP-49_Die439 +Package_CSP:ST_WLCSP-49_Die447 +Package_CSP:ST_WLCSP-49_Die448 +Package_CSP:ST_WLCSP-52_3.09x3.15mm_Layout13x8_P0.4mm_Stagger +Package_CSP:ST_WLCSP-56_3.38x3.38mm_Layout14x8_P0.4mm_Stagger +Package_CSP:ST_WLCSP-63_Die427 +Package_CSP:ST_WLCSP-64_3.56x3.52mm_Layout8x8_P0.4mm +Package_CSP:ST_WLCSP-64_Die414 +Package_CSP:ST_WLCSP-64_Die427 +Package_CSP:ST_WLCSP-64_Die435 +Package_CSP:ST_WLCSP-64_Die436 +Package_CSP:ST_WLCSP-64_Die441 +Package_CSP:ST_WLCSP-64_Die442 +Package_CSP:ST_WLCSP-64_Die462 +Package_CSP:ST_WLCSP-66_Die411 +Package_CSP:ST_WLCSP-66_Die432 +Package_CSP:ST_WLCSP-72_3.38x3.38mm_Layout16x9_P0.35mm_Stagger +Package_CSP:ST_WLCSP-72_Die415 +Package_CSP:ST_WLCSP-80_3.5x3.27mm_Layout10x16_P0.35mm_Stagger_Offcenter +Package_CSP:ST_WLCSP-81_4.02x4.27mm_Layout9x9_P0.4mm +Package_CSP:ST_WLCSP-81_4.36x4.07mm_Layout9x9_P0.4mm +Package_CSP:ST_WLCSP-81_Die415 +Package_CSP:ST_WLCSP-81_Die421 +Package_CSP:ST_WLCSP-81_Die463 +Package_CSP:ST_WLCSP-90_4.2x3.95mm_Layout18x10_P0.4mm_Stagger +Package_CSP:ST_WLCSP-90_Die413 +Package_CSP:ST_WLCSP-99_4.42x3.77mm_Layout11x9_P0.35mm +Package_CSP:WLCSP-12_1.403x1.555mm_Layout6x4_P0.4mm_Stagger +Package_CSP:WLCSP-12_1.56x1.56mm_P0.4mm +Package_CSP:WLCSP-16_1.409x1.409mm_Layout4x4_P0.35mm +Package_CSP:WLCSP-16_2.225x2.17mm_Layout4x4_P0.5mm +Package_CSP:WLCSP-16_4x4_B2.17x2.32mm_P0.5mm +Package_CSP:WLCSP-20_1.934x2.434mm_Layout4x5_P0.4mm +Package_CSP:WLCSP-20_1.994x1.609mm_Layout5x4_P0.4mm +Package_CSP:WLCSP-20_1.994x1.94mm_Layout4x5_P0.4mm +Package_CSP:WLCSP-36_2.374x2.459mm_Layout6x6_P0.35mm +Package_CSP:WLCSP-36_2.82x2.67mm_Layout6x6_P0.4mm +Package_CSP:WLCSP-4_0.64x0.64mm_Layout2x2_P0.35mm +Package_CSP:WLCSP-4_0.89x0.89mm_Layout2x2_P0.5mm +Package_CSP:WLCSP-56_3.170x3.444mm_Layout7x8_P0.4mm +Package_CSP:WLCSP-6_1.4x1.0mm_P0.4mm +Package_CSP:WLCSP-81_4.41x3.76mm_P0.4mm +Package_CSP:WLCSP-8_1.551x2.284mm_Layout2x4_P0.5mm +Package_CSP:WLCSP-8_1.58x1.63x0.35mm_Layout3x5_P0.35x0.4mm_Ball0.25mm_Pad0.25mm_NSMD +Package_CSP:WLCSP-9_1.21x1.22mm_Layout3x3_P0.4mm +Package_DFN_QFN:AMS_QFN-4-1EP_2x2mm_P0.95mm_EP0.7x1.6mm +Package_DFN_QFN:Analog_QFN-28-36-2EP_5x6mm_P0.5mm +Package_DFN_QFN:AO_AOZ666xDI_DFN-8-1EP_3x3mm_P0.65mm_EP1.25x2.7mm +Package_DFN_QFN:AO_DFN-8-1EP_5.55x5.2mm_P1.27mm_EP4.12x4.6mm +Package_DFN_QFN:ArtInChip_QFN-100-1EP_12x12mm_P0.4mm_EP7.4x7.4mm +Package_DFN_QFN:ArtInChip_QFN-100-1EP_12x12mm_P0.4mm_EP7.4x7.4mm_ThermalVias +Package_DFN_QFN:ArtInChip_QFN-68-1EP_7x7mm_P0.35mm_EP5.49x5.49mm +Package_DFN_QFN:ArtInChip_QFN-68-1EP_7x7mm_P0.35mm_EP5.49x5.49mm_ThermalVias +Package_DFN_QFN:ArtInChip_QFN-88-1EP_10x10mm_P0.4mm_EP6.74x6.74mm +Package_DFN_QFN:ArtInChip_QFN-88-1EP_10x10mm_P0.4mm_EP6.74x6.74mm_ThermalVias +Package_DFN_QFN:Cypress_QFN-56-1EP_8x8mm_P0.5mm_EP6.22x6.22mm_ThermalVias +Package_DFN_QFN:DFN-10-1EP_2.6x2.6mm_P0.5mm_EP1.3x2.2mm +Package_DFN_QFN:DFN-10-1EP_2.6x2.6mm_P0.5mm_EP1.3x2.2mm_ThermalVias +Package_DFN_QFN:DFN-10-1EP_2x3mm_P0.5mm_EP0.64x2.4mm +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.55x2.48mm +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.58x2.35mm +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.58x2.35mm_ThermalVias +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.65x2.38mm +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.75x2.7mm +Package_DFN_QFN:DFN-10-1EP_3x3mm_P0.5mm_EP1.7x2.5mm +Package_DFN_QFN:DFN-10_2x2mm_P0.4mm +Package_DFN_QFN:DFN-12-1EP_2x3mm_P0.45mm_EP0.64x2.4mm +Package_DFN_QFN:DFN-12-1EP_3x3mm_P0.45mm_EP1.65x2.38mm +Package_DFN_QFN:DFN-12-1EP_3x3mm_P0.45mm_EP1.65x2.38mm_ThermalVias +Package_DFN_QFN:DFN-12-1EP_3x3mm_P0.5mm_EP1.6x2.5mm +Package_DFN_QFN:DFN-12-1EP_3x3mm_P0.5mm_EP1.6x2.5mm_ThermalVias +Package_DFN_QFN:DFN-12-1EP_3x3mm_P0.5mm_EP2.05x2.86mm +Package_DFN_QFN:DFN-12-1EP_3x4mm_P0.5mm_EP1.7x3.3mm +Package_DFN_QFN:DFN-12-1EP_4x4mm_P0.5mm_EP2.66x3.38mm +Package_DFN_QFN:DFN-12-1EP_4x4mm_P0.65mm_EP2.64x3.54mm +Package_DFN_QFN:DFN-14-1EP_3x3mm_P0.4mm_EP1.78x2.35mm +Package_DFN_QFN:DFN-14-1EP_3x4.5mm_P0.65mm_EP1.65x4.25mm +Package_DFN_QFN:DFN-14-1EP_3x4.5mm_P0.65mm_EP1.65x4.25mm_ThermalVias +Package_DFN_QFN:DFN-14-1EP_3x4mm_P0.5mm_EP1.7x3.3mm +Package_DFN_QFN:DFN-14_1.35x3.5mm_P0.5mm +Package_DFN_QFN:DFN-16-1EP_3x4mm_P0.45mm_EP1.7x3.3mm +Package_DFN_QFN:DFN-16-1EP_3x5mm_P0.5mm_EP1.66x4.4mm +Package_DFN_QFN:DFN-16-1EP_4x5mm_P0.5mm_EP2.44x4.34mm +Package_DFN_QFN:DFN-16-1EP_5x5mm_P0.5mm_EP3.46x4mm +Package_DFN_QFN:DFN-18-1EP_3x5mm_P0.5mm_EP1.66x4.4mm +Package_DFN_QFN:DFN-18-1EP_4x5mm_P0.5mm_EP2.44x4.34mm +Package_DFN_QFN:DFN-20-1EP_5x6mm_P0.5mm_EP3.24x4.24mm +Package_DFN_QFN:DFN-22-1EP_5x6mm_P0.5mm_EP3.14x4.3mm +Package_DFN_QFN:DFN-24-1EP_4x7mm_P0.5mm_EP2.64x6.44mm +Package_DFN_QFN:DFN-32-1EP_4x7mm_P0.4mm_EP2.64x6.44mm +Package_DFN_QFN:DFN-44-1EP_5x8.9mm_P0.4mm_EP3.7x8.4mm +Package_DFN_QFN:DFN-4_5x7mm_P5.08mm +Package_DFN_QFN:DFN-6-1EP_1.2x1.2mm_P0.4mm_EP0.3x0.94mm_PullBack +Package_DFN_QFN:DFN-6-1EP_2x1.6mm_P0.5mm_EP1.15x1.3mm +Package_DFN_QFN:DFN-6-1EP_2x1.8mm_P0.5mm_EP1.2x1.6mm +Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.5mm_EP0.61x1.42mm +Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.5mm_EP0.6x1.37mm +Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.5mm_EP0.7x1.6mm +Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.65mm_EP1.01x1.7mm +Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.65mm_EP1x1.6mm +Package_DFN_QFN:DFN-6-1EP_3x2mm_P0.5mm_EP1.65x1.35mm +Package_DFN_QFN:DFN-6-1EP_3x3mm_P0.95mm_EP1.7x2.6mm +Package_DFN_QFN:DFN-6-1EP_3x3mm_P1mm_EP1.5x2.4mm +Package_DFN_QFN:DFN-6_1.3x1.2mm_P0.4mm +Package_DFN_QFN:DFN-6_1.6x1.3mm_P0.4mm +Package_DFN_QFN:DFN-8-1EP_1.5x1.5mm_P0.4mm_EP0.7x1.2mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.45mm_EP0.64x1.37mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP0.6x1.2mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP0.86x1.55mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP0.8x1.6mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP0.9x1.3mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP0.9x1.5mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP0.9x1.7mm +Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP1.05x1.75mm +Package_DFN_QFN:DFN-8-1EP_2x3mm_P0.5mm_EP0.61x2.2mm +Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.45mm_EP1.66x1.36mm +Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.36x1.46mm +Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.5mm +Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.75x1.45mm +Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.7x1.4mm +Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.7x1.6mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.7x2.4mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.5mm_EP1.7x2.4mm_ThermalVias +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.55x2.4mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.5x2.25mm +Package_DFN_QFN:DFN-8-1EP_3x3mm_P0.65mm_EP1.7x2.05mm +Package_DFN_QFN:DFN-8-1EP_4x4mm_P0.8mm_EP2.39x2.21mm +Package_DFN_QFN:DFN-8-1EP_4x4mm_P0.8mm_EP2.3x3.24mm +Package_DFN_QFN:DFN-8-1EP_4x4mm_P0.8mm_EP2.5x3.6mm +Package_DFN_QFN:DFN-8-1EP_6x5mm_P1.27mm_EP2x2mm +Package_DFN_QFN:DFN-8-1EP_6x5mm_P1.27mm_EP4x4mm +Package_DFN_QFN:DFN-8_2x2mm_P0.5mm +Package_DFN_QFN:DFN-S-8-1EP_6x5mm_P1.27mm +Package_DFN_QFN:DHVQFN-14-1EP_2.5x3mm_P0.5mm_EP1x1.5mm +Package_DFN_QFN:DHVQFN-16-1EP_2.5x3.5mm_P0.5mm_EP1x2mm +Package_DFN_QFN:DHVQFN-20-1EP_2.5x4.5mm_P0.5mm_EP1x3mm +Package_DFN_QFN:Diodes_DFN1006-3 +Package_DFN_QFN:Diodes_UDFN-10_1.0x2.5mm_P0.5mm +Package_DFN_QFN:Diodes_UDFN2020-6_Type-F +Package_DFN_QFN:Diodes_UDFN3810-9_TYPE_B +Package_DFN_QFN:Diodes_ZL32_TQFN-32-1EP_3x6mm_P0.4mm_EP1.25x3.5mm +Package_DFN_QFN:EPC_QFN-13-3EP_3.5x5mm_P0.5mm +Package_DFN_QFN:HVQFN-16-1EP_3x3mm_P0.5mm_EP1.5x1.5mm +Package_DFN_QFN:HVQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:HVQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm +Package_DFN_QFN:HVQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias +Package_DFN_QFN:HVQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:HVQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:HVQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:HVQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:HVQFN-40-1EP_6x6mm_P0.5mm_EP4.1x4.1mm +Package_DFN_QFN:HVQFN-40-1EP_6x6mm_P0.5mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:HXQFN-16-1EP_3x3mm_P0.5mm_EP1.85x1.85mm +Package_DFN_QFN:HXQFN-16-1EP_3x3mm_P0.5mm_EP1.85x1.85mm_ThermalVias +Package_DFN_QFN:Infineon_MLPQ-16-14-1EP_4x4mm_P0.5mm +Package_DFN_QFN:Infineon_MLPQ-40-32-1EP_7x7mm_P0.5mm +Package_DFN_QFN:Infineon_MLPQ-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm +Package_DFN_QFN:Infineon_MLPQ-48-1EP_7x7mm_P0.5mm_EP5.55x5.55mm +Package_DFN_QFN:Infineon_PQFN-22-15-4EP_6x5mm_P0.65mm +Package_DFN_QFN:Infineon_PQFN-44-31-5EP_7x7mm_P0.5mm +Package_DFN_QFN:Linear_DE14MA +Package_DFN_QFN:Linear_UGK52_QFN-46-52 +Package_DFN_QFN:LQFN-10-1EP_2x2mm_P0.5mm_EP0.7x0.7mm +Package_DFN_QFN:LQFN-12-1EP_2x2mm_P0.5mm_EP0.7x0.7mm +Package_DFN_QFN:LQFN-16-1EP_3x3mm_P0.5mm_EP1.7x1.7mm +Package_DFN_QFN:Maxim_FC2QFN-14_2.5x2.5mm_P0.5mm +Package_DFN_QFN:Maxim_TDFN-12-1EP_3x3mm_P0.5mm_EP1.7x2.5mm +Package_DFN_QFN:Maxim_TDFN-12-1EP_3x3mm_P0.5mm_EP1.7x2.5mm_ThermalVias +Package_DFN_QFN:Maxim_TDFN-6-1EP_3x3mm_P0.95mm_EP1.5x2.3mm +Package_DFN_QFN:Micrel_MLF-8-1EP_2x2mm_P0.5mm_EP0.6x1.2mm +Package_DFN_QFN:Micrel_MLF-8-1EP_2x2mm_P0.5mm_EP0.6x1.2mm_ThermalVias +Package_DFN_QFN:Micrel_MLF-8-1EP_2x2mm_P0.5mm_EP0.8x1.3mm_ThermalVias +Package_DFN_QFN:Microchip_8E-16 +Package_DFN_QFN:Microchip_DRQFN-44-1EP_5x5mm_P0.7mm_EP2.65x2.65mm +Package_DFN_QFN:Microchip_DRQFN-44-1EP_5x5mm_P0.7mm_EP2.65x2.65mm_ThermalVias +Package_DFN_QFN:Microchip_DRQFN-64-1EP_7x7mm_P0.65mm_EP4.1x4.1mm +Package_DFN_QFN:Microchip_DRQFN-64-1EP_7x7mm_P0.65mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:Microsemi_QFN-40-32-2EP_6x8mm_P0.5mm +Package_DFN_QFN:Mini-Circuits_DL805 +Package_DFN_QFN:Mini-Circuits_FG873-4_3x3mm +Package_DFN_QFN:MLF-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:MLF-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:MLF-6-1EP_1.6x1.6mm_P0.5mm_EP0.5x1.26mm +Package_DFN_QFN:MLF-8-1EP_3x3mm_P0.65mm_EP1.55x2.3mm +Package_DFN_QFN:MLF-8-1EP_3x3mm_P0.65mm_EP1.55x2.3mm_ThermalVias +Package_DFN_QFN:MLPQ-16-1EP_4x4mm_P0.65mm_EP2.8x2.8mm +Package_DFN_QFN:MPS_QFN-12_2x2mm_P0.4mm +Package_DFN_QFN:Nordic_AQFN-73-1EP_7x7mm_P0.5mm +Package_DFN_QFN:Nordic_AQFN-94-1EP_7x7mm_P0.4mm +Package_DFN_QFN:NXP_LQFN-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm_16xMask0.45x0.45 +Package_DFN_QFN:NXP_LQFN-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm_16xMask0.45x0.45_ThermalVias +Package_DFN_QFN:OnSemi_DFN-14-1EP_4x4mm_P0.5mm_EP2.7x3.4mm +Package_DFN_QFN:OnSemi_DFN-8_2x2mm_P0.5mm +Package_DFN_QFN:OnSemi_SIP-38-6EP-9x7mm_P0.65mm_EP1.2x1.2mm +Package_DFN_QFN:OnSemi_UDFN-16-1EP_1.35x3.3mm_P0.4mm_EP0.4x2.8mm +Package_DFN_QFN:OnSemi_UDFN-8_1.2x1.8mm_P0.4mm +Package_DFN_QFN:OnSemi_VCT-28_3.5x3.5mm_P0.4mm +Package_DFN_QFN:OnSemi_XDFN-10_1.35x2.2mm_P0.4mm +Package_DFN_QFN:OnSemi_XDFN4-1EP_1.0x1.0mm_EP0.52x0.52mm +Package_DFN_QFN:Panasonic_HQFN-16-1EP_4x4mm_P0.65mm_EP2.9x2.9mm +Package_DFN_QFN:Panasonic_HSON-8_8x8mm_P2.00mm +Package_DFN_QFN:PQFN-8-EP_6x5mm_P1.27mm_Generic +Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.51mm_EP1.45x1.45mm +Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm +Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm_ThermalVias +Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.6x1.6mm +Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.6x1.6mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_1.8x2.6mm_P0.4mm_EP0.7x1.5mm +Package_DFN_QFN:QFN-16-1EP_1.8x2.6mm_P0.4mm_EP0.7x1.5mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.675x1.675mm +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.7x1.7mm +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.7x1.7mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.9x1.9mm +Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.9x1.9mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.5mm_EP2.45x2.45mm +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.5mm_EP2.45x2.45mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.15x2.15mm +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.15x2.15mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.5x2.5mm +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.5x2.5mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm_PullBack +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm_PullBack_ThermalVias +Package_DFN_QFN:QFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:QFN-16-1EP_5x5mm_P0.8mm_EP2.7x2.7mm +Package_DFN_QFN:QFN-16-1EP_5x5mm_P0.8mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_3.5x3.5mm_P0.5mm_EP2x2mm +Package_DFN_QFN:QFN-20-1EP_3.5x3.5mm_P0.5mm_EP2x2mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_3x3mm_P0.45mm_EP1.6x1.6mm +Package_DFN_QFN:QFN-20-1EP_3x3mm_P0.45mm_EP1.6x1.6mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_3x3mm_P0.4mm_EP1.65x1.65mm +Package_DFN_QFN:QFN-20-1EP_3x3mm_P0.4mm_EP1.65x1.65mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_3x4mm_P0.5mm_EP1.65x2.65mm +Package_DFN_QFN:QFN-20-1EP_3x4mm_P0.5mm_EP1.65x2.65mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm +Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_4x5mm_P0.5mm_EP2.65x3.65mm +Package_DFN_QFN:QFN-20-1EP_4x5mm_P0.5mm_EP2.65x3.65mm_ThermalVias +Package_DFN_QFN:QFN-20-1EP_5x5mm_P0.65mm_EP3.35x3.35mm +Package_DFN_QFN:QFN-20-1EP_5x5mm_P0.65mm_EP3.35x3.35mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_3x3mm_P0.4mm_EP1.75x1.6mm +Package_DFN_QFN:QFN-24-1EP_3x3mm_P0.4mm_EP1.75x1.6mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_3x4mm_P0.4mm_EP1.65x2.65mm +Package_DFN_QFN:QFN-24-1EP_3x4mm_P0.4mm_EP1.65x2.65mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.15x2.15mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.15x2.15mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.65x2.65mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.65x2.65mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.75x2.75mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.75x2.75mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.6mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.6mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm +Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_4x5mm_P0.5mm_EP2.65x3.65mm +Package_DFN_QFN:QFN-24-1EP_4x5mm_P0.5mm_EP2.65x3.65mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.25x3.25mm +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.2x3.2mm +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.2x3.2mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.4x3.4mm +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.4x3.4mm_ThermalVias +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.6x3.6mm +Package_DFN_QFN:QFN-24-1EP_5x5mm_P0.65mm_EP3.6x3.6mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_3x6mm_P0.5mm_EP1.7x4.75mm +Package_DFN_QFN:QFN-28-1EP_3x6mm_P0.5mm_EP1.7x4.75mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.45mm_EP2.4x2.4mm +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.45mm_EP2.4x2.4mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.45mm_EP2.6x2.6mm +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.3x2.3mm +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.3x2.3mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.4x2.4mm +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.4x2.4mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.6x2.6mm +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.4mm_EP2.7x2.7mm +Package_DFN_QFN:QFN-28-1EP_4x5mm_P0.5mm_EP2.65x3.65mm +Package_DFN_QFN:QFN-28-1EP_4x5mm_P0.5mm_EP2.65x3.65mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.35x3.35mm +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.35x3.35mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.75x3.75mm +Package_DFN_QFN:QFN-28-1EP_5x5mm_P0.5mm_EP3.75x3.75mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_5x6mm_P0.5mm_EP3.65x4.65mm +Package_DFN_QFN:QFN-28-1EP_5x6mm_P0.5mm_EP3.65x4.65mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_6x6mm_P0.65mm_EP4.25x4.25mm +Package_DFN_QFN:QFN-28-1EP_6x6mm_P0.65mm_EP4.25x4.25mm_ThermalVias +Package_DFN_QFN:QFN-28-1EP_6x6mm_P0.65mm_EP4.8x4.8mm +Package_DFN_QFN:QFN-28-1EP_6x6mm_P0.65mm_EP4.8x4.8mm_ThermalVias +Package_DFN_QFN:QFN-28_4x4mm_P0.5mm +Package_DFN_QFN:QFN-32-1EP_4x4mm_P0.4mm_EP2.65x2.65mm +Package_DFN_QFN:QFN-32-1EP_4x4mm_P0.4mm_EP2.65x2.65mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_4x4mm_P0.4mm_EP2.9x2.9mm +Package_DFN_QFN:QFN-32-1EP_4x4mm_P0.4mm_EP2.9x2.9mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.3x3.3mm +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.3x3.3mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.65x3.65mm +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.65x3.65mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.6x3.6mm +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.6x3.6mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.7x3.7mm +Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.7x3.7mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_7x7mm_P0.65mm_EP4.65x4.65mm +Package_DFN_QFN:QFN-32-1EP_7x7mm_P0.65mm_EP4.65x4.65mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_7x7mm_P0.65mm_EP4.7x4.7mm +Package_DFN_QFN:QFN-32-1EP_7x7mm_P0.65mm_EP4.7x4.7mm_ThermalVias +Package_DFN_QFN:QFN-32-1EP_7x7mm_P0.65mm_EP5.4x5.4mm +Package_DFN_QFN:QFN-32-1EP_7x7mm_P0.65mm_EP5.4x5.4mm_ThermalVias +Package_DFN_QFN:QFN-36-1EP_5x6mm_P0.5mm_EP3.6x4.1mm +Package_DFN_QFN:QFN-36-1EP_5x6mm_P0.5mm_EP3.6x4.1mm_ThermalVias +Package_DFN_QFN:QFN-36-1EP_5x6mm_P0.5mm_EP3.6x4.6mm +Package_DFN_QFN:QFN-36-1EP_5x6mm_P0.5mm_EP3.6x4.6mm_ThermalVias +Package_DFN_QFN:QFN-36-1EP_6x6mm_P0.5mm_EP3.7x3.7mm +Package_DFN_QFN:QFN-36-1EP_6x6mm_P0.5mm_EP3.7x3.7mm_ThermalVias +Package_DFN_QFN:QFN-36-1EP_6x6mm_P0.5mm_EP4.1x4.1mm +Package_DFN_QFN:QFN-36-1EP_6x6mm_P0.5mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:QFN-38-1EP_4x6mm_P0.4mm_EP2.65x4.65mm +Package_DFN_QFN:QFN-38-1EP_4x6mm_P0.4mm_EP2.65x4.65mm_ThermalVias +Package_DFN_QFN:QFN-38-1EP_5x7mm_P0.5mm_EP3.15x5.15mm +Package_DFN_QFN:QFN-38-1EP_5x7mm_P0.5mm_EP3.15x5.15mm_ThermalVias +Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm +Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm_ThermalVias +Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.8x3.8mm +Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.8x3.8mm_ThermalVias +Package_DFN_QFN:QFN-40-1EP_6x6mm_P0.5mm_EP4.6x4.6mm +Package_DFN_QFN:QFN-40-1EP_6x6mm_P0.5mm_EP4.6x4.6mm_ThermalVias +Package_DFN_QFN:QFN-42-1EP_5x6mm_P0.4mm_EP3.7x4.7mm +Package_DFN_QFN:QFN-42-1EP_5x6mm_P0.4mm_EP3.7x4.7mm_ThermalVias +Package_DFN_QFN:QFN-44-1EP_7x7mm_P0.5mm_EP5.15x5.15mm +Package_DFN_QFN:QFN-44-1EP_7x7mm_P0.5mm_EP5.15x5.15mm_ThermalVias +Package_DFN_QFN:QFN-44-1EP_7x7mm_P0.5mm_EP5.2x5.2mm +Package_DFN_QFN:QFN-44-1EP_7x7mm_P0.5mm_EP5.2x5.2mm_ThermalVias +Package_DFN_QFN:QFN-44-1EP_8x8mm_P0.65mm_EP6.45x6.45mm +Package_DFN_QFN:QFN-44-1EP_8x8mm_P0.65mm_EP6.45x6.45mm_ThermalVias +Package_DFN_QFN:QFN-44-1EP_9x9mm_P0.65mm_EP7.5x7.5mm +Package_DFN_QFN:QFN-44-1EP_9x9mm_P0.65mm_EP7.5x7.5mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_5x5mm_P0.35mm_EP3.7x3.7mm +Package_DFN_QFN:QFN-48-1EP_5x5mm_P0.35mm_EP3.7x3.7mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.3x4.3mm +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.3x4.3mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.66x4.66mm +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.66x4.66mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.6x4.6mm +Package_DFN_QFN:QFN-48-1EP_6x6mm_P0.4mm_EP4.6x4.6mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.45x5.45mm +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.45x5.45mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.6x5.6mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.7x5.7mm +Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.7x5.7mm_ThermalVias +Package_DFN_QFN:QFN-48-1EP_8x8mm_P0.5mm_EP6.2x6.2mm +Package_DFN_QFN:QFN-48-1EP_8x8mm_P0.5mm_EP6.2x6.2mm_ThermalVias +Package_DFN_QFN:QFN-52-1EP_7x8mm_P0.5mm_EP5.41x6.45mm +Package_DFN_QFN:QFN-52-1EP_7x8mm_P0.5mm_EP5.41x6.45mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP3.2x3.2mm +Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP3.2x3.2mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP4x4mm +Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP4x4mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP5.6x5.6mm +Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP5.6x5.6mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP4.3x4.3mm +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP4.3x4.3mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP4.5x5.2mm +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP4.5x5.2mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP4.5x5.2mm_ThermalVias_TopTented +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP5.6x5.6mm +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP5.6x5.6mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP5.9x5.9mm +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP5.9x5.9mm_ThermalVias +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP6.1x6.1mm +Package_DFN_QFN:QFN-56-1EP_8x8mm_P0.5mm_EP6.1x6.1mm_ThermalVias +Package_DFN_QFN:QFN-60-1EP_7x7mm_P0.4mm_EP3.4x3.4mm +Package_DFN_QFN:QFN-60-1EP_7x7mm_P0.4mm_EP3.4x3.4mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_8x8mm_P0.4mm_EP6.5x6.5mm +Package_DFN_QFN:QFN-64-1EP_8x8mm_P0.4mm_EP6.5x6.5mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP3.4x3.4mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP3.4x3.4mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP3.8x3.8mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP3.8x3.8mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP4.1x4.1mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP4.35x4.35mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP4.35x4.35mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP4.7x4.7mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP4.7x4.7mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.2x5.2mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.2x5.2mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.45x5.45mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.45x5.45mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.4x5.4mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.4x5.4mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.7x5.7mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP5.7x5.7mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP6x6mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP6x6mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.15x7.15mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.15x7.15mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.25x7.25mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.35x7.35mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.3x7.3mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.3x7.3mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.5x7.5mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.5x7.5mm_ThermalVias +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.65x7.65mm +Package_DFN_QFN:QFN-64-1EP_9x9mm_P0.5mm_EP7.65x7.65mm_ThermalVias +Package_DFN_QFN:QFN-68-1EP_8x8mm_P0.4mm_EP5.2x5.2mm +Package_DFN_QFN:QFN-68-1EP_8x8mm_P0.4mm_EP5.2x5.2mm_ThermalVias +Package_DFN_QFN:QFN-68-1EP_8x8mm_P0.4mm_EP6.4x6.4mm +Package_DFN_QFN:QFN-68-1EP_8x8mm_P0.4mm_EP6.4x6.4mm_ThermalVias +Package_DFN_QFN:QFN-72-1EP_10x10mm_P0.5mm_EP6x6mm +Package_DFN_QFN:QFN-72-1EP_10x10mm_P0.5mm_EP6x6mm_ThermalVias +Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP3.8x3.8mm +Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP3.8x3.8mm_ThermalVias +Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP5.81x6.31mm +Package_DFN_QFN:QFN-76-1EP_9x9mm_P0.4mm_EP5.81x6.31mm_ThermalVias +Package_DFN_QFN:QFN-80-1EP_10x10mm_P0.4mm_EP3.4x3.4mm +Package_DFN_QFN:QFN-80-1EP_10x10mm_P0.4mm_EP3.4x3.4mm_ThermalVias +Package_DFN_QFN:Qorvo_DFN-8-1EP_2x2mm_P0.5mm +Package_DFN_QFN:Qorvo_TQFN66-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm +Package_DFN_QFN:Qorvo_TQFN66-48-1EP_6x6mm_P0.4mm_EP4.2x4.2mm_ThermalVias +Package_DFN_QFN:ROHM_DFN0604-3 +Package_DFN_QFN:SiliconLabs_QFN-20-1EP_3x3mm_P0.5mm_EP1.8x1.8mm +Package_DFN_QFN:SiliconLabs_QFN-20-1EP_3x3mm_P0.5mm_EP1.8x1.8mm_ThermalVias +Package_DFN_QFN:ST_UFDFPN-12-1EP_3x3mm_P0.5mm_EP1.4x2.55mm +Package_DFN_QFN:ST_UFQFPN-20_3x3mm_P0.5mm +Package_DFN_QFN:ST_UQFN-6L_1.5x1.7mm_P0.5mm +Package_DFN_QFN:TDFN-10-1EP_2x3mm_P0.5mm_EP0.9x2mm +Package_DFN_QFN:TDFN-10-1EP_2x3mm_P0.5mm_EP0.9x2mm_ThermalVias +Package_DFN_QFN:TDFN-12-1EP_3x3mm_P0.4mm_EP1.7x2.45mm +Package_DFN_QFN:TDFN-12-1EP_3x3mm_P0.4mm_EP1.7x2.45mm_ThermalVias +Package_DFN_QFN:TDFN-12_2x3mm_P0.5mm +Package_DFN_QFN:TDFN-14-1EP_3x3mm_P0.4mm_EP1.78x2.35mm +Package_DFN_QFN:TDFN-14-1EP_3x3mm_P0.4mm_EP1.78x2.35mm_ThermalVias +Package_DFN_QFN:TDFN-6-1EP_2.5x2.5mm_P0.65mm_EP1.3x2mm +Package_DFN_QFN:TDFN-6-1EP_2.5x2.5mm_P0.65mm_EP1.3x2mm_ThermalVias +Package_DFN_QFN:TDFN-8-1EP_2x2mm_P0.5mm_EP0.8x1.2mm +Package_DFN_QFN:TDFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.4mm +Package_DFN_QFN:TDFN-8-1EP_3x2mm_P0.5mm_EP1.4x1.4mm +Package_DFN_QFN:TDFN-8-1EP_3x2mm_P0.5mm_EP1.80x1.65mm +Package_DFN_QFN:TDFN-8-1EP_3x2mm_P0.5mm_EP1.80x1.65mm_ThermalVias +Package_DFN_QFN:TDFN-8_1.4x1.6mm_P0.4mm +Package_DFN_QFN:Texas_B3QFN-14-1EP_5x5.5mm_P0.65mm +Package_DFN_QFN:Texas_B3QFN-14-1EP_5x5.5mm_P0.65mm_ThermalVia +Package_DFN_QFN:Texas_DRB0008A +Package_DFN_QFN:Texas_MOF0009A +Package_DFN_QFN:Texas_PicoStar_DFN-3_0.69x0.60mm +Package_DFN_QFN:Texas_QFN-41_10x16mm +Package_DFN_QFN:Texas_R-PUQFN-N10 +Package_DFN_QFN:Texas_R-PUQFN-N12 +Package_DFN_QFN:Texas_REF0038A_WQFN-38-2EP_6x4mm_P0.4 +Package_DFN_QFN:Texas_RGC0064B_VQFN-64-1EP_9x9mm_P0.5mm_EP4.25x4.25mm +Package_DFN_QFN:Texas_RGC0064B_VQFN-64-1EP_9x9mm_P0.5mm_EP4.25x4.25mm_ThermalVias +Package_DFN_QFN:Texas_RGE0024C_VQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:Texas_RGE0024C_VQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:Texas_RGE0024H_VQFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:Texas_RGE0024H_VQFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:Texas_RGP0020D_VQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:Texas_RGP0020D_VQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:Texas_RGP0020H_VQFN-20-1EP_4x4mm_P0.5mm_EP2.4x2.4mm +Package_DFN_QFN:Texas_RGP0020H_VQFN-20-1EP_4x4mm_P0.5mm_EP2.4x2.4mm_ThermalVias +Package_DFN_QFN:Texas_RGV0016A_VQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm +Package_DFN_QFN:Texas_RGV0016A_VQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:Texas_RGW0020A_VQFN-20-1EP_5x5mm_P0.65mm_EP3.15x3.15mm +Package_DFN_QFN:Texas_RGW0020A_VQFN-20-1EP_5x5mm_P0.65mm_EP3.15x3.15mm_ThermalVias +Package_DFN_QFN:Texas_RGY_R-PVQFN-N16_EP2.05x2.55mm +Package_DFN_QFN:Texas_RGY_R-PVQFN-N16_EP2.05x2.55mm_ThermalVias +Package_DFN_QFN:Texas_RGY_R-PVQFN-N20_EP2.05x3.05mm +Package_DFN_QFN:Texas_RGY_R-PVQFN-N20_EP2.05x3.05mm_ThermalVias +Package_DFN_QFN:Texas_RGY_R-PVQFN-N24_EP2.05x3.1mm +Package_DFN_QFN:Texas_RGY_R-PVQFN-N24_EP2.05x3.1mm_ThermalVias +Package_DFN_QFN:Texas_RGZ0048A_VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm +Package_DFN_QFN:Texas_RGZ0048A_VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm_ThermalVias +Package_DFN_QFN:Texas_RHA0040B_VQFN-40-1EP_6x6mm_P0.5mm_EP4.15x4.15mm +Package_DFN_QFN:Texas_RHA0040B_VQFN-40-1EP_6x6mm_P0.5mm_EP4.15x4.15mm_ThermalVias +Package_DFN_QFN:Texas_RHA0040D_VQFN-40-1EP_6x6mm_P0.5mm_EP2.9x2.9mm +Package_DFN_QFN:Texas_RHA0040D_VQFN-40-1EP_6x6mm_P0.5mm_EP2.9x2.9mm_ThermalVias +Package_DFN_QFN:Texas_RHA0040E_VQFN-40-1EP_6x6mm_P0.5mm_EP3.52x2.62mm +Package_DFN_QFN:Texas_RHA0040E_VQFN-40-1EP_6x6mm_P0.5mm_EP3.52x2.62mm_ThermalVias +Package_DFN_QFN:Texas_RHA_VQFN-40-1EP_6x6mm_P0.5mm_EP4.6x4.6mm +Package_DFN_QFN:Texas_RHA_VQFN-40-1EP_6x6mm_P0.5mm_EP4.6x4.6mm_ThermalVias +Package_DFN_QFN:Texas_RHB0032E_VQFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm +Package_DFN_QFN:Texas_RHB0032E_VQFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm_ThermalVias +Package_DFN_QFN:Texas_RHB0032M_VQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:Texas_RHB0032M_VQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:Texas_RHH0036C_VQFN-36-1EP_6x6mm_P0.5mm_EP4.4x4.4mm +Package_DFN_QFN:Texas_RHH0036C_VQFN-36-1EP_6x6mm_P0.5mm_EP4.4x4.4mm_ThermalVias +Package_DFN_QFN:Texas_RJE0020A_VQFN-20-1EP_3x3mm_P0.45mm_EP0.675x0.76mm +Package_DFN_QFN:Texas_RJE0020A_VQFN-20-1EP_3x3mm_P0.45mm_EP0.675x0.76mm_ThermalVias +Package_DFN_QFN:Texas_RMG0012A_WQFN-12_1.8x1.8mm_P0.4mm +Package_DFN_QFN:Texas_RMQ0024A_WQFN-24-1EP_3x3mm_P0.4mm_EP1.9x1.9mm +Package_DFN_QFN:Texas_RMQ0024A_WQFN-24-1EP_3x3mm_P0.4mm_EP1.9x1.9mm_ThermalVias +Package_DFN_QFN:Texas_RNN0018A +Package_DFN_QFN:Texas_RNP0030B_WQFN-30-1EP_4x6mm_P0.5mm_EP1.8x4.5mm +Package_DFN_QFN:Texas_RNP0030B_WQFN-30-1EP_4x6mm_P0.5mm_EP1.8x4.5mm_ThermalVias +Package_DFN_QFN:Texas_RPU0010A_VQFN-HR-10_2x2mm_P0.5mm +Package_DFN_QFN:Texas_RSA_VQFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm +Package_DFN_QFN:Texas_RSA_VQFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:Texas_RSN_WQFN-32-1EP_4x4mm_P0.4mm_EP2.8x2.8mm +Package_DFN_QFN:Texas_RSN_WQFN-32-1EP_4x4mm_P0.4mm_EP2.8x2.8mm_ThermalVias +Package_DFN_QFN:Texas_RSW0010A_UQFN-10_1.4x1.8mm_P0.4mm +Package_DFN_QFN:Texas_RTE0016D_WQFN-16-1EP_3x3mm_P0.5mm_EP0.8x0.8mm +Package_DFN_QFN:Texas_RTE0016D_WQFN-16-1EP_3x3mm_P0.5mm_EP0.8x0.8mm_ThermalVias +Package_DFN_QFN:Texas_RTE_WQFN-16-1EP_3x3mm_P0.5mm_EP1.2x0.8mm +Package_DFN_QFN:Texas_RTE_WQFN-16-1EP_3x3mm_P0.5mm_EP1.2x0.8mm_ThermalVias +Package_DFN_QFN:Texas_RTW_WQFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:Texas_RTW_WQFN-24-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:Texas_RTY_WQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm +Package_DFN_QFN:Texas_RTY_WQFN-16-1EP_4x4mm_P0.65mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:Texas_RUM0016A_WQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm +Package_DFN_QFN:Texas_RUM0016A_WQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:Texas_RUN0010A_WQFN-10_2x2mm_P0.5mm +Package_DFN_QFN:Texas_RVA_VQFN-16-1EP_3.5x3.5mm_P0.5mm_EP2.14x2.14mm +Package_DFN_QFN:Texas_RVA_VQFN-16-1EP_3.5x3.5mm_P0.5mm_EP2.14x2.14mm_ThermalVias +Package_DFN_QFN:Texas_RVE0028A_VQFN-28-1EP_3.5x4.5mm_P0.4mm_EP2.1x3.1mm +Package_DFN_QFN:Texas_RVE0028A_VQFN-28-1EP_3.5x4.5mm_P0.4mm_EP2.1x3.1mm_ThermalVias +Package_DFN_QFN:Texas_RWH0032A +Package_DFN_QFN:Texas_RWH0032A_ThermalVias +Package_DFN_QFN:Texas_RWU0007A_VQFN-7_2x2mm_P0.5mm +Package_DFN_QFN:Texas_S-PDSO-N10_EP1.2x2mm +Package_DFN_QFN:Texas_S-PDSO-N10_EP1.2x2mm_ThermalVias +Package_DFN_QFN:Texas_S-PVQFN-N14 +Package_DFN_QFN:Texas_S-PVQFN-N14_ThermalVias +Package_DFN_QFN:Texas_S-PWQFN-N100_EP5.5x5.5mm +Package_DFN_QFN:Texas_S-PWQFN-N100_EP5.5x5.5mm_ThermalVias +Package_DFN_QFN:Texas_S-PWQFN-N20 +Package_DFN_QFN:Texas_S-PX2QFN-14 +Package_DFN_QFN:Texas_UQFN-10_1.5x2mm_P0.5mm +Package_DFN_QFN:Texas_VQFN-HR-12_2x2.5mm_P0.5mm +Package_DFN_QFN:Texas_VQFN-HR-12_2x2.5mm_P0.5mm_ThermalVias +Package_DFN_QFN:Texas_VQFN-HR-20_3x2.5mm_P0.5mm_RQQ0011A +Package_DFN_QFN:Texas_VQFN-RHL-20 +Package_DFN_QFN:Texas_VQFN-RHL-20_ThermalVias +Package_DFN_QFN:Texas_VQFN-RNR0011A-11 +Package_DFN_QFN:Texas_WQFN-MR-100_3x3-DapStencil +Package_DFN_QFN:Texas_WQFN-MR-100_ThermalVias_3x3-DapStencil +Package_DFN_QFN:Texas_X2QFN-12_1.6x1.6mm_P0.4mm +Package_DFN_QFN:Texas_X2QFN-RUE-12_1.4x2mm_P0.4mm +Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mm +Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mm_ThermalVias +Package_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP2.29x2.29mm +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP2.29x2.29mm_ThermalVias +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP3.1x3.1mm +Package_DFN_QFN:TQFN-16-1EP_5x5mm_P0.8mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.9x2.9mm +Package_DFN_QFN:TQFN-20-1EP_4x4mm_P0.5mm_EP2.9x2.9mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.1x3.1mm +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.25x3.25mm +Package_DFN_QFN:TQFN-20-1EP_5x5mm_P0.65mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_PullBack +Package_DFN_QFN:TQFN-24-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_PullBack_ThermalVias +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP2.7x2.7mm_ThermalVias +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm +Package_DFN_QFN:TQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.4x3.4mm +Package_DFN_QFN:TQFN-32-1EP_5x5mm_P0.5mm_EP3.4x3.4mm_ThermalVias +Package_DFN_QFN:TQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm +Package_DFN_QFN:TQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm_ThermalVias +Package_DFN_QFN:TQFN-44-1EP_7x7mm_P0.5mm_EP4.7x4.7mm +Package_DFN_QFN:TQFN-44-1EP_7x7mm_P0.5mm_EP4.7x4.7mm_ThermalVias +Package_DFN_QFN:TQFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm +Package_DFN_QFN:TQFN-48-1EP_7x7mm_P0.5mm_EP5.1x5.1mm_ThermalVias +Package_DFN_QFN:UDC-QFN-20-4EP_3x4mm_P0.5mm_EP0.41x0.25mm +Package_DFN_QFN:UDFN-10_1.35x2.6mm_P0.5mm +Package_DFN_QFN:UDFN-4-1EP_1x1mm_P0.65mm_EP0.48x0.48mm +Package_DFN_QFN:UDFN-9_1.0x3.8mm_P0.5mm +Package_DFN_QFN:UFQFPN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm +Package_DFN_QFN:UFQFPN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm_ThermalVias +Package_DFN_QFN:UQFN-10_1.3x1.8mm_P0.4mm +Package_DFN_QFN:UQFN-10_1.4x1.8mm_P0.4mm +Package_DFN_QFN:UQFN-10_1.6x2.1mm_P0.5mm +Package_DFN_QFN:UQFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm +Package_DFN_QFN:UQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm +Package_DFN_QFN:UQFN-16-1EP_4x4mm_P0.65mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:UQFN-16-1EP_4x4mm_P0.65mm_EP2.7x2.7mm +Package_DFN_QFN:UQFN-16_1.8x2.6mm_P0.4mm +Package_DFN_QFN:UQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm +Package_DFN_QFN:UQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm_ThermalVias +Package_DFN_QFN:UQFN-20-1EP_3x3mm_P0.4mm_EP1.85x1.85mm +Package_DFN_QFN:UQFN-20-1EP_3x3mm_P0.4mm_EP1.85x1.85mm_ThermalVias +Package_DFN_QFN:UQFN-20-1EP_4x4mm_P0.5mm_EP2.8x2.8mm +Package_DFN_QFN:UQFN-20-1EP_4x4mm_P0.5mm_EP2.8x2.8mm_ThermalVias +Package_DFN_QFN:UQFN-20_3x3mm_P0.4mm +Package_DFN_QFN:UQFN-28-1EP_4x4mm_P0.4mm_EP2.35x2.35mm +Package_DFN_QFN:UQFN-28-1EP_4x4mm_P0.4mm_EP2.35x2.35mm_ThermalVias +Package_DFN_QFN:UQFN-40-1EP_5x5mm_P0.4mm_EP3.8x3.8mm +Package_DFN_QFN:UQFN-40-1EP_5x5mm_P0.4mm_EP3.8x3.8mm_ThermalVias +Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.45x4.45mm +Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.45x4.45mm_ThermalVias +Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.62x4.62mm +Package_DFN_QFN:UQFN-48-1EP_6x6mm_P0.4mm_EP4.62x4.62mm_ThermalVias +Package_DFN_QFN:VDFN-8-1EP_2x2mm_P0.5mm_EP0.9x1.7mm +Package_DFN_QFN:Vishay_PowerPAK_MLP44-24L +Package_DFN_QFN:Vishay_PowerPAK_MLP44-24L_ThermalVias +Package_DFN_QFN:VQFN-100-1EP_12x12mm_P0.4mm_EP8x8mm +Package_DFN_QFN:VQFN-100-1EP_12x12mm_P0.4mm_EP8x8mm_ThermalVias +Package_DFN_QFN:VQFN-12-1EP_4x4mm_P0.8mm_EP2.1x2.1mm +Package_DFN_QFN:VQFN-12-1EP_4x4mm_P0.8mm_EP2.1x2.1mm_ThermalVias +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.1x1.1mm +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.1x1.1mm_ThermalVias +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm_ThermalVias +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm_ThermalVias +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm_ThermalVias +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.8x1.8mm +Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.8x1.8mm_ThermalVias +Package_DFN_QFN:VQFN-20-1EP_3x3mm_P0.45mm_EP1.55x1.55mm +Package_DFN_QFN:VQFN-20-1EP_3x3mm_P0.45mm_EP1.55x1.55mm_ThermalVias +Package_DFN_QFN:VQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm +Package_DFN_QFN:VQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm_ThermalVias +Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm +Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm_ThermalVias +Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm +Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias +Package_DFN_QFN:VQFN-28-1EP_4x4mm_P0.45mm_EP2.4x2.4mm +Package_DFN_QFN:VQFN-28-1EP_4x4mm_P0.45mm_EP2.4x2.4mm_ThermalVias +Package_DFN_QFN:VQFN-28-1EP_4x5mm_P0.5mm_EP2.55x3.55mm +Package_DFN_QFN:VQFN-28-1EP_4x5mm_P0.5mm_EP2.55x3.55mm_ThermalVias +Package_DFN_QFN:VQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm +Package_DFN_QFN:VQFN-28-1EP_5x5mm_P0.5mm_EP3.25x3.25mm_ThermalVias +Package_DFN_QFN:VQFN-32-1EP_4x4mm_P0.4mm_EP2.8x2.8mm +Package_DFN_QFN:VQFN-32-1EP_4x4mm_P0.4mm_EP2.8x2.8mm_ThermalVias +Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.15x3.15mm +Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.15x3.15mm_ThermalVias +Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm_ThermalVias +Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm +Package_DFN_QFN:VQFN-32-1EP_5x5mm_P0.5mm_EP3.5x3.5mm_ThermalVias +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.5x3.5mm_ThermalVias +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm +Package_DFN_QFN:VQFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm_ThermalVias +Package_DFN_QFN:VQFN-46-1EP_5x6mm_P0.4mm_EP2.8x3.8mm +Package_DFN_QFN:VQFN-46-1EP_5x6mm_P0.4mm_EP2.8x3.8mm_ThermalVias +Package_DFN_QFN:VQFN-48-1EP_6x6mm_P0.4mm_EP4.1x4.1mm +Package_DFN_QFN:VQFN-48-1EP_6x6mm_P0.4mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.1x4.1mm +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP4.1x4.1mm_ThermalVias +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm +Package_DFN_QFN:VQFN-48-1EP_7x7mm_P0.5mm_EP5.15x5.15mm_ThermalVias +Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP5.4x5.4mm +Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP5.4x5.4mm_ThermalVias +Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP7.15x7.15mm +Package_DFN_QFN:VQFN-64-1EP_9x9mm_P0.5mm_EP7.15x7.15mm_ThermalVias +Package_DFN_QFN:W-PDFN-8-1EP_6x5mm_P1.27mm_EP3x3mm +Package_DFN_QFN:WCH_QFN-16-1EP_3x3mm_P0.5mm_EP1.8x1.8mm +Package_DFN_QFN:WDFN-10-1EP_3x3mm_P0.5mm_EP1.8x2.5mm +Package_DFN_QFN:WDFN-10-1EP_3x3mm_P0.5mm_EP1.8x2.5mm_ThermalVias +Package_DFN_QFN:WDFN-12-1EP_3x3mm_P0.45mm_EP1.7x2.5mm +Package_DFN_QFN:WDFN-6-2EP_4.0x2.6mm_P0.65mm +Package_DFN_QFN:WDFN-8-1EP_2x2.2mm_P0.5mm_EP0.80x0.54 +Package_DFN_QFN:WDFN-8-1EP_2x2mm_P0.5mm_EP0.8x1.2mm +Package_DFN_QFN:WDFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.4mm +Package_DFN_QFN:WDFN-8-1EP_4x3mm_P0.65mm_EP2.4x1.8mm +Package_DFN_QFN:WDFN-8-1EP_4x3mm_P0.65mm_EP2.4x1.8mm_ThermalVias +Package_DFN_QFN:WDFN-8-1EP_6x5mm_P1.27mm_EP3.4x4mm +Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm +Package_DFN_QFN:WDFN-8-1EP_8x6mm_P1.27mm_EP6x4.8mm_ThermalVias +Package_DFN_QFN:WDFN-8_2x2mm_P0.5mm +Package_DFN_QFN:WFDFPN-8-1EP_3x2mm_P0.5mm_EP1.25x1.35mm +Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm +Package_DFN_QFN:WQFN-14-1EP_2.5x2.5mm_P0.5mm_EP1.45x1.45mm_ThermalVias +Package_DFN_QFN:WQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm +Package_DFN_QFN:WQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm_ThermalVias +Package_DFN_QFN:WQFN-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm +Package_DFN_QFN:WQFN-16-1EP_3x3mm_P0.5mm_EP1.6x1.6mm_ThermalVias +Package_DFN_QFN:WQFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm +Package_DFN_QFN:WQFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias +Package_DFN_QFN:WQFN-16-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:WQFN-16-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:WQFN-20-1EP_2.5x4.5mm_P0.5mm_EP1x2.9mm +Package_DFN_QFN:WQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm +Package_DFN_QFN:WQFN-20-1EP_3x3mm_P0.4mm_EP1.7x1.7mm_ThermalVias +Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm +Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm_ThermalVias +Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm +Package_DFN_QFN:WQFN-24-1EP_4x4mm_P0.5mm_EP2.6x2.6mm_ThermalVias +Package_DFN_QFN:WQFN-32-1EP_5x5mm_P0.5mm_EP3.1x3.1mm +Package_DFN_QFN:WQFN-42-1EP_3.5x9mm_P0.5mm_EP2.05x7.55mm +Package_DFN_QFN:WQFN-42-1EP_3.5x9mm_P0.5mm_EP2.05x7.55mm_ThermalVias +Package_DIP:CERDIP-14_W7.62mm_SideBrazed +Package_DIP:CERDIP-14_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-14_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-14_W7.62mm_SideBrazed_Socket +Package_DIP:CERDIP-16_W7.62mm_SideBrazed +Package_DIP:CERDIP-16_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-16_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-16_W7.62mm_SideBrazed_Socket +Package_DIP:CERDIP-18_W7.62mm_SideBrazed +Package_DIP:CERDIP-18_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-18_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-18_W7.62mm_SideBrazed_Socket +Package_DIP:CERDIP-20_W7.62mm_SideBrazed +Package_DIP:CERDIP-20_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-20_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-20_W7.62mm_SideBrazed_Socket +Package_DIP:CERDIP-22_W7.62mm_SideBrazed +Package_DIP:CERDIP-22_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-22_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-22_W7.62mm_SideBrazed_Socket +Package_DIP:CERDIP-24_W7.62mm_SideBrazed +Package_DIP:CERDIP-24_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-24_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-24_W7.62mm_SideBrazed_Socket +Package_DIP:CERDIP-28_W7.62mm_SideBrazed +Package_DIP:CERDIP-28_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-28_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-28_W7.62mm_SideBrazed_Socket +Package_DIP:CERDIP-8_W7.62mm_SideBrazed +Package_DIP:CERDIP-8_W7.62mm_SideBrazed_LongPads +Package_DIP:CERDIP-8_W7.62mm_SideBrazed_LongPads_Socket +Package_DIP:CERDIP-8_W7.62mm_SideBrazed_Socket +Package_DIP:DIP-10_W10.16mm +Package_DIP:DIP-10_W10.16mm_LongPads +Package_DIP:DIP-10_W7.62mm +Package_DIP:DIP-10_W7.62mm_LongPads +Package_DIP:DIP-10_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-10_W7.62mm_Socket +Package_DIP:DIP-10_W7.62mm_Socket_LongPads +Package_DIP:DIP-10_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-12_W10.16mm +Package_DIP:DIP-12_W10.16mm_LongPads +Package_DIP:DIP-12_W7.62mm +Package_DIP:DIP-12_W7.62mm_LongPads +Package_DIP:DIP-12_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-12_W7.62mm_Socket +Package_DIP:DIP-12_W7.62mm_Socket_LongPads +Package_DIP:DIP-12_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-14_W10.16mm +Package_DIP:DIP-14_W10.16mm_LongPads +Package_DIP:DIP-14_W7.62mm +Package_DIP:DIP-14_W7.62mm_LongPads +Package_DIP:DIP-14_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-14_W7.62mm_Socket +Package_DIP:DIP-14_W7.62mm_Socket_LongPads +Package_DIP:DIP-14_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-16_W10.16mm +Package_DIP:DIP-16_W10.16mm_LongPads +Package_DIP:DIP-16_W7.62mm +Package_DIP:DIP-16_W7.62mm_LongPads +Package_DIP:DIP-16_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-16_W7.62mm_Socket +Package_DIP:DIP-16_W7.62mm_Socket_LongPads +Package_DIP:DIP-16_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-18_W7.62mm +Package_DIP:DIP-18_W7.62mm_LongPads +Package_DIP:DIP-18_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-18_W7.62mm_Socket +Package_DIP:DIP-18_W7.62mm_Socket_LongPads +Package_DIP:DIP-18_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-20_W7.62mm +Package_DIP:DIP-20_W7.62mm_LongPads +Package_DIP:DIP-20_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-20_W7.62mm_Socket +Package_DIP:DIP-20_W7.62mm_Socket_LongPads +Package_DIP:DIP-20_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-22_W10.16mm +Package_DIP:DIP-22_W10.16mm_LongPads +Package_DIP:DIP-22_W10.16mm_SMDSocket_SmallPads +Package_DIP:DIP-22_W10.16mm_Socket +Package_DIP:DIP-22_W10.16mm_Socket_LongPads +Package_DIP:DIP-22_W11.43mm_SMDSocket_LongPads +Package_DIP:DIP-22_W7.62mm +Package_DIP:DIP-22_W7.62mm_LongPads +Package_DIP:DIP-22_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-22_W7.62mm_Socket +Package_DIP:DIP-22_W7.62mm_Socket_LongPads +Package_DIP:DIP-22_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-24_18.0mmx34.29mm_W15.24mm +Package_DIP:DIP-24_18.0mmx34.29mm_W15.24mm_LongPads +Package_DIP:DIP-24_18.0mmx34.29mm_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-24_18.0mmx34.29mm_W15.24mm_Socket +Package_DIP:DIP-24_18.0mmx34.29mm_W15.24mm_Socket_LongPads +Package_DIP:DIP-24_W10.16mm +Package_DIP:DIP-24_W10.16mm_LongPads +Package_DIP:DIP-24_W10.16mm_SMDSocket_SmallPads +Package_DIP:DIP-24_W10.16mm_Socket +Package_DIP:DIP-24_W10.16mm_Socket_LongPads +Package_DIP:DIP-24_W11.43mm_SMDSocket_LongPads +Package_DIP:DIP-24_W15.24mm +Package_DIP:DIP-24_W15.24mm_LongPads +Package_DIP:DIP-24_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-24_W15.24mm_Socket +Package_DIP:DIP-24_W15.24mm_Socket_LongPads +Package_DIP:DIP-24_W16.51mm_SMDSocket_LongPads +Package_DIP:DIP-24_W7.62mm +Package_DIP:DIP-24_W7.62mm_LongPads +Package_DIP:DIP-24_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-24_W7.62mm_Socket +Package_DIP:DIP-24_W7.62mm_Socket_LongPads +Package_DIP:DIP-24_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-28_W15.24mm +Package_DIP:DIP-28_W15.24mm_LongPads +Package_DIP:DIP-28_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-28_W15.24mm_Socket +Package_DIP:DIP-28_W15.24mm_Socket_LongPads +Package_DIP:DIP-28_W16.51mm_SMDSocket_LongPads +Package_DIP:DIP-28_W7.62mm +Package_DIP:DIP-28_W7.62mm_LongPads +Package_DIP:DIP-28_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-28_W7.62mm_Socket +Package_DIP:DIP-28_W7.62mm_Socket_LongPads +Package_DIP:DIP-28_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-32_W15.24mm +Package_DIP:DIP-32_W15.24mm_LongPads +Package_DIP:DIP-32_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-32_W15.24mm_Socket +Package_DIP:DIP-32_W15.24mm_Socket_LongPads +Package_DIP:DIP-32_W16.51mm_SMDSocket_LongPads +Package_DIP:DIP-32_W7.62mm +Package_DIP:DIP-40_W15.24mm +Package_DIP:DIP-40_W15.24mm_LongPads +Package_DIP:DIP-40_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-40_W15.24mm_Socket +Package_DIP:DIP-40_W15.24mm_Socket_LongPads +Package_DIP:DIP-40_W16.51mm_SMDSocket_LongPads +Package_DIP:DIP-40_W25.4mm +Package_DIP:DIP-40_W25.4mm_LongPads +Package_DIP:DIP-40_W25.4mm_SMDSocket_SmallPads +Package_DIP:DIP-40_W25.4mm_Socket +Package_DIP:DIP-40_W25.4mm_Socket_LongPads +Package_DIP:DIP-40_W26.67mm_SMDSocket_LongPads +Package_DIP:DIP-42_W15.24mm +Package_DIP:DIP-42_W15.24mm_LongPads +Package_DIP:DIP-42_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-42_W15.24mm_Socket +Package_DIP:DIP-42_W15.24mm_Socket_LongPads +Package_DIP:DIP-42_W16.51mm_SMDSocket_LongPads +Package_DIP:DIP-48_W15.24mm +Package_DIP:DIP-48_W15.24mm_LongPads +Package_DIP:DIP-48_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-48_W15.24mm_Socket +Package_DIP:DIP-48_W15.24mm_Socket_LongPads +Package_DIP:DIP-48_W16.51mm_SMDSocket_LongPads +Package_DIP:DIP-4_W10.16mm +Package_DIP:DIP-4_W10.16mm_LongPads +Package_DIP:DIP-4_W7.62mm +Package_DIP:DIP-4_W7.62mm_LongPads +Package_DIP:DIP-4_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-4_W7.62mm_Socket +Package_DIP:DIP-4_W7.62mm_Socket_LongPads +Package_DIP:DIP-4_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-5-6_W10.16mm +Package_DIP:DIP-5-6_W10.16mm_LongPads +Package_DIP:DIP-5-6_W7.62mm +Package_DIP:DIP-5-6_W7.62mm_LongPads +Package_DIP:DIP-5-6_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-5-6_W7.62mm_Socket +Package_DIP:DIP-5-6_W7.62mm_Socket_LongPads +Package_DIP:DIP-5-6_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-64_W15.24mm +Package_DIP:DIP-64_W15.24mm_LongPads +Package_DIP:DIP-64_W15.24mm_SMDSocket_SmallPads +Package_DIP:DIP-64_W15.24mm_Socket +Package_DIP:DIP-64_W15.24mm_Socket_LongPads +Package_DIP:DIP-64_W16.51mm_SMDSocket_LongPads +Package_DIP:DIP-64_W22.86mm +Package_DIP:DIP-64_W22.86mm_LongPads +Package_DIP:DIP-64_W22.86mm_SMDSocket_SmallPads +Package_DIP:DIP-64_W22.86mm_Socket +Package_DIP:DIP-64_W22.86mm_Socket_LongPads +Package_DIP:DIP-64_W24.13mm_SMDSocket_LongPads +Package_DIP:DIP-64_W25.4mm +Package_DIP:DIP-64_W25.4mm_LongPads +Package_DIP:DIP-64_W25.4mm_SMDSocket_SmallPads +Package_DIP:DIP-64_W25.4mm_Socket +Package_DIP:DIP-64_W25.4mm_Socket_LongPads +Package_DIP:DIP-64_W26.67mm_SMDSocket_LongPads +Package_DIP:DIP-6_W10.16mm +Package_DIP:DIP-6_W10.16mm_LongPads +Package_DIP:DIP-6_W7.62mm +Package_DIP:DIP-6_W7.62mm_LongPads +Package_DIP:DIP-6_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-6_W7.62mm_Socket +Package_DIP:DIP-6_W7.62mm_Socket_LongPads +Package_DIP:DIP-6_W8.89mm_SMDSocket_LongPads +Package_DIP:DIP-8-16_W7.62mm +Package_DIP:DIP-8-16_W7.62mm_Socket +Package_DIP:DIP-8-16_W7.62mm_Socket_LongPads +Package_DIP:DIP-8-N6_W7.62mm +Package_DIP:DIP-8-N7_W7.62mm +Package_DIP:DIP-8_W10.16mm +Package_DIP:DIP-8_W10.16mm_LongPads +Package_DIP:DIP-8_W7.62mm +Package_DIP:DIP-8_W7.62mm_LongPads +Package_DIP:DIP-8_W7.62mm_SMDSocket_SmallPads +Package_DIP:DIP-8_W7.62mm_Socket +Package_DIP:DIP-8_W7.62mm_Socket_LongPads +Package_DIP:DIP-8_W8.89mm_SMDSocket_LongPads +Package_DIP:Fairchild_LSOP-8 +Package_DIP:IXYS_Flatpak-8_6.3x9.7mm_P2.54mm +Package_DIP:IXYS_SMD-8_6.3x9.7mm_P2.54mm +Package_DIP:PowerIntegrations_eDIP-12B +Package_DIP:PowerIntegrations_PDIP-8B +Package_DIP:PowerIntegrations_PDIP-8C +Package_DIP:PowerIntegrations_SDIP-10C +Package_DIP:PowerIntegrations_SMD-8 +Package_DIP:PowerIntegrations_SMD-8B +Package_DIP:PowerIntegrations_SMD-8C +Package_DIP:SMDIP-10_W11.48mm +Package_DIP:SMDIP-10_W7.62mm +Package_DIP:SMDIP-10_W9.53mm +Package_DIP:SMDIP-10_W9.53mm_Clearance8mm +Package_DIP:SMDIP-12_W11.48mm +Package_DIP:SMDIP-12_W7.62mm +Package_DIP:SMDIP-12_W9.53mm +Package_DIP:SMDIP-12_W9.53mm_Clearance8mm +Package_DIP:SMDIP-14_W11.48mm +Package_DIP:SMDIP-14_W7.62mm +Package_DIP:SMDIP-14_W9.53mm +Package_DIP:SMDIP-14_W9.53mm_Clearance8mm +Package_DIP:SMDIP-16_W11.48mm +Package_DIP:SMDIP-16_W7.62mm +Package_DIP:SMDIP-16_W9.53mm +Package_DIP:SMDIP-16_W9.53mm_Clearance8mm +Package_DIP:SMDIP-18_W11.48mm +Package_DIP:SMDIP-18_W7.62mm +Package_DIP:SMDIP-18_W9.53mm +Package_DIP:SMDIP-18_W9.53mm_Clearance8mm +Package_DIP:SMDIP-20_W11.48mm +Package_DIP:SMDIP-20_W7.62mm +Package_DIP:SMDIP-20_W9.53mm +Package_DIP:SMDIP-20_W9.53mm_Clearance8mm +Package_DIP:SMDIP-22_W11.48mm +Package_DIP:SMDIP-22_W7.62mm +Package_DIP:SMDIP-22_W9.53mm +Package_DIP:SMDIP-22_W9.53mm_Clearance8mm +Package_DIP:SMDIP-24_W11.48mm +Package_DIP:SMDIP-24_W15.24mm +Package_DIP:SMDIP-24_W7.62mm +Package_DIP:SMDIP-24_W9.53mm +Package_DIP:SMDIP-28_W15.24mm +Package_DIP:SMDIP-32_W11.48mm +Package_DIP:SMDIP-32_W15.24mm +Package_DIP:SMDIP-32_W7.62mm +Package_DIP:SMDIP-32_W9.53mm +Package_DIP:SMDIP-40_W15.24mm +Package_DIP:SMDIP-40_W25.24mm +Package_DIP:SMDIP-42_W15.24mm +Package_DIP:SMDIP-48_W15.24mm +Package_DIP:SMDIP-4_W11.48mm +Package_DIP:SMDIP-4_W7.62mm +Package_DIP:SMDIP-4_W9.53mm +Package_DIP:SMDIP-4_W9.53mm_Clearance8mm +Package_DIP:SMDIP-64_W15.24mm +Package_DIP:SMDIP-6_W11.48mm +Package_DIP:SMDIP-6_W7.62mm +Package_DIP:SMDIP-6_W9.53mm +Package_DIP:SMDIP-6_W9.53mm_Clearance8mm +Package_DIP:SMDIP-8_W11.48mm +Package_DIP:SMDIP-8_W7.62mm +Package_DIP:SMDIP-8_W9.53mm +Package_DIP:SMDIP-8_W9.53mm_Clearance8mm +Package_DIP:Toshiba_11-7A9 +Package_DIP:Vishay_HVM-DIP-3_W7.62mm +Package_DirectFET:DirectFET_L4 +Package_DirectFET:DirectFET_L6 +Package_DirectFET:DirectFET_L8 +Package_DirectFET:DirectFET_LA +Package_DirectFET:DirectFET_M2 +Package_DirectFET:DirectFET_M4 +Package_DirectFET:DirectFET_MA +Package_DirectFET:DirectFET_MB +Package_DirectFET:DirectFET_MC +Package_DirectFET:DirectFET_MD +Package_DirectFET:DirectFET_ME +Package_DirectFET:DirectFET_MF +Package_DirectFET:DirectFET_MN +Package_DirectFET:DirectFET_MP +Package_DirectFET:DirectFET_MQ +Package_DirectFET:DirectFET_MT +Package_DirectFET:DirectFET_MU +Package_DirectFET:DirectFET_MX +Package_DirectFET:DirectFET_MZ +Package_DirectFET:DirectFET_S1 +Package_DirectFET:DirectFET_S2 +Package_DirectFET:DirectFET_S3C +Package_DirectFET:DirectFET_SA +Package_DirectFET:DirectFET_SB +Package_DirectFET:DirectFET_SC +Package_DirectFET:DirectFET_SH +Package_DirectFET:DirectFET_SJ +Package_DirectFET:DirectFET_SQ +Package_DirectFET:DirectFET_ST +Package_LCC:Analog_LCC-8_5x5mm_P1.27mm +Package_LCC:PLCC-20 +Package_LCC:PLCC-20_SMD-Socket +Package_LCC:PLCC-20_THT-Socket +Package_LCC:PLCC-28 +Package_LCC:PLCC-28_SMD-Socket +Package_LCC:PLCC-28_THT-Socket +Package_LCC:PLCC-32_11.4x14.0mm_P1.27mm +Package_LCC:PLCC-32_THT-Socket +Package_LCC:PLCC-44 +Package_LCC:PLCC-44_16.6x16.6mm_P1.27mm +Package_LCC:PLCC-44_SMD-Socket +Package_LCC:PLCC-44_THT-Socket +Package_LCC:PLCC-52 +Package_LCC:PLCC-52_SMD-Socket +Package_LCC:PLCC-52_THT-Socket +Package_LCC:PLCC-68 +Package_LCC:PLCC-68_24.2x24.2mm_P1.27mm +Package_LCC:PLCC-68_SMD-Socket +Package_LCC:PLCC-68_THT-Socket +Package_LCC:PLCC-84 +Package_LCC:PLCC-84_29.3x29.3mm_P1.27mm +Package_LCC:PLCC-84_SMD-Socket +Package_LCC:PLCC-84_THT-Socket +Package_LGA:AMS_LGA-10-1EP_2.7x4mm_P0.6mm +Package_LGA:AMS_LGA-20_4.7x4.5mm_P0.65mm +Package_LGA:AMS_OLGA-8_2x3.1mm_P0.8mm +Package_LGA:Bosch_LGA-14_3x2.5mm_P0.5mm +Package_LGA:Bosch_LGA-8_2.5x2.5mm_P0.65mm_ClockwisePinNumbering +Package_LGA:Bosch_LGA-8_2x2.5mm_P0.65mm_ClockwisePinNumbering +Package_LGA:Bosch_LGA-8_3x3mm_P0.8mm_ClockwisePinNumbering +Package_LGA:Infineon_PG-TSNP-6-10_0.7x1.1mm_0.7x1.1mm_P0.4mm +Package_LGA:Kionix_LGA-12_2x2mm_P0.5mm_LayoutBorder2x4y +Package_LGA:LGA-12_2x2mm_P0.5mm +Package_LGA:LGA-14_2x2mm_P0.35mm_LayoutBorder3x4y +Package_LGA:LGA-14_3x2.5mm_P0.5mm_LayoutBorder3x4y +Package_LGA:LGA-14_3x5mm_P0.8mm_LayoutBorder1x6y +Package_LGA:LGA-16_3x3mm_P0.5mm +Package_LGA:LGA-16_3x3mm_P0.5mm_LayoutBorder3x5y +Package_LGA:LGA-16_4x4mm_P0.65mm_LayoutBorder4x4y +Package_LGA:LGA-24L_3x3.5mm_P0.43mm +Package_LGA:LGA-28_5.2x3.8mm_P0.5mm +Package_LGA:LGA-8_3x5mm_P1.25mm +Package_LGA:LGA-8_8x6.2mm_P1.27mm +Package_LGA:LGA-8_8x6mm_P1.27mm +Package_LGA:Linear_LGA-133_15.0x15.0mm_Layout12x12_P1.27mm +Package_LGA:MPS_LGA-18-10EP_12x12mm_P3.3mm +Package_LGA:Nordic_nRF9160-SIxx_LGA-102-59EP_16.0x10.5mm_P0.5mm +Package_LGA:NXP_LGA-8_3x5mm_P1.25mm_H1.1mm +Package_LGA:NXP_LGA-8_3x5mm_P1.25mm_H1.2mm +Package_LGA:Rohm_MLGA010V020A_LGA-10_2x2mm_P0.45mm_LayoutBorder2x3y +Package_LGA:ST_CCLGA-7L_2.8x2.8mm_P1.15mm_H1.95mm +Package_LGA:ST_HLGA-10_2.5x2.5mm_P0.6mm_LayoutBorder3x2y +Package_LGA:ST_HLGA-10_2x2mm_P0.5mm_LayoutBorder3x2y +Package_LGA:Texas_SIL0008D_MicroSiP-8-1EP_2.8x3mm_P0.65mm_EP1.1x1.9mm +Package_LGA:Texas_SIL0008D_MicroSiP-8-1EP_2.8x3mm_P0.65mm_EP1.1x1.9mm_ThermalVias +Package_LGA:Texas_SIL0010A_MicroSiP-10-1EP_3.8x3mm_P0.6mm_EP0.7x2.9mm +Package_LGA:Texas_SIL0010A_MicroSiP-10-1EP_3.8x3mm_P0.6mm_EP0.7x2.9mm_ThermalVias +Package_LGA:VLGA-4_2x2.5mm_P1.65mm +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP4x4mm +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP4x4mm_ThermalVias +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP5x5mm +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP5x5mm_ThermalVias +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP6.61x5.615mm +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP6.61x5.615mm_ThermalVias +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP7.2x6.35mm +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP7.2x6.35mm_ThermalVias +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP8.93x8.7mm +Package_QFP:EQFP-144-1EP_20x20mm_P0.5mm_EP8.93x8.7mm_ThermalVias +Package_QFP:HTQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm +Package_QFP:HTQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm_Mask4.4x4.4mm_ThermalVias +Package_QFP:LQFP-100_14x14mm_P0.5mm +Package_QFP:LQFP-128_14x14mm_P0.4mm +Package_QFP:LQFP-128_14x20mm_P0.5mm +Package_QFP:LQFP-144-1EP_20x20mm_P0.5mm_EP6.5x6.5mm +Package_QFP:LQFP-144-1EP_20x20mm_P0.5mm_EP6.5x6.5mm_ThermalVias +Package_QFP:LQFP-144_20x20mm_P0.5mm +Package_QFP:LQFP-160_24x24mm_P0.5mm +Package_QFP:LQFP-176_20x20mm_P0.4mm +Package_QFP:LQFP-176_24x24mm_P0.5mm +Package_QFP:LQFP-208_28x28mm_P0.5mm +Package_QFP:LQFP-216_24x24mm_P0.4mm +Package_QFP:LQFP-32_5x5mm_P0.5mm +Package_QFP:LQFP-32_7x7mm_P0.8mm +Package_QFP:LQFP-36_7x7mm_P0.65mm +Package_QFP:LQFP-44_10x10mm_P0.8mm +Package_QFP:LQFP-48-1EP_7x7mm_P0.5mm_EP3.6x3.6mm +Package_QFP:LQFP-48-1EP_7x7mm_P0.5mm_EP3.6x3.6mm_ThermalVias +Package_QFP:LQFP-48_7x7mm_P0.5mm +Package_QFP:LQFP-52-1EP_10x10mm_P0.65mm_EP4.8x4.8mm +Package_QFP:LQFP-52-1EP_10x10mm_P0.65mm_EP4.8x4.8mm_ThermalVias +Package_QFP:LQFP-52_10x10mm_P0.65mm +Package_QFP:LQFP-52_14x14mm_P1mm +Package_QFP:LQFP-64-1EP_10x10mm_P0.5mm_EP5x5mm +Package_QFP:LQFP-64-1EP_10x10mm_P0.5mm_EP5x5mm_ThermalVias +Package_QFP:LQFP-64-1EP_10x10mm_P0.5mm_EP6.5x6.5mm +Package_QFP:LQFP-64-1EP_10x10mm_P0.5mm_EP6.5x6.5mm_ThermalVias +Package_QFP:LQFP-64_10x10mm_P0.5mm +Package_QFP:LQFP-64_14x14mm_P0.8mm +Package_QFP:LQFP-64_7x7mm_P0.4mm +Package_QFP:LQFP-80_10x10mm_P0.4mm +Package_QFP:LQFP-80_12x12mm_P0.5mm +Package_QFP:LQFP-80_14x14mm_P0.65mm +Package_QFP:Microchip_PQFP-44_10x10mm_P0.8mm +Package_QFP:MQFP-44_10x10mm_P0.8mm +Package_QFP:PQFP-100_14x20mm_P0.65mm +Package_QFP:PQFP-112_20x20mm_P0.65mm +Package_QFP:PQFP-132_24x24mm_P0.635mm +Package_QFP:PQFP-132_24x24mm_P0.635mm_i386 +Package_QFP:PQFP-144_28x28mm_P0.65mm +Package_QFP:PQFP-160_28x28mm_P0.65mm +Package_QFP:PQFP-168_28x28mm_P0.65mm +Package_QFP:PQFP-208_28x28mm_P0.5mm +Package_QFP:PQFP-240_32.1x32.1mm_P0.5mm +Package_QFP:PQFP-256_28x28mm_P0.4mm +Package_QFP:PQFP-32_5x5mm_P0.5mm +Package_QFP:PQFP-44_10x10mm_P0.8mm +Package_QFP:PQFP-64_14x14mm_P0.8mm +Package_QFP:PQFP-80_14x20mm_P0.8mm +Package_QFP:Texas_PHP0048E_HTQFP-48-1EP_7x7mm_P0.5mm_EP6.5x6.5mm_Mask3.62x3.62mm +Package_QFP:Texas_PHP0048E_HTQFP-48-1EP_7x7mm_P0.5mm_EP6.5x6.5mm_Mask3.62x3.62mm_ThermalVias +Package_QFP:TQFP-100-1EP_14x14mm_P0.5mm_EP5x5mm +Package_QFP:TQFP-100-1EP_14x14mm_P0.5mm_EP5x5mm_ThermalVias +Package_QFP:TQFP-100_12x12mm_P0.4mm +Package_QFP:TQFP-100_14x14mm_P0.5mm +Package_QFP:TQFP-120_14x14mm_P0.4mm +Package_QFP:TQFP-128_14x14mm_P0.4mm +Package_QFP:TQFP-144_16x16mm_P0.4mm +Package_QFP:TQFP-144_20x20mm_P0.5mm +Package_QFP:TQFP-176_24x24mm_P0.5mm +Package_QFP:TQFP-32_7x7mm_P0.8mm +Package_QFP:TQFP-44-1EP_10x10mm_P0.8mm_EP4.5x4.5mm +Package_QFP:TQFP-44_10x10mm_P0.8mm +Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP3.5x3.5mm +Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP4.11x4.11mm +Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP5x5mm +Package_QFP:TQFP-48-1EP_7x7mm_P0.5mm_EP5x5mm_ThermalVias +Package_QFP:TQFP-48_7x7mm_P0.5mm +Package_QFP:TQFP-52-1EP_10x10mm_P0.65mm_EP6.5x6.5mm +Package_QFP:TQFP-52-1EP_10x10mm_P0.65mm_EP6.5x6.5mm_ThermalVias +Package_QFP:TQFP-64-1EP_10x10mm_P0.5mm_EP5.305x5.305mm +Package_QFP:TQFP-64-1EP_10x10mm_P0.5mm_EP5.305x5.305mm_ThermalVias +Package_QFP:TQFP-64-1EP_10x10mm_P0.5mm_EP8x8mm +Package_QFP:TQFP-64_10x10mm_P0.5mm +Package_QFP:TQFP-64_14x14mm_P0.8mm +Package_QFP:TQFP-64_7x7mm_P0.4mm +Package_QFP:TQFP-80-1EP_14x14mm_P0.65mm_EP9.5x9.5mm +Package_QFP:TQFP-80_12x12mm_P0.5mm +Package_QFP:TQFP-80_14x14mm_P0.65mm +Package_QFP:VQFP-100_14x14mm_P0.5mm +Package_QFP:VQFP-128_14x14mm_P0.4mm +Package_QFP:VQFP-176_20x20mm_P0.4mm +Package_QFP:VQFP-80_14x14mm_P0.65mm +Package_SIP:PowerIntegrations_eSIP-7C +Package_SIP:PowerIntegrations_eSIP-7F +Package_SIP:Sanyo_STK4xx-15_59.2x8.0mm_P2.54mm +Package_SIP:Sanyo_STK4xx-15_78.0x8.0mm_P2.54mm +Package_SIP:SIP-8_19x3mm_P2.54mm +Package_SIP:SIP-9_21.54x3mm_P2.54mm +Package_SIP:SIP-9_22.3x3mm_P2.54mm +Package_SIP:SIP3_11.6x8.5mm +Package_SIP:SIP4_Sharp-SSR_P7.62mm_Angled +Package_SIP:SIP4_Sharp-SSR_P7.62mm_Angled_NoHole +Package_SIP:SIP4_Sharp-SSR_P7.62mm_Straight +Package_SIP:SIP9_Housing +Package_SIP:SIP9_Housing_BigPads +Package_SIP:SLA704XM +Package_SIP:STK672-040-E +Package_SIP:STK672-080-E +Package_SO:Analog_MSOP-12-16-1EP_3x4.039mm_P0.5mm_EP1.651x2.845mm +Package_SO:Analog_MSOP-12-16-1EP_3x4.039mm_P0.5mm_EP1.651x2.845mm_ThermalVias +Package_SO:Analog_MSOP-12-16_3x4.039mm_P0.5mm +Package_SO:Diodes_PSOP-8 +Package_SO:Diodes_SO-8EP +Package_SO:ETSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3x4.2mm +Package_SO:HSOP-20-1EP_11.0x15.9mm_P1.27mm_SlugDown +Package_SO:HSOP-20-1EP_11.0x15.9mm_P1.27mm_SlugDown_ThermalVias +Package_SO:HSOP-20-1EP_11.0x15.9mm_P1.27mm_SlugUp +Package_SO:HSOP-32-1EP_7.5x11mm_P0.65mm_EP4.7x4.7mm +Package_SO:HSOP-36-1EP_11.0x15.9mm_P0.65mm_SlugDown +Package_SO:HSOP-36-1EP_11.0x15.9mm_P0.65mm_SlugDown_ThermalVias +Package_SO:HSOP-36-1EP_11.0x15.9mm_P0.65mm_SlugUp +Package_SO:HSOP-54-1EP_7.5x17.9mm_P0.65mm_EP4.6x4.6mm +Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.3x2.3mm +Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.3x2.3mm_ThermalVias +Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm +Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm_ThermalVias +Package_SO:HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.4x3.2mm +Package_SO:HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.4x3.2mm_ThermalVias +Package_SO:HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3.1mm +Package_SO:HTSSOP-14-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3.1mm_ThermalVias +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask2.46x2.31mm_ThermalVias +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mm_Mask3x3mm_ThermalVias +Package_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP2.74x3.86mm +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP2.85x4mm +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.4x3.7mm +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.75x3.43mm +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.75x3.43mm_ThermalVias +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.75x3.43mm_ThermalVias_HandSolder +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.96x2.96mm +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_Mask2.96x2.96mm_ThermalVias +Package_SO:HTSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP3.4x6.5mm_ThermalVias +Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.2x5mm +Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.4x2.98mm +Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.4x2.98mm_ThermalVias +Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.4x4.68mm +Package_SO:HTSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.4x7.8mm_Mask2.4x4.68mm_ThermalVias +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.75x6.2mm +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.75x6.2mm_ThermalVias +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.85x5.4mm +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.85x5.4mm_ThermalVias +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.5mm +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.5mm_Mask2.4x6.17mm +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.5mm_Mask2.4x6.17mm_ThermalVias +Package_SO:HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.5mm_ThermalVias +Package_SO:HTSSOP-32-1EP_6.1x11mm_P0.65mm_EP5.2x11mm_Mask4.11x4.36mm +Package_SO:HTSSOP-32-1EP_6.1x11mm_P0.65mm_EP5.2x11mm_Mask4.11x4.36mm_ThermalVias +Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP1.5x3.3mm +Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP1.5x3.3mm_ThermalVias +Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm +Package_SO:HTSSOP-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm_ThermalVias +Package_SO:HTSSOP-38-1EP_6.1x12.5mm_P0.65mm_EP5.2x12.5mm_Mask3.39x6.35mm +Package_SO:HTSSOP-38-1EP_6.1x12.5mm_P0.65mm_EP5.2x12.5mm_Mask3.39x6.35mm_ThermalVias +Package_SO:HTSSOP-44-1EP_6.1x14mm_P0.635mm_EP5.2x14mm_Mask4.31x8.26mm +Package_SO:HTSSOP-44-1EP_6.1x14mm_P0.635mm_EP5.2x14mm_Mask4.31x8.26mm_ThermalVias +Package_SO:HTSSOP-44_6.1x14mm_P0.635mm_TopEP4.14x7.01mm +Package_SO:HTSSOP-56-1EP_6.1x14mm_P0.5mm_EP3.61x6.35mm +Package_SO:HVSSOP-10-1EP_3x3mm_P0.5mm_EP1.57x1.88mm +Package_SO:HVSSOP-10-1EP_3x3mm_P0.5mm_EP1.57x1.88mm_ThermalVias +Package_SO:HVSSOP-8-1EP_3x3mm_P0.65mm_EP1.57x1.89mm +Package_SO:HVSSOP-8-1EP_3x3mm_P0.65mm_EP1.57x1.89mm_ThermalVias +Package_SO:Infineon_PG-DSO-12-11 +Package_SO:Infineon_PG-DSO-12-11_ThermalVias +Package_SO:Infineon_PG-DSO-12-9 +Package_SO:Infineon_PG-DSO-12-9_ThermalVias +Package_SO:Infineon_PG-DSO-20-30 +Package_SO:Infineon_PG-DSO-20-30_ThermalVias +Package_SO:Infineon_PG-DSO-20-32 +Package_SO:Infineon_PG-DSO-20-85 +Package_SO:Infineon_PG-DSO-20-85_ThermalVias +Package_SO:Infineon_PG-DSO-20-87 +Package_SO:Infineon_PG-DSO-20-U03_7.5x12.8mm +Package_SO:Infineon_PG-DSO-8-24_4x5mm +Package_SO:Infineon_PG-DSO-8-27_3.9x4.9mm_EP2.65x3mm +Package_SO:Infineon_PG-DSO-8-27_3.9x4.9mm_EP2.65x3mm_ThermalVias +Package_SO:Infineon_PG-DSO-8-43 +Package_SO:Infineon_PG-DSO-8-59_7.5x6.3mm +Package_SO:Infineon_PG-TSDSO-14-22 +Package_SO:Infineon_SOIC-20W_7.6x12.8mm_P1.27mm +Package_SO:Linear_HTSSOP-31-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm +Package_SO:Linear_HTSSOP-31-38-1EP_4.4x9.7mm_P0.5mm_EP2.74x4.75mm_ThermalVias +Package_SO:MFSOP6-4_4.4x3.6mm_P1.27mm +Package_SO:MFSOP6-5_4.4x3.6mm_P1.27mm +Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP1.68x1.88mm +Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP1.68x1.88mm_ThermalVias +Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP1.73x1.98mm +Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP1.73x1.98mm_ThermalVias +Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP2.2x3.1mm_Mask1.83x1.89mm +Package_SO:MSOP-10-1EP_3x3mm_P0.5mm_EP2.2x3.1mm_Mask1.83x1.89mm_ThermalVias +Package_SO:MSOP-10_3x3mm_P0.5mm +Package_SO:MSOP-12-1EP_3x4.039mm_P0.65mm_EP1.651x2.845mm +Package_SO:MSOP-12-1EP_3x4.039mm_P0.65mm_EP1.651x2.845mm_ThermalVias +Package_SO:MSOP-12_3x4.039mm_P0.65mm +Package_SO:MSOP-16-1EP_3x4.039mm_P0.5mm_EP1.651x2.845mm +Package_SO:MSOP-16-1EP_3x4.039mm_P0.5mm_EP1.651x2.845mm_ThermalVias +Package_SO:MSOP-16_3x4.039mm_P0.5mm +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.5x1.8mm +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.5x1.8mm_ThermalVias +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.68x1.88mm +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.68x1.88mm_ThermalVias +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.73x1.85mm +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.73x1.85mm_ThermalVias +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.95x2.15mm +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.95x2.15mm_ThermalVias +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP2.5x3mm_Mask1.73x2.36mm +Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP2.5x3mm_Mask1.73x2.36mm_ThermalVias +Package_SO:MSOP-8_3x3mm_P0.65mm +Package_SO:NXP_HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.2x3.4mm +Package_SO:NXP_HTSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.2x3.4mm_ThermalVias +Package_SO:OnSemi_Micro8 +Package_SO:ONSemi_SO-8FL_488AA +Package_SO:PowerIntegrations_eSOP-12B +Package_SO:PowerIntegrations_SO-8 +Package_SO:PowerIntegrations_SO-8B +Package_SO:PowerIntegrations_SO-8C +Package_SO:PowerPAK_SO-8L_Single +Package_SO:PowerPAK_SO-8_Dual +Package_SO:PowerPAK_SO-8_Single +Package_SO:PowerSSO-16-1EP_3.9x4.9mm_P0.5mm_EP2.5x3.61mm +Package_SO:PowerSSO-16-1EP_3.9x4.9mm_P0.5mm_EP2.5x3.61mm_ThermalVias +Package_SO:PSOP-44_16.9x27.17mm_P1.27mm +Package_SO:QSOP-16_3.9x4.9mm_P0.635mm +Package_SO:QSOP-20_3.9x8.7mm_P0.635mm +Package_SO:QSOP-24_3.9x8.7mm_P0.635mm +Package_SO:QSOP-28_3.9x9.9mm_P0.635mm +Package_SO:Renesas_SOP-32_11.4x20.75mm_P1.27mm +Package_SO:SO-14_3.9x8.65mm_P1.27mm +Package_SO:SO-14_5.3x10.2mm_P1.27mm +Package_SO:SO-16_3.9x9.9mm_P1.27mm +Package_SO:SO-16_5.3x10.2mm_P1.27mm +Package_SO:SO-20-1EP_7.52x12.825mm_P1.27mm_EP6.045x12.09mm_Mask3.56x4.47mm +Package_SO:SO-20-1EP_7.52x12.825mm_P1.27mm_EP6.045x12.09mm_Mask3.56x4.47mm_ThermalVias +Package_SO:SO-20_12.8x7.5mm_P1.27mm +Package_SO:SO-20_5.3x12.6mm_P1.27mm +Package_SO:SO-24_5.3x15mm_P1.27mm +Package_SO:SO-4_4.4x2.3mm_P1.27mm +Package_SO:SO-4_4.4x3.6mm_P2.54mm +Package_SO:SO-4_4.4x3.9mm_P2.54mm +Package_SO:SO-4_4.4x4.3mm_P2.54mm +Package_SO:SO-4_7.6x3.6mm_P2.54mm +Package_SO:SO-5-6_4.55x3.7mm_P1.27mm +Package_SO:SO-5_4.4x3.6mm_P1.27mm +Package_SO:SO-6L_10x3.84mm_P1.27mm +Package_SO:SO-6_4.4x3.6mm_P1.27mm +Package_SO:SO-8_3.9x4.9mm_P1.27mm +Package_SO:SOIC-10_3.9x4.9mm_P1mm +Package_SO:SOIC-14-16_3.9x9.9mm_P1.27mm +Package_SO:SOIC-14W_7.5x9mm_P1.27mm +Package_SO:SOIC-14_3.9x8.7mm_P1.27mm +Package_SO:SOIC-16W-12_7.5x10.3mm_P1.27mm +Package_SO:SOIC-16W_5.3x10.2mm_P1.27mm +Package_SO:SOIC-16W_7.5x10.3mm_P1.27mm +Package_SO:SOIC-16W_7.5x12.8mm_P1.27mm +Package_SO:SOIC-16_3.9x9.9mm_P1.27mm +Package_SO:SOIC-16_4.55x10.3mm_P1.27mm +Package_SO:SOIC-18W_7.5x11.6mm_P1.27mm +Package_SO:SOIC-20W_7.5x12.8mm_P1.27mm +Package_SO:SOIC-20W_7.5x15.4mm_P1.27mm +Package_SO:SOIC-24W_7.5x15.4mm_P1.27mm +Package_SO:SOIC-28W_7.5x17.9mm_P1.27mm +Package_SO:SOIC-28W_7.5x18.7mm_P1.27mm +Package_SO:SOIC-32_7.518x20.777mm_P1.27mm +Package_SO:SOIC-4_4.55x2.6mm_P1.27mm +Package_SO:SOIC-4_4.55x3.7mm_P2.54mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.29x3mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.29x3mm_ThermalVias +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.3mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.3mm_ThermalVias +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.81mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.81mm_ThermalVias +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.514x3.2mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.514x3.2mm_ThermalVias +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.62x3.51mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.62x3.51mm_ThermalVias +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.71x3.4mm +Package_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.71x3.4mm_ThermalVias +Package_SO:SOIC-8-N7_3.9x4.9mm_P1.27mm +Package_SO:SOIC-8_3.9x4.9mm_P1.27mm +Package_SO:SOIC-8_5.3x5.3mm_P1.27mm +Package_SO:SOIC-8_5.3x6.2mm_P1.27mm +Package_SO:SOIC-8_7.5x5.85mm_P1.27mm +Package_SO:SOJ-24_7.62x15.875mm_P1.27mm +Package_SO:SOJ-28_10.16x18.415mm_P1.27mm +Package_SO:SOJ-28_7.62x18.415mm_P1.27mm +Package_SO:SOJ-32_10.16x20.955mm_P1.27mm +Package_SO:SOJ-32_7.62x20.955mm_P1.27mm +Package_SO:SOJ-36_10.16x23.495mm_P1.27mm +Package_SO:SOJ-44_10.16x28.575mm_P1.27mm +Package_SO:SOP-16_3.9x9.9mm_P1.27mm +Package_SO:SOP-16_4.4x10.4mm_P1.27mm +Package_SO:SOP-16_4.55x10.3mm_P1.27mm +Package_SO:SOP-18_7.495x11.515mm_P1.27mm +Package_SO:SOP-18_7x12.5mm_P1.27mm +Package_SO:SOP-20_7.5x12.8mm_P1.27mm +Package_SO:SOP-24_7.5x15.4mm_P1.27mm +Package_SO:SOP-28_8.4x18.16mm_P1.27mm +Package_SO:SOP-32_11.305x20.495mm_P1.27mm +Package_SO:SOP-44_12.6x28.5mm_P1.27mm +Package_SO:SOP-44_13.3x28.2mm_P1.27mm +Package_SO:SOP-4_3.8x4.1mm_P2.54mm +Package_SO:SOP-4_4.4x2.6mm_P1.27mm +Package_SO:SOP-4_7.5x4.1mm_P2.54mm +Package_SO:SOP-8-1EP_4.57x4.57mm_P1.27mm_EP4.57x4.45mm +Package_SO:SOP-8-1EP_4.57x4.57mm_P1.27mm_EP4.57x4.45mm_ThermalVias +Package_SO:SOP-8_3.76x4.96mm_P1.27mm +Package_SO:SOP-8_3.9x4.9mm_P1.27mm +Package_SO:SOP-8_6.605x9.655mm_P2.54mm +Package_SO:SOP-8_6.62x9.15mm_P2.54mm +Package_SO:SSO-4_6.7x5.1mm_P2.54mm_Clearance8mm +Package_SO:SSO-6_6.8x4.6mm_P1.27mm_Clearance7mm +Package_SO:SSO-6_6.8x4.6mm_P1.27mm_Clearance8mm +Package_SO:SSO-7-8_6.4x9.78mm_P2.54mm +Package_SO:SSO-8-7_6.4x9.7mm_P2.54mm +Package_SO:SSO-8_13.6x6.3mm_P1.27mm_Clearance14.2mm +Package_SO:SSO-8_6.7x9.8mm_P2.54mm_Clearance8mm +Package_SO:SSO-8_6.8x5.9mm_P1.27mm_Clearance7mm +Package_SO:SSO-8_6.8x5.9mm_P1.27mm_Clearance8mm +Package_SO:SSO-8_9.6x6.3mm_P1.27mm_Clearance10.5mm +Package_SO:SSOP-10-1EP_3.9x4.9mm_P1mm_EP2.1x3.3mm +Package_SO:SSOP-10-1EP_3.9x4.9mm_P1mm_EP2.1x3.3mm_ThermalVias +Package_SO:SSOP-10_3.9x4.9mm_P1.00mm +Package_SO:SSOP-14_5.3x6.2mm_P0.65mm +Package_SO:SSOP-16_3.9x4.9mm_P0.635mm +Package_SO:SSOP-16_4.4x5.2mm_P0.65mm +Package_SO:SSOP-16_5.3x6.2mm_P0.65mm +Package_SO:SSOP-18_4.4x6.5mm_P0.65mm +Package_SO:SSOP-20_3.9x8.7mm_P0.635mm +Package_SO:SSOP-20_4.4x6.5mm_P0.65mm +Package_SO:SSOP-20_5.3x7.2mm_P0.65mm +Package_SO:SSOP-24_3.9x8.7mm_P0.635mm +Package_SO:SSOP-24_5.3x8.2mm_P0.65mm +Package_SO:SSOP-28_3.9x9.9mm_P0.635mm +Package_SO:SSOP-28_5.3x10.2mm_P0.65mm +Package_SO:SSOP-44_5.3x12.8mm_P0.5mm +Package_SO:SSOP-48_5.3x12.8mm_P0.5mm +Package_SO:SSOP-48_7.5x15.9mm_P0.635mm +Package_SO:SSOP-4_4.4x2.6mm_P1.27mm +Package_SO:SSOP-56_7.5x18.5mm_P0.635mm +Package_SO:SSOP-8_2.95x2.8mm_P0.65mm +Package_SO:SSOP-8_3.95x5.21x3.27mm_P1.27mm +Package_SO:SSOP-8_3.9x5.05mm_P1.27mm +Package_SO:SSOP-8_5.25x5.24mm_P1.27mm +Package_SO:STC_SOP-16_3.9x9.9mm_P1.27mm +Package_SO:ST_MultiPowerSO-30 +Package_SO:ST_PowerSSO-24_SlugDown +Package_SO:ST_PowerSSO-24_SlugDown_ThermalVias +Package_SO:ST_PowerSSO-24_SlugUp +Package_SO:ST_PowerSSO-36_SlugDown +Package_SO:ST_PowerSSO-36_SlugDown_ThermalVias +Package_SO:ST_PowerSSO-36_SlugUp +Package_SO:Texas_DAD0032A_HTSSOP-32_6.1x11mm_P0.65mm_TopEP3.71x3.81mm +Package_SO:Texas_DGN0008B_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x3mm_Mask1.88x1.98mm +Package_SO:Texas_DGN0008B_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x3mm_Mask1.88x1.98mm_ThermalVias +Package_SO:Texas_DGN0008D_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.57x1.89mm +Package_SO:Texas_DGN0008D_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.57x1.89mm_ThermalVias +Package_SO:Texas_DGN0008G_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.846x2.15mm +Package_SO:Texas_DGN0008G_VSSOP-8-1EP_3x3mm_P0.65mm_EP2x2.94mm_Mask1.846x2.15mm_ThermalVias +Package_SO:Texas_DKD0036A_HSSOP-36_11x15.9mm_P0.65mm_TopEP5.85x12.65mm +Package_SO:Texas_DYY0016A_TSOT-23-16_4.2x2.0mm_P0.5mm +Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm +Package_SO:Texas_HSOP-8-1EP_3.9x4.9mm_P1.27mm_ThermalVias +Package_SO:Texas_HTSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias +Package_SO:Texas_PW0020A_TSSOP-20_4.4x6.5mm_P0.65mm +Package_SO:Texas_PWP0020A +Package_SO:Texas_PWP0028V_TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask2.94x5.62mm +Package_SO:Texas_PWP0028V_TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask2.94x5.62mm_ThermalVias +Package_SO:Texas_R-PDSO-G8_EP2.95x4.9mm_Mask2.4x3.1mm +Package_SO:Texas_R-PDSO-G8_EP2.95x4.9mm_Mask2.4x3.1mm_ThermalVias +Package_SO:Texas_S-PDSO-G8_3x3mm_P0.65mm +Package_SO:TI_SO-PowerPAD-8 +Package_SO:TI_SO-PowerPAD-8_ThermalVias +Package_SO:TSOP-5_1.65x3.05mm_P0.95mm +Package_SO:TSOP-6_1.65x3.05mm_P0.95mm +Package_SO:TSOP-I-24_12.4x6mm_P0.5mm +Package_SO:TSOP-I-24_14.4x6mm_P0.5mm +Package_SO:TSOP-I-24_16.4x6mm_P0.5mm +Package_SO:TSOP-I-24_18.4x6mm_P0.5mm +Package_SO:TSOP-I-28_11.8x8mm_P0.55mm +Package_SO:TSOP-I-32_11.8x8mm_P0.5mm +Package_SO:TSOP-I-32_12.4x8mm_P0.5mm +Package_SO:TSOP-I-32_14.4x8mm_P0.5mm +Package_SO:TSOP-I-32_16.4x8mm_P0.5mm +Package_SO:TSOP-I-32_18.4x8mm_P0.5mm +Package_SO:TSOP-I-32_18.4x8mm_P0.5mm_Reverse +Package_SO:TSOP-I-40_12.4x10mm_P0.5mm +Package_SO:TSOP-I-40_14.4x10mm_P0.5mm +Package_SO:TSOP-I-40_16.4x10mm_P0.5mm +Package_SO:TSOP-I-40_18.4x10mm_P0.5mm +Package_SO:TSOP-I-48_12.4x12mm_P0.5mm +Package_SO:TSOP-I-48_14.4x12mm_P0.5mm +Package_SO:TSOP-I-48_16.4x12mm_P0.5mm +Package_SO:TSOP-I-48_18.4x12mm_P0.5mm +Package_SO:TSOP-I-56_14.4x14mm_P0.5mm +Package_SO:TSOP-I-56_16.4x14mm_P0.5mm +Package_SO:TSOP-I-56_18.4x14mm_P0.5mm +Package_SO:TSOP-II-32_21.0x10.2mm_P1.27mm +Package_SO:TSOP-II-40-44_10.16x18.37mm_P0.8mm +Package_SO:TSOP-II-44_10.16x18.41mm_P0.8mm +Package_SO:TSOP-II-54_22.2x10.16mm_P0.8mm +Package_SO:TSSOP-100_6.1x20.8mm_P0.4mm +Package_SO:TSSOP-10_3x3mm_P0.5mm +Package_SO:TSSOP-14-1EP_4.4x5mm_P0.65mm +Package_SO:TSSOP-14_4.4x3.6mm_P0.4mm +Package_SO:TSSOP-14_4.4x5mm_P0.65mm +Package_SO:TSSOP-16-1EP_4.4x5mm_P0.65mm +Package_SO:TSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm +Package_SO:TSSOP-16-1EP_4.4x5mm_P0.65mm_EP3x3mm_ThermalVias +Package_SO:TSSOP-16_4.4x3.6mm_P0.4mm +Package_SO:TSSOP-16_4.4x5mm_P0.65mm +Package_SO:TSSOP-20-1EP_4.4x6.5mm_P0.65mm_EP2.15x3.35mm +Package_SO:TSSOP-20_4.4x5mm_P0.4mm +Package_SO:TSSOP-20_4.4x5mm_P0.5mm +Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm +Package_SO:TSSOP-24-1EP_4.4x7.8mm_P0.65mm_EP3.2x5mm +Package_SO:TSSOP-24_4.4x5mm_P0.4mm +Package_SO:TSSOP-24_4.4x6.5mm_P0.5mm +Package_SO:TSSOP-24_4.4x7.8mm_P0.65mm +Package_SO:TSSOP-24_6.1x7.8mm_P0.65mm +Package_SO:TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.74x4.75mm +Package_SO:TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.74x4.75mm_ThermalVias +Package_SO:TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP2.85x6.7mm +Package_SO:TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.05x7.56mm +Package_SO:TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.05x7.56mm_ThermalVias +Package_SO:TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask3.1x4.05mm +Package_SO:TSSOP-28-1EP_4.4x9.7mm_P0.65mm_EP3.4x9.7mm_Mask3.1x4.05mm_ThermalVias +Package_SO:TSSOP-28_4.4x7.8mm_P0.5mm +Package_SO:TSSOP-28_4.4x9.7mm_P0.65mm +Package_SO:TSSOP-28_6.1x7.8mm_P0.5mm +Package_SO:TSSOP-28_6.1x9.7mm_P0.65mm +Package_SO:TSSOP-28_8x9.7mm_P0.65mm +Package_SO:TSSOP-30_4.4x7.8mm_P0.5mm +Package_SO:TSSOP-30_6.1x9.7mm_P0.65mm +Package_SO:TSSOP-32_4.4x6.5mm_P0.4mm +Package_SO:TSSOP-32_6.1x11mm_P0.65mm +Package_SO:TSSOP-32_8x11mm_P0.65mm +Package_SO:TSSOP-36_4.4x7.8mm_P0.4mm +Package_SO:TSSOP-36_4.4x9.7mm_P0.5mm +Package_SO:TSSOP-36_6.1x12.5mm_P0.65mm +Package_SO:TSSOP-36_6.1x7.8mm_P0.4mm +Package_SO:TSSOP-36_6.1x9.7mm_P0.5mm +Package_SO:TSSOP-36_8x12.5mm_P0.65mm +Package_SO:TSSOP-36_8x9.7mm_P0.5mm +Package_SO:TSSOP-38_4.4x9.7mm_P0.5mm +Package_SO:TSSOP-38_6.1x12.5mm_P0.65mm +Package_SO:TSSOP-40_6.1x11mm_P0.5mm +Package_SO:TSSOP-40_6.1x14mm_P0.65mm +Package_SO:TSSOP-40_8x11mm_P0.5mm +Package_SO:TSSOP-40_8x14mm_P0.65mm +Package_SO:TSSOP-44_4.4x11.2mm_P0.5mm +Package_SO:TSSOP-44_4.4x11mm_P0.5mm +Package_SO:TSSOP-44_6.1x11mm_P0.5mm +Package_SO:TSSOP-48_4.4x9.7mm_P0.4mm +Package_SO:TSSOP-48_6.1x12.5mm_P0.5mm +Package_SO:TSSOP-48_6.1x9.7mm_P0.4mm +Package_SO:TSSOP-48_8x12.5mm_P0.5mm +Package_SO:TSSOP-48_8x9.7mm_P0.4mm +Package_SO:TSSOP-4_4.4x5mm_P4mm +Package_SO:TSSOP-50_4.4x12.5mm_P0.5mm +Package_SO:TSSOP-52_6.1x11mm_P0.4mm +Package_SO:TSSOP-52_8x11mm_P0.4mm +Package_SO:TSSOP-56_4.4x11.3mm_P0.4mm +Package_SO:TSSOP-56_6.1x12.5mm_P0.4mm +Package_SO:TSSOP-56_6.1x14mm_P0.5mm +Package_SO:TSSOP-56_8x12.5mm_P0.4mm +Package_SO:TSSOP-56_8x14mm_P0.5mm +Package_SO:TSSOP-60_8x12.5mm_P0.4mm +Package_SO:TSSOP-64_6.1x14mm_P0.4mm +Package_SO:TSSOP-64_6.1x17mm_P0.5mm +Package_SO:TSSOP-64_8x14mm_P0.4mm +Package_SO:TSSOP-68_8x14mm_P0.4mm +Package_SO:TSSOP-80_6.1x17mm_P0.4mm +Package_SO:TSSOP-8_3x3mm_P0.65mm +Package_SO:TSSOP-8_4.4x3mm_P0.65mm +Package_SO:Vishay_PowerPAK_1212-8_Dual +Package_SO:Vishay_PowerPAK_1212-8_Single +Package_SO:VSO-40_7.6x15.4mm_P0.762mm +Package_SO:VSO-56_11.1x21.5mm_P0.75mm +Package_SO:VSSOP-10_3x3mm_P0.5mm +Package_SO:VSSOP-8_2.3x2mm_P0.5mm +Package_SO:VSSOP-8_3x3mm_P0.65mm +Package_SO:Zetex_SM8 +Package_SON:Diodes_PowerDI3333-8 +Package_SON:Diodes_PowerDI3333-8_UXC_3.3x3.3mm_P0.65mm +Package_SON:EPSON_CE-USON-10_USON-10_3.2x2.5mm_P0.7mm +Package_SON:Fairchild_DualPower33-6_3x3mm +Package_SON:Fairchild_MicroPak-6_1.0x1.45mm_P0.5mm +Package_SON:Fairchild_MicroPak2-6_1.0x1.0mm_P0.35mm +Package_SON:HUSON-3-1EP_2x2mm_P1.3mm_EP1.1x1.6mm +Package_SON:HVSON-8-1EP_3x3mm_P0.65mm_EP1.6x2.4mm +Package_SON:HVSON-8-1EP_4x4mm_P0.8mm_EP2.2x3.1mm +Package_SON:Infineon_PG-LSON-8-1 +Package_SON:Infineon_PG-TDSON-8_6.15x5.15mm +Package_SON:Infineon_PG-TISON-8-2 +Package_SON:Infineon_PG-TISON-8-3 +Package_SON:Infineon_PG-TISON-8-4 +Package_SON:Infineon_PG-TISON-8-5 +Package_SON:MicroCrystal_C7_SON-8_1.5x3.2mm_P0.9mm +Package_SON:Nexperia_HUSON-12_USON-12-1EP_1.35x2.5mm_P0.4mm_EP0.4x2mm +Package_SON:Nexperia_HUSON-16_USON-16-1EP_1.35x3.3mm_P0.4mm_EP0.4x2.8mm +Package_SON:Nexperia_HUSON-8_USON-8-1EP_1.35x1.7mm_P0.4mm_EP0.4x1.2mm +Package_SON:NXP_XSON-16 +Package_SON:ROHM_VML0806 +Package_SON:RTC_SMD_MicroCrystal_C3_2.5x3.7mm +Package_SON:SON-8-1EP_3x2mm_P0.5mm_EP1.4x1.6mm +Package_SON:ST_PowerFLAT-6L_5x6mm_P1.27mm +Package_SON:ST_PowerFLAT_HV-5_8x8mm +Package_SON:ST_PowerFLAT_HV-8_5x6mm +Package_SON:Texas_DPY0002A_0.6x1mm_P0.65mm +Package_SON:Texas_DQK +Package_SON:Texas_DQX002A +Package_SON:Texas_DRC0010J +Package_SON:Texas_DRC0010J_ThermalVias +Package_SON:Texas_DRX_WSON-10_2.5x2.5mm_P0.5mm +Package_SON:Texas_DSC0010J +Package_SON:Texas_DSC0010J_ThermalVias +Package_SON:Texas_PWSON-N6 +Package_SON:Texas_R-PUSON-N14 +Package_SON:Texas_R-PUSON-N8_USON-8-1EP_1.6x2.1mm_P0.5mm_EP0.4x1.7mm +Package_SON:Texas_R-PWSON-N12_EP0.4x2mm +Package_SON:Texas_S-PDSO-N12 +Package_SON:Texas_S-PVSON-N10 +Package_SON:Texas_S-PVSON-N10_ThermalVias +Package_SON:Texas_S-PVSON-N8 +Package_SON:Texas_S-PVSON-N8_ThermalVias +Package_SON:Texas_S-PWSON-N10 +Package_SON:Texas_S-PWSON-N10_ThermalVias +Package_SON:Texas_S-PWSON-N8_EP1.2x2mm +Package_SON:Texas_S-PWSON-N8_EP1.2x2mm_ThermalVias +Package_SON:Texas_USON-6_1x1.45mm_P0.5mm_SMD +Package_SON:Texas_VSON-HR-8_1.5x2mm_P0.5mm +Package_SON:Texas_X2SON-4_1x1mm_P0.65mm +Package_SON:Texas_X2SON-5_0.8x0.8mm_P0.48mm +Package_SON:Texas_X2SON-5_0.8x0.8mm_P0.48mm_RoutingVia +Package_SON:USON-10_2.5x1.0mm_P0.5mm +Package_SON:USON-20_2x4mm_P0.4mm +Package_SON:VSON-10-1EP_3x3mm_P0.5mm_EP1.2x2mm +Package_SON:VSON-10-1EP_3x3mm_P0.5mm_EP1.2x2mm_ThermalVias +Package_SON:VSON-10-1EP_3x3mm_P0.5mm_EP1.65x2.4mm +Package_SON:VSON-10-1EP_3x3mm_P0.5mm_EP1.65x2.4mm_ThermalVias +Package_SON:VSON-14-1EP_3x4.45mm_P0.65mm_EP1.6x4.2mm +Package_SON:VSON-14-1EP_3x4.45mm_P0.65mm_EP1.6x4.2mm_ThermalVias +Package_SON:VSON-8-1EP_3x3mm_P0.65mm_EP1.65x2.4mm +Package_SON:VSON-8-1EP_3x3mm_P0.65mm_EP1.65x2.4mm_ThermalVias +Package_SON:VSON-8-1EP_3x3mm_P0.65mm_EP1.6x2.4mm +Package_SON:VSON-8_1.5x2mm_P0.5mm +Package_SON:VSON-8_3.3x3.3mm_P0.65mm_NexFET +Package_SON:VSONP-8-1EP_5x6_P1.27mm +Package_SON:Winbond_USON-8-1EP_3x2mm_P0.5mm_EP0.2x1.6mm +Package_SON:Winbond_USON-8-2EP_3x4mm_P0.8mm_EP0.2x0.8mm +Package_SON:WSON-10-1EP_2.5x2.5mm_P0.5mm_EP1.2x2mm +Package_SON:WSON-10-1EP_2.5x2.5mm_P0.5mm_EP1.2x2mm_ThermalVias +Package_SON:WSON-10-1EP_2x3mm_P0.5mm_EP0.84x2.4mm +Package_SON:WSON-10-1EP_2x3mm_P0.5mm_EP0.84x2.4mm_ThermalVias +Package_SON:WSON-10-1EP_4x3mm_P0.5mm_EP2.2x2mm +Package_SON:WSON-10-1EP_4x4mm_P0.8mm_EP2.6x3mm +Package_SON:WSON-10-1EP_4x4mm_P0.8mm_EP2.6x3mm_ThermalVias +Package_SON:WSON-12-1EP_3x2mm_P0.5mm_EP1x2.65 +Package_SON:WSON-12-1EP_3x2mm_P0.5mm_EP1x2.65_ThermalVias +Package_SON:WSON-12-1EP_3x3mm_P0.5mm_EP1.5x2.5mm +Package_SON:WSON-12-1EP_3x3mm_P0.5mm_EP1.5x2.5mm_ThermalVias +Package_SON:WSON-12-1EP_4x4mm_P0.5mm_EP2.6x3mm +Package_SON:WSON-12-1EP_4x4mm_P0.5mm_EP2.6x3mm_ThermalVias +Package_SON:WSON-14-1EP_4.0x4.0mm_P0.5mm_EP2.6x2.6mm +Package_SON:WSON-16_3.3x1.35_P0.4mm +Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm +Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm_ThermalVias +Package_SON:WSON-6-1EP_3x3mm_P0.95mm +Package_SON:WSON-6_1.5x1.5mm_P0.5mm +Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm +Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm_ThermalVias +Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack +Package_SON:WSON-8-1EP_3x2.5mm_P0.5mm_EP1.2x1.5mm_PullBack_ThermalVias +Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm +Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.2x2mm_ThermalVias +Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.45x2.4mm +Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.45x2.4mm_ThermalVias +Package_SON:WSON-8-1EP_3x3mm_P0.5mm_EP1.6x2.0mm +Package_SON:WSON-8-1EP_4x4mm_P0.8mm_EP1.98x3mm +Package_SON:WSON-8-1EP_4x4mm_P0.8mm_EP1.98x3mm_ThermalVias +Package_SON:WSON-8-1EP_4x4mm_P0.8mm_EP2.2x3mm +Package_SON:WSON-8-1EP_4x4mm_P0.8mm_EP2.2x3mm_ThermalVias +Package_SON:WSON-8-1EP_4x4mm_P0.8mm_EP2.6x3mm +Package_SON:WSON-8-1EP_4x4mm_P0.8mm_EP2.6x3mm_ThermalVias +Package_SON:WSON-8-1EP_6x5mm_P1.27mm_EP3.4x4.3mm +Package_SON:WSON-8-1EP_6x5mm_P1.27mm_EP3.4x4mm +Package_SON:WSON-8-1EP_8x6mm_P1.27mm_EP3.4x4.3mm +Package_SON:X2SON-8_1.4x1mm_P0.35mm +Package_SO_J-Lead:TSOC-6_3.76x3.94mm_P1.27mm +Package_TO_SOT_SMD:Analog_KS-4 +Package_TO_SOT_SMD:ATPAK-2 +Package_TO_SOT_SMD:Diodes_SOT-553 +Package_TO_SOT_SMD:HVSOF5 +Package_TO_SOT_SMD:HVSOF6 +Package_TO_SOT_SMD:Infineon_PG-HDSOP-10-1 +Package_TO_SOT_SMD:Infineon_PG-HSOF-8-1 +Package_TO_SOT_SMD:Infineon_PG-HSOF-8-1_ThermalVias +Package_TO_SOT_SMD:Infineon_PG-HSOF-8-2 +Package_TO_SOT_SMD:Infineon_PG-HSOF-8-2_ThermalVias +Package_TO_SOT_SMD:Infineon_PG-HSOF-8-2_ThermalVias2 +Package_TO_SOT_SMD:Infineon_PG-HSOF-8-3 +Package_TO_SOT_SMD:Infineon_PG-HSOF-8-3_ThermalVias +Package_TO_SOT_SMD:Infineon_PG-TO-220-7Lead_TabPin8 +Package_TO_SOT_SMD:Infineon_PG-TSFP-3-1 +Package_TO_SOT_SMD:LFPAK33 +Package_TO_SOT_SMD:LFPAK56 +Package_TO_SOT_SMD:LFPAK88 +Package_TO_SOT_SMD:Nexperia_CFP15_SOT-1289 +Package_TO_SOT_SMD:OnSemi_ECH8 +Package_TO_SOT_SMD:PowerMacro_M234_NoHole +Package_TO_SOT_SMD:PowerMacro_M234_WithHole +Package_TO_SOT_SMD:PQFN_8x8 +Package_TO_SOT_SMD:Rohm_HRP7 +Package_TO_SOT_SMD:ROHM_SOT-457_ClockwisePinNumbering +Package_TO_SOT_SMD:SC-59 +Package_TO_SOT_SMD:SC-59_Handsoldering +Package_TO_SOT_SMD:SC-70-8 +Package_TO_SOT_SMD:SC-70-8_Handsoldering +Package_TO_SOT_SMD:SC-74-6_1.55x2.9mm_P0.95mm +Package_TO_SOT_SMD:SC-74A-5_1.55x2.9mm_P0.95mm +Package_TO_SOT_SMD:SC-82AA +Package_TO_SOT_SMD:SC-82AA_Handsoldering +Package_TO_SOT_SMD:SC-82AB +Package_TO_SOT_SMD:SC-82AB_Handsoldering +Package_TO_SOT_SMD:SOT-1123 +Package_TO_SOT_SMD:SOT-1333-1 +Package_TO_SOT_SMD:SOT-1334-1 +Package_TO_SOT_SMD:SOT-143 +Package_TO_SOT_SMD:SOT-143R +Package_TO_SOT_SMD:SOT-143R_Handsoldering +Package_TO_SOT_SMD:SOT-143_Handsoldering +Package_TO_SOT_SMD:SOT-223-3_TabPin2 +Package_TO_SOT_SMD:SOT-223-5 +Package_TO_SOT_SMD:SOT-223-6 +Package_TO_SOT_SMD:SOT-223-6_TabPin3 +Package_TO_SOT_SMD:SOT-223-8 +Package_TO_SOT_SMD:SOT-223 +Package_TO_SOT_SMD:SOT-23-3 +Package_TO_SOT_SMD:SOT-23-5 +Package_TO_SOT_SMD:SOT-23-5_HandSoldering +Package_TO_SOT_SMD:SOT-23-6 +Package_TO_SOT_SMD:SOT-23-6_Handsoldering +Package_TO_SOT_SMD:SOT-23-8 +Package_TO_SOT_SMD:SOT-23-8_Handsoldering +Package_TO_SOT_SMD:SOT-23 +Package_TO_SOT_SMD:SOT-23W +Package_TO_SOT_SMD:SOT-23W_Handsoldering +Package_TO_SOT_SMD:SOT-23_Handsoldering +Package_TO_SOT_SMD:SOT-323_SC-70 +Package_TO_SOT_SMD:SOT-323_SC-70_Handsoldering +Package_TO_SOT_SMD:SOT-343_SC-70-4 +Package_TO_SOT_SMD:SOT-343_SC-70-4_Handsoldering +Package_TO_SOT_SMD:SOT-353_SC-70-5 +Package_TO_SOT_SMD:SOT-353_SC-70-5_Handsoldering +Package_TO_SOT_SMD:SOT-363_SC-70-6 +Package_TO_SOT_SMD:SOT-363_SC-70-6_Handsoldering +Package_TO_SOT_SMD:SOT-383F +Package_TO_SOT_SMD:SOT-383FL +Package_TO_SOT_SMD:SOT-416 +Package_TO_SOT_SMD:SOT-523 +Package_TO_SOT_SMD:SOT-543 +Package_TO_SOT_SMD:SOT-553 +Package_TO_SOT_SMD:SOT-563 +Package_TO_SOT_SMD:SOT-583-8 +Package_TO_SOT_SMD:SOT-665 +Package_TO_SOT_SMD:SOT-666 +Package_TO_SOT_SMD:SOT-723 +Package_TO_SOT_SMD:SOT-883 +Package_TO_SOT_SMD:SOT-886 +Package_TO_SOT_SMD:SOT-89-3 +Package_TO_SOT_SMD:SOT-89-3_Handsoldering +Package_TO_SOT_SMD:SOT-89-5 +Package_TO_SOT_SMD:SOT-89-5_Handsoldering +Package_TO_SOT_SMD:SOT-963 +Package_TO_SOT_SMD:SuperSOT-3 +Package_TO_SOT_SMD:SuperSOT-6 +Package_TO_SOT_SMD:SuperSOT-8 +Package_TO_SOT_SMD:TDSON-8-1 +Package_TO_SOT_SMD:Texas_DRT-3 +Package_TO_SOT_SMD:Texas_NDQ +Package_TO_SOT_SMD:Texas_NDW-7_TabPin4 +Package_TO_SOT_SMD:Texas_NDW-7_TabPin8 +Package_TO_SOT_SMD:Texas_NDY0011A +Package_TO_SOT_SMD:Texas_R-PDSO-G5_DCK-5 +Package_TO_SOT_SMD:Texas_R-PDSO-G6 +Package_TO_SOT_SMD:Texas_R-PDSO-N5_DRL-5 +Package_TO_SOT_SMD:Texas_R-PDSO-N6_DRL-6 +Package_TO_SOT_SMD:TO-252-2 +Package_TO_SOT_SMD:TO-252-2_TabPin1 +Package_TO_SOT_SMD:TO-252-3_TabPin2 +Package_TO_SOT_SMD:TO-252-3_TabPin4 +Package_TO_SOT_SMD:TO-252-4 +Package_TO_SOT_SMD:TO-252-5_TabPin3 +Package_TO_SOT_SMD:TO-252-5_TabPin6 +Package_TO_SOT_SMD:TO-263-2 +Package_TO_SOT_SMD:TO-263-2_TabPin1 +Package_TO_SOT_SMD:TO-263-3_TabPin2 +Package_TO_SOT_SMD:TO-263-3_TabPin4 +Package_TO_SOT_SMD:TO-263-4 +Package_TO_SOT_SMD:TO-263-5_TabPin3 +Package_TO_SOT_SMD:TO-263-5_TabPin6 +Package_TO_SOT_SMD:TO-263-6 +Package_TO_SOT_SMD:TO-263-7_TabPin4 +Package_TO_SOT_SMD:TO-263-7_TabPin8 +Package_TO_SOT_SMD:TO-263-9_TabPin10 +Package_TO_SOT_SMD:TO-263-9_TabPin5 +Package_TO_SOT_SMD:TO-268-2 +Package_TO_SOT_SMD:TO-269AA +Package_TO_SOT_SMD:TO-277A +Package_TO_SOT_SMD:TO-277B +Package_TO_SOT_SMD:TO-50-3_LongPad-NoHole_Housing +Package_TO_SOT_SMD:TO-50-3_LongPad-WithHole_Housing +Package_TO_SOT_SMD:TO-50-3_ShortPad-NoHole_Housing +Package_TO_SOT_SMD:TO-50-3_ShortPad-WithHole_Housing +Package_TO_SOT_SMD:TO-50-4_LongPad-NoHole_Housing +Package_TO_SOT_SMD:TO-50-4_LongPad-WithHole_Housing +Package_TO_SOT_SMD:TO-50-4_ShortPad-NoHole_Housing +Package_TO_SOT_SMD:TO-50-4_ShortPad-WithHole_Housing +Package_TO_SOT_SMD:TSOT-23-5 +Package_TO_SOT_SMD:TSOT-23-5_HandSoldering +Package_TO_SOT_SMD:TSOT-23-6 +Package_TO_SOT_SMD:TSOT-23-6_HandSoldering +Package_TO_SOT_SMD:TSOT-23-8 +Package_TO_SOT_SMD:TSOT-23-8_HandSoldering +Package_TO_SOT_SMD:TSOT-23 +Package_TO_SOT_SMD:TSOT-23_HandSoldering +Package_TO_SOT_SMD:Vishay_PowerPAK_SC70-6L_Dual +Package_TO_SOT_SMD:Vishay_PowerPAK_SC70-6L_Single +Package_TO_SOT_SMD:VSOF5 +Package_TO_SOT_THT:Analog_TO-46-4_ThermalShield +Package_TO_SOT_THT:Fairchild_TO-220F-6L +Package_TO_SOT_THT:Heraeus_TO-92-2 +Package_TO_SOT_THT:NEC_Molded_7x4x9mm +Package_TO_SOT_THT:PowerIntegrations_TO-220-7C +Package_TO_SOT_THT:SIPAK-1EP_Horizontal_TabDown +Package_TO_SOT_THT:SIPAK_Vertical +Package_TO_SOT_THT:SOD-70_P2.54mm +Package_TO_SOT_THT:SOD-70_P5.08mm +Package_TO_SOT_THT:SOT-227 +Package_TO_SOT_THT:TO-100-10 +Package_TO_SOT_THT:TO-100-10_Window +Package_TO_SOT_THT:TO-11-2 +Package_TO_SOT_THT:TO-11-2_Window +Package_TO_SOT_THT:TO-11-3 +Package_TO_SOT_THT:TO-11-3_Window +Package_TO_SOT_THT:TO-12-4 +Package_TO_SOT_THT:TO-12-4_Window +Package_TO_SOT_THT:TO-126-2_Horizontal_TabDown +Package_TO_SOT_THT:TO-126-2_Horizontal_TabUp +Package_TO_SOT_THT:TO-126-2_Vertical +Package_TO_SOT_THT:TO-126-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-126-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-126-3_Vertical +Package_TO_SOT_THT:TO-17-4 +Package_TO_SOT_THT:TO-17-4_Window +Package_TO_SOT_THT:TO-18-2 +Package_TO_SOT_THT:TO-18-2_Lens +Package_TO_SOT_THT:TO-18-2_Window +Package_TO_SOT_THT:TO-18-3 +Package_TO_SOT_THT:TO-18-3_Lens +Package_TO_SOT_THT:TO-18-3_Window +Package_TO_SOT_THT:TO-18-4 +Package_TO_SOT_THT:TO-18-4_Lens +Package_TO_SOT_THT:TO-18-4_Window +Package_TO_SOT_THT:TO-218-2_Horizontal_TabDown +Package_TO_SOT_THT:TO-218-2_Horizontal_TabUp +Package_TO_SOT_THT:TO-218-2_Vertical +Package_TO_SOT_THT:TO-218-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-218-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-218-3_Vertical +Package_TO_SOT_THT:TO-220-11_P3.4x2.54mm_StaggerEven_Lead5.84mm_TabDown +Package_TO_SOT_THT:TO-220-11_P3.4x2.54mm_StaggerOdd_Lead5.84mm_TabDown +Package_TO_SOT_THT:TO-220-11_P3.4x5.08mm_StaggerEven_Lead4.58mm_Vertical +Package_TO_SOT_THT:TO-220-11_P3.4x5.08mm_StaggerOdd_Lead4.58mm_Vertical +Package_TO_SOT_THT:TO-220-11_P3.4x5.08mm_StaggerOdd_Lead8.45mm_TabDown +Package_TO_SOT_THT:TO-220-15_P2.54x2.54mm_StaggerEven_Lead5.84mm_TabDown +Package_TO_SOT_THT:TO-220-15_P2.54x2.54mm_StaggerOdd_Lead5.84mm_TabDown +Package_TO_SOT_THT:TO-220-15_P2.54x5.08mm_StaggerEven_Lead4.58mm_Vertical +Package_TO_SOT_THT:TO-220-15_P2.54x5.08mm_StaggerOdd_Lead4.58mm_Vertical +Package_TO_SOT_THT:TO-220-2_Horizontal_TabDown +Package_TO_SOT_THT:TO-220-2_Horizontal_TabUp +Package_TO_SOT_THT:TO-220-2_Vertical +Package_TO_SOT_THT:TO-220-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-220-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-220-3_Vertical +Package_TO_SOT_THT:TO-220-4_Horizontal_TabDown +Package_TO_SOT_THT:TO-220-4_Horizontal_TabUp +Package_TO_SOT_THT:TO-220-4_P5.08x3.7mm_StaggerEven_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-4_P5.08x3.7mm_StaggerOdd_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-4_P5.08x3.8mm_StaggerEven_Lead5.85mm_TabDown +Package_TO_SOT_THT:TO-220-4_P5.08x3.8mm_StaggerOdd_Lead5.85mm_TabDown +Package_TO_SOT_THT:TO-220-4_Vertical +Package_TO_SOT_THT:TO-220-5_Horizontal_TabDown +Package_TO_SOT_THT:TO-220-5_Horizontal_TabUp +Package_TO_SOT_THT:TO-220-5_P3.4x3.7mm_StaggerEven_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-5_P3.4x3.7mm_StaggerOdd_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-5_P3.4x3.8mm_StaggerEven_Lead7.13mm_TabDown +Package_TO_SOT_THT:TO-220-5_P3.4x3.8mm_StaggerOdd_Lead7.13mm_TabDown +Package_TO_SOT_THT:TO-220-5_Vertical +Package_TO_SOT_THT:TO-220-7_P2.54x3.7mm_StaggerEven_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-7_P2.54x3.7mm_StaggerOdd_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-7_P2.54x3.8mm_StaggerEven_Lead5.85mm_TabDown +Package_TO_SOT_THT:TO-220-7_P2.54x3.8mm_StaggerOdd_Lead5.85mm_TabDown +Package_TO_SOT_THT:TO-220-7_P2.54x5.08mm_StaggerOdd_Lead3.08mm_Vertical +Package_TO_SOT_THT:TO-220-7_P2.54x5.1mm_StaggerOdd_Lead8.025mm_TabDown +Package_TO_SOT_THT:TO-220-8_Vertical +Package_TO_SOT_THT:TO-220-9_P1.94x3.7mm_StaggerEven_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-9_P1.94x3.7mm_StaggerOdd_Lead3.8mm_Vertical +Package_TO_SOT_THT:TO-220-9_P1.94x3.8mm_StaggerEven_Lead5.85mm_TabDown +Package_TO_SOT_THT:TO-220-9_P1.94x3.8mm_StaggerOdd_Lead5.85mm_TabDown +Package_TO_SOT_THT:TO-220F-11_P3.4x5.08mm_StaggerEven_Lead5.08mm_Vertical +Package_TO_SOT_THT:TO-220F-11_P3.4x5.08mm_StaggerOdd_Lead5.08mm_Vertical +Package_TO_SOT_THT:TO-220F-15_P2.54x5.08mm_StaggerEven_Lead5.08mm_Vertical +Package_TO_SOT_THT:TO-220F-15_P2.54x5.08mm_StaggerOdd_Lead5.08mm_Vertical +Package_TO_SOT_THT:TO-220F-2_Horizontal_TabDown +Package_TO_SOT_THT:TO-220F-2_Horizontal_TabUp +Package_TO_SOT_THT:TO-220F-2_Vertical +Package_TO_SOT_THT:TO-220F-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-220F-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-220F-3_Vertical +Package_TO_SOT_THT:TO-220F-4_Horizontal_TabDown +Package_TO_SOT_THT:TO-220F-4_Horizontal_TabUp +Package_TO_SOT_THT:TO-220F-4_P5.08x2.05mm_StaggerEven_Lead1.85mm_Vertical +Package_TO_SOT_THT:TO-220F-4_P5.08x2.05mm_StaggerOdd_Lead1.85mm_Vertical +Package_TO_SOT_THT:TO-220F-4_P5.08x3.7mm_StaggerEven_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-220F-4_P5.08x3.7mm_StaggerOdd_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-220F-4_Vertical +Package_TO_SOT_THT:TO-220F-5_Horizontal_TabDown +Package_TO_SOT_THT:TO-220F-5_Horizontal_TabUp +Package_TO_SOT_THT:TO-220F-5_P3.4x2.06mm_StaggerEven_Lead1.86mm_Vertical +Package_TO_SOT_THT:TO-220F-5_P3.4x2.06mm_StaggerOdd_Lead1.86mm_Vertical +Package_TO_SOT_THT:TO-220F-5_P3.4x3.7mm_StaggerEven_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-220F-5_P3.4x3.7mm_StaggerOdd_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-220F-5_Vertical +Package_TO_SOT_THT:TO-220F-7_P2.54x3.7mm_StaggerEven_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-220F-7_P2.54x3.7mm_StaggerOdd_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-220F-9_P1.8x3.7mm_StaggerEven_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-220F-9_P1.8x3.7mm_StaggerOdd_Lead3.5mm_Vertical +Package_TO_SOT_THT:TO-247-2_Horizontal_TabDown +Package_TO_SOT_THT:TO-247-2_Horizontal_TabUp +Package_TO_SOT_THT:TO-247-2_Vertical +Package_TO_SOT_THT:TO-247-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-247-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-247-3_Vertical +Package_TO_SOT_THT:TO-247-4_Horizontal_TabDown +Package_TO_SOT_THT:TO-247-4_Horizontal_TabUp +Package_TO_SOT_THT:TO-247-4_Vertical +Package_TO_SOT_THT:TO-247-5_Horizontal_TabDown +Package_TO_SOT_THT:TO-247-5_Horizontal_TabUp +Package_TO_SOT_THT:TO-247-5_Vertical +Package_TO_SOT_THT:TO-251-2-1EP_Horizontal_TabDown +Package_TO_SOT_THT:TO-251-2_Vertical +Package_TO_SOT_THT:TO-251-3-1EP_Horizontal_TabDown +Package_TO_SOT_THT:TO-251-3_Vertical +Package_TO_SOT_THT:TO-262-3-1EP_Horizontal_TabDown +Package_TO_SOT_THT:TO-262-3_Vertical +Package_TO_SOT_THT:TO-262-5-1EP_Horizontal_TabDown +Package_TO_SOT_THT:TO-262-5_Vertical +Package_TO_SOT_THT:TO-264-2_Horizontal_TabDown +Package_TO_SOT_THT:TO-264-2_Horizontal_TabUp +Package_TO_SOT_THT:TO-264-2_Vertical +Package_TO_SOT_THT:TO-264-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-264-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-264-3_Vertical +Package_TO_SOT_THT:TO-264-5_Horizontal_TabDown +Package_TO_SOT_THT:TO-264-5_Horizontal_TabUp +Package_TO_SOT_THT:TO-264-5_Vertical +Package_TO_SOT_THT:TO-3 +Package_TO_SOT_THT:TO-33-4 +Package_TO_SOT_THT:TO-33-4_Window +Package_TO_SOT_THT:TO-38-2 +Package_TO_SOT_THT:TO-38-2_Window +Package_TO_SOT_THT:TO-38-3 +Package_TO_SOT_THT:TO-38-3_Window +Package_TO_SOT_THT:TO-39-10 +Package_TO_SOT_THT:TO-39-10_Window +Package_TO_SOT_THT:TO-39-2 +Package_TO_SOT_THT:TO-39-2_Window +Package_TO_SOT_THT:TO-39-3 +Package_TO_SOT_THT:TO-39-3_Window +Package_TO_SOT_THT:TO-39-4 +Package_TO_SOT_THT:TO-39-4_Window +Package_TO_SOT_THT:TO-39-6 +Package_TO_SOT_THT:TO-39-6_Window +Package_TO_SOT_THT:TO-39-8 +Package_TO_SOT_THT:TO-39-8_Window +Package_TO_SOT_THT:TO-3P-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-3P-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-3P-3_Vertical +Package_TO_SOT_THT:TO-3PB-3_Horizontal_TabDown +Package_TO_SOT_THT:TO-3PB-3_Horizontal_TabUp +Package_TO_SOT_THT:TO-3PB-3_Vertical +Package_TO_SOT_THT:TO-46-2 +Package_TO_SOT_THT:TO-46-2_Pin2Center +Package_TO_SOT_THT:TO-46-2_Pin2Center_Window +Package_TO_SOT_THT:TO-46-2_Window +Package_TO_SOT_THT:TO-46-3 +Package_TO_SOT_THT:TO-46-3_Pin2Center +Package_TO_SOT_THT:TO-46-3_Pin2Center_Window +Package_TO_SOT_THT:TO-46-3_Window +Package_TO_SOT_THT:TO-46-4 +Package_TO_SOT_THT:TO-46-4_Window +Package_TO_SOT_THT:TO-5-10 +Package_TO_SOT_THT:TO-5-10_Window +Package_TO_SOT_THT:TO-5-2 +Package_TO_SOT_THT:TO-5-2_Window +Package_TO_SOT_THT:TO-5-3 +Package_TO_SOT_THT:TO-5-3_Window +Package_TO_SOT_THT:TO-5-4 +Package_TO_SOT_THT:TO-5-4_Window +Package_TO_SOT_THT:TO-5-6 +Package_TO_SOT_THT:TO-5-6_Window +Package_TO_SOT_THT:TO-5-8 +Package_TO_SOT_THT:TO-5-8_PD5.08 +Package_TO_SOT_THT:TO-5-8_PD5.08_Window +Package_TO_SOT_THT:TO-5-8_Window +Package_TO_SOT_THT:TO-52-2 +Package_TO_SOT_THT:TO-52-2_Window +Package_TO_SOT_THT:TO-52-3 +Package_TO_SOT_THT:TO-52-3_Window +Package_TO_SOT_THT:TO-72-4 +Package_TO_SOT_THT:TO-72-4_Window +Package_TO_SOT_THT:TO-75-6 +Package_TO_SOT_THT:TO-75-6_Window +Package_TO_SOT_THT:TO-78-10 +Package_TO_SOT_THT:TO-78-10_Window +Package_TO_SOT_THT:TO-78-6 +Package_TO_SOT_THT:TO-78-6_Window +Package_TO_SOT_THT:TO-78-8 +Package_TO_SOT_THT:TO-78-8_Window +Package_TO_SOT_THT:TO-8-2 +Package_TO_SOT_THT:TO-8-2_Window +Package_TO_SOT_THT:TO-8-3 +Package_TO_SOT_THT:TO-8-3_Window +Package_TO_SOT_THT:TO-92-2 +Package_TO_SOT_THT:TO-92-2_Horizontal1 +Package_TO_SOT_THT:TO-92-2_Horizontal2 +Package_TO_SOT_THT:TO-92-2_W4.0mm_Horizontal_FlatSideDown +Package_TO_SOT_THT:TO-92-2_W4.0mm_Horizontal_FlatSideUp +Package_TO_SOT_THT:TO-92-2_Wide +Package_TO_SOT_THT:TO-92 +Package_TO_SOT_THT:TO-92Flat +Package_TO_SOT_THT:TO-92L +Package_TO_SOT_THT:TO-92L_HandSolder +Package_TO_SOT_THT:TO-92L_Inline +Package_TO_SOT_THT:TO-92L_Inline_Wide +Package_TO_SOT_THT:TO-92L_Wide +Package_TO_SOT_THT:TO-92Mini-2 +Package_TO_SOT_THT:TO-92S-2 +Package_TO_SOT_THT:TO-92S +Package_TO_SOT_THT:TO-92S_Wide +Package_TO_SOT_THT:TO-92_HandSolder +Package_TO_SOT_THT:TO-92_Horizontal1 +Package_TO_SOT_THT:TO-92_Horizontal2 +Package_TO_SOT_THT:TO-92_Inline +Package_TO_SOT_THT:TO-92_Inline_Horizontal1 +Package_TO_SOT_THT:TO-92_Inline_Horizontal2 +Package_TO_SOT_THT:TO-92_Inline_W4.0mm_Horizontal_FlatSideDown +Package_TO_SOT_THT:TO-92_Inline_W4.0mm_Horizontal_FlatSideUp +Package_TO_SOT_THT:TO-92_Inline_Wide +Package_TO_SOT_THT:TO-92_W4.0mm_StaggerEven_Horizontal_FlatSideDown +Package_TO_SOT_THT:TO-92_W4.0mm_StaggerEven_Horizontal_FlatSideUp +Package_TO_SOT_THT:TO-92_Wide +Package_TO_SOT_THT:TO-99-6 +Package_TO_SOT_THT:TO-99-6_Window +Package_TO_SOT_THT:TO-99-8 +Package_TO_SOT_THT:TO-99-8_Window +Potentiometer_SMD:Potentiometer_ACP_CA14-VSMD_Vertical +Potentiometer_SMD:Potentiometer_ACP_CA14-VSMD_Vertical_Hole +Potentiometer_SMD:Potentiometer_ACP_CA6-VSMD_Vertical +Potentiometer_SMD:Potentiometer_ACP_CA6-VSMD_Vertical_Hole +Potentiometer_SMD:Potentiometer_ACP_CA9-VSMD_Vertical +Potentiometer_SMD:Potentiometer_ACP_CA9-VSMD_Vertical_Hole +Potentiometer_SMD:Potentiometer_Bourns_3214G_Horizontal +Potentiometer_SMD:Potentiometer_Bourns_3214J_Horizontal +Potentiometer_SMD:Potentiometer_Bourns_3214W_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3214X_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3224G_Horizontal +Potentiometer_SMD:Potentiometer_Bourns_3224J_Horizontal +Potentiometer_SMD:Potentiometer_Bourns_3224W_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3224X_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3269P_Horizontal +Potentiometer_SMD:Potentiometer_Bourns_3269W_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3269X_Horizontal +Potentiometer_SMD:Potentiometer_Bourns_3314G_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3314J_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3314R-1_Vertical_Hole +Potentiometer_SMD:Potentiometer_Bourns_3314R-GM5_Vertical +Potentiometer_SMD:Potentiometer_Bourns_3314S_Horizontal +Potentiometer_SMD:Potentiometer_Bourns_PRS11S_Vertical +Potentiometer_SMD:Potentiometer_Bourns_TC33X_Vertical +Potentiometer_SMD:Potentiometer_Vishay_TS53YJ_Vertical +Potentiometer_SMD:Potentiometer_Vishay_TS53YL_Vertical +Potentiometer_THT:Potentiometer_ACP_CA14-H2,5_Horizontal +Potentiometer_THT:Potentiometer_ACP_CA14-H4_Horizontal +Potentiometer_THT:Potentiometer_ACP_CA14-H5_Horizontal +Potentiometer_THT:Potentiometer_ACP_CA14V-15_Vertical +Potentiometer_THT:Potentiometer_ACP_CA14V-15_Vertical_Hole +Potentiometer_THT:Potentiometer_ACP_CA6-H2,5_Horizontal +Potentiometer_THT:Potentiometer_ACP_CA9-H2,5_Horizontal +Potentiometer_THT:Potentiometer_ACP_CA9-H3,8_Horizontal +Potentiometer_THT:Potentiometer_ACP_CA9-H5_Horizontal +Potentiometer_THT:Potentiometer_ACP_CA9-V10_Vertical +Potentiometer_THT:Potentiometer_ACP_CA9-V10_Vertical_Hole +Potentiometer_THT:Potentiometer_Alpha_RD901F-40-00D_Single_Vertical +Potentiometer_THT:Potentiometer_Alpha_RD901F-40-00D_Single_Vertical_CircularHoles +Potentiometer_THT:Potentiometer_Alpha_RD902F-40-00D_Dual_Vertical +Potentiometer_THT:Potentiometer_Alpha_RD902F-40-00D_Dual_Vertical_CircularHoles +Potentiometer_THT:Potentiometer_Alps_RK097_Dual_Horizontal +Potentiometer_THT:Potentiometer_Alps_RK097_Dual_Horizontal_Switch +Potentiometer_THT:Potentiometer_Alps_RK097_Single_Horizontal +Potentiometer_THT:Potentiometer_Alps_RK097_Single_Horizontal_Switch +Potentiometer_THT:Potentiometer_Alps_RK09K_Single_Horizontal +Potentiometer_THT:Potentiometer_Alps_RK09K_Single_Vertical +Potentiometer_THT:Potentiometer_Alps_RK09L_Double_Horizontal +Potentiometer_THT:Potentiometer_Alps_RK09L_Double_Vertical +Potentiometer_THT:Potentiometer_Alps_RK09L_Single_Horizontal +Potentiometer_THT:Potentiometer_Alps_RK09L_Single_Vertical +Potentiometer_THT:Potentiometer_Alps_RK09Y11_Single_Horizontal +Potentiometer_THT:Potentiometer_Alps_RK163_Dual_Horizontal +Potentiometer_THT:Potentiometer_Alps_RK163_Single_Horizontal +Potentiometer_THT:Potentiometer_Bourns_20P_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3005_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3006P_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3006W_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3006Y_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3009P_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3009Y_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3266P_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3266W_Vertical +Potentiometer_THT:Potentiometer_Bourns_3266X_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3266Y_Vertical +Potentiometer_THT:Potentiometer_Bourns_3266Z_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3296P_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3296W_Vertical +Potentiometer_THT:Potentiometer_Bourns_3296X_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3296Y_Vertical +Potentiometer_THT:Potentiometer_Bourns_3296Z_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3299P_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3299W_Vertical +Potentiometer_THT:Potentiometer_Bourns_3299X_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3299Y_Vertical +Potentiometer_THT:Potentiometer_Bourns_3299Z_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3339H_Vertical +Potentiometer_THT:Potentiometer_Bourns_3339P_Vertical +Potentiometer_THT:Potentiometer_Bourns_3339P_Vertical_HandSoldering +Potentiometer_THT:Potentiometer_Bourns_3339S_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3339W_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3386C_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3386F_Vertical +Potentiometer_THT:Potentiometer_Bourns_3386P_Vertical +Potentiometer_THT:Potentiometer_Bourns_3386W_Horizontal +Potentiometer_THT:Potentiometer_Bourns_3386X_Horizontal +Potentiometer_THT:Potentiometer_Bourns_PTA1543_Single_Slide +Potentiometer_THT:Potentiometer_Bourns_PTA2043_Single_Slide +Potentiometer_THT:Potentiometer_Bourns_PTA3043_Single_Slide +Potentiometer_THT:Potentiometer_Bourns_PTA4543_Single_Slide +Potentiometer_THT:Potentiometer_Bourns_PTA6043_Single_Slide +Potentiometer_THT:Potentiometer_Bourns_PTV09A-1_Single_Vertical +Potentiometer_THT:Potentiometer_Bourns_PTV09A-2_Single_Horizontal +Potentiometer_THT:Potentiometer_Bourns_PTV112-4_Dual_Vertical +Potentiometer_THT:Potentiometer_Omeg_PC16BU_Horizontal +Potentiometer_THT:Potentiometer_Omeg_PC16BU_Vertical +Potentiometer_THT:Potentiometer_Piher_PC-16_Dual_Horizontal +Potentiometer_THT:Potentiometer_Piher_PC-16_Single_Horizontal +Potentiometer_THT:Potentiometer_Piher_PC-16_Single_Vertical +Potentiometer_THT:Potentiometer_Piher_PC-16_Triple_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-10-H01_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-10-H05_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-10-V05_Vertical +Potentiometer_THT:Potentiometer_Piher_PT-10-V10_Vertical +Potentiometer_THT:Potentiometer_Piher_PT-10-V10_Vertical_Hole +Potentiometer_THT:Potentiometer_Piher_PT-15-H01_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-15-H05_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-15-H06_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-15-H25_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-15-V02_Vertical +Potentiometer_THT:Potentiometer_Piher_PT-15-V02_Vertical_Hole +Potentiometer_THT:Potentiometer_Piher_PT-15-V15_Vertical +Potentiometer_THT:Potentiometer_Piher_PT-15-V15_Vertical_Hole +Potentiometer_THT:Potentiometer_Piher_PT-6-H_Horizontal +Potentiometer_THT:Potentiometer_Piher_PT-6-V_Vertical +Potentiometer_THT:Potentiometer_Piher_PT-6-V_Vertical_Hole +Potentiometer_THT:Potentiometer_Piher_T-16H_Double_Horizontal +Potentiometer_THT:Potentiometer_Piher_T-16H_Single_Horizontal +Potentiometer_THT:Potentiometer_Piher_T-16L_Single_Vertical_Hole +Potentiometer_THT:Potentiometer_Runtron_RM-063_Horizontal +Potentiometer_THT:Potentiometer_Runtron_RM-065_Vertical +Potentiometer_THT:Potentiometer_TT_P0915N +Potentiometer_THT:Potentiometer_Vishay_148-149_Dual_Horizontal +Potentiometer_THT:Potentiometer_Vishay_148-149_Single_Horizontal +Potentiometer_THT:Potentiometer_Vishay_148-149_Single_Vertical +Potentiometer_THT:Potentiometer_Vishay_148E-149E_Dual_Horizontal +Potentiometer_THT:Potentiometer_Vishay_148E-149E_Single_Horizontal +Potentiometer_THT:Potentiometer_Vishay_248BH-249BH_Single_Horizontal +Potentiometer_THT:Potentiometer_Vishay_248GJ-249GJ_Single_Horizontal +Potentiometer_THT:Potentiometer_Vishay_248GJ-249GJ_Single_Vertical +Potentiometer_THT:Potentiometer_Vishay_43_Horizontal +Potentiometer_THT:Potentiometer_Vishay_T7-YA_Single_Vertical +Potentiometer_THT:Potentiometer_Vishay_T73XW_Horizontal +Potentiometer_THT:Potentiometer_Vishay_T73XX_Horizontal +Potentiometer_THT:Potentiometer_Vishay_T73YP_Vertical +Potentiometer_THT:Potentiometer_Vishay_T93XA_Horizontal +Potentiometer_THT:Potentiometer_Vishay_T93YA_Vertical +Relay_SMD:Relay_2P2T_10x6mm_TE_IMxxG +Relay_SMD:Relay_DPDT_AXICOM_IMSeries_JLeg +Relay_SMD:Relay_DPDT_FRT5_SMD +Relay_SMD:Relay_DPDT_Kemet_EE2_NU +Relay_SMD:Relay_DPDT_Kemet_EE2_NUH +Relay_SMD:Relay_DPDT_Kemet_EE2_NUH_DoubleCoil +Relay_SMD:Relay_DPDT_Kemet_EE2_NUX_DoubleCoil +Relay_SMD:Relay_DPDT_Kemet_EE2_NUX_NKX +Relay_SMD:Relay_DPDT_Kemet_EE2_NU_DoubleCoil +Relay_SMD:Relay_DPDT_Omron_G6H-2F +Relay_SMD:Relay_DPDT_Omron_G6K-2F-Y +Relay_SMD:Relay_DPDT_Omron_G6K-2F +Relay_SMD:Relay_DPDT_Omron_G6K-2G-Y +Relay_SMD:Relay_DPDT_Omron_G6K-2G +Relay_SMD:Relay_DPDT_Omron_G6S-2F +Relay_SMD:Relay_DPDT_Omron_G6S-2G +Relay_SMD:Relay_DPDT_Omron_G6SK-2F +Relay_SMD:Relay_DPDT_Omron_G6SK-2G +Relay_SMD:Relay_Fujitsu_FTR-B3S +Relay_SMD:Relay_SPDT_AXICOM_HF3Series_50ohms_Pitch1.27mm +Relay_SMD:Relay_SPDT_AXICOM_HF3Series_75ohms_Pitch1.27mm +Relay_THT:Relay_1-Form-A_Schrack-RYII_RM5mm +Relay_THT:Relay_1-Form-B_Schrack-RYII_RM5mm +Relay_THT:Relay_1-Form-C_Schrack-RYII_RM3.2mm +Relay_THT:Relay_3PST_COTO_3650 +Relay_THT:Relay_3PST_COTO_3660 +Relay_THT:Relay_DPDT_AXICOM_IMSeries_Pitch3.2mm +Relay_THT:Relay_DPDT_AXICOM_IMSeries_Pitch5.08mm +Relay_THT:Relay_DPDT_Finder_30.22 +Relay_THT:Relay_DPDT_Finder_40.52 +Relay_THT:Relay_DPDT_Finder_40.62 +Relay_THT:Relay_DPDT_FRT5 +Relay_THT:Relay_DPDT_Fujitsu_FTR-F1C +Relay_THT:Relay_DPDT_Hongfa_HF115F-2Z-x4 +Relay_THT:Relay_DPDT_Kemet_EC2_NJ +Relay_THT:Relay_DPDT_Kemet_EC2_NJ_DoubleCoil +Relay_THT:Relay_DPDT_Kemet_EC2_NU +Relay_THT:Relay_DPDT_Kemet_EC2_NU_DoubleCoil +Relay_THT:Relay_DPDT_Omron_G2RL-2 +Relay_THT:Relay_DPDT_Omron_G5V-2 +Relay_THT:Relay_DPDT_Omron_G6A +Relay_THT:Relay_DPDT_Omron_G6AK +Relay_THT:Relay_DPDT_Omron_G6H-2 +Relay_THT:Relay_DPDT_Omron_G6K-2P-Y +Relay_THT:Relay_DPDT_Omron_G6K-2P +Relay_THT:Relay_DPDT_Omron_G6S-2 +Relay_THT:Relay_DPDT_Omron_G6SK-2 +Relay_THT:Relay_DPDT_Panasonic_JW2 +Relay_THT:Relay_DPDT_Schrack-RT2-FormC-Dual-Coil_RM5mm +Relay_THT:Relay_DPDT_Schrack-RT2-FormC_RM5mm +Relay_THT:Relay_DPST_COTO_3602 +Relay_THT:Relay_DPST_Fujitsu_FTR-F1A +Relay_THT:Relay_DPST_Omron_G2RL-2A +Relay_THT:Relay_DPST_Schrack-RT2-FormA_RM5mm +Relay_THT:Relay_NCR_HHG1D-1 +Relay_THT:Relay_Socket_3PDT_Omron_PLE11-0 +Relay_THT:Relay_Socket_4PDT_Omron_PY14-02 +Relay_THT:Relay_Socket_DPDT_Finder_96.12 +Relay_THT:Relay_Socket_DPDT_Omron_PLE08-0 +Relay_THT:Relay_SPDT_Finder_32.21-x000 +Relay_THT:Relay_SPDT_Finder_34.51_Horizontal +Relay_THT:Relay_SPDT_Finder_34.51_Vertical +Relay_THT:Relay_SPDT_Finder_36.11 +Relay_THT:Relay_SPDT_Finder_40.11 +Relay_THT:Relay_SPDT_Finder_40.31 +Relay_THT:Relay_SPDT_Finder_40.41 +Relay_THT:Relay_SPDT_Finder_40.51 +Relay_THT:Relay_SPDT_Finder_40.61 +Relay_THT:Relay_SPDT_Fujitsu_FTR-LYCA005x_FormC_Vertical +Relay_THT:Relay_SPDT_HJR-4102 +Relay_THT:Relay_SPDT_Hongfa_HF3F-L-xx-1ZL1T +Relay_THT:Relay_SPDT_Hongfa_HF3F-L-xx-1ZL2T-R +Relay_THT:Relay_SPDT_Hongfa_HF3F-L-xx-1ZL2T +Relay_THT:Relay_SPDT_Hongfa_JQC-3FF_0XX-1Z +Relay_THT:Relay_SPDT_HsinDa_Y14 +Relay_THT:Relay_SPDT_Omron-G5LE-1 +Relay_THT:Relay_SPDT_Omron-G5Q-1 +Relay_THT:Relay_SPDT_Omron_G2RL-1-E +Relay_THT:Relay_SPDT_Omron_G2RL-1 +Relay_THT:Relay_SPDT_Omron_G5V-1 +Relay_THT:Relay_SPDT_Omron_G6E +Relay_THT:Relay_SPDT_Omron_G6EK +Relay_THT:Relay_SPDT_Panasonic_DR-L +Relay_THT:Relay_SPDT_Panasonic_DR-L2 +Relay_THT:Relay_SPDT_Panasonic_DR +Relay_THT:Relay_SPDT_Panasonic_JW1_FormC +Relay_THT:Relay_SPDT_PotterBrumfield_T9AP5D52_12V30A +Relay_THT:Relay_SPDT_RAYEX-L90 +Relay_THT:Relay_SPDT_RAYEX-L90S +Relay_THT:Relay_SPDT_SANYOU_SRD_Series_Form_C +Relay_THT:Relay_SPDT_Schrack-RP-II-1-16A-FormC_RM5mm +Relay_THT:Relay_SPDT_Schrack-RP-II-1-FormC_RM3.5mm +Relay_THT:Relay_SPDT_Schrack-RP-II-1-FormC_RM5mm +Relay_THT:Relay_SPDT_Schrack-RT1-16A-FormC_RM5mm +Relay_THT:Relay_SPDT_Schrack-RT1-FormC_RM3.5mm +Relay_THT:Relay_SPDT_Schrack-RT1-FormC_RM5mm +Relay_THT:Relay_SPDT_StandexMeder_SIL_Form1C +Relay_THT:Relay_SPST-NO_Fujitsu_FTR-LYAA005x_FormA_Vertical +Relay_THT:Relay_SPST_Finder_32.21-x300 +Relay_THT:Relay_SPST_Hongfa_HF3F-L-xx-1HL1T +Relay_THT:Relay_SPST_Hongfa_HF3F-L-xx-1HL2T-R +Relay_THT:Relay_SPST_Hongfa_HF3F-L-xx-1HL2T +Relay_THT:Relay_SPST_Hongfa_JQC-3FF_0XX-1H +Relay_THT:Relay_SPST_Omron-G5Q-1A +Relay_THT:Relay_SPST_Omron_G2RL-1A-E +Relay_THT:Relay_SPST_Omron_G2RL-1A +Relay_THT:Relay_SPST_Omron_G5NB +Relay_THT:Relay_SPST_Omron_G5PZ +Relay_THT:Relay_SPST_Panasonic_ADW11 +Relay_THT:Relay_SPST_Panasonic_ALFG_FormA +Relay_THT:Relay_SPST_Panasonic_ALFG_FormA_CircularHoles +Relay_THT:Relay_SPST_Panasonic_JW1_FormA +Relay_THT:Relay_SPST_PotterBrumfield_T9AP1D52_12V30A +Relay_THT:Relay_SPST_RAYEX-L90A +Relay_THT:Relay_SPST_RAYEX-L90AS +Relay_THT:Relay_SPST_RAYEX-L90B +Relay_THT:Relay_SPST_RAYEX-L90BS +Relay_THT:Relay_SPST_SANYOU_SRD_Series_Form_A +Relay_THT:Relay_SPST_SANYOU_SRD_Series_Form_B +Relay_THT:Relay_SPST_Schrack-RP-II-1-16A-FormA_RM5mm +Relay_THT:Relay_SPST_Schrack-RP-II-1-FormA_RM3.5mm +Relay_THT:Relay_SPST_Schrack-RP-II-1-FormA_RM5mm +Relay_THT:Relay_SPST_Schrack-RP3SL-1coil_RM5mm +Relay_THT:Relay_SPST_Schrack-RP3SL_RM5mm +Relay_THT:Relay_SPST_Schrack-RT1-16A-FormA_RM5mm +Relay_THT:Relay_SPST_Schrack-RT1-FormA_RM3.5mm +Relay_THT:Relay_SPST_Schrack-RT1-FormA_RM5mm +Relay_THT:Relay_SPST_StandexMeder_MS_Form1AB +Relay_THT:Relay_SPST_StandexMeder_SIL_Form1A +Relay_THT:Relay_SPST_StandexMeder_SIL_Form1B +Relay_THT:Relay_SPST_TE_PCH-1xxx2M +Relay_THT:Relay_SPST_TE_PCN-1xxD3MHZ +Relay_THT:Relay_SPST_Zettler-AZSR131 +Relay_THT:Relay_StandexMeder_DIP_HighProfile +Relay_THT:Relay_StandexMeder_DIP_LowProfile +Relay_THT:Relay_StandexMeder_UMS +Relay_THT:Relay_Tyco_V23072_Sealed +Resistor_SMD:R_01005_0402Metric +Resistor_SMD:R_01005_0402Metric_Pad0.57x0.30mm_HandSolder +Resistor_SMD:R_0201_0603Metric +Resistor_SMD:R_0201_0603Metric_Pad0.64x0.40mm_HandSolder +Resistor_SMD:R_0402_1005Metric +Resistor_SMD:R_0402_1005Metric_Pad0.72x0.64mm_HandSolder +Resistor_SMD:R_0603_1608Metric +Resistor_SMD:R_0603_1608Metric_Pad0.98x0.95mm_HandSolder +Resistor_SMD:R_0612_1632Metric +Resistor_SMD:R_0612_1632Metric_Pad1.18x3.40mm_HandSolder +Resistor_SMD:R_0805_2012Metric +Resistor_SMD:R_0805_2012Metric_Pad1.20x1.40mm_HandSolder +Resistor_SMD:R_0815_2038Metric +Resistor_SMD:R_0815_2038Metric_Pad1.20x4.05mm_HandSolder +Resistor_SMD:R_1020_2550Metric +Resistor_SMD:R_1020_2550Metric_Pad1.33x5.20mm_HandSolder +Resistor_SMD:R_1206_3216Metric +Resistor_SMD:R_1206_3216Metric_Pad1.30x1.75mm_HandSolder +Resistor_SMD:R_1210_3225Metric +Resistor_SMD:R_1210_3225Metric_Pad1.30x2.65mm_HandSolder +Resistor_SMD:R_1218_3246Metric +Resistor_SMD:R_1218_3246Metric_Pad1.22x4.75mm_HandSolder +Resistor_SMD:R_1812_4532Metric +Resistor_SMD:R_1812_4532Metric_Pad1.30x3.40mm_HandSolder +Resistor_SMD:R_2010_5025Metric +Resistor_SMD:R_2010_5025Metric_Pad1.40x2.65mm_HandSolder +Resistor_SMD:R_2512_6332Metric +Resistor_SMD:R_2512_6332Metric_Pad1.40x3.35mm_HandSolder +Resistor_SMD:R_2816_7142Metric +Resistor_SMD:R_2816_7142Metric_Pad3.20x4.45mm_HandSolder +Resistor_SMD:R_4020_10251Metric +Resistor_SMD:R_4020_10251Metric_Pad1.65x5.30mm_HandSolder +Resistor_SMD:R_Array_Concave_2x0603 +Resistor_SMD:R_Array_Concave_4x0402 +Resistor_SMD:R_Array_Concave_4x0603 +Resistor_SMD:R_Array_Convex_2x0402 +Resistor_SMD:R_Array_Convex_2x0603 +Resistor_SMD:R_Array_Convex_2x0606 +Resistor_SMD:R_Array_Convex_2x1206 +Resistor_SMD:R_Array_Convex_4x0402 +Resistor_SMD:R_Array_Convex_4x0603 +Resistor_SMD:R_Array_Convex_4x0612 +Resistor_SMD:R_Array_Convex_4x1206 +Resistor_SMD:R_Array_Convex_5x0603 +Resistor_SMD:R_Array_Convex_5x1206 +Resistor_SMD:R_Array_Convex_8x0602 +Resistor_SMD:R_Cat16-2 +Resistor_SMD:R_Cat16-4 +Resistor_SMD:R_Cat16-8 +Resistor_SMD:R_MELF_MMB-0207 +Resistor_SMD:R_MicroMELF_MMU-0102 +Resistor_SMD:R_MiniMELF_MMA-0204 +Resistor_SMD:R_Shunt_Isabellenhuette_BVR4026 +Resistor_SMD:R_Shunt_Ohmite_LVK12 +Resistor_SMD:R_Shunt_Ohmite_LVK20 +Resistor_SMD:R_Shunt_Ohmite_LVK24 +Resistor_SMD:R_Shunt_Ohmite_LVK25 +Resistor_SMD:R_Shunt_Vishay_WSK2512_6332Metric_T1.19mm +Resistor_SMD:R_Shunt_Vishay_WSK2512_6332Metric_T2.21mm +Resistor_SMD:R_Shunt_Vishay_WSK2512_6332Metric_T2.66mm +Resistor_SMD:R_Shunt_Vishay_WSKW0612 +Resistor_SMD:R_Shunt_Vishay_WSR2_WSR3 +Resistor_SMD:R_Shunt_Vishay_WSR2_WSR3_KelvinConnection +Resistor_THT:R_Array_SIP10 +Resistor_THT:R_Array_SIP11 +Resistor_THT:R_Array_SIP12 +Resistor_THT:R_Array_SIP13 +Resistor_THT:R_Array_SIP14 +Resistor_THT:R_Array_SIP4 +Resistor_THT:R_Array_SIP5 +Resistor_THT:R_Array_SIP6 +Resistor_THT:R_Array_SIP7 +Resistor_THT:R_Array_SIP8 +Resistor_THT:R_Array_SIP9 +Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P1.90mm_Vertical +Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P2.54mm_Vertical +Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P5.08mm_Horizontal +Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P7.62mm_Horizontal +Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal +Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P15.24mm_Horizontal +Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical +Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal +Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P12.70mm_Horizontal +Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P15.24mm_Horizontal +Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P2.54mm_Vertical +Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P20.32mm_Horizontal +Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P12.70mm_Horizontal +Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P15.24mm_Horizontal +Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P20.32mm_Horizontal +Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P7.62mm_Vertical +Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P15.24mm_Horizontal +Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P20.32mm_Horizontal +Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P7.62mm_Vertical +Resistor_THT:R_Axial_DIN0516_L15.5mm_D5.0mm_P20.32mm_Horizontal +Resistor_THT:R_Axial_DIN0516_L15.5mm_D5.0mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0516_L15.5mm_D5.0mm_P30.48mm_Horizontal +Resistor_THT:R_Axial_DIN0516_L15.5mm_D5.0mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0516_L15.5mm_D5.0mm_P7.62mm_Vertical +Resistor_THT:R_Axial_DIN0614_L14.3mm_D5.7mm_P15.24mm_Horizontal +Resistor_THT:R_Axial_DIN0614_L14.3mm_D5.7mm_P20.32mm_Horizontal +Resistor_THT:R_Axial_DIN0614_L14.3mm_D5.7mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0614_L14.3mm_D5.7mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0614_L14.3mm_D5.7mm_P7.62mm_Vertical +Resistor_THT:R_Axial_DIN0617_L17.0mm_D6.0mm_P20.32mm_Horizontal +Resistor_THT:R_Axial_DIN0617_L17.0mm_D6.0mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0617_L17.0mm_D6.0mm_P30.48mm_Horizontal +Resistor_THT:R_Axial_DIN0617_L17.0mm_D6.0mm_P5.08mm_Vertical +Resistor_THT:R_Axial_DIN0617_L17.0mm_D6.0mm_P7.62mm_Vertical +Resistor_THT:R_Axial_DIN0918_L18.0mm_D9.0mm_P22.86mm_Horizontal +Resistor_THT:R_Axial_DIN0918_L18.0mm_D9.0mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0918_L18.0mm_D9.0mm_P30.48mm_Horizontal +Resistor_THT:R_Axial_DIN0918_L18.0mm_D9.0mm_P7.62mm_Vertical +Resistor_THT:R_Axial_DIN0922_L20.0mm_D9.0mm_P25.40mm_Horizontal +Resistor_THT:R_Axial_DIN0922_L20.0mm_D9.0mm_P30.48mm_Horizontal +Resistor_THT:R_Axial_DIN0922_L20.0mm_D9.0mm_P7.62mm_Vertical +Resistor_THT:R_Axial_Power_L20.0mm_W6.4mm_P22.40mm +Resistor_THT:R_Axial_Power_L20.0mm_W6.4mm_P25.40mm +Resistor_THT:R_Axial_Power_L20.0mm_W6.4mm_P30.48mm +Resistor_THT:R_Axial_Power_L20.0mm_W6.4mm_P5.08mm_Vertical +Resistor_THT:R_Axial_Power_L20.0mm_W6.4mm_P7.62mm_Vertical +Resistor_THT:R_Axial_Power_L25.0mm_W6.4mm_P27.94mm +Resistor_THT:R_Axial_Power_L25.0mm_W6.4mm_P30.48mm +Resistor_THT:R_Axial_Power_L25.0mm_W9.0mm_P10.16mm_Vertical +Resistor_THT:R_Axial_Power_L25.0mm_W9.0mm_P27.94mm +Resistor_THT:R_Axial_Power_L25.0mm_W9.0mm_P30.48mm +Resistor_THT:R_Axial_Power_L25.0mm_W9.0mm_P7.62mm_Vertical +Resistor_THT:R_Axial_Power_L38.0mm_W6.4mm_P40.64mm +Resistor_THT:R_Axial_Power_L38.0mm_W6.4mm_P45.72mm +Resistor_THT:R_Axial_Power_L38.0mm_W9.0mm_P40.64mm +Resistor_THT:R_Axial_Power_L38.0mm_W9.0mm_P45.72mm +Resistor_THT:R_Axial_Power_L48.0mm_W12.5mm_P10.16mm_Vertical +Resistor_THT:R_Axial_Power_L48.0mm_W12.5mm_P55.88mm +Resistor_THT:R_Axial_Power_L48.0mm_W12.5mm_P60.96mm +Resistor_THT:R_Axial_Power_L48.0mm_W12.5mm_P7.62mm_Vertical +Resistor_THT:R_Axial_Power_L50.0mm_W9.0mm_P55.88mm +Resistor_THT:R_Axial_Power_L50.0mm_W9.0mm_P60.96mm +Resistor_THT:R_Axial_Power_L60.0mm_W14.0mm_P10.16mm_Vertical +Resistor_THT:R_Axial_Power_L60.0mm_W14.0mm_P66.04mm +Resistor_THT:R_Axial_Power_L60.0mm_W14.0mm_P71.12mm +Resistor_THT:R_Axial_Power_L75.0mm_W9.0mm_P81.28mm +Resistor_THT:R_Axial_Power_L75.0mm_W9.0mm_P86.36mm +Resistor_THT:R_Axial_Shunt_L22.2mm_W8.0mm_PS14.30mm_P25.40mm +Resistor_THT:R_Axial_Shunt_L22.2mm_W9.5mm_PS14.30mm_P25.40mm +Resistor_THT:R_Axial_Shunt_L35.3mm_W9.5mm_PS25.40mm_P38.10mm +Resistor_THT:R_Axial_Shunt_L47.6mm_W12.7mm_PS34.93mm_P50.80mm +Resistor_THT:R_Axial_Shunt_L47.6mm_W9.5mm_PS34.93mm_P50.80mm +Resistor_THT:R_Bare_Metal_Element_L12.4mm_W4.8mm_P11.40mm +Resistor_THT:R_Bare_Metal_Element_L16.3mm_W4.8mm_P15.30mm +Resistor_THT:R_Bare_Metal_Element_L21.3mm_W4.8mm_P20.30mm +Resistor_THT:R_Box_L13.0mm_W4.0mm_P9.00mm +Resistor_THT:R_Box_L14.0mm_W5.0mm_P9.00mm +Resistor_THT:R_Box_L26.0mm_W5.0mm_P20.00mm +Resistor_THT:R_Box_L8.4mm_W2.5mm_P5.08mm +Resistor_THT:R_Radial_Power_L11.0mm_W7.0mm_P5.00mm +Resistor_THT:R_Radial_Power_L12.0mm_W8.0mm_P5.00mm +Resistor_THT:R_Radial_Power_L13.0mm_W9.0mm_P5.00mm +Resistor_THT:R_Radial_Power_L16.1mm_W9.0mm_P7.37mm +Resistor_THT:R_Radial_Power_L7.0mm_W8.0mm_Px2.40mm_Py2.30mm +Resistor_THT:R_Radial_Power_L9.0mm_W10.0mm_Px2.70mm_Py2.30mm +RF:Skyworks_SKY13575_639LF +RF:Skyworks_SKY65404-31 +RF_Antenna:Abracon_APAES868R8060C16-T +RF_Antenna:Abracon_PRO-OB-440 +RF_Antenna:Abracon_PRO-OB-471 +RF_Antenna:Antenova_SR4G013_GPS +RF_Antenna:Astrocast_AST50127-00 +RF_Antenna:AVX_M620720 +RF_Antenna:Coilcraft_MA5532-AE_RFID +RF_Antenna:Johanson_2450AT18x100 +RF_Antenna:Johanson_2450AT43F0100 +RF_Antenna:Molex_47948-0001_2.4Ghz +RF_Antenna:NiceRF_SW868-TH13_868Mhz +RF_Antenna:Pulse_W3000 +RF_Antenna:Pulse_W3011 +RF_Antenna:Texas_SWRA117D_2.4GHz_Left +RF_Antenna:Texas_SWRA117D_2.4GHz_Right +RF_Antenna:Texas_SWRA416_868MHz_915MHz +RF_Converter:Anaren_0805_2012Metric-6 +RF_Converter:Balun_Johanson_0896BM15A0001 +RF_Converter:Balun_Johanson_0900FM15K0039 +RF_Converter:Balun_Johanson_0900PC15J0013 +RF_Converter:Balun_Johanson_1.6x0.8mm +RF_Converter:Balun_Johanson_5400BL15B050E +RF_Converter:RF_Attenuator_Susumu_PAT1220 +RF_GPS:Linx_RXM-GPS +RF_GPS:OriginGPS_ORG1510 +RF_GPS:Quectel_L70-R +RF_GPS:Quectel_L76 +RF_GPS:Quectel_L80-R +RF_GPS:Quectel_L96 +RF_GPS:Sierra_XA11X0 +RF_GPS:Sierra_XM11X0 +RF_GPS:SIM28ML +RF_GPS:ublox_LEA +RF_GPS:ublox_MAX +RF_GPS:ublox_NEO +RF_GPS:ublox_SAM-M8Q +RF_GPS:ublox_SAM-M8Q_HandSolder +RF_GPS:ublox_ZED +RF_GPS:ublox_ZOE_M8 +RF_GSM:Quectel_BC66 +RF_GSM:Quectel_BC95 +RF_GSM:Quectel_BG95 +RF_GSM:Quectel_BG96 +RF_GSM:Quectel_M95 +RF_GSM:SIMCom_SIM800C +RF_GSM:SIMCom_SIM900 +RF_GSM:Telit_SE150A4 +RF_GSM:Telit_xL865 +RF_GSM:ublox_LENA-R8_LGA-100 +RF_GSM:ublox_SARA_LGA-96 +RF_Mini-Circuits:Mini-Circuits_BK377 +RF_Mini-Circuits:Mini-Circuits_BK377_LandPatternPL-005 +RF_Mini-Circuits:Mini-Circuits_CD541_H2.08mm +RF_Mini-Circuits:Mini-Circuits_CD542_H2.84mm +RF_Mini-Circuits:Mini-Circuits_CD542_LandPatternPL-052 +RF_Mini-Circuits:Mini-Circuits_CD542_LandPatternPL-094 +RF_Mini-Circuits:Mini-Circuits_CD636_H4.11mm +RF_Mini-Circuits:Mini-Circuits_CD636_LandPatternPL-035 +RF_Mini-Circuits:Mini-Circuits_CD637_H5.23mm +RF_Mini-Circuits:Mini-Circuits_CK605 +RF_Mini-Circuits:Mini-Circuits_CK605_LandPatternPL-012 +RF_Mini-Circuits:Mini-Circuits_DB1627 +RF_Mini-Circuits:Mini-Circuits_GP1212 +RF_Mini-Circuits:Mini-Circuits_GP1212_LandPatternPL-176 +RF_Mini-Circuits:Mini-Circuits_GP731 +RF_Mini-Circuits:Mini-Circuits_GP731_LandPatternPL-176 +RF_Mini-Circuits:Mini-Circuits_HF1139 +RF_Mini-Circuits:Mini-Circuits_HF1139_LandPatternPL-230 +RF_Mini-Circuits:Mini-Circuits_HQ1157 +RF_Mini-Circuits:Mini-Circuits_HZ1198 +RF_Mini-Circuits:Mini-Circuits_HZ1198_LandPatternPL-247 +RF_Mini-Circuits:Mini-Circuits_MMM168 +RF_Mini-Circuits:Mini-Circuits_MMM168_LandPatternPL-225 +RF_Mini-Circuits:Mini-Circuits_QQQ130_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_QQQ130_LandPattern_PL-236_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_TT1224_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_TT1224_LandPatternPL-258_ClockwisePinNumbering +RF_Mini-Circuits:Mini-Circuits_TTT167 +RF_Mini-Circuits:Mini-Circuits_TTT167_LandPatternPL-079 +RF_Mini-Circuits:Mini-Circuits_YY161 +RF_Mini-Circuits:Mini-Circuits_YY161_LandPatternPL-049 +RF_Module:Ai-Thinker-Ra-01-LoRa +RF_Module:Astrocast_AST50147-00 +RF_Module:Atmel_ATSAMR21G18-MR210UA_NoRFPads +RF_Module:BLE112-A +RF_Module:BM78SPPS5xC2 +RF_Module:CMWX1ZZABZ +RF_Module:CYBLE-21Pin-10x10mm +RF_Module:DecaWave_DWM1001 +RF_Module:Digi_XBee_SMT +RF_Module:DWM1000 +RF_Module:E18-MS1-PCB +RF_Module:E73-2G4M04S +RF_Module:ESP-01 +RF_Module:ESP-07 +RF_Module:ESP-12E +RF_Module:ESP-WROOM-02 +RF_Module:ESP32-C3-DevKitM-1 +RF_Module:ESP32-C3-WROOM-02 +RF_Module:ESP32-C3-WROOM-02U +RF_Module:ESP32-C6-MINI-1 +RF_Module:ESP32-S2-MINI-1 +RF_Module:ESP32-S2-MINI-1U +RF_Module:ESP32-S2-WROVER +RF_Module:ESP32-S3-WROOM-1 +RF_Module:ESP32-S3-WROOM-1U +RF_Module:ESP32-S3-WROOM-2 +RF_Module:ESP32-WROOM-32 +RF_Module:ESP32-WROOM-32D +RF_Module:ESP32-WROOM-32E +RF_Module:ESP32-WROOM-32U +RF_Module:ESP32-WROOM-32UE +RF_Module:Garmin_M8-35_9.8x14.0mm_Layout6x6_P1.5mm +RF_Module:Heltec_HT-CT62 +RF_Module:HOPERF_RFM69HW +RF_Module:HOPERF_RFM9XW_SMD +RF_Module:HOPERF_RFM9XW_THT +RF_Module:IQRF_TRx2DA_KON-SIM-01 +RF_Module:IQRF_TRx2D_KON-SIM-01 +RF_Module:Jadak_Thingmagic_M6e-Nano +RF_Module:Laird_BL652 +RF_Module:MCU_Seeed_ESP32C3 +RF_Module:Microchip_BM83 +RF_Module:Microchip_RN4871 +RF_Module:MOD-nRF8001 +RF_Module:Modtronix_inAir9 +RF_Module:MonoWireless_TWE-L-WX +RF_Module:NINA-B111 +RF_Module:nRF24L01_Breakout +RF_Module:Particle_P1 +RF_Module:RAK3172 +RF_Module:RAK4200 +RF_Module:RAK811 +RF_Module:Raytac_MDBT42Q +RF_Module:Raytac_MDBT50Q +RF_Module:RFDigital_RFD77101 +RF_Module:RN2483 +RF_Module:RN42 +RF_Module:RN42N +RF_Module:ST-SiP-LGA-86-11x7.3mm +RF_Module:ST_SPBTLE +RF_Module:Taiyo-Yuden_EYSGJNZWY +RF_Module:TD1205 +RF_Module:TD1208 +RF_Module:WEMOS_C3_mini +RF_Module:WEMOS_D1_mini_light +RF_Module:ZETA-433-SO_SMD +RF_Module:ZETA-433-SO_THT +RF_Shielding:Laird_Technologies_97-2002_25.40x25.40mm +RF_Shielding:Laird_Technologies_97-2003_12.70x13.37mm +RF_Shielding:Laird_Technologies_BMI-S-101_13.66x12.70mm +RF_Shielding:Laird_Technologies_BMI-S-102_16.50x16.50mm +RF_Shielding:Laird_Technologies_BMI-S-103_26.21x26.21mm +RF_Shielding:Laird_Technologies_BMI-S-104_32.00x32.00mm +RF_Shielding:Laird_Technologies_BMI-S-105_38.10x25.40mm +RF_Shielding:Laird_Technologies_BMI-S-106_36.83x33.68mm +RF_Shielding:Laird_Technologies_BMI-S-107_44.37x44.37mm +RF_Shielding:Laird_Technologies_BMI-S-201-F_13.66x12.70mm +RF_Shielding:Laird_Technologies_BMI-S-202-F_16.50x16.50mm +RF_Shielding:Laird_Technologies_BMI-S-203-F_26.21x26.21mm +RF_Shielding:Laird_Technologies_BMI-S-204-F_32.00x32.00mm +RF_Shielding:Laird_Technologies_BMI-S-205-F_38.10x25.40mm +RF_Shielding:Laird_Technologies_BMI-S-206-F_36.83x33.68mm +RF_Shielding:Laird_Technologies_BMI-S-207-F_44.37x44.37mm +RF_Shielding:Laird_Technologies_BMI-S-208-F_39.60x39.60mm +RF_Shielding:Laird_Technologies_BMI-S-209-F_29.36x18.50mm +RF_Shielding:Laird_Technologies_BMI-S-210-F_44.00x30.50mm +RF_Shielding:Laird_Technologies_BMI-S-230-F_50.8x38.1mm +RF_Shielding:Wuerth_36103205_20x20mm +RF_Shielding:Wuerth_36103255_25x25mm +RF_Shielding:Wuerth_36103305_30x30mm +RF_Shielding:Wuerth_36103505_50x50mm +RF_Shielding:Wuerth_36103605_60x60mm +RF_Shielding:Wuerth_36503205_20x20mm +RF_Shielding:Wuerth_36503255_25x25mm +RF_Shielding:Wuerth_36503305_30x30mm +RF_Shielding:Wuerth_36503505_50x50mm +RF_Shielding:Wuerth_36503605_60x60mm +RF_WiFi:USR-C322 +Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm +Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm_CircularMountingHoles +Rotary_Encoder:RotaryEncoder_Alps_EC11E-Switch_Vertical_H20mm_MountingHoles +Rotary_Encoder:RotaryEncoder_Alps_EC11E_Vertical_H20mm +Rotary_Encoder:RotaryEncoder_Alps_EC11E_Vertical_H20mm_CircularMountingHoles +Rotary_Encoder:RotaryEncoder_Alps_EC12E-Switch_Vertical_H20mm +Rotary_Encoder:RotaryEncoder_Alps_EC12E-Switch_Vertical_H20mm_CircularMountingHoles +Rotary_Encoder:RotaryEncoder_Alps_EC12E_Vertical_H20mm +Rotary_Encoder:RotaryEncoder_Alps_EC12E_Vertical_H20mm_CircularMountingHoles +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEC09-2xxxF-Nxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEC09-2xxxF-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEC12R-2x17F-Nxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEC12R-2x17F-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEL12D-2x16F-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEL12D-2x18F-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEL12D-2x21F-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEL12D-2x25S-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEL12D-2x26F-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Horizontal_PEL12D-2x31F-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEC12R-3x17F-Nxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEC12R-3x17F-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEL12D-4x25S-Sxxxx +Rotary_Encoder:RotaryEncoder_Bourns_Vertical_PEL12D-4xxxF-Sxxxx +Sensor:Aosong_DHT11_5.5x12.0_P2.54mm +Sensor:ASAIR_AM2302_P2.54mm_Lead2.75mm_TabDown +Sensor:ASAIR_AM2302_P2.54mm_Vertical +Sensor:Avago_APDS-9960 +Sensor:LuminOX_LOX-O2 +Sensor:MQ-6 +Sensor:Rohm_RPR-0521RS +Sensor:Senseair_S8_Down +Sensor:Senseair_S8_Up +Sensor:Sensirion_SCD4x-1EP_10.1x10.1mm_P1.25mm_EP4.8x4.8mm +Sensor:Sensortech_MiCS_5x7mm_P1.25mm +Sensor:SHT1x +Sensor:SPEC_110-xxx_SMD-10Pin_20x20mm_P4.0mm +Sensor:TGS-5141 +Sensor:Winson_GM-402B_5x5mm_P1.27mm +Sensor_Audio:CUI_CMC-4013-SMT +Sensor_Audio:Infineon_PG-LLGA-5-1 +Sensor_Audio:Infineon_PG-LLGA-5-2 +Sensor_Audio:InvenSense_ICS-43434-6_3.5x2.65mm +Sensor_Audio:Knowles_LGA-5_3.5x2.65mm +Sensor_Audio:Knowles_LGA-6_4.72x3.76mm +Sensor_Audio:Knowles_SPH0645LM4H-6_3.5x2.65mm +Sensor_Audio:POM-2244P-C3310-2-R +Sensor_Audio:ST_HLGA-6_3.76x4.72mm_P1.65mm +Sensor_Current:AKM_CQ_7 +Sensor_Current:AKM_CQ_7S +Sensor_Current:AKM_CQ_VSOP-24_5.6x7.9mm_P0.65mm +Sensor_Current:AKM_CZ_SSOP-10_6.5x8.1mm_P0.95mm +Sensor_Current:Allegro_CB_PFF +Sensor_Current:Allegro_CB_PSF +Sensor_Current:Allegro_CB_PSS +Sensor_Current:Allegro_PSOF-7_4.8x6.4mm_P1.60mm +Sensor_Current:Allegro_QFN-12-10-1EP_3x3mm_P0.5mm +Sensor_Current:Allegro_QSOP-24_3.9x8.7mm_P0.635mm +Sensor_Current:Allegro_SIP-3 +Sensor_Current:Allegro_SIP-4 +Sensor_Current:Diodes_SIP-3_4.1x1.5mm_P1.27mm +Sensor_Current:Diodes_SIP-3_4.1x1.5mm_P2.65mm +Sensor_Current:Honeywell_CSLW +Sensor_Current:LEM_CKSR +Sensor_Current:LEM_HO40-NP +Sensor_Current:LEM_HO8-NP +Sensor_Current:LEM_HO8-NSM +Sensor_Current:LEM_HTFS +Sensor_Current:LEM_HX02-P +Sensor_Current:LEM_HX03-P-SP2 +Sensor_Current:LEM_HX04-P +Sensor_Current:LEM_HX05-NP +Sensor_Current:LEM_HX05-P-SP2 +Sensor_Current:LEM_HX06-P +Sensor_Current:LEM_HX10-NP +Sensor_Current:LEM_HX10-P-SP2 +Sensor_Current:LEM_HX15-NP +Sensor_Current:LEM_HX15-P-SP2 +Sensor_Current:LEM_HX20-P-SP2 +Sensor_Current:LEM_HX25-P-SP2 +Sensor_Current:LEM_HX50-P-SP2 +Sensor_Current:LEM_LA25-NP +Sensor_Current:LEM_LA25-P +Sensor_Current:LEM_LTSR-NP +Sensor_Distance:AMS_OLGA12 +Sensor_Distance:ST_VL53L1x +Sensor_Humidity:Sensirion_DFN-4-1EP_2x2mm_P1mm_EP0.7x1.6mm +Sensor_Humidity:Sensirion_DFN-4_1.5x1.5mm_P0.8mm_SHT4x_NoCentralPad +Sensor_Humidity:Sensirion_DFN-8-1EP_2.5x2.5mm_P0.5mm_EP1.1x1.7mm +Sensor_Motion:Analog_LGA-16_3.25x3mm_P0.5mm_LayoutBorder3x5y +Sensor_Motion:InvenSense_QFN-24_3x3mm_P0.4mm +Sensor_Motion:InvenSense_QFN-24_3x3mm_P0.4mm_NoMask +Sensor_Motion:InvenSense_QFN-24_4x4mm_P0.5mm +Sensor_Motion:InvenSense_QFN-24_4x4mm_P0.5mm_NoMask +Sensor_Pressure:CFSensor_XGZP6859D_7x7mm +Sensor_Pressure:CFSensor_XGZP6897x +Sensor_Pressure:CFSensor_XGZP6899x +Sensor_Pressure:Freescale_98ARH99066A +Sensor_Pressure:Freescale_98ARH99089A +Sensor_Pressure:Honeywell_40PCxxxG1A +Sensor_Pressure:TE_MS5525DSO-DBxxxyS +Sensor_Pressure:TE_MS5837-xxBA +Sensor_Voltage:LEM_LV25-P +Socket:3M_Textool_240-1288-00-0602J_2x20_P2.54mm +Socket:DIP_Socket-14_W4.3_W5.08_W7.62_W10.16_W10.9_3M_214-3339-00-0602J +Socket:DIP_Socket-16_W4.3_W5.08_W7.62_W10.16_W10.9_3M_216-3340-00-0602J +Socket:DIP_Socket-18_W4.3_W5.08_W7.62_W10.16_W10.9_3M_218-3341-00-0602J +Socket:DIP_Socket-20_W4.3_W5.08_W7.62_W10.16_W10.9_3M_220-3342-00-0602J +Socket:DIP_Socket-22_W6.9_W7.62_W10.16_W12.7_W13.5_3M_222-3343-00-0602J +Socket:DIP_Socket-24_W11.9_W12.7_W15.24_W17.78_W18.5_3M_224-1275-00-0602J +Socket:DIP_Socket-24_W4.3_W5.08_W7.62_W10.16_W10.9_3M_224-5248-00-0602J +Socket:DIP_Socket-28_W11.9_W12.7_W15.24_W17.78_W18.5_3M_228-1277-00-0602J +Socket:DIP_Socket-28_W6.9_W7.62_W10.16_W12.7_W13.5_3M_228-4817-00-0602J +Socket:DIP_Socket-32_W11.9_W12.7_W15.24_W17.78_W18.5_3M_232-1285-00-0602J +Socket:DIP_Socket-40_W11.9_W12.7_W15.24_W17.78_W18.5_3M_240-1280-00-0602J +Socket:DIP_Socket-40_W22.1_W22.86_W25.4_W27.94_W28.7_3M_240-3639-00-0602J +Socket:DIP_Socket-42_W11.9_W12.7_W15.24_W17.78_W18.5_3M_242-1281-00-0602J +Socket:Wells_648-0482211SA01 +Symbol:CE-Logo_11.2x8mm_SilkScreen +Symbol:CE-Logo_16.8x12mm_SilkScreen +Symbol:CE-Logo_28x20mm_SilkScreen +Symbol:CE-Logo_42x30mm_SilkScreen +Symbol:CE-Logo_56.1x40mm_SilkScreen +Symbol:CE-Logo_8.5x6mm_SilkScreen +Symbol:EasterEgg_EWG1308-2013_ClassA +Symbol:ESD-Logo_13.2x12mm_SilkScreen +Symbol:ESD-Logo_22x20mm_SilkScreen +Symbol:ESD-Logo_33x30mm_SilkScreen +Symbol:ESD-Logo_44.1x40mm_SilkScreen +Symbol:ESD-Logo_6.6x6mm_SilkScreen +Symbol:ESD-Logo_8.9x8mm_SilkScreen +Symbol:FCC-Logo_14.6x12mm_SilkScreen +Symbol:FCC-Logo_24.2x20mm_SilkScreen +Symbol:FCC-Logo_36.3x30mm_SilkScreen +Symbol:FCC-Logo_48.3x40mm_SilkScreen +Symbol:FCC-Logo_7.3x6mm_SilkScreen +Symbol:FCC-Logo_9.6x8mm_SilkScreen +Symbol:KiCad-Logo2_12mm_Copper +Symbol:KiCad-Logo2_12mm_SilkScreen +Symbol:KiCad-Logo2_20mm_Copper +Symbol:KiCad-Logo2_20mm_SilkScreen +Symbol:KiCad-Logo2_30mm_Copper +Symbol:KiCad-Logo2_30mm_SilkScreen +Symbol:KiCad-Logo2_40mm_Copper +Symbol:KiCad-Logo2_40mm_SilkScreen +Symbol:KiCad-Logo2_5mm_Copper +Symbol:KiCad-Logo2_5mm_SilkScreen +Symbol:KiCad-Logo2_6mm_Copper +Symbol:KiCad-Logo2_6mm_SilkScreen +Symbol:KiCad-Logo2_8mm_Copper +Symbol:KiCad-Logo2_8mm_SilkScreen +Symbol:KiCad-Logo_12mm_Copper +Symbol:KiCad-Logo_12mm_SilkScreen +Symbol:KiCad-Logo_20mm_Copper +Symbol:KiCad-Logo_20mm_SilkScreen +Symbol:KiCad-Logo_30mm_Copper +Symbol:KiCad-Logo_30mm_SilkScreen +Symbol:KiCad-Logo_40mm_Copper +Symbol:KiCad-Logo_40mm_SilkScreen +Symbol:KiCad-Logo_5mm_Copper +Symbol:KiCad-Logo_5mm_SilkScreen +Symbol:KiCad-Logo_6mm_Copper +Symbol:KiCad-Logo_6mm_SilkScreen +Symbol:KiCad-Logo_8mm_Copper +Symbol:KiCad-Logo_8mm_SilkScreen +Symbol:OSHW-Logo2_14.6x12mm_Copper +Symbol:OSHW-Logo2_14.6x12mm_SilkScreen +Symbol:OSHW-Logo2_24.3x20mm_Copper +Symbol:OSHW-Logo2_24.3x20mm_SilkScreen +Symbol:OSHW-Logo2_36.5x30mm_Copper +Symbol:OSHW-Logo2_36.5x30mm_SilkScreen +Symbol:OSHW-Logo2_48.7x40mm_Copper +Symbol:OSHW-Logo2_48.7x40mm_SilkScreen +Symbol:OSHW-Logo2_7.3x6mm_Copper +Symbol:OSHW-Logo2_7.3x6mm_SilkScreen +Symbol:OSHW-Logo2_9.8x8mm_Copper +Symbol:OSHW-Logo2_9.8x8mm_SilkScreen +Symbol:OSHW-Logo_11.4x12mm_Copper +Symbol:OSHW-Logo_11.4x12mm_SilkScreen +Symbol:OSHW-Logo_19x20mm_Copper +Symbol:OSHW-Logo_19x20mm_SilkScreen +Symbol:OSHW-Logo_28.5x30mm_Copper +Symbol:OSHW-Logo_28.5x30mm_SilkScreen +Symbol:OSHW-Logo_38.1x40mm_Copper +Symbol:OSHW-Logo_38.1x40mm_SilkScreen +Symbol:OSHW-Logo_5.7x6mm_Copper +Symbol:OSHW-Logo_5.7x6mm_SilkScreen +Symbol:OSHW-Logo_7.5x8mm_Copper +Symbol:OSHW-Logo_7.5x8mm_SilkScreen +Symbol:OSHW-Symbol_13.4x12mm_Copper +Symbol:OSHW-Symbol_13.4x12mm_SilkScreen +Symbol:OSHW-Symbol_22.3x20mm_Copper +Symbol:OSHW-Symbol_22.3x20mm_SilkScreen +Symbol:OSHW-Symbol_33.5x30mm_Copper +Symbol:OSHW-Symbol_33.5x30mm_SilkScreen +Symbol:OSHW-Symbol_44.5x40mm_Copper +Symbol:OSHW-Symbol_44.5x40mm_SilkScreen +Symbol:OSHW-Symbol_6.7x6mm_Copper +Symbol:OSHW-Symbol_6.7x6mm_SilkScreen +Symbol:OSHW-Symbol_8.9x8mm_Copper +Symbol:OSHW-Symbol_8.9x8mm_SilkScreen +Symbol:Polarity_Center_Negative_12mm_SilkScreen +Symbol:Polarity_Center_Negative_20mm_SilkScreen +Symbol:Polarity_Center_Negative_30mm_SilkScreen +Symbol:Polarity_Center_Negative_40mm_SilkScreen +Symbol:Polarity_Center_Negative_6mm_SilkScreen +Symbol:Polarity_Center_Negative_8mm_SilkScreen +Symbol:Polarity_Center_Positive_12mm_SilkScreen +Symbol:Polarity_Center_Positive_20mm_SilkScreen +Symbol:Polarity_Center_Positive_30mm_SilkScreen +Symbol:Polarity_Center_Positive_40mm_SilkScreen +Symbol:Polarity_Center_Positive_6mm_SilkScreen +Symbol:Polarity_Center_Positive_8mm_SilkScreen +Symbol:RoHS-Logo_12mm_SilkScreen +Symbol:RoHS-Logo_20mm_SilkScreen +Symbol:RoHS-Logo_30mm_SilkScreen +Symbol:RoHS-Logo_40mm_SilkScreen +Symbol:RoHS-Logo_6mm_SilkScreen +Symbol:RoHS-Logo_8mm_SilkScreen +Symbol:Smolhaj_Scale_0.1 +Symbol:Symbol_Attention_Triangle_17x15mm_Copper +Symbol:Symbol_Attention_Triangle_8x7mm_Copper +Symbol:Symbol_Barrel_Polarity +Symbol:Symbol_CC-Attribution_CopperTop_Big +Symbol:Symbol_CC-Attribution_CopperTop_Small +Symbol:Symbol_CC-Noncommercial_CopperTop_Big +Symbol:Symbol_CC-Noncommercial_CopperTop_Small +Symbol:Symbol_CC-PublicDomain_CopperTop_Big +Symbol:Symbol_CC-PublicDomain_CopperTop_Small +Symbol:Symbol_CC-PublicDomain_SilkScreenTop_Big +Symbol:Symbol_CC-ShareAlike_CopperTop_Big +Symbol:Symbol_CC-ShareAlike_CopperTop_Small +Symbol:Symbol_CreativeCommonsPublicDomain_CopperTop_Small +Symbol:Symbol_CreativeCommonsPublicDomain_SilkScreenTop_Small +Symbol:Symbol_CreativeCommons_CopperTop_Type1_Big +Symbol:Symbol_CreativeCommons_CopperTop_Type2_Big +Symbol:Symbol_CreativeCommons_CopperTop_Type2_Small +Symbol:Symbol_CreativeCommons_SilkScreenTop_Type2_Big +Symbol:Symbol_Danger_18x16mm_Copper +Symbol:Symbol_Danger_8x8mm_Copper +Symbol:Symbol_ESD-Logo-Text_CopperTop +Symbol:Symbol_ESD-Logo_CopperTop +Symbol:Symbol_GNU-GPL_CopperTop_Big +Symbol:Symbol_GNU-GPL_CopperTop_Small +Symbol:Symbol_GNU-Logo_CopperTop +Symbol:Symbol_GNU-Logo_SilkscreenTop +Symbol:Symbol_HighVoltage_NoTriangle_2x5mm_Copper +Symbol:Symbol_HighVoltage_NoTriangle_6x15mm_Copper +Symbol:Symbol_HighVoltage_Triangle_17x15mm_Copper +Symbol:Symbol_HighVoltage_Triangle_6x6mm_Copper +Symbol:Symbol_HighVoltage_Triangle_8x7mm_Copper +Symbol:UKCA-Logo_12x12mm_SilkScreen +Symbol:UKCA-Logo_20x20mm_SilkScreen +Symbol:UKCA-Logo_30x30mm_SilkScreen +Symbol:UKCA-Logo_40x40mm_SilkScreen +Symbol:UKCA-Logo_6x6mm_SilkScreen +Symbol:UKCA-Logo_8x8mm_SilkScreen +Symbol:WEEE-Logo_14x20mm_SilkScreen +Symbol:WEEE-Logo_21x30mm_SilkScreen +Symbol:WEEE-Logo_28.1x40mm_SilkScreen +Symbol:WEEE-Logo_4.2x6mm_SilkScreen +Symbol:WEEE-Logo_5.6x8mm_SilkScreen +Symbol:WEEE-Logo_8.4x12mm_SilkScreen +TerminalBlock:TerminalBlock_Altech_AK300-2_P5.00mm +TerminalBlock:TerminalBlock_Altech_AK300-3_P5.00mm +TerminalBlock:TerminalBlock_Altech_AK300-4_P5.00mm +TerminalBlock:TerminalBlock_bornier-2_P5.08mm +TerminalBlock:TerminalBlock_bornier-3_P5.08mm +TerminalBlock:TerminalBlock_bornier-4_P5.08mm +TerminalBlock:TerminalBlock_bornier-5_P5.08mm +TerminalBlock:TerminalBlock_bornier-6_P5.08mm +TerminalBlock:TerminalBlock_bornier-8_P5.08mm +TerminalBlock:TerminalBlock_Degson_DG246-3.81-03P +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-02P_1x02_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-03P_1x03_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-04P_1x04_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-05P_1x05_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-06P_1x06_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-07P_1x07_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-08P_1x08_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-09P_1x09_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-10P_1x10_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-11P_1x11_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-12P_1x12_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-13P_1x13_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-14P_1x14_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-15P_1x15_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-16P_1x16_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-17P_1x17_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-18P_1x18_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-19P_1x19_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-20P_1x20_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-21P_1x21_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-22P_1x22_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-23P_1x23_P5.00mm +TerminalBlock:TerminalBlock_MaiXu_MX126-5.0-24P_1x24_P5.00mm +TerminalBlock:TerminalBlock_Wuerth_691311400102_P7.62mm +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-10P_1x10_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-11P_1x11_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-12P_1x12_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-13P_1x13_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-14P_1x14_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-15P_1x15_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-16P_1x16_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-17P_1x17_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-18P_1x18_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-19P_1x19_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-20P_1x20_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-21P_1x21_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-22P_1x22_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-23P_1x23_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-2P_1x02_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-3P_1x03_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-4P_1x04_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-5P_1x05_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-6P_1x06_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-7P_1x07_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-8P_1x08_P2.54mm_Horizontal +TerminalBlock:TerminalBlock_Xinya_XY308-2.54-9P_1x09_P2.54mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x02_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x02_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x03_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x03_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x04_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x04_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x05_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x05_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x06_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x06_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x07_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x07_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x08_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x08_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x09_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x09_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x10_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x10_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x11_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x11_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x12_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x12_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x13_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x13_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x14_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x14_P3.50mm_Vertical +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x15_P3.50mm_Horizontal +TerminalBlock_4Ucon:TerminalBlock_4Ucon_1x15_P3.50mm_Vertical +TerminalBlock_Altech:Altech_AK100_1x02_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x03_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x04_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x05_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x06_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x07_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x08_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x09_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x10_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x11_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x12_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x13_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x14_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x15_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x16_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x17_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x18_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x19_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x20_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x21_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x22_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x23_P5.00mm +TerminalBlock_Altech:Altech_AK100_1x24_P5.00mm +TerminalBlock_Altech:Altech_AK300_1x02_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x03_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x04_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x05_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x06_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x07_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x08_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x09_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x10_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x11_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x12_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x13_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x14_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x15_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x16_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x17_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x18_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x19_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x20_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x21_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x22_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x23_P5.00mm_45-Degree +TerminalBlock_Altech:Altech_AK300_1x24_P5.00mm_45-Degree +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-02_1x02_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-03_1x03_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-04_1x04_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-05_1x05_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-06_1x06_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-07_1x07_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-08_1x08_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-09_1x09_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-10_1x10_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-11_1x11_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-12_1x12_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-13_1x13_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-14_1x14_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-15_1x15_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-16_1x16_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-17_1x17_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-18_1x18_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-19_1x19_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-20_1x20_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-21_1x21_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-22_1x22_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-23_1x23_P5.08mm_Horizontal +TerminalBlock_CUI:TerminalBlock_CUI_TB007-508-24_1x24_P5.08mm_Horizontal +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-02_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-03_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-04_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-05_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-06_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-07_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-08_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-09_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-10_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-11_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-12_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-13_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-14_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-15_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-16_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-17_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-18_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-19_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-20_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-21_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-22_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-23_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-24_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-25_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-26_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-27_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-28_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-29_P10.00mm +TerminalBlock_Dinkle:TerminalBlock_Dinkle_DT-55-B01X-30_P10.00mm +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360271_1x01_Horizontal_ScrewM3.0_Boxed +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360272_1x01_Horizontal_ScrewM2.6 +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360273_1x01_Horizontal_ScrewM2.6_WireProtection +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360291_1x01_Horizontal_ScrewM3.0_Boxed +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360322_1x01_Horizontal_ScrewM3.0_WireProtection +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360381_1x01_Horizontal_ScrewM3.0 +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360410_1x01_Horizontal_ScrewM3.0 +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_360425_1x01_Horizontal_ScrewM4.0_Boxed +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05502HBWC_1x02_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05503HBWC_1x03_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05504HBWC_1x04_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05505HBWC_1x05_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type011_RT05506HBWC_1x06_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type055_RT01502HDWU_1x02_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type055_RT01503HDWU_1x03_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type055_RT01504HDWU_1x04_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type059_RT06302HBWC_1x02_P3.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type059_RT06303HBWC_1x03_P3.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type059_RT06304HBWC_1x04_P3.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type059_RT06305HBWC_1x05_P3.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type059_RT06306HBWC_1x06_P3.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type067_RT01902HDWC_1x02_P10.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type067_RT01903HDWC_1x03_P10.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type067_RT01904HDWC_1x04_P10.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type067_RT01905HDWC_1x05_P10.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type073_RT02602HBLU_1x02_P5.08mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type073_RT02603HBLU_1x03_P5.08mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type086_RT03402HBLC_1x02_P3.81mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type086_RT03403HBLC_1x03_P3.81mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type086_RT03404HBLC_1x04_P3.81mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type086_RT03405HBLC_1x05_P3.81mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type086_RT03406HBLC_1x06_P3.81mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type094_RT03502HBLU_1x02_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type094_RT03503HBLU_1x03_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type094_RT03504HBLU_1x04_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type094_RT03505HBLU_1x05_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type094_RT03506HBLU_1x06_P5.00mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type101_RT01602HBWC_1x02_P5.08mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type101_RT01603HBWC_1x03_P5.08mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type101_RT01604HBWC_1x04_P5.08mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type101_RT01605HBWC_1x05_P5.08mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type101_RT01606HBWC_1x06_P5.08mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type171_RT13702HBWC_1x02_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type171_RT13703HBWC_1x03_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type171_RT13704HBWC_1x04_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type171_RT13705HBWC_1x05_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type171_RT13706HBWC_1x06_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type175_RT02702HBLC_1x02_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type175_RT02703HBLC_1x03_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type175_RT02704HBLC_1x04_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type175_RT02705HBLC_1x05_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type175_RT02706HBLC_1x06_P7.50mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type205_RT04502UBLC_1x02_P5.00mm_45Degree +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type205_RT04503UBLC_1x03_P5.00mm_45Degree +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type205_RT04504UBLC_1x04_P5.00mm_45Degree +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type205_RT04505UBLC_1x05_P5.00mm_45Degree +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type205_RT04506UBLC_1x06_P5.00mm_45Degree +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type701_RT11L02HGLU_1x02_P6.35mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type701_RT11L03HGLU_1x03_P6.35mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type703_RT10N02HGLU_1x02_P9.52mm_Horizontal +TerminalBlock_MetzConnect:TerminalBlock_MetzConnect_Type703_RT10N03HGLU_1x03_P9.52mm_Horizontal +TerminalBlock_Philmore:TerminalBlock_Philmore_TB132_1x02_P5.00mm_Horizontal +TerminalBlock_Philmore:TerminalBlock_Philmore_TB133_1x03_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-10-5.08_1x10_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-10_1x10_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-11-5.08_1x11_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-11_1x11_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-12-5.08_1x12_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-12_1x12_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-13-5.08_1x13_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-13_1x13_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-14-5.08_1x14_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-14_1x14_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-15-5.08_1x15_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-15_1x15_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-16-5.08_1x16_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-16_1x16_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2-5.08_1x02_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-3-5.08_1x03_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-3_1x03_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-4-5.08_1x04_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-4_1x04_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-5-5.08_1x05_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-5_1x05_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-6-5.08_1x06_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-6_1x06_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-7-5.08_1x07_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-7_1x07_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-8-5.08_1x08_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-8_1x08_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-9-5.08_1x09_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-9_1x09_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-10-5.08_1x10_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-11-5.08_1x11_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-12-5.08_1x12_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-13-5.08_1x13_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-14-5.08_1x14_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-15-5.08_1x15_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-16-5.08_1x16_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-2-5.08_1x02_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-3-5.08_1x03_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-4-5.08_1x04_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-5-5.08_1x05_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-6-5.08_1x06_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-7-5.08_1x07_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-8-5.08_1x08_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-3-9-5.08_1x09_P5.08mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-10-2.54_1x10_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-11-2.54_1x11_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-12-2.54_1x12_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-2-2.54_1x02_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-3-2.54_1x03_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-4-2.54_1x04_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-5-2.54_1x05_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-6-2.54_1x06_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-7-2.54_1x07_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-8-2.54_1x08_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_MPT-0,5-9-2.54_1x09_P2.54mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-10-3.5-H_1x10_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-10-5.0-H_1x10_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-11-3.5-H_1x11_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-11-5.0-H_1x11_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-12-3.5-H_1x12_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-12-5.0-H_1x12_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-13-3.5-H_1x13_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-13-5.0-H_1x13_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-14-3.5-H_1x14_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-14-5.0-H_1x14_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-15-3.5-H_1x15_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-15-5.0-H_1x15_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-16-3.5-H_1x16_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-16-5.0-H_1x16_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-2-3.5-H_1x02_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-2-5.0-H_1x02_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-3-3.5-H_1x03_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-3-5.0-H_1x03_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-4-3.5-H_1x04_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-4-5.0-H_1x04_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-5-3.5-H_1x05_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-5-5.0-H_1x05_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-6-3.5-H_1x06_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-6-5.0-H_1x06_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-7-3.5-H_1x07_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-7-5.0-H_1x07_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-8-3.5-H_1x08_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-8-5.0-H_1x08_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-9-3.5-H_1x09_P3.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PT-1,5-9-5.0-H_1x09_P5.00mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-2-2,5-V-SMD_1x02-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-2-2.5-H-THR_1x02_P2.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-2-2.5-V-THR_1x02_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-2-HV-2.5-SMD_1x02-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-3-2,5-V-SMD_1x03-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-3-2.5-H-THR_1x03_P2.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-3-2.5-V-THR_1x03_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-3-HV-2.5-SMD_1x03-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-4-2,5-V-SMD_1x04-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-4-2.5-H-THR_1x04_P2.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-4-2.5-V-THR_1x04_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-4-HV-2.5-SMD_1x04-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-5-2,5-V-SMD_1x05-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-5-2.5-H-THR_1x05_P2.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-5-2.5-V-THR_1x05_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-5-HV-2.5-SMD_1x05-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-6-2,5-V-SMD_1x06-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-6-2.5-H-THR_1x06_P2.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-6-2.5-V-THR_1x06_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-6-HV-2.5-SMD_1x06-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-7-2,5-V-SMD_1x07-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-7-2.5-H-THR_1x07_P2.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-7-2.5-V-THR_1x07_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-7-HV-2.5-SMD_1x07-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-8-2,5-V-SMD_1x08-1MP_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-8-2.5-H-THR_1x08_P2.50mm_Horizontal +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-8-2.5-V-THR_1x08_P2.50mm_Vertical +TerminalBlock_Phoenix:TerminalBlock_Phoenix_PTSM-0,5-8-HV-2.5-SMD_1x08-1MP_P2.50mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00001_1x02_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00002_1x03_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00003_1x04_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00004_1x05_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00005_1x06_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00006_1x07_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00007_1x08_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00008_1x09_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00009_1x10_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00010_1x11_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00011_1x12_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00012_1x02_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00013_1x03_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00014_1x04_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00015_1x05_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00016_1x06_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00017_1x07_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00018_1x08_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00019_1x09_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00020_1x10_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00021_1x11_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00022_1x12_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00023_1x02_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00024_1x03_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00025_1x04_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00026_1x05_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00027_1x06_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00028_1x07_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00029_1x08_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00030_1x09_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00031_1x10_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00032_1x11_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00033_1x12_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00045_1x02_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00046_1x03_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00047_1x04_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00048_1x05_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00049_1x06_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00050_1x07_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00051_1x08_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00052_1x09_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00053_1x10_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00054_1x11_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00055_1x12_P5.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00056_1x02_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00057_1x03_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00058_1x04_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00059_1x05_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00060_1x06_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00061_1x07_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00062_1x08_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00063_1x09_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00064_1x10_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00065_1x11_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00066_1x12_P5.00mm_45Degree +TerminalBlock_RND:TerminalBlock_RND_205-00067_1x02_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00068_1x03_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00069_1x04_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00070_1x05_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00071_1x06_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00072_1x07_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00073_1x08_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00074_1x09_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00075_1x10_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00076_1x11_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00077_1x12_P7.50mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00078_1x02_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00079_1x03_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00080_1x04_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00081_1x05_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00082_1x06_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00083_1x07_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00084_1x08_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00085_1x09_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00086_1x10_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00087_1x11_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00088_1x12_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00232_1x02_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00233_1x03_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00234_1x04_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00235_1x05_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00236_1x06_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00237_1x07_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00238_1x08_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00239_1x09_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00240_1x10_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00241_1x02_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00241_1x11_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00242_1x03_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00242_1x12_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00243_1x04_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00244_1x05_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00245_1x06_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00246_1x07_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00247_1x08_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00248_1x09_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00249_1x10_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00250_1x11_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00251_1x12_P10.16mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00276_1x02_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00277_1x03_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00278_1x04_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00279_1x05_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00280_1x06_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00281_1x07_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00282_1x08_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00283_1x09_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00284_1x10_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00285_1x11_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00286_1x12_P5.00mm_Vertical +TerminalBlock_RND:TerminalBlock_RND_205-00287_1x02_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00288_1x03_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00289_1x04_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00290_1x05_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00291_1x06_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00292_1x07_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00293_1x08_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00294_1x09_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00295_1x10_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00296_1x11_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00297_1x12_P5.08mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00298_1x02_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00299_1x03_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00300_1x04_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00301_1x05_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00302_1x06_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00303_1x07_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00304_1x08_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00305_1x09_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00306_1x10_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00307_1x11_P10.00mm_Horizontal +TerminalBlock_RND:TerminalBlock_RND_205-00308_1x12_P10.00mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_1-282834-0_1x10_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_1-282834-1_1x11_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_1-282834-2_1x12_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-2_1x02_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-3_1x03_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-4_1x04_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-5_1x05_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-6_1x06_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-7_1x07_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-8_1x08_P2.54mm_Horizontal +TerminalBlock_TE-Connectivity:TerminalBlock_TE_282834-9_1x09_P2.54mm_Horizontal +TerminalBlock_WAGO:TerminalBlock_WAGO_233-502_2x02_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-503_2x03_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-504_2x04_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-505_2x05_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-506_2x06_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-507_2x07_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-508_2x08_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-509_2x09_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-510_2x10_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_233-512_2x12_P2.54mm +TerminalBlock_WAGO:TerminalBlock_WAGO_236-101_1x01_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-102_1x02_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-103_1x03_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-104_1x04_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-105_1x05_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-106_1x06_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-107_1x07_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-108_1x08_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-109_1x09_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-110_1x10_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-111_1x11_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-112_1x12_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-113_1x13_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-114_1x14_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-115_1x15_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-116_1x16_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-124_1x24_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-136_1x36_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-148_1x48_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-201_1x01_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-202_1x02_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-203_1x03_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-204_1x04_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-205_1x05_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-206_1x06_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-207_1x07_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-208_1x08_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-209_1x09_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-210_1x10_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-211_1x11_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-212_1x12_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-213_1x13_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-214_1x14_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-215_1x15_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-216_1x16_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-224_1x24_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-301_1x01_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-302_1x02_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-303_1x03_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-304_1x04_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-305_1x05_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-306_1x06_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-307_1x07_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-308_1x08_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-309_1x09_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-310_1x10_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-311_1x11_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-312_1x12_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-313_1x13_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-314_1x14_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-315_1x15_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-316_1x16_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-324_1x24_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-401_1x01_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-402_1x02_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-403_1x03_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-404_1x04_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-405_1x05_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-406_1x06_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-407_1x07_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-408_1x08_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-409_1x09_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-410_1x10_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-411_1x11_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-412_1x12_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-413_1x13_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-414_1x14_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-415_1x15_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-416_1x16_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-424_1x24_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-436_1x36_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-448_1x48_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-501_1x01_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-502_1x02_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-503_1x03_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-504_1x04_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-505_1x05_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-506_1x06_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-507_1x07_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-508_1x08_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-509_1x09_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-510_1x10_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-511_1x11_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-512_1x12_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-513_1x13_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-514_1x14_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-515_1x15_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-516_1x16_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-524_1x24_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-601_1x01_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-602_1x02_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-603_1x03_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-604_1x04_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-605_1x05_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-606_1x06_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-607_1x07_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-608_1x08_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-609_1x09_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-610_1x10_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-611_1x11_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-612_1x12_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-613_1x13_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-614_1x14_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-615_1x15_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-616_1x16_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_236-624_1x24_P10.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-101_1x01_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-102_1x02_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-103_1x03_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-104_1x04_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-105_1x05_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-106_1x06_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-107_1x07_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-108_1x08_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-109_1x09_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-110_1x10_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-111_1x11_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-112_1x12_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-113_1x13_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-114_1x14_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-115_1x15_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-116_1x16_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-124_1x24_P5.00mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-301_1x01_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-302_1x02_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-303_1x03_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-304_1x04_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-305_1x05_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-306_1x06_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-307_1x07_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-308_1x08_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-309_1x09_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-310_1x10_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-311_1x11_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-312_1x12_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-316_1x16_P7.50mm_45Degree +TerminalBlock_WAGO:TerminalBlock_WAGO_804-324_1x24_P7.50mm_45Degree +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650073_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650074_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650094_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650173_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650174_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650194_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74650195_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRBU_74655095_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRSH_74651173_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRSH_74651174_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRSH_74651175_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRSH_74651194_THR +TerminalBlock_Wuerth:Wuerth_REDCUBE-THR_WP-THRSH_74651195_THR +TestPoint:TestPoint_2Pads_Pitch2.54mm_Drill0.8mm +TestPoint:TestPoint_2Pads_Pitch5.08mm_Drill1.3mm +TestPoint:TestPoint_Bridge_Pitch2.0mm_Drill0.7mm +TestPoint:TestPoint_Bridge_Pitch2.54mm_Drill0.7mm +TestPoint:TestPoint_Bridge_Pitch2.54mm_Drill1.0mm +TestPoint:TestPoint_Bridge_Pitch2.54mm_Drill1.3mm +TestPoint:TestPoint_Bridge_Pitch3.81mm_Drill1.3mm +TestPoint:TestPoint_Bridge_Pitch5.08mm_Drill0.7mm +TestPoint:TestPoint_Bridge_Pitch5.08mm_Drill1.3mm +TestPoint:TestPoint_Bridge_Pitch6.35mm_Drill1.3mm +TestPoint:TestPoint_Bridge_Pitch7.62mm_Drill1.3mm +TestPoint:TestPoint_Keystone_5000-5004_Miniature +TestPoint:TestPoint_Keystone_5005-5009_Compact +TestPoint:TestPoint_Keystone_5010-5014_Multipurpose +TestPoint:TestPoint_Keystone_5015_Micro_Mini +TestPoint:TestPoint_Keystone_5019_Miniature +TestPoint:TestPoint_Loop_D1.80mm_Drill1.0mm_Beaded +TestPoint:TestPoint_Loop_D2.50mm_Drill1.0mm +TestPoint:TestPoint_Loop_D2.50mm_Drill1.0mm_LowProfile +TestPoint:TestPoint_Loop_D2.50mm_Drill1.85mm +TestPoint:TestPoint_Loop_D2.54mm_Drill1.5mm_Beaded +TestPoint:TestPoint_Loop_D2.60mm_Drill0.9mm_Beaded +TestPoint:TestPoint_Loop_D2.60mm_Drill1.4mm_Beaded +TestPoint:TestPoint_Loop_D2.60mm_Drill1.6mm_Beaded +TestPoint:TestPoint_Loop_D3.50mm_Drill0.9mm_Beaded +TestPoint:TestPoint_Loop_D3.50mm_Drill1.4mm_Beaded +TestPoint:TestPoint_Loop_D3.80mm_Drill2.0mm +TestPoint:TestPoint_Loop_D3.80mm_Drill2.5mm +TestPoint:TestPoint_Loop_D3.80mm_Drill2.8mm +TestPoint:TestPoint_Pad_1.0x1.0mm +TestPoint:TestPoint_Pad_1.5x1.5mm +TestPoint:TestPoint_Pad_2.0x2.0mm +TestPoint:TestPoint_Pad_2.5x2.5mm +TestPoint:TestPoint_Pad_3.0x3.0mm +TestPoint:TestPoint_Pad_4.0x4.0mm +TestPoint:TestPoint_Pad_D1.0mm +TestPoint:TestPoint_Pad_D1.5mm +TestPoint:TestPoint_Pad_D2.0mm +TestPoint:TestPoint_Pad_D2.5mm +TestPoint:TestPoint_Pad_D3.0mm +TestPoint:TestPoint_Pad_D4.0mm +TestPoint:TestPoint_Plated_Hole_D2.0mm +TestPoint:TestPoint_Plated_Hole_D3.0mm +TestPoint:TestPoint_Plated_Hole_D4.0mm +TestPoint:TestPoint_Plated_Hole_D5.0mm +TestPoint:TestPoint_THTPad_1.0x1.0mm_Drill0.5mm +TestPoint:TestPoint_THTPad_1.5x1.5mm_Drill0.7mm +TestPoint:TestPoint_THTPad_2.0x2.0mm_Drill1.0mm +TestPoint:TestPoint_THTPad_2.5x2.5mm_Drill1.2mm +TestPoint:TestPoint_THTPad_3.0x3.0mm_Drill1.5mm +TestPoint:TestPoint_THTPad_4.0x4.0mm_Drill2.0mm +TestPoint:TestPoint_THTPad_D1.0mm_Drill0.5mm +TestPoint:TestPoint_THTPad_D1.5mm_Drill0.7mm +TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm +TestPoint:TestPoint_THTPad_D2.5mm_Drill1.2mm +TestPoint:TestPoint_THTPad_D3.0mm_Drill1.5mm +TestPoint:TestPoint_THTPad_D4.0mm_Drill2.0mm +Transformer_SMD:Pulse_P0926NL +Transformer_SMD:Pulse_PA1323NL +Transformer_SMD:Pulse_PA2001NL +Transformer_SMD:Pulse_PA2002NL-PA2008NL-PA2009NL +Transformer_SMD:Pulse_PA2004NL +Transformer_SMD:Pulse_PA2005NL +Transformer_SMD:Pulse_PA2006NL +Transformer_SMD:Pulse_PA2007NL +Transformer_SMD:Pulse_PA2777NL +Transformer_SMD:Pulse_PA3493NL +Transformer_SMD:Transformer_Coilcraft_CST1 +Transformer_SMD:Transformer_Coilcraft_CST2 +Transformer_SMD:Transformer_Coilcraft_CST2010 +Transformer_SMD:Transformer_CurrentSense_8.4x7.2mm +Transformer_SMD:Transformer_ED8_4-Lead_10.5x8mm_P5mm +Transformer_SMD:Transformer_Ethernet_Bel_S558-5999-T7-F +Transformer_SMD:Transformer_Ethernet_Bourns_PT61017PEL +Transformer_SMD:Transformer_Ethernet_Bourns_PT61020EL +Transformer_SMD:Transformer_Ethernet_Halo_N2_SO-16_7.11x12.7mm +Transformer_SMD:Transformer_Ethernet_Halo_N5_SO-16_7.11x12.7mm +Transformer_SMD:Transformer_Ethernet_Halo_N6_SO-16_7.11x14.73mm +Transformer_SMD:Transformer_Ethernet_HALO_TG111-MSC13 +Transformer_SMD:Transformer_Ethernet_Wuerth_749013011A +Transformer_SMD:Transformer_Ethernet_YDS_30F-51NL_SO-24_7.1x15.1mm +Transformer_SMD:Transformer_MACOM_SM-22 +Transformer_SMD:Transformer_MiniCircuits_AT224-1A +Transformer_SMD:Transformer_Murata_78250JC +Transformer_SMD:Transformer_NF_ETAL_P2781 +Transformer_SMD:Transformer_NF_ETAL_P2781_HandSoldering +Transformer_SMD:Transformer_NF_ETAL_P3000 +Transformer_SMD:Transformer_NF_ETAL_P3000_HandSoldering +Transformer_SMD:Transformer_NF_ETAL_P3181 +Transformer_SMD:Transformer_NF_ETAL_P3181_HandSoldering +Transformer_SMD:Transformer_NF_ETAL_P3188 +Transformer_SMD:Transformer_NF_ETAL_P3188_HandSoldering +Transformer_SMD:Transformer_NF_ETAL_P3191 +Transformer_SMD:Transformer_NF_ETAL_P3191_HandSoldering +Transformer_SMD:Transformer_Pulse_H1100NL +Transformer_SMD:Transformer_Wuerth_750315371 +Transformer_SMD:Transformer_Wurth_WE-AGDT-EP7 +Transformer_THT:Autotransformer_Toroid_1Tap_Horizontal_D10.5mm_Amidon-T37 +Transformer_THT:Autotransformer_Toroid_1Tap_Horizontal_D12.5mm_Amidon-T44 +Transformer_THT:Autotransformer_Toroid_1Tap_Horizontal_D14.0mm_Amidon-T50 +Transformer_THT:Autotransformer_Toroid_1Tap_Horizontal_D9.0mm_Amidon-T30 +Transformer_THT:Autotransformer_ZS1052-AC +Transformer_THT:Transformer_37x44 +Transformer_THT:Transformer_Breve_TEZ-22x24 +Transformer_THT:Transformer_Breve_TEZ-28x33 +Transformer_THT:Transformer_Breve_TEZ-35x42 +Transformer_THT:Transformer_Breve_TEZ-38x45 +Transformer_THT:Transformer_Breve_TEZ-44x52 +Transformer_THT:Transformer_Breve_TEZ-47x57 +Transformer_THT:Transformer_CHK_EI30-2VA_1xSec +Transformer_THT:Transformer_CHK_EI30-2VA_2xSec +Transformer_THT:Transformer_CHK_EI30-2VA_Neutral +Transformer_THT:Transformer_CHK_EI38-3VA_1xSec +Transformer_THT:Transformer_CHK_EI38-3VA_2xSec +Transformer_THT:Transformer_CHK_EI38-3VA_Neutral +Transformer_THT:Transformer_CHK_EI42-5VA_1xSec +Transformer_THT:Transformer_CHK_EI42-5VA_2xSec +Transformer_THT:Transformer_CHK_EI42-5VA_Neutral +Transformer_THT:Transformer_CHK_EI48-10VA_1xSec +Transformer_THT:Transformer_CHK_EI48-10VA_2xSec +Transformer_THT:Transformer_CHK_EI48-10VA_Neutral +Transformer_THT:Transformer_CHK_EI48-8VA_1xSec +Transformer_THT:Transformer_CHK_EI48-8VA_2xSec +Transformer_THT:Transformer_CHK_EI48-8VA_Neutral +Transformer_THT:Transformer_CHK_EI54-12VA_1xSec +Transformer_THT:Transformer_CHK_EI54-12VA_2xSec +Transformer_THT:Transformer_CHK_EI54-12VA_Neutral +Transformer_THT:Transformer_CHK_EI54-16VA_1xSec +Transformer_THT:Transformer_CHK_EI54-16VA_2xSec +Transformer_THT:Transformer_CHK_EI54-16VA_Neutral +Transformer_THT:Transformer_CHK_UI30-4VA_Flat +Transformer_THT:Transformer_CHK_UI39-10VA_Flat +Transformer_THT:Transformer_Coilcraft_Q4434-B_Rhombus-T1311 +Transformer_THT:Transformer_EPCOS_B66359A1013T_Horizontal +Transformer_THT:Transformer_EPCOS_B66359J1014T_Vertical +Transformer_THT:Transformer_Microphone_Lundahl_LL1538 +Transformer_THT:Transformer_Microphone_Lundahl_LL1587 +Transformer_THT:Transformer_Myrra_74040_Horizontal +Transformer_THT:Transformer_Myrra_EF20_7408x +Transformer_THT:Transformer_Myrra_EI30-5_44000_Horizontal +Transformer_THT:Transformer_NF_ETAL_1-1_P1200 +Transformer_THT:Transformer_NF_ETAL_P1165 +Transformer_THT:Transformer_NF_ETAL_P3324 +Transformer_THT:Transformer_NF_ETAL_P3356 +Transformer_THT:Transformer_Toroid_Horizontal_D10.5mm_Amidon-T37 +Transformer_THT:Transformer_Toroid_Horizontal_D12.5mm_Amidon-T44 +Transformer_THT:Transformer_Toroid_Horizontal_D14.0mm_Amidon-T50 +Transformer_THT:Transformer_Toroid_Horizontal_D18.0mm +Transformer_THT:Transformer_Toroid_Horizontal_D9.0mm_Amidon-T30 +Transformer_THT:Transformer_Toroid_Tapped_Horizontal_D10.5mm_Amidon-T37 +Transformer_THT:Transformer_Toroid_Tapped_Horizontal_D12.5mm_Amidon-T44 +Transformer_THT:Transformer_Toroid_Tapped_Horizontal_D14.0mm_Amidon-T50 +Transformer_THT:Transformer_Toroid_Tapped_Horizontal_D9.0mm_Amidon-T30 +Transformer_THT:Transformer_Triad_VPP16-310 +Transformer_THT:Transformer_Wuerth_750343373 +Transformer_THT:Transformer_Wuerth_760871131 +Transformer_THT:Transformer_Zeming_ZMCT103C +Transformer_THT:Transformer_Zeming_ZMPT101K +Transistor_Power:GaN_Systems_GaNPX-3_5x6.6mm_Drain2.93x0.6mm +Transistor_Power:GaN_Systems_GaNPX-3_5x6.6mm_Drain3.76x0.6mm +Transistor_Power:GaN_Systems_GaNPX-4_7x8.4mm +Transistor_Power_Module:Infineon_AG-ECONO2 +Transistor_Power_Module:Infineon_AG-ECONO3 +Transistor_Power_Module:Infineon_AG-ECONO3B +Transistor_Power_Module:Infineon_EasyPACK-1B +Transistor_Power_Module:Infineon_EasyPACK-1B_PressFIT +Transistor_Power_Module:Infineon_EasyPIM-1B +Transistor_Power_Module:Infineon_EasyPIM-2B +Transistor_Power_Module:Littelfuse_Package_H_XBN2MM +Transistor_Power_Module:Littelfuse_Package_H_XN2MM +Transistor_Power_Module:Littelfuse_Package_W_XBN2MM +Transistor_Power_Module:Littelfuse_Package_W_XN2MM +Transistor_Power_Module:ST_ACEPACK-2-CIB +Transistor_Power_Module:ST_ACEPACK-2-CIB_PressFIT +Transistor_Power_Module:ST_SDIP-25L +Valve:Valve_ECC-83-1 +Valve:Valve_ECC-83-2 +Valve:Valve_EURO +Valve:Valve_Glimm +Valve:Valve_Mini_G +Valve:Valve_Mini_P +Valve:Valve_Mini_Pentode_Linear +Valve:Valve_Noval_G +Valve:Valve_Noval_P +Valve:Valve_Octal +Varistor:RV_Disc_D12mm_W3.9mm_P7.5mm +Varistor:RV_Disc_D12mm_W4.2mm_P7.5mm +Varistor:RV_Disc_D12mm_W4.3mm_P7.5mm +Varistor:RV_Disc_D12mm_W4.4mm_P7.5mm +Varistor:RV_Disc_D12mm_W4.5mm_P7.5mm +Varistor:RV_Disc_D12mm_W4.6mm_P7.5mm +Varistor:RV_Disc_D12mm_W4.7mm_P7.5mm +Varistor:RV_Disc_D12mm_W4.8mm_P7.5mm +Varistor:RV_Disc_D12mm_W4mm_P7.5mm +Varistor:RV_Disc_D12mm_W5.1mm_P7.5mm +Varistor:RV_Disc_D12mm_W5.4mm_P7.5mm +Varistor:RV_Disc_D12mm_W5.8mm_P7.5mm +Varistor:RV_Disc_D12mm_W5mm_P7.5mm +Varistor:RV_Disc_D12mm_W6.1mm_P7.5mm +Varistor:RV_Disc_D12mm_W6.2mm_P7.5mm +Varistor:RV_Disc_D12mm_W6.3mm_P7.5mm +Varistor:RV_Disc_D12mm_W6.7mm_P7.5mm +Varistor:RV_Disc_D12mm_W7.1mm_P7.5mm +Varistor:RV_Disc_D12mm_W7.5mm_P7.5mm +Varistor:RV_Disc_D12mm_W7.9mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W11mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W3.9mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.2mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.3mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.4mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.5mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.6mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.7mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.8mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4.9mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W4mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W5.2mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W5.4mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W5.9mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W5mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W6.1mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W6.3mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W6.4mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W6.8mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W7.2mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W7.5mm_P7.5mm +Varistor:RV_Disc_D15.5mm_W8mm_P7.5mm +Varistor:RV_Disc_D16.5mm_W6.7mm_P7.5mm +Varistor:RV_Disc_D21.5mm_W11.4mm_P10mm +Varistor:RV_Disc_D21.5mm_W4.3mm_P10mm +Varistor:RV_Disc_D21.5mm_W4.4mm_P10mm +Varistor:RV_Disc_D21.5mm_W4.5mm_P10mm +Varistor:RV_Disc_D21.5mm_W4.6mm_P10mm +Varistor:RV_Disc_D21.5mm_W4.7mm_P10mm +Varistor:RV_Disc_D21.5mm_W4.8mm_P10mm +Varistor:RV_Disc_D21.5mm_W4.9mm_P10mm +Varistor:RV_Disc_D21.5mm_W5.1mm_P10mm +Varistor:RV_Disc_D21.5mm_W5.3mm_P10mm +Varistor:RV_Disc_D21.5mm_W5.4mm_P10mm +Varistor:RV_Disc_D21.5mm_W5.6mm_P10mm +Varistor:RV_Disc_D21.5mm_W5.8mm_P10mm +Varistor:RV_Disc_D21.5mm_W5mm_P10mm +Varistor:RV_Disc_D21.5mm_W6.1mm_P7.5mm +Varistor:RV_Disc_D21.5mm_W6.3mm_P10mm +Varistor:RV_Disc_D21.5mm_W6.5mm_P10mm +Varistor:RV_Disc_D21.5mm_W6.7mm_P10mm +Varistor:RV_Disc_D21.5mm_W6.8mm_P10mm +Varistor:RV_Disc_D21.5mm_W7.1mm_P10mm +Varistor:RV_Disc_D21.5mm_W7.5mm_P10mm +Varistor:RV_Disc_D21.5mm_W7.9mm_P10mm +Varistor:RV_Disc_D21.5mm_W8.4mm_P10mm +Varistor:RV_Disc_D7mm_W3.4mm_P5mm +Varistor:RV_Disc_D7mm_W3.5mm_P5mm +Varistor:RV_Disc_D7mm_W3.6mm_P5mm +Varistor:RV_Disc_D7mm_W3.7mm_P5mm +Varistor:RV_Disc_D7mm_W3.8mm_P5mm +Varistor:RV_Disc_D7mm_W3.9mm_P5mm +Varistor:RV_Disc_D7mm_W4.2mm_P5mm +Varistor:RV_Disc_D7mm_W4.3mm_P5mm +Varistor:RV_Disc_D7mm_W4.5mm_P5mm +Varistor:RV_Disc_D7mm_W4.8mm_P5mm +Varistor:RV_Disc_D7mm_W4.9mm_P5mm +Varistor:RV_Disc_D7mm_W4mm_P5mm +Varistor:RV_Disc_D7mm_W5.1mm_P5mm +Varistor:RV_Disc_D7mm_W5.4mm_P5mm +Varistor:RV_Disc_D7mm_W5.5mm_P5mm +Varistor:RV_Disc_D7mm_W5.7mm_P5mm +Varistor:RV_Disc_D9mm_W3.3mm_P5mm +Varistor:RV_Disc_D9mm_W3.4mm_P5mm +Varistor:RV_Disc_D9mm_W3.5mm_P5mm +Varistor:RV_Disc_D9mm_W3.6mm_P5mm +Varistor:RV_Disc_D9mm_W3.7mm_P5mm +Varistor:RV_Disc_D9mm_W3.8mm_P5mm +Varistor:RV_Disc_D9mm_W3.9mm_P5mm +Varistor:RV_Disc_D9mm_W4.1mm_P5mm +Varistor:RV_Disc_D9mm_W4.2mm_P5mm +Varistor:RV_Disc_D9mm_W4.4mm_P5mm +Varistor:RV_Disc_D9mm_W4.5mm_P5mm +Varistor:RV_Disc_D9mm_W4.8mm_P5mm +Varistor:RV_Disc_D9mm_W4mm_P5mm +Varistor:RV_Disc_D9mm_W5.2mm_P5mm +Varistor:RV_Disc_D9mm_W5.4mm_P5mm +Varistor:RV_Disc_D9mm_W5.5mm_P5mm +Varistor:RV_Disc_D9mm_W5.7mm_P5mm +Varistor:RV_Disc_D9mm_W6.1mm_P5mm +Varistor:RV_Rect_V25S440P_L26.5mm_W8.2mm_P12.7mm +Varistor:Varistor_Panasonic_VF diff --git a/public/kicad/symbols.txt b/public/kicad/symbols.txt new file mode 100644 index 00000000..1cee1692 --- /dev/null +++ b/public/kicad/symbols.txt @@ -0,0 +1,21807 @@ +# This file contains all the KiCad symbols available in the official library +# Generated by symbols.sh +# on Sun Feb 16 21:42:01 CET 2025 +4xxx:14528 +4xxx:14529 +4xxx:14538 +4xxx:4001 +4xxx:4002 +4xxx:4009 +4xxx:4010 +4xxx:40106 +4xxx:4011 +4xxx:4012 +4xxx:4013 +4xxx:4016 +4xxx:4017 +4xxx:4020 +4xxx:4021 +4xxx:4022 +4xxx:4023 +4xxx:4025 +4xxx:4027 +4xxx:4028 +4xxx:4029 +4xxx:4040 +4xxx:4046 +4xxx:4047 +4xxx:4049 +4xxx:4050 +4xxx:4051 +4xxx:4052 +4xxx:4053 +4xxx:4056 +4xxx:4060 +4xxx:4066 +4xxx:4069 +4xxx:4070 +4xxx:4071 +4xxx:4072 +4xxx:4073 +4xxx:4075 +4xxx:4077 +4xxx:4081 +4xxx:4098 +4xxx:4504 +4xxx:4510 +4xxx:4518 +4xxx:4520 +4xxx:4528 +4xxx:4538 +4xxx:4543 +4xxx:CD4033B +4xxx:HEF4093B +4xxx:HEF4094B +4xxx_IEEE:4001 +4xxx_IEEE:4002 +4xxx_IEEE:4006 +4xxx_IEEE:4008 +4xxx_IEEE:4009 +4xxx_IEEE:4010 +4xxx_IEEE:40104 +4xxx_IEEE:40106 +4xxx_IEEE:4011 +4xxx_IEEE:40110 +4xxx_IEEE:4012 +4xxx_IEEE:4013 +4xxx_IEEE:4014 +4xxx_IEEE:4015 +4xxx_IEEE:4016 +4xxx_IEEE:40160 +4xxx_IEEE:40161 +4xxx_IEEE:40162 +4xxx_IEEE:40163 +4xxx_IEEE:4017 +4xxx_IEEE:40174 +4xxx_IEEE:40175 +4xxx_IEEE:4018 +4xxx_IEEE:4019 +4xxx_IEEE:40192 +4xxx_IEEE:40193 +4xxx_IEEE:40194 +4xxx_IEEE:4020 +4xxx_IEEE:4021 +4xxx_IEEE:4022 +4xxx_IEEE:4023 +4xxx_IEEE:4024 +4xxx_IEEE:40240 +4xxx_IEEE:40244 +4xxx_IEEE:40245 +4xxx_IEEE:4025 +4xxx_IEEE:4027 +4xxx_IEEE:4028 +4xxx_IEEE:4029 +4xxx_IEEE:4030 +4xxx_IEEE:40373 +4xxx_IEEE:40374 +4xxx_IEEE:4040 +4xxx_IEEE:4041 +4xxx_IEEE:4042 +4xxx_IEEE:4043 +4xxx_IEEE:4044 +4xxx_IEEE:4046 +4xxx_IEEE:4048 +4xxx_IEEE:4049 +4xxx_IEEE:4050 +4xxx_IEEE:4051 +4xxx_IEEE:4052 +4xxx_IEEE:4053 +4xxx_IEEE:4060 +4xxx_IEEE:4066 +4xxx_IEEE:4068 +4xxx_IEEE:4069 +4xxx_IEEE:4070 +4xxx_IEEE:4071 +4xxx_IEEE:4072 +4xxx_IEEE:4073 +4xxx_IEEE:4075 +4xxx_IEEE:4077 +4xxx_IEEE:4078 +4xxx_IEEE:4081 +4xxx_IEEE:4082 +4xxx_IEEE:4093 +4xxx_IEEE:4095 +4xxx_IEEE:4096 +4xxx_IEEE:4099 +4xxx_IEEE:4104 +4xxx_IEEE:4160 +4xxx_IEEE:4161 +4xxx_IEEE:4162 +4xxx_IEEE:4163 +4xxx_IEEE:4174 +4xxx_IEEE:4175 +4xxx_IEEE:4502 +4xxx_IEEE:4504 +4xxx_IEEE:4507 +4xxx_IEEE:4508 +4xxx_IEEE:4510 +4xxx_IEEE:4511 +4xxx_IEEE:4512 +4xxx_IEEE:4514 +4xxx_IEEE:4515 +4xxx_IEEE:4518 +4xxx_IEEE:4520 +4xxx_IEEE:4528 +4xxx_IEEE:4529 +4xxx_IEEE:4530 +4xxx_IEEE:4538 +4xxx_IEEE:4539 +4xxx_IEEE:4543 +4xxx_IEEE:4555 +4xxx_IEEE:4556 +4xxx_IEEE:4584 +4xxx_IEEE:4585 +74xGxx:74AHC1G00 +74xGxx:74AHC1G02 +74xGxx:74AHC1G04 +74xGxx:74AHC1G08 +74xGxx:74AHC1G125 +74xGxx:74AHC1G126 +74xGxx:74AHC1G14 +74xGxx:74AHC1G32 +74xGxx:74AHC1G4210 +74xGxx:74AHC1G86 +74xGxx:74AHC1GU04 +74xGxx:74AHC2G00 +74xGxx:74AHCT1G00 +74xGxx:74AHCT1G02 +74xGxx:74AHCT1G04 +74xGxx:74AHCT1G08 +74xGxx:74AHCT1G125 +74xGxx:74AHCT1G126 +74xGxx:74AHCT1G14 +74xGxx:74AHCT1G32 +74xGxx:74AHCT1G86 +74xGxx:74AHCT1GU04 +74xGxx:74AHCT2G00 +74xGxx:74AUC1G00 +74xGxx:74AUC1G02 +74xGxx:74AUC1G04 +74xGxx:74AUC1G06 +74xGxx:74AUC1G07 +74xGxx:74AUC1G08 +74xGxx:74AUC1G125 +74xGxx:74AUC1G126 +74xGxx:74AUC1G14 +74xGxx:74AUC1G17 +74xGxx:74AUC1G18 +74xGxx:74AUC1G19 +74xGxx:74AUC1G240 +74xGxx:74AUC1G32 +74xGxx:74AUC1G66 +74xGxx:74AUC1G74 +74xGxx:74AUC1G79 +74xGxx:74AUC1G80 +74xGxx:74AUC1G86 +74xGxx:74AUC1GU04 +74xGxx:74AUC2G00 +74xGxx:74AUC2G02 +74xGxx:74AUC2G04 +74xGxx:74AUC2G06 +74xGxx:74AUC2G07 +74xGxx:74AUC2G08 +74xGxx:74AUC2G125 +74xGxx:74AUC2G126 +74xGxx:74AUC2G240 +74xGxx:74AUC2G241 +74xGxx:74AUC2G32 +74xGxx:74AUC2G34 +74xGxx:74AUC2G53 +74xGxx:74AUC2G66 +74xGxx:74AUC2G79 +74xGxx:74AUC2G80 +74xGxx:74AUC2G86 +74xGxx:74AUC2GU04 +74xGxx:74AUP1G00 +74xGxx:74AUP1G02 +74xGxx:74AUP1G04 +74xGxx:74AUP1G06 +74xGxx:74AUP1G07 +74xGxx:74AUP1G08 +74xGxx:74AUP1G125 +74xGxx:74AUP1G126 +74xGxx:74AUP1G14 +74xGxx:74AUP1G17 +74xGxx:74AUP1G240 +74xGxx:74AUP1G32 +74xGxx:74AUP1G34 +74xGxx:74AUP1G57 +74xGxx:74AUP1G58 +74xGxx:74AUP1G74 +74xGxx:74AUP1G79 +74xGxx:74AUP1G80 +74xGxx:74AUP1G97 +74xGxx:74AUP1G98 +74xGxx:74AUP1G99 +74xGxx:74AUP1GU04 +74xGxx:74CB3T1G125 +74xGxx:74CBT1G125 +74xGxx:74CBT1G384 +74xGxx:74CBTD1G125 +74xGxx:74CBTD1G384 +74xGxx:74CBTLV1G125 +74xGxx:74LVC1G00 +74xGxx:74LVC1G02 +74xGxx:74LVC1G04 +74xGxx:74LVC1G06 +74xGxx:74LVC1G07 +74xGxx:74LVC1G08 +74xGxx:74LVC1G0832 +74xGxx:74LVC1G10 +74xGxx:74LVC1G11 +74xGxx:74LVC1G123 +74xGxx:74LVC1G125 +74xGxx:74LVC1G126 +74xGxx:74LVC1G139 +74xGxx:74LVC1G14 +74xGxx:74LVC1G17 +74xGxx:74LVC1G175 +74xGxx:74LVC1G18 +74xGxx:74LVC1G19 +74xGxx:74LVC1G240 +74xGxx:74LVC1G27 +74xGxx:74LVC1G29 +74xGxx:74LVC1G3157 +74xGxx:74LVC1G32 +74xGxx:74LVC1G3208 +74xGxx:74LVC1G332 +74xGxx:74LVC1G34 +74xGxx:74LVC1G373 +74xGxx:74LVC1G374 +74xGxx:74LVC1G38 +74xGxx:74LVC1G386 +74xGxx:74LVC1G57 +74xGxx:74LVC1G58 +74xGxx:74LVC1G66 +74xGxx:74LVC1G79 +74xGxx:74LVC1G80 +74xGxx:74LVC1G86 +74xGxx:74LVC1G97 +74xGxx:74LVC1G98 +74xGxx:74LVC1G99 +74xGxx:74LVC1GU04 +74xGxx:74LVC1GU04DRL +74xGxx:74LVC2G00 +74xGxx:74LVC2G02 +74xGxx:74LVC2G04 +74xGxx:74LVC2G06 +74xGxx:74LVC2G07 +74xGxx:74LVC2G08 +74xGxx:74LVC2G125 +74xGxx:74LVC2G126 +74xGxx:74LVC2G14 +74xGxx:74LVC2G157 +74xGxx:74LVC2G17 +74xGxx:74LVC2G240 +74xGxx:74LVC2G241 +74xGxx:74LVC2G32 +74xGxx:74LVC2G34 +74xGxx:74LVC2G38 +74xGxx:74LVC2G53 +74xGxx:74LVC2G66 +74xGxx:74LVC2G74 +74xGxx:74LVC2G79 +74xGxx:74LVC2G80 +74xGxx:74LVC2G86 +74xGxx:74LVC2GU04 +74xGxx:74LVC3G04 +74xGxx:74LVC3G06 +74xGxx:74LVC3G07 +74xGxx:74LVC3G14 +74xGxx:74LVC3G17 +74xGxx:74LVC3G34 +74xGxx:74LVC3GU04 +74xGxx:Inverter_Schmitt_Dual +74xGxx:NC7SVU04P5X +74xGxx:NC7SZ125M5X +74xGxx:NC7SZ125P5X +74xGxx:SN74LVC1G00DBV +74xGxx:SN74LVC1G00DCK +74xGxx:SN74LVC1G00DRL +74xGxx:SN74LVC1G125DBV +74xGxx:SN74LVC1G125DCK +74xGxx:SN74LVC1G125DRL +74xGxx:SN74LVC1G14DBV +74xGxx:SN74LVC1G14DRL +74xGxx:SN74LVC2G14DBV +74xGxx:TC7PZ14FU +74xx:7400 +74xx:7402 +74xx:74469 +74xx:7454 +74xx:74AHC04 +74xx:74AHC240 +74xx:74AHC244 +74xx:74AHC273 +74xx:74AHC373 +74xx:74AHC374 +74xx:74AHC541 +74xx:74AHC595 +74xx:74AHCT04 +74xx:74AHCT123 +74xx:74AHCT125 +74xx:74AHCT240 +74xx:74AHCT244 +74xx:74AHCT273 +74xx:74AHCT373 +74xx:74AHCT374 +74xx:74AHCT541 +74xx:74AHCT595 +74xx:74ALVC164245 +74xx:74CB3Q16210DGG +74xx:74CB3Q16210DGV +74xx:74CB3Q16210DL +74xx:74CB3T16210DGG +74xx:74CB3T16210DGV +74xx:74CBT16210CDGG +74xx:74CBT16210CDGV +74xx:74CBT16210CDL +74xx:74CBT3861 +74xx:74CBTD16210DGG +74xx:74CBTD16210DGV +74xx:74CBTD16210DL +74xx:74CBTD3861 +74xx:74CBTLV16212 +74xx:74CBTLV3257 +74xx:74CBTLV3861 +74xx:74HC00 +74xx:74HC02 +74xx:74HC04 +74xx:74HC123 +74xx:74HC137 +74xx:74HC138 +74xx:74HC14 +74xx:74HC164 +74xx:74HC165 +74xx:74HC173 +74xx:74HC192 +74xx:74HC193 +74xx:74HC237 +74xx:74HC238 +74xx:74HC240 +74xx:74HC244 +74xx:74HC245 +74xx:74HC273 +74xx:74HC373 +74xx:74HC374 +74xx:74HC4024 +74xx:74HC4051 +74xx:74HC4060 +74xx:74HC590 +74xx:74HC590A +74xx:74HC594 +74xx:74HC595 +74xx:74HC596 +74xx:74HC688 +74xx:74HC7014 +74xx:74HC74 +74xx:74HC85 +74xx:74HC86 +74xx:74HCT00 +74xx:74HCT02 +74xx:74HCT04 +74xx:74HCT123 +74xx:74HCT137 +74xx:74HCT138 +74xx:74HCT164 +74xx:74HCT173 +74xx:74HCT193 +74xx:74HCT237 +74xx:74HCT238 +74xx:74HCT240 +74xx:74HCT244 +74xx:74HCT273 +74xx:74HCT373 +74xx:74HCT374 +74xx:74HCT4051 +74xx:74HCT541 +74xx:74HCT574 +74xx:74HCT595 +74xx:74HCT596 +74xx:74HCT688 +74xx:74HCT74 +74xx:74HCT85 +74xx:74LCX07 +74xx:74LS00 +74xx:74LS01 +74xx:74LS02 +74xx:74LS03 +74xx:74LS04 +74xx:74LS05 +74xx:74LS06 +74xx:74LS06N +74xx:74LS07 +74xx:74LS08 +74xx:74LS09 +74xx:74LS10 +74xx:74LS107 +74xx:74LS109 +74xx:74LS11 +74xx:74LS112 +74xx:74LS113 +74xx:74LS114 +74xx:74LS12 +74xx:74LS121 +74xx:74LS122 +74xx:74LS123 +74xx:74LS125 +74xx:74LS126 +74xx:74LS13 +74xx:74LS132 +74xx:74LS133 +74xx:74LS136 +74xx:74LS137 +74xx:74LS138 +74xx:74LS139 +74xx:74LS14 +74xx:74LS145 +74xx:74LS147 +74xx:74LS148 +74xx:74LS15 +74xx:74LS151 +74xx:74LS153 +74xx:74LS154 +74xx:74LS155 +74xx:74LS156 +74xx:74LS157 +74xx:74LS158 +74xx:74LS160 +74xx:74LS161 +74xx:74LS162 +74xx:74LS163 +74xx:74LS165 +74xx:74LS166 +74xx:74LS168 +74xx:74LS169 +74xx:74LS170 +74xx:74LS173 +74xx:74LS174 +74xx:74LS175 +74xx:74LS181 +74xx:74LS182 +74xx:74LS190 +74xx:74LS191 +74xx:74LS192 +74xx:74LS193 +74xx:74LS194 +74xx:74LS195 +74xx:74LS196 +74xx:74LS197 +74xx:74LS20 +74xx:74LS21 +74xx:74LS22 +74xx:74LS221 +74xx:74LS240 +74xx:74LS240_Split +74xx:74LS241 +74xx:74LS241_Split +74xx:74LS242 +74xx:74LS243 +74xx:74LS244 +74xx:74LS244_Split +74xx:74LS245 +74xx:74LS246 +74xx:74LS247 +74xx:74LS248 +74xx:74LS249 +74xx:74LS251 +74xx:74LS253 +74xx:74LS256 +74xx:74LS257 +74xx:74LS258 +74xx:74LS259 +74xx:74LS26 +74xx:74LS27 +74xx:74LS273 +74xx:74LS279 +74xx:74LS28 +74xx:74LS280 +74xx:74LS283 +74xx:74LS290 +74xx:74LS293 +74xx:74LS295 +74xx:74LS298 +74xx:74LS299 +74xx:74LS30 +74xx:74LS32 +74xx:74LS322 +74xx:74LS323 +74xx:74LS33 +74xx:74LS348 +74xx:74LS352 +74xx:74LS353 +74xx:74LS365 +74xx:74LS366 +74xx:74LS367 +74xx:74LS368 +74xx:74LS37 +74xx:74LS373 +74xx:74LS374 +74xx:74LS375 +74xx:74LS377 +74xx:74LS378 +74xx:74LS379 +74xx:74LS38 +74xx:74LS385 +74xx:74LS386 +74xx:74LS390 +74xx:74LS393 +74xx:74LS395 +74xx:74LS398 +74xx:74LS399 +74xx:74LS40 +74xx:74LS42 +74xx:74LS46 +74xx:74LS47 +74xx:74LS48 +74xx:74LS49 +74xx:74LS51 +74xx:74LS540 +74xx:74LS541 +74xx:74LS54N +74xx:74LS55 +74xx:74LS573 +74xx:74LS574 +74xx:74LS590 +74xx:74LS595 +74xx:74LS596 +74xx:74LS629 +74xx:74LS670 +74xx:74LS688 +74xx:74LS73 +74xx:74LS74 +74xx:74LS75 +74xx:74LS76 +74xx:74LS77 +74xx:74LS78 +74xx:74LS83 +74xx:74LS85 +74xx:74LS86 +74xx:74LS90 +74xx:74LS91 +74xx:74LS92 +74xx:74LS93 +74xx:74LS95 +74xx:74LV14 +74xx:74LV8154 +74xx:74LVC125 +74xx:74VHC9164FT +74xx:CD74AC238 +74xx:CD74HC4067M +74xx:CD74HC4067SM +74xx:MC74LCX16245DT +74xx:MM74C923 +74xx:SN74ALVC164245DGG +74xx:SN74ALVC164245DL +74xx:SN74AVC16827DGGR +74xx:SN74CB3Q3384ADBQ +74xx:SN74CB3Q3384APW +74xx:SN74LS07 +74xx:SN74LS07N +74xx:SN74LV4T125 +74xx_IEEE:7400 +74xx_IEEE:7401 +74xx_IEEE:7402 +74xx_IEEE:7403 +74xx_IEEE:7404 +74xx_IEEE:7405 +74xx_IEEE:7406 +74xx_IEEE:7407 +74xx_IEEE:7408 +74xx_IEEE:7409 +74xx_IEEE:7410 +74xx_IEEE:7411 +74xx_IEEE:7412 +74xx_IEEE:74125 +74xx_IEEE:74126 +74xx_IEEE:74128 +74xx_IEEE:7413 +74xx_IEEE:74132 +74xx_IEEE:74136 +74xx_IEEE:7414 +74xx_IEEE:74141 +74xx_IEEE:74145 +74xx_IEEE:74147 +74xx_IEEE:74148 +74xx_IEEE:74151 +74xx_IEEE:74153 +74xx_IEEE:74154 +74xx_IEEE:74155 +74xx_IEEE:74156 +74xx_IEEE:74157 +74xx_IEEE:74158 +74xx_IEEE:74159 +74xx_IEEE:7416 +74xx_IEEE:74164 +74xx_IEEE:74165 +74xx_IEEE:74166 +74xx_IEEE:7417 +74xx_IEEE:74173 +74xx_IEEE:74176 +74xx_IEEE:74196 +74xx_IEEE:7420 +74xx_IEEE:7421 +74xx_IEEE:7422 +74xx_IEEE:74246 +74xx_IEEE:74247 +74xx_IEEE:74248 +74xx_IEEE:74249 +74xx_IEEE:7425 +74xx_IEEE:74251 +74xx_IEEE:74253 +74xx_IEEE:7426 +74xx_IEEE:7427 +74xx_IEEE:74278 +74xx_IEEE:7428 +74xx_IEEE:74293 +74xx_IEEE:7430 +74xx_IEEE:7432 +74xx_IEEE:7433 +74xx_IEEE:7437 +74xx_IEEE:7438 +74xx_IEEE:7439 +74xx_IEEE:7440 +74xx_IEEE:7442 +74xx_IEEE:74425 +74xx_IEEE:74426 +74xx_IEEE:7443 +74xx_IEEE:7444 +74xx_IEEE:7445 +74xx_IEEE:7446 +74xx_IEEE:7447 +74xx_IEEE:7448 +74xx_IEEE:7451 +74xx_IEEE:7454 +74xx_IEEE:7483 +74xx_IEEE:7485 +74xx_IEEE:7486 +74xx_IEEE:7490 +74xx_IEEE:7491 +74xx_IEEE:7492 +74xx_IEEE:7493 +74xx_IEEE:7495 +74xx_IEEE:7496 +74xx_IEEE:74HC237 +74xx_IEEE:74HC238 +74xx_IEEE:74HC36 +74xx_IEEE:74HC804 +74xx_IEEE:74HC805 +74xx_IEEE:74HC808 +74xx_IEEE:74HC832 +74xx_IEEE:74LS133 +74xx_IEEE:74LS137 +74xx_IEEE:74LS138 +74xx_IEEE:74LS139 +74xx_IEEE:74LS15 +74xx_IEEE:74LS152 +74xx_IEEE:74LS160 +74xx_IEEE:74LS161 +74xx_IEEE:74LS162 +74xx_IEEE:74LS163 +74xx_IEEE:74LS168 +74xx_IEEE:74LS169 +74xx_IEEE:74LS170 +74xx_IEEE:74LS177 +74xx_IEEE:74LS18 +74xx_IEEE:74LS19 +74xx_IEEE:74LS190 +74xx_IEEE:74LS191 +74xx_IEEE:74LS192 +74xx_IEEE:74LS193 +74xx_IEEE:74LS194 +74xx_IEEE:74LS195 +74xx_IEEE:74LS197 +74xx_IEEE:74LS239 +74xx_IEEE:74LS24 +74xx_IEEE:74LS240 +74xx_IEEE:74LS241 +74xx_IEEE:74LS242 +74xx_IEEE:74LS243 +74xx_IEEE:74LS244 +74xx_IEEE:74LS245 +74xx_IEEE:74LS257 +74xx_IEEE:74LS258 +74xx_IEEE:74LS266 +74xx_IEEE:74LS280 +74xx_IEEE:74LS283 +74xx_IEEE:74LS290 +74xx_IEEE:74LS295 +74xx_IEEE:74LS298 +74xx_IEEE:74LS299 +74xx_IEEE:74LS323 +74xx_IEEE:74LS347 +74xx_IEEE:74LS348 +74xx_IEEE:74LS352 +74xx_IEEE:74LS353 +74xx_IEEE:74LS365 +74xx_IEEE:74LS366 +74xx_IEEE:74LS367 +74xx_IEEE:74LS368 +74xx_IEEE:74LS386 +74xx_IEEE:74LS390 +74xx_IEEE:74LS395 +74xx_IEEE:74LS396 +74xx_IEEE:74LS398 +74xx_IEEE:74LS399 +74xx_IEEE:74LS445 +74xx_IEEE:74LS447 +74xx_IEEE:74LS465 +74xx_IEEE:74LS466 +74xx_IEEE:74LS467 +74xx_IEEE:74LS468 +74xx_IEEE:74LS49 +74xx_IEEE:74LS540 +74xx_IEEE:74LS541 +74xx_IEEE:74LS55 +74xx_IEEE:74LS56 +74xx_IEEE:74LS57 +74xx_IEEE:74LS590 +74xx_IEEE:74LS591 +74xx_IEEE:74LS594 +74xx_IEEE:74LS595 +74xx_IEEE:74LS596 +74xx_IEEE:74LS597 +74xx_IEEE:74LS599 +74xx_IEEE:74LS620 +74xx_IEEE:74LS621 +74xx_IEEE:74LS622 +74xx_IEEE:74LS623 +74xx_IEEE:74LS638 +74xx_IEEE:74LS639 +74xx_IEEE:74LS640 +74xx_IEEE:74LS641 +74xx_IEEE:74LS642 +74xx_IEEE:74LS645 +74xx_IEEE:74LS668 +74xx_IEEE:74LS669 +74xx_IEEE:74LS670 +74xx_IEEE:74LS682 +74xx_IEEE:74LS683 +74xx_IEEE:74LS684 +74xx_IEEE:74LS685 +74xx_IEEE:74LS686 +74xx_IEEE:74LS687 +74xx_IEEE:74LS688 +74xx_IEEE:74LS689 +74xx_IEEE:74S140 +Amplifier_Audio:IR4301 +Amplifier_Audio:IR4302 +Amplifier_Audio:IR4311 +Amplifier_Audio:IR4312 +Amplifier_Audio:IR4321 +Amplifier_Audio:IR4322 +Amplifier_Audio:IRS2052M +Amplifier_Audio:IRS2092 +Amplifier_Audio:IRS2092S +Amplifier_Audio:IRS2093M +Amplifier_Audio:IRS20957S +Amplifier_Audio:IRS20965S +Amplifier_Audio:IRS2452AM +Amplifier_Audio:IS31AP4991-GRLS2 +Amplifier_Audio:IS31AP4991-SLS2 +Amplifier_Audio:LM1875 +Amplifier_Audio:LM1876 +Amplifier_Audio:LM1877 +Amplifier_Audio:LM2876 +Amplifier_Audio:LM380N +Amplifier_Audio:LM380N-8 +Amplifier_Audio:LM384 +Amplifier_Audio:LM386 +Amplifier_Audio:LM3886 +Amplifier_Audio:LM4752TS +Amplifier_Audio:LM4755TS +Amplifier_Audio:LM4766 +Amplifier_Audio:LM4810 +Amplifier_Audio:LM4811 +Amplifier_Audio:LM4950TA +Amplifier_Audio:LM4950TS +Amplifier_Audio:LM4990ITL +Amplifier_Audio:LM4990LD +Amplifier_Audio:LM4990MH +Amplifier_Audio:LM4990MM +Amplifier_Audio:LME49600 +Amplifier_Audio:MA12040 +Amplifier_Audio:MA12040P +Amplifier_Audio:MA12070 +Amplifier_Audio:MA12070P +Amplifier_Audio:MAX9701xTG +Amplifier_Audio:MAX9715xTE+ +Amplifier_Audio:MAX9744 +Amplifier_Audio:MAX9814 +Amplifier_Audio:MAX98306xDT +Amplifier_Audio:MAX98396EWB+ +Amplifier_Audio:MAX9850xTI +Amplifier_Audio:OPA1622 +Amplifier_Audio:PAM8301 +Amplifier_Audio:PAM8302AAD +Amplifier_Audio:PAM8302AAS +Amplifier_Audio:PAM8302AAY +Amplifier_Audio:PAM8403D +Amplifier_Audio:PAM8406D +Amplifier_Audio:SSM2017P +Amplifier_Audio:SSM2018 +Amplifier_Audio:SSM2120 +Amplifier_Audio:SSM2122 +Amplifier_Audio:SSM2165 +Amplifier_Audio:SSM2167 +Amplifier_Audio:SSM2211CP +Amplifier_Audio:SSM2211S +Amplifier_Audio:STK433_Sanyo +Amplifier_Audio:STK435_Sanyo +Amplifier_Audio:STK436_Sanyo +Amplifier_Audio:STK437_Sanyo +Amplifier_Audio:STK439_Sanyo +Amplifier_Audio:STK441_Sanyo +Amplifier_Audio:STK443_Sanyo +Amplifier_Audio:Si8241BB +Amplifier_Audio:Si8241CB +Amplifier_Audio:Si8244BB +Amplifier_Audio:Si8244CB +Amplifier_Audio:TAS5825MRHB +Amplifier_Audio:TDA1308 +Amplifier_Audio:TDA2003 +Amplifier_Audio:TDA2005 +Amplifier_Audio:TDA2030 +Amplifier_Audio:TDA2050 +Amplifier_Audio:TDA7052A +Amplifier_Audio:TDA7264 +Amplifier_Audio:TDA7265 +Amplifier_Audio:TDA7265B +Amplifier_Audio:TDA7266 +Amplifier_Audio:TDA7266D +Amplifier_Audio:TDA7266M +Amplifier_Audio:TDA7266P +Amplifier_Audio:TDA7269A +Amplifier_Audio:TDA7292 +Amplifier_Audio:TDA7293 +Amplifier_Audio:TDA7294 +Amplifier_Audio:TDA7295 +Amplifier_Audio:TDA7296 +Amplifier_Audio:TDA7297 +Amplifier_Audio:TDA7496 +Amplifier_Audio:TFA9879HN +Amplifier_Audio:THAT151xx08 +Amplifier_Audio:THAT2180 +Amplifier_Audio:THAT2181 +Amplifier_Audio:TPA3251 +Amplifier_Audio:TPA6110A2DGN +Amplifier_Audio:TPA6132A2RTE +Amplifier_Audio:TPA6203A1DGN +Amplifier_Audio:TPA6203A1DRB +Amplifier_Buffer:BUF602xD +Amplifier_Buffer:BUF602xDBV +Amplifier_Buffer:BUF634AxD +Amplifier_Buffer:BUF634AxDDA +Amplifier_Buffer:BUF634AxDRB +Amplifier_Buffer:BUF634U +Amplifier_Buffer:EL2001CN +Amplifier_Buffer:LH0002H +Amplifier_Buffer:LM6321H +Amplifier_Buffer:LM6321M +Amplifier_Buffer:LM6321N +Amplifier_Current:AD8202 +Amplifier_Current:AD8203 +Amplifier_Current:AD8205 +Amplifier_Current:AD8206 +Amplifier_Current:AD8208 +Amplifier_Current:AD8209 +Amplifier_Current:AD8210 +Amplifier_Current:AD8211 +Amplifier_Current:AD8212 +Amplifier_Current:AD8213 +Amplifier_Current:AD8215 +Amplifier_Current:AD8216 +Amplifier_Current:AD8217 +Amplifier_Current:AD8218xCP +Amplifier_Current:AD8218xRM +Amplifier_Current:AD8219 +Amplifier_Current:AD8417 +Amplifier_Current:AD8418 +Amplifier_Current:BQ500100DCK +Amplifier_Current:INA138 +Amplifier_Current:INA139 +Amplifier_Current:INA168 +Amplifier_Current:INA169 +Amplifier_Current:INA180A1 +Amplifier_Current:INA180A2 +Amplifier_Current:INA180A3 +Amplifier_Current:INA180A4 +Amplifier_Current:INA180B1 +Amplifier_Current:INA180B2 +Amplifier_Current:INA180B3 +Amplifier_Current:INA180B4 +Amplifier_Current:INA181 +Amplifier_Current:INA185 +Amplifier_Current:INA193 +Amplifier_Current:INA194 +Amplifier_Current:INA195 +Amplifier_Current:INA196 +Amplifier_Current:INA197 +Amplifier_Current:INA198 +Amplifier_Current:INA199xxDCK +Amplifier_Current:INA200D +Amplifier_Current:INA200DGK +Amplifier_Current:INA201D +Amplifier_Current:INA201DGK +Amplifier_Current:INA202D +Amplifier_Current:INA202DGK +Amplifier_Current:INA225 +Amplifier_Current:INA240A1D +Amplifier_Current:INA240A1PW +Amplifier_Current:INA240A2D +Amplifier_Current:INA240A2PW +Amplifier_Current:INA240A3D +Amplifier_Current:INA240A3PW +Amplifier_Current:INA240A4D +Amplifier_Current:INA240A4PW +Amplifier_Current:INA241A1xD +Amplifier_Current:INA241A1xDDF +Amplifier_Current:INA241A1xDGK +Amplifier_Current:INA241A2xD +Amplifier_Current:INA241A2xDDF +Amplifier_Current:INA241A2xDGK +Amplifier_Current:INA241A3xD +Amplifier_Current:INA241A3xDDF +Amplifier_Current:INA241A3xDGK +Amplifier_Current:INA241A4xD +Amplifier_Current:INA241A4xDDF +Amplifier_Current:INA241A4xDGK +Amplifier_Current:INA241A5xD +Amplifier_Current:INA241A5xDDF +Amplifier_Current:INA241A5xDGK +Amplifier_Current:INA241B1xD +Amplifier_Current:INA241B1xDDF +Amplifier_Current:INA241B1xDGK +Amplifier_Current:INA241B2xD +Amplifier_Current:INA241B2xDDF +Amplifier_Current:INA241B2xDGK +Amplifier_Current:INA241B3xD +Amplifier_Current:INA241B3xDDF +Amplifier_Current:INA241B3xDGK +Amplifier_Current:INA241B4xD +Amplifier_Current:INA241B4xDDF +Amplifier_Current:INA241B4xDGK +Amplifier_Current:INA241B5xD +Amplifier_Current:INA241B5xDDF +Amplifier_Current:INA241B5xDGK +Amplifier_Current:INA253 +Amplifier_Current:INA281A1 +Amplifier_Current:INA281A2 +Amplifier_Current:INA281A3 +Amplifier_Current:INA281A4 +Amplifier_Current:INA281A5 +Amplifier_Current:INA282 +Amplifier_Current:INA283 +Amplifier_Current:INA284 +Amplifier_Current:INA285 +Amplifier_Current:INA286 +Amplifier_Current:INA293A1 +Amplifier_Current:INA293A2 +Amplifier_Current:INA293A3 +Amplifier_Current:INA293A4 +Amplifier_Current:INA293A5 +Amplifier_Current:INA293B1 +Amplifier_Current:INA293B2 +Amplifier_Current:INA293B3 +Amplifier_Current:INA293B4 +Amplifier_Current:INA293B5 +Amplifier_Current:INA4180A1 +Amplifier_Current:INA4180A2 +Amplifier_Current:INA4180A3 +Amplifier_Current:INA4180A4 +Amplifier_Current:LMP8640 +Amplifier_Current:LT6106 +Amplifier_Current:LTC6102HVxDD +Amplifier_Current:LTC6102HVxMS8 +Amplifier_Current:LTC6102xDD +Amplifier_Current:LTC6102xDD-1 +Amplifier_Current:LTC6102xMS8 +Amplifier_Current:LTC6102xMS8-1 +Amplifier_Current:MAX4080F +Amplifier_Current:MAX4080S +Amplifier_Current:MAX4080T +Amplifier_Current:MAX4081F +Amplifier_Current:MAX4081S +Amplifier_Current:MAX4081T +Amplifier_Current:MAX471 +Amplifier_Current:MAX472 +Amplifier_Current:NCS210 +Amplifier_Current:NCS211 +Amplifier_Current:NCS213 +Amplifier_Current:NCS214 +Amplifier_Current:NCV210 +Amplifier_Current:NCV211 +Amplifier_Current:NCV213 +Amplifier_Current:NCV214 +Amplifier_Current:ZXCT1009F +Amplifier_Current:ZXCT1009T8 +Amplifier_Current:ZXCT1010 +Amplifier_Current:ZXCT1107 +Amplifier_Current:ZXCT1109 +Amplifier_Current:ZXCT1110 +Amplifier_Difference:AD628 +Amplifier_Difference:AD8207 +Amplifier_Difference:AD8276 +Amplifier_Difference:AD8475ACPZ +Amplifier_Difference:AD8475xRMZ +Amplifier_Difference:ADA4938-1 +Amplifier_Difference:ADA4940-1xCP +Amplifier_Difference:ADA4940-2 +Amplifier_Difference:AMC1100DWV +Amplifier_Difference:AMC1200BDWV +Amplifier_Difference:AMC1300BDWV +Amplifier_Difference:AMC1300DWV +Amplifier_Difference:INA105KP +Amplifier_Difference:INA105KU +Amplifier_Difference:LM733CH +Amplifier_Difference:LM733CN +Amplifier_Difference:LM733H +Amplifier_Difference:LTC1992-x-xMS8 +Amplifier_Difference:THS4521ID +Amplifier_Difference:THS4521IDGK +Amplifier_Difference:THS4551xRGT +Amplifier_Instrumentation:AD620 +Amplifier_Instrumentation:AD623 +Amplifier_Instrumentation:AD623AN +Amplifier_Instrumentation:AD623ANZ +Amplifier_Instrumentation:AD623AR +Amplifier_Instrumentation:AD623ARM +Amplifier_Instrumentation:AD623ARMZ +Amplifier_Instrumentation:AD623ARZ +Amplifier_Instrumentation:AD623BN +Amplifier_Instrumentation:AD623BNZ +Amplifier_Instrumentation:AD623BR +Amplifier_Instrumentation:AD623BRZ +Amplifier_Instrumentation:AD8230 +Amplifier_Instrumentation:AD8231 +Amplifier_Instrumentation:AD8236 +Amplifier_Instrumentation:AD8236ARMZ +Amplifier_Instrumentation:AD8421 +Amplifier_Instrumentation:AD8421ARMZ +Amplifier_Instrumentation:AD8421ARZ +Amplifier_Instrumentation:AD8421BRMZ +Amplifier_Instrumentation:AD8421BRZ +Amplifier_Instrumentation:AD8422 +Amplifier_Instrumentation:AD8422ARMZ +Amplifier_Instrumentation:AD8422ARZ +Amplifier_Instrumentation:AD8422BRMZ +Amplifier_Instrumentation:AD8422BRZ +Amplifier_Instrumentation:AD8429 +Amplifier_Instrumentation:AD8429ARZ +Amplifier_Instrumentation:AD8429BRZ +Amplifier_Instrumentation:INA128 +Amplifier_Instrumentation:INA129 +Amplifier_Instrumentation:INA326 +Amplifier_Instrumentation:INA327 +Amplifier_Instrumentation:INA333xxDGK +Amplifier_Instrumentation:INA333xxDRG +Amplifier_Instrumentation:INA849D +Amplifier_Instrumentation:INA849DGK +Amplifier_Instrumentation:LTC1100xN8 +Amplifier_Instrumentation:LTC1100xSW +Amplifier_Operational:AD797 +Amplifier_Operational:AD8001AN +Amplifier_Operational:AD8001AR +Amplifier_Operational:AD8015 +Amplifier_Operational:AD8021AR +Amplifier_Operational:AD8021ARM +Amplifier_Operational:AD817 +Amplifier_Operational:AD8603 +Amplifier_Operational:AD8606ARM +Amplifier_Operational:AD8606ARZ +Amplifier_Operational:AD8610xR +Amplifier_Operational:AD8610xRM +Amplifier_Operational:AD8620 +Amplifier_Operational:AD8620xRM +Amplifier_Operational:AD8655 +Amplifier_Operational:AD8656 +Amplifier_Operational:AD8676xR +Amplifier_Operational:ADA4075-2 +Amplifier_Operational:ADA4077-1xR +Amplifier_Operational:ADA4077-1xRM +Amplifier_Operational:ADA4084-4xCP +Amplifier_Operational:ADA4099-1xUJ +Amplifier_Operational:ADA4099-2xCP +Amplifier_Operational:ADA4099-2xR +Amplifier_Operational:ADA4099-2xRM +Amplifier_Operational:ADA4522-1 +Amplifier_Operational:ADA4522-2 +Amplifier_Operational:ADA4522-4 +Amplifier_Operational:ADA4530-1 +Amplifier_Operational:ADA4610-1xR +Amplifier_Operational:ADA4610-1xRJ +Amplifier_Operational:ADA4610-2xCP +Amplifier_Operational:ADA4610-2xR +Amplifier_Operational:ADA4610-2xRM +Amplifier_Operational:ADA4610-4xCP +Amplifier_Operational:ADA4610-4xR +Amplifier_Operational:ADA4622-2xCP +Amplifier_Operational:ADA4622-4xCP +Amplifier_Operational:ADA4625-1ARDZ +Amplifier_Operational:ADA4625-2ARDZ +Amplifier_Operational:ADA4807-1 +Amplifier_Operational:ADA4807-2ACP +Amplifier_Operational:ADA4807-2ARM +Amplifier_Operational:ADA4807-4ARUZ +Amplifier_Operational:ADA4817-1ACP +Amplifier_Operational:ADA4817-1ARD +Amplifier_Operational:ADA4817-2ACP +Amplifier_Operational:ADA4841-1YRJ +Amplifier_Operational:ADA4870ARRZ +Amplifier_Operational:ADA4898-1YRDZ +Amplifier_Operational:ADA4898-2 +Amplifier_Operational:AS13704 +Amplifier_Operational:CA3080 +Amplifier_Operational:CA3080A +Amplifier_Operational:CA3130 +Amplifier_Operational:CA3140 +Amplifier_Operational:HMC799LP3E +Amplifier_Operational:L272 +Amplifier_Operational:L272D +Amplifier_Operational:L272M +Amplifier_Operational:LF155 +Amplifier_Operational:LF156 +Amplifier_Operational:LF256 +Amplifier_Operational:LF257 +Amplifier_Operational:LF351D +Amplifier_Operational:LF351N +Amplifier_Operational:LF355 +Amplifier_Operational:LF356 +Amplifier_Operational:LF357 +Amplifier_Operational:LM101 +Amplifier_Operational:LM13600 +Amplifier_Operational:LM13700 +Amplifier_Operational:LM201 +Amplifier_Operational:LM2902 +Amplifier_Operational:LM2904 +Amplifier_Operational:LM301 +Amplifier_Operational:LM318H +Amplifier_Operational:LM318J +Amplifier_Operational:LM318M +Amplifier_Operational:LM318N +Amplifier_Operational:LM321 +Amplifier_Operational:LM324 +Amplifier_Operational:LM324A +Amplifier_Operational:LM358 +Amplifier_Operational:LM358_DFN +Amplifier_Operational:LM4250 +Amplifier_Operational:LM4562 +Amplifier_Operational:LM6142xIx +Amplifier_Operational:LM6144xIx +Amplifier_Operational:LM6171D +Amplifier_Operational:LM6171xxN +Amplifier_Operational:LM6172 +Amplifier_Operational:LM6361 +Amplifier_Operational:LM675 +Amplifier_Operational:LM7171xIM +Amplifier_Operational:LM7171xIN +Amplifier_Operational:LM7332 +Amplifier_Operational:LM741 +Amplifier_Operational:LM8261 +Amplifier_Operational:LMC6062 +Amplifier_Operational:LMC6082 +Amplifier_Operational:LMC6482 +Amplifier_Operational:LMC6484 +Amplifier_Operational:LMH6551MA +Amplifier_Operational:LMH6551MM +Amplifier_Operational:LMH6609MA +Amplifier_Operational:LMH6609MF +Amplifier_Operational:LMH6611 +Amplifier_Operational:LMH6702MA +Amplifier_Operational:LMH6702MF +Amplifier_Operational:LMH6733 +Amplifier_Operational:LMV321 +Amplifier_Operational:LMV324 +Amplifier_Operational:LMV358 +Amplifier_Operational:LMV601 +Amplifier_Operational:LOG114AxRGV +Amplifier_Operational:LPV811DBV +Amplifier_Operational:LPV812DGK +Amplifier_Operational:LT1012 +Amplifier_Operational:LT1363 +Amplifier_Operational:LT1492 +Amplifier_Operational:LT1493 +Amplifier_Operational:LT6015xS5 +Amplifier_Operational:LT6230xS6 +Amplifier_Operational:LT6234 +Amplifier_Operational:LT6237 +Amplifier_Operational:LTC1151CN8 +Amplifier_Operational:LTC1151CSW +Amplifier_Operational:LTC1152 +Amplifier_Operational:LTC6081xDD +Amplifier_Operational:LTC6081xMS8 +Amplifier_Operational:LTC6082xDHC +Amplifier_Operational:LTC6082xGN +Amplifier_Operational:LTC6228xDC +Amplifier_Operational:LTC6228xS6 +Amplifier_Operational:LTC6228xS8 +Amplifier_Operational:LTC6229xDD +Amplifier_Operational:LTC6229xMS8E +Amplifier_Operational:LTC6253xMS8 +Amplifier_Operational:LTC6268xS6-10 +Amplifier_Operational:LTC6268xS8-10 +Amplifier_Operational:LTC6269xDD +Amplifier_Operational:LTC6269xMS8E +Amplifier_Operational:LTC6362xDD +Amplifier_Operational:LTC6362xMS8 +Amplifier_Operational:MAX4238ASA +Amplifier_Operational:MAX4238AUT +Amplifier_Operational:MAX4239ASA +Amplifier_Operational:MAX4239AUT +Amplifier_Operational:MAX4395ESD +Amplifier_Operational:MAX4395EUD +Amplifier_Operational:MC33078 +Amplifier_Operational:MC33079 +Amplifier_Operational:MC33172 +Amplifier_Operational:MC33174 +Amplifier_Operational:MC33178 +Amplifier_Operational:MC33179 +Amplifier_Operational:MCP6001-OT +Amplifier_Operational:MCP6001R +Amplifier_Operational:MCP6001U +Amplifier_Operational:MCP6001x-LT +Amplifier_Operational:MCP6002-xMC +Amplifier_Operational:MCP6002-xMS +Amplifier_Operational:MCP6002-xP +Amplifier_Operational:MCP6002-xSN +Amplifier_Operational:MCP6004 +Amplifier_Operational:MCP601-xOT +Amplifier_Operational:MCP601-xP +Amplifier_Operational:MCP601-xSN +Amplifier_Operational:MCP601-xST +Amplifier_Operational:MCP601R +Amplifier_Operational:MCP602 +Amplifier_Operational:MCP6022 +Amplifier_Operational:MCP603-xCH +Amplifier_Operational:MCP603-xP +Amplifier_Operational:MCP603-xSN +Amplifier_Operational:MCP603-xST +Amplifier_Operational:MCP604 +Amplifier_Operational:MCP6401RT-xOT +Amplifier_Operational:MCP6401T-xLT +Amplifier_Operational:MCP6401T-xOT +Amplifier_Operational:MCP6401UT-xOT +Amplifier_Operational:MCP6L01Rx-xOT +Amplifier_Operational:MCP6L01Ux-xOT +Amplifier_Operational:MCP6L01x-xLT +Amplifier_Operational:MCP6L01x-xOT +Amplifier_Operational:MCP6L02x-xMS +Amplifier_Operational:MCP6L02x-xSN +Amplifier_Operational:MCP6L04-xST +Amplifier_Operational:MCP6L04x-xSL +Amplifier_Operational:MCP6L91RT-EOT +Amplifier_Operational:MCP6L91T-EOT +Amplifier_Operational:MCP6L92 +Amplifier_Operational:MCP6L94 +Amplifier_Operational:MCP6V67EMS +Amplifier_Operational:MCP6V67xMNY +Amplifier_Operational:NCS20071SN +Amplifier_Operational:NCS20071XV +Amplifier_Operational:NCS20072D +Amplifier_Operational:NCS20072DM +Amplifier_Operational:NCS20072DTB +Amplifier_Operational:NCS20074D +Amplifier_Operational:NCS20074DTB +Amplifier_Operational:NCS2325D +Amplifier_Operational:NCS2325DM +Amplifier_Operational:NCS325 +Amplifier_Operational:NCS4325 +Amplifier_Operational:NE5532 +Amplifier_Operational:NE5534 +Amplifier_Operational:NJM2043 +Amplifier_Operational:NJM2114 +Amplifier_Operational:NJM4556A +Amplifier_Operational:NJM4558 +Amplifier_Operational:NJM4559 +Amplifier_Operational:NJM4560 +Amplifier_Operational:NJM4580 +Amplifier_Operational:NJM5532 +Amplifier_Operational:OP07 +Amplifier_Operational:OP1177AR +Amplifier_Operational:OP1177ARM +Amplifier_Operational:OP179GRT +Amplifier_Operational:OP179GS +Amplifier_Operational:OP249 +Amplifier_Operational:OP249GS +Amplifier_Operational:OP275 +Amplifier_Operational:OP279 +Amplifier_Operational:OP77 +Amplifier_Operational:OPA121KM +Amplifier_Operational:OPA121KP +Amplifier_Operational:OPA121KU +Amplifier_Operational:OPA134 +Amplifier_Operational:OPA1602 +Amplifier_Operational:OPA1604 +Amplifier_Operational:OPA1612AxD +Amplifier_Operational:OPA1641 +Amplifier_Operational:OPA1655D +Amplifier_Operational:OPA1655DBV +Amplifier_Operational:OPA1656ID +Amplifier_Operational:OPA1678 +Amplifier_Operational:OPA1679 +Amplifier_Operational:OPA1692xD +Amplifier_Operational:OPA1692xDGK +Amplifier_Operational:OPA188xxD +Amplifier_Operational:OPA188xxDBV +Amplifier_Operational:OPA196xD +Amplifier_Operational:OPA196xDBV +Amplifier_Operational:OPA196xDGK +Amplifier_Operational:OPA197xD +Amplifier_Operational:OPA197xDBV +Amplifier_Operational:OPA197xDGK +Amplifier_Operational:OPA2134 +Amplifier_Operational:OPA2156xD +Amplifier_Operational:OPA2156xDGK +Amplifier_Operational:OPA2196xD +Amplifier_Operational:OPA2196xDGK +Amplifier_Operational:OPA2197xD +Amplifier_Operational:OPA2197xDGK +Amplifier_Operational:OPA2277 +Amplifier_Operational:OPA2325 +Amplifier_Operational:OPA2333xxD +Amplifier_Operational:OPA2333xxDGK +Amplifier_Operational:OPA2333xxDRB +Amplifier_Operational:OPA2340 +Amplifier_Operational:OPA2356xxD +Amplifier_Operational:OPA2356xxDGK +Amplifier_Operational:OPA2376xxD +Amplifier_Operational:OPA2376xxDGK +Amplifier_Operational:OPA2376xxYZD +Amplifier_Operational:OPA2604AP +Amplifier_Operational:OPA2691 +Amplifier_Operational:OPA2691-14 +Amplifier_Operational:OPA2695xD +Amplifier_Operational:OPA2695xRGT +Amplifier_Operational:OPA2890ID +Amplifier_Operational:OPA2890IDGS +Amplifier_Operational:OPA2994xD +Amplifier_Operational:OPA310SxDBV +Amplifier_Operational:OPA310SxDCK +Amplifier_Operational:OPA310xDBV +Amplifier_Operational:OPA310xDCK +Amplifier_Operational:OPA330xxD +Amplifier_Operational:OPA330xxDBV +Amplifier_Operational:OPA330xxDCK +Amplifier_Operational:OPA330xxYFF +Amplifier_Operational:OPA333xxD +Amplifier_Operational:OPA333xxDBV +Amplifier_Operational:OPA333xxDCK +Amplifier_Operational:OPA336Nx +Amplifier_Operational:OPA336Ux +Amplifier_Operational:OPA340NA +Amplifier_Operational:OPA340P +Amplifier_Operational:OPA340UA +Amplifier_Operational:OPA355NA +Amplifier_Operational:OPA356xxD +Amplifier_Operational:OPA356xxDBV +Amplifier_Operational:OPA365xxD +Amplifier_Operational:OPA365xxDBV +Amplifier_Operational:OPA376xxD +Amplifier_Operational:OPA376xxDBV +Amplifier_Operational:OPA376xxDCK +Amplifier_Operational:OPA4134 +Amplifier_Operational:OPA4196xD +Amplifier_Operational:OPA4196xPW +Amplifier_Operational:OPA4197xD +Amplifier_Operational:OPA4197xPW +Amplifier_Operational:OPA4340EA +Amplifier_Operational:OPA4340UA +Amplifier_Operational:OPA4376 +Amplifier_Operational:OPA551P +Amplifier_Operational:OPA551U +Amplifier_Operational:OPA552P +Amplifier_Operational:OPA552U +Amplifier_Operational:OPA569DWP +Amplifier_Operational:OPA690xD +Amplifier_Operational:OPA690xDBV +Amplifier_Operational:OPA810xD +Amplifier_Operational:OPA810xDBV +Amplifier_Operational:OPA810xDCK +Amplifier_Operational:OPA818xDRG +Amplifier_Operational:OPA842xD +Amplifier_Operational:OPA842xDBV +Amplifier_Operational:OPA843xD +Amplifier_Operational:OPA843xDBV +Amplifier_Operational:OPA846xD +Amplifier_Operational:OPA846xDBV +Amplifier_Operational:OPA847xD +Amplifier_Operational:OPA847xDBV +Amplifier_Operational:OPA855xDSG +Amplifier_Operational:OPA858xDSG +Amplifier_Operational:OPA859xDSG +Amplifier_Operational:OPA890xD +Amplifier_Operational:OPA890xDBV +Amplifier_Operational:RC4558 +Amplifier_Operational:RC4560 +Amplifier_Operational:RC4580 +Amplifier_Operational:SA5532 +Amplifier_Operational:SA5534 +Amplifier_Operational:THS3491xDDA +Amplifier_Operational:THS4631D +Amplifier_Operational:THS4631DDA +Amplifier_Operational:THS4631DGN +Amplifier_Operational:TL061 +Amplifier_Operational:TL062 +Amplifier_Operational:TL064 +Amplifier_Operational:TL071 +Amplifier_Operational:TL072 +Amplifier_Operational:TL074 +Amplifier_Operational:TL081 +Amplifier_Operational:TL082 +Amplifier_Operational:TL084 +Amplifier_Operational:TLC272 +Amplifier_Operational:TLC274 +Amplifier_Operational:TLC277 +Amplifier_Operational:TLC279 +Amplifier_Operational:TLC27M2xD +Amplifier_Operational:TLC27M2xPS +Amplifier_Operational:TLC27M2xPW +Amplifier_Operational:TLC27M7xD +Amplifier_Operational:TLC27M7xPS +Amplifier_Operational:TLE2141ACD +Amplifier_Operational:TLE2141ACP +Amplifier_Operational:TLE2141AID +Amplifier_Operational:TLE2141AIP +Amplifier_Operational:TLE2141CD +Amplifier_Operational:TLE2141CP +Amplifier_Operational:TLE2141ID +Amplifier_Operational:TLE2141IP +Amplifier_Operational:TLE2141MD +Amplifier_Operational:TLV172IDCK +Amplifier_Operational:TLV2371D +Amplifier_Operational:TLV2371DBV +Amplifier_Operational:TLV2371P +Amplifier_Operational:TLV2372 +Amplifier_Operational:TLV6001DCK +Amplifier_Operational:TLV9001IDCK +Amplifier_Operational:TLV9004xRUCR +Amplifier_Operational:TLV9061xDBV +Amplifier_Operational:TLV9061xDCK +Amplifier_Operational:TLV9061xDPW +Amplifier_Operational:TLV9062 +Amplifier_Operational:TLV9062xD +Amplifier_Operational:TLV9062xDSG +Amplifier_Operational:TLV9064 +Amplifier_Operational:TLV9064xRTE +Amplifier_Operational:TLV9301xDBV +Amplifier_Operational:TLV9301xDCK +Amplifier_Operational:TLV9302xD +Amplifier_Operational:TLV9302xDDF +Amplifier_Operational:TLV9302xDGK +Amplifier_Operational:TLV9302xPW +Amplifier_Operational:TLV9304xD +Amplifier_Operational:TLV9304xPW +Amplifier_Operational:TS881xCx +Amplifier_Operational:TS881xLx +Amplifier_Operational:TS912 +Amplifier_Operational:TSV524xIQ4T +Amplifier_Operational:TSV911IDT +Amplifier_Operational:TSV911RILT +Amplifier_Operational:TSV911xxLx +Amplifier_Operational:TSV912IDT +Amplifier_Operational:TSV912IQ2T +Amplifier_Operational:TSV912IST +Amplifier_Operational:TSV914 +Amplifier_Operational:TSV991AILT +Amplifier_Operational:TSV991AIQ1T +Amplifier_Operational:TSV994 +Amplifier_Video:AD813 +Amplifier_Video:MAX453 +Amplifier_Video:THS7374 +Analog:AD5593R +Analog:AD630ARZ +Analog:AD637xQ +Analog:AD637xRZ +Analog:AD654JN +Analog:AD654JR +Analog:LF398H +Analog:LF398_DIP8 +Analog:LF398_SOIC14 +Analog:LF398_SOIC8 +Analog:LM231N +Analog:LM331N +Analog:LTC1966 +Analog:LTC1967 +Analog:LTC1968 +Analog:MLX90314xDF +Analog:MLX90320xFR +Analog:MPY634KP +Analog:MPY634KU +Analog:PGA112 +Analog:PGA113 +Analog_ADC:AD40xxBCPZ +Analog_ADC:AD40xxBRMZ +Analog_ADC:AD574A +Analog_ADC:AD6644 +Analog_ADC:AD6645 +Analog_ADC:AD7171 +Analog_ADC:AD7298 +Analog_ADC:AD7321 +Analog_ADC:AD7322 +Analog_ADC:AD7323 +Analog_ADC:AD7324 +Analog_ADC:AD7327 +Analog_ADC:AD7328 +Analog_ADC:AD7329 +Analog_ADC:AD7606 +Analog_ADC:AD7606-4 +Analog_ADC:AD7606-6 +Analog_ADC:AD7616 +Analog_ADC:AD7682BCP +Analog_ADC:AD7689xCP +Analog_ADC:AD7699BCP +Analog_ADC:AD7722 +Analog_ADC:AD7745 +Analog_ADC:AD7746 +Analog_ADC:AD7794 +Analog_ADC:AD7795 +Analog_ADC:AD7819 +Analog_ADC:AD7949BCP +Analog_ADC:AD9280ARS +Analog_ADC:AD9283 +Analog_ADC:ADC0800 +Analog_ADC:ADC08060 +Analog_ADC:ADC081C021CIMM +Analog_ADC:ADC0832 +Analog_ADC:ADC101C021CIMK +Analog_ADC:ADC101C021CIMM +Analog_ADC:ADC1173 +Analog_ADC:ADC121C021CIMM +Analog_ADC:ADC1283 +Analog_ADC:ADC128D818 +Analog_ADC:ADS1013IDGS +Analog_ADC:ADS1014IDGS +Analog_ADC:ADS1015IDGS +Analog_ADC:ADS1018IDGS +Analog_ADC:ADS1110 +Analog_ADC:ADS1113IDGS +Analog_ADC:ADS1114IDGS +Analog_ADC:ADS1115IDGS +Analog_ADC:ADS1118IDGS +Analog_ADC:ADS1120-PW +Analog_ADC:ADS1120-RVA +Analog_ADC:ADS1220xPW +Analog_ADC:ADS1232IPW +Analog_ADC:ADS1234IPW +Analog_ADC:ADS1243 +Analog_ADC:ADS1251 +Analog_ADC:ADS127L01IPBS +Analog_ADC:ADS1298xPAG +Analog_ADC:ADS7029 +Analog_ADC:ADS7039 +Analog_ADC:ADS7040xDCU +Analog_ADC:ADS7041xDCU +Analog_ADC:ADS7042xDCU +Analog_ADC:ADS7043xDCU +Analog_ADC:ADS7044xDCU +Analog_ADC:ADS7049 +Analog_ADC:ADS7828 +Analog_ADC:ADS7866 +Analog_ADC:ADS7867 +Analog_ADC:ADS7868 +Analog_ADC:ADS8681RUM +Analog_ADC:ADS8684 +Analog_ADC:ADS8685RUM +Analog_ADC:ADS8688 +Analog_ADC:ADS8689RUM +Analog_ADC:AMC3336 +Analog_ADC:CA3300 +Analog_ADC:HX711 +Analog_ADC:ICL7106CPL +Analog_ADC:ICL7107CPL +Analog_ADC:INA234AxYBJ +Analog_ADC:LTC1406CGN +Analog_ADC:LTC1406IGN +Analog_ADC:LTC1594CS +Analog_ADC:LTC1594IS +Analog_ADC:LTC1598CG +Analog_ADC:LTC1598IG +Analog_ADC:LTC1742 +Analog_ADC:LTC1744 +Analog_ADC:LTC1746 +Analog_ADC:LTC1748 +Analog_ADC:LTC1864 +Analog_ADC:LTC1864L +Analog_ADC:LTC1865-MS +Analog_ADC:LTC1865-S8 +Analog_ADC:LTC1865L-MS +Analog_ADC:LTC1865L-S8 +Analog_ADC:LTC2282xUP +Analog_ADC:LTC2284xUP +Analog_ADC:LTC2290xUP +Analog_ADC:LTC2291xUP +Analog_ADC:LTC2292xUP +Analog_ADC:LTC2293xUP +Analog_ADC:LTC2294xUP +Analog_ADC:LTC2295xUP +Analog_ADC:LTC2296xUP +Analog_ADC:LTC2297xUP +Analog_ADC:LTC2298xUP +Analog_ADC:LTC2299xUP +Analog_ADC:LTC2309xF +Analog_ADC:LTC2309xUF +Analog_ADC:LTC2311-16 +Analog_ADC:LTC2325-16 +Analog_ADC:LTC2358-16 +Analog_ADC:LTC2358-18 +Analog_ADC:LTC2451xDDB +Analog_ADC:LTC2451xTS8 +Analog_ADC:LTC2508CDKD-32 +Analog_ADC:LTC2508IDKD-32 +Analog_ADC:MAX1112 +Analog_ADC:MAX11120xTI +Analog_ADC:MAX11121xTI +Analog_ADC:MAX11122xTI +Analog_ADC:MAX11123xTI +Analog_ADC:MAX11124xTI +Analog_ADC:MAX11125xTI +Analog_ADC:MAX11126xTI +Analog_ADC:MAX11127xTI +Analog_ADC:MAX11128xTI +Analog_ADC:MAX1113 +Analog_ADC:MAX11612 +Analog_ADC:MAX11613 +Analog_ADC:MAX11614 +Analog_ADC:MAX11615 +Analog_ADC:MAX11616 +Analog_ADC:MAX11617 +Analog_ADC:MAX1248 +Analog_ADC:MAX1249 +Analog_ADC:MAX1274 +Analog_ADC:MAX1275 +Analog_ADC:MCP3002 +Analog_ADC:MCP3004 +Analog_ADC:MCP3008 +Analog_ADC:MCP3201 +Analog_ADC:MCP3202 +Analog_ADC:MCP3204 +Analog_ADC:MCP3208 +Analog_ADC:MCP3221 +Analog_ADC:MCP3301 +Analog_ADC:MCP3421A0T-ECH +Analog_ADC:MCP3422Axx-xMS +Analog_ADC:MCP3422Axx-xSN +Analog_ADC:MCP3423x-xUN +Analog_ADC:MCP3424x-xSL +Analog_ADC:MCP3424x-xST +Analog_ADC:MCP3425Axx-xCH +Analog_ADC:MCP3426Axx-xMC +Analog_ADC:MCP3426Axx-xMS +Analog_ADC:MCP3426Axx-xSN +Analog_ADC:MCP3427x-xMF +Analog_ADC:MCP3427x-xUN +Analog_ADC:MCP3428x-xSL +Analog_ADC:MCP3428x-xST +Analog_ADC:MCP3550-50-EMS +Analog_ADC:MCP3550-60-ESN +Analog_ADC:MCP3551-EMS +Analog_ADC:MCP3553-ESN +Analog_DAC:AD390JD +Analog_DAC:AD390KD +Analog_DAC:AD558JN +Analog_DAC:AD558JP +Analog_DAC:AD558KN +Analog_DAC:AD558KP +Analog_DAC:AD5687BCPZ +Analog_DAC:AD5687BRUZ +Analog_DAC:AD5687RBCPZ +Analog_DAC:AD5687RBRUZ +Analog_DAC:AD5689BCPZ +Analog_DAC:AD5689BRUZ +Analog_DAC:AD5689RxCPZ +Analog_DAC:AD5689RxRUZ +Analog_DAC:AD5691RxRM +Analog_DAC:AD5692RxRM +Analog_DAC:AD5693RxRM +Analog_DAC:AD5697RBCPZ +Analog_DAC:AD5697RBRUZ +Analog_DAC:AD5781xRUZ +Analog_DAC:AD5791xRUZ +Analog_DAC:AD7224KN +Analog_DAC:AD7224KP +Analog_DAC:AD7224KR-1 +Analog_DAC:AD7224KR-18 +Analog_DAC:AD7224LN +Analog_DAC:AD7224LP +Analog_DAC:AD7224LR-1 +Analog_DAC:AD7224LR-18 +Analog_DAC:AD7225BRS +Analog_DAC:AD7225CRS +Analog_DAC:AD7225KN +Analog_DAC:AD7225KP +Analog_DAC:AD7225KR +Analog_DAC:AD7225LN +Analog_DAC:AD7225LP +Analog_DAC:AD7225LR +Analog_DAC:AD7226BRSZ +Analog_DAC:AD7226KN +Analog_DAC:AD7226KP +Analog_DAC:AD7226KR +Analog_DAC:AD7228ABN +Analog_DAC:AD7228ABP +Analog_DAC:AD7228ABR +Analog_DAC:AD7228ACN +Analog_DAC:AD7228ACP +Analog_DAC:AD7228ACR +Analog_DAC:AD7304 +Analog_DAC:AD7305 +Analog_DAC:AD7390 +Analog_DAC:AD7391 +Analog_DAC:AD7533JN +Analog_DAC:AD7533JP +Analog_DAC:AD7533KN +Analog_DAC:AD7533KP +Analog_DAC:AD7533KR +Analog_DAC:AD7533LN +Analog_DAC:AD775 +Analog_DAC:AD9106BCP +Analog_DAC:AD9142 +Analog_DAC:AD9744 +Analog_DAC:ADS7830 +Analog_DAC:CS434x-xZZ +Analog_DAC:DAC08 +Analog_DAC:DAC0808_DIP +Analog_DAC:DAC0808_SOIC +Analog_DAC:DAC081C081CIMK +Analog_DAC:DAC1006LCN +Analog_DAC:DAC1006LCWM +Analog_DAC:DAC1007LCN +Analog_DAC:DAC1008LCN +Analog_DAC:DAC101C081CIMK +Analog_DAC:DAC121C081CIMK +Analog_DAC:DAC1220E +Analog_DAC:DAC5311xDCK +Analog_DAC:DAC5578xPW +Analog_DAC:DAC5578xRGE +Analog_DAC:DAC60502 +Analog_DAC:DAC60504 +Analog_DAC:DAC6311xDCK +Analog_DAC:DAC6578xPW +Analog_DAC:DAC6578xRGE +Analog_DAC:DAC70502 +Analog_DAC:DAC70504 +Analog_DAC:DAC7311xDCK +Analog_DAC:DAC7513_DCN +Analog_DAC:DAC7565 +Analog_DAC:DAC7578xPW +Analog_DAC:DAC7578xRGE +Analog_DAC:DAC7750xRHA +Analog_DAC:DAC80502 +Analog_DAC:DAC80504 +Analog_DAC:DAC8165 +Analog_DAC:DAC8501E +Analog_DAC:DAC8531E +Analog_DAC:DAC8531IDRB +Analog_DAC:DAC8550IxDGK +Analog_DAC:DAC8551IxDGK +Analog_DAC:DAC8552 +Analog_DAC:DAC8560IxDGK +Analog_DAC:DAC8565 +Analog_DAC:DAC8571IDGK +Analog_DAC:DAC8750xRHA +Analog_DAC:LTC1257 +Analog_DAC:LTC1446 +Analog_DAC:LTC1446L +Analog_DAC:LTC1664CGN +Analog_DAC:LTC1664CN +Analog_DAC:LTC1664IGN +Analog_DAC:LTC1664IN +Analog_DAC:MAX5138 +Analog_DAC:MAX5139 +Analog_DAC:MAX5215 +Analog_DAC:MAX5217 +Analog_DAC:MAX5717xSD +Analog_DAC:MAX5719xSD +Analog_DAC:MAX5741 +Analog_DAC:MAX5813 +Analog_DAC:MAX5813WLP +Analog_DAC:MAX5814 +Analog_DAC:MAX5814WLP +Analog_DAC:MAX5815 +Analog_DAC:MAX5815WLP +Analog_DAC:MC1408_DIP +Analog_DAC:MC1408_SOIC +Analog_DAC:MCP4725xxx-xCH +Analog_DAC:MCP4728 +Analog_DAC:MCP4801 +Analog_DAC:MCP4801-EMC +Analog_DAC:MCP4802 +Analog_DAC:MCP4811 +Analog_DAC:MCP4811-EMC +Analog_DAC:MCP4812 +Analog_DAC:MCP4821 +Analog_DAC:MCP4821-EMC +Analog_DAC:MCP4822 +Analog_DAC:MCP4901 +Analog_DAC:MCP4901-EMC +Analog_DAC:MCP4902 +Analog_DAC:MCP4911 +Analog_DAC:MCP4911-EMC +Analog_DAC:MCP4912 +Analog_DAC:MCP4921 +Analog_DAC:MCP4921-EMC +Analog_DAC:MCP4921-EMS +Analog_DAC:MCP4921-EP +Analog_DAC:MCP4921-ESN +Analog_DAC:MCP4922 +Analog_DAC:MCP4922-EP +Analog_DAC:MCP4922-ESL +Analog_DAC:MCP4922-EST +Analog_DAC:THS5641AxDW +Analog_DAC:THS5641AxPW +Analog_DAC:TLV5627CD +Analog_DAC:TLV5627CPW +Analog_Switch:ADG1207BCPZ +Analog_Switch:ADG1408YRUZ +Analog_Switch:ADG1414BRU +Analog_Switch:ADG1607xCP +Analog_Switch:ADG417BN +Analog_Switch:ADG417BR +Analog_Switch:ADG419BN +Analog_Switch:ADG419BR +Analog_Switch:ADG419BRM +Analog_Switch:ADG633YCP +Analog_Switch:ADG633YRU +Analog_Switch:ADG658YCP +Analog_Switch:ADG707BRU +Analog_Switch:ADG715 +Analog_Switch:ADG728 +Analog_Switch:ADG729 +Analog_Switch:ADG733BRQ +Analog_Switch:ADG733BRU +Analog_Switch:ADG734 +Analog_Switch:ADG758CPZ +Analog_Switch:ADG824BCP +Analog_Switch:ADG884xCP +Analog_Switch:ADG884xRM +Analog_Switch:CBTL02043A +Analog_Switch:CBTL02043B +Analog_Switch:CD4051B +Analog_Switch:CD4052B +Analog_Switch:CD4053B +Analog_Switch:CD4066BE +Analog_Switch:CD4066BM +Analog_Switch:CD4066BNS +Analog_Switch:CD4066BPW +Analog_Switch:CD4097B +Analog_Switch:DG308AxJ +Analog_Switch:DG308AxY +Analog_Switch:DG309xJ +Analog_Switch:DG309xY +Analog_Switch:DG411xJ +Analog_Switch:DG411xUE +Analog_Switch:DG411xY +Analog_Switch:DG412xJ +Analog_Switch:DG412xUE +Analog_Switch:DG412xY +Analog_Switch:DG413xJ +Analog_Switch:DG413xUE +Analog_Switch:DG413xY +Analog_Switch:DG417LDJ +Analog_Switch:DG417LDJ_Maxim +Analog_Switch:DG417LDY +Analog_Switch:DG417LDY_Maxim +Analog_Switch:DG417LEUA +Analog_Switch:DG417LEUA_Maxim +Analog_Switch:DG417xJ +Analog_Switch:DG417xY +Analog_Switch:DG418LDJ +Analog_Switch:DG418LDJ_Maxim +Analog_Switch:DG418LDY +Analog_Switch:DG418LDY_Maxim +Analog_Switch:DG418LEUA +Analog_Switch:DG418LEUA_Maxim +Analog_Switch:DG418xJ +Analog_Switch:DG418xY +Analog_Switch:DG419LDJ +Analog_Switch:DG419LDJ_Maxim +Analog_Switch:DG419LDY +Analog_Switch:DG419LDY_Maxim +Analog_Switch:DG419LEUA +Analog_Switch:DG419LEUA_Maxim +Analog_Switch:DG419xJ +Analog_Switch:DG419xY +Analog_Switch:DG441xJ +Analog_Switch:DG441xY +Analog_Switch:DG442xJ +Analog_Switch:DG442xY +Analog_Switch:DG884DN +Analog_Switch:DG9421DV +Analog_Switch:DG9422DV +Analog_Switch:FSA3157L6X +Analog_Switch:FSA3157P6X +Analog_Switch:HEF4066BT +Analog_Switch:HI524 +Analog_Switch:MAX14662 +Analog_Switch:MAX14759 +Analog_Switch:MAX14761 +Analog_Switch:MAX14778 +Analog_Switch:MAX312CPE +Analog_Switch:MAX312CSE +Analog_Switch:MAX312CUE +Analog_Switch:MAX313CPE +Analog_Switch:MAX313CSE +Analog_Switch:MAX313CUE +Analog_Switch:MAX314CPE +Analog_Switch:MAX314CSE +Analog_Switch:MAX314CUE +Analog_Switch:MAX317xPA +Analog_Switch:MAX317xSA +Analog_Switch:MAX318xPA +Analog_Switch:MAX318xSA +Analog_Switch:MAX319xPA +Analog_Switch:MAX319xSA +Analog_Switch:MAX323CPA +Analog_Switch:MAX323CSA +Analog_Switch:MAX323CUA +Analog_Switch:MAX324CPA +Analog_Switch:MAX324CSA +Analog_Switch:MAX324CUA +Analog_Switch:MAX325CPA +Analog_Switch:MAX325CSA +Analog_Switch:MAX325CUA +Analog_Switch:MAX333 +Analog_Switch:MAX333A +Analog_Switch:MAX394 +Analog_Switch:MAX40200ANS +Analog_Switch:MAX40200AUK +Analog_Switch:NC7SB3157L6X +Analog_Switch:NC7SB3157P6X +Analog_Switch:NX3L4051HR +Analog_Switch:NX3L4051PW +Analog_Switch:SN74CBT3253 +Analog_Switch:TMUX1101DBV +Analog_Switch:TMUX1101DCK +Analog_Switch:TMUX1102DBV +Analog_Switch:TMUX1102DCK +Analog_Switch:TMUX1108PW +Analog_Switch:TMUX154EDGS +Analog_Switch:TMUX154ERSW +Analog_Switch:TS3A24159DGS +Analog_Switch:TS3A24159DRC +Analog_Switch:TS3A24159YZP +Analog_Switch:TS3A27518EPW +Analog_Switch:TS3A27518ERTW +Analog_Switch:TS3A5017RGY +Analog_Switch:TS3A5017RSV +Analog_Switch:TS3A5223RSW +Analog_Switch:TS3DS10224RUK +Analog_Switch:TS3L501ERUA +Analog_Switch:TS5A23159DGS +Analog_Switch:TS5A23159RSE +Analog_Switch:TS5A3159ADBVR +Analog_Switch:TS5A3159ADCK +Analog_Switch:TS5A3159AYZPR +Analog_Switch:TS5A3159DBV +Analog_Switch:TS5A3159DCK +Analog_Switch:TS5A3160DBV +Analog_Switch:TS5A3160DCK +Analog_Switch:TS5A3166DBVR +Analog_Switch:TS5A3166DCKR +Analog_Switch:TS5A63157DBV +Audio:AD1853 +Audio:AD1855 +Audio:AD1955 +Audio:ADAU1978xBCP +Audio:ADAU1979xBCP +Audio:AK5392VS +Audio:AK5393VS +Audio:AK5394AVS +Audio:AK5720VT +Audio:AK7742EQ +Audio:AS3310 +Audio:AS3320 +Audio:AS3320F +Audio:AS3330 +Audio:AS3330F +Audio:AS3340 +Audio:AS3345 +Audio:AS3345F +Audio:AS3360 +Audio:CS4245 +Audio:CS4265 +Audio:CS4270 +Audio:CS4272 +Audio:CS4334 +Audio:CS4344 +Audio:CS4345 +Audio:CS4348 +Audio:CS43L21 +Audio:CS5343 +Audio:CS5344 +Audio:CS5361 +Audio:CS8406 +Audio:CS8414 +Audio:CS8416-xNZ +Audio:CS8416-xSZ +Audio:CS8416-xZZ +Audio:CS8420 +Audio:DSD1794A +Audio:ISD25120P +Audio:ISD25120S +Audio:ISD2560E +Audio:ISD2560P +Audio:ISD2560S +Audio:ISD2575E +Audio:ISD2575P +Audio:ISD2575S +Audio:ISD2590E +Audio:ISD2590P +Audio:ISD2590S +Audio:MAX98357A +Audio:MAX98357B +Audio:MN3005 +Audio:MN3007 +Audio:MN3207 +Audio:MSGEQ7 +Audio:PCM1754DBQ +Audio:PCM1780 +Audio:PCM1792A +Audio:PCM1794A +Audio:PCM2902 +Audio:PCM3060 +Audio:PCM5100 +Audio:PCM5100A +Audio:PCM5101 +Audio:PCM5101A +Audio:PCM5102 +Audio:PCM5102A +Audio:PCM5121PW +Audio:PCM5122PW +Audio:PGA2310PA +Audio:PGA2310UA +Audio:PGA2500 +Audio:PGA4311 +Audio:PT2258 +Audio:PT2258-S +Audio:PT2399 +Audio:RD5106A +Audio:RD5107A +Audio:RE46C317 +Audio:RE46C318 +Audio:SAD1024 +Audio:SAD512 +Audio:SGTL5000XNAA3 +Audio:SGTL5000XNLA3 +Audio:SPN1001 +Audio:SRC4392xPFB +Audio:SSI2144 +Audio:SSI2164 +Audio:TDA1022 +Audio:THAT1580 +Audio:THAT1583 +Audio:THAT5171 +Audio:THAT5173 +Audio:THAT5263 +Audio:THAT6261 +Audio:THAT6262 +Audio:THAT6263 +Audio:TLV320AIC23BPW +Audio:TLV320AIC23BRHD +Audio:TLV320AIC23BxQE +Audio:TLV320AIC3100 +Audio:TPA5050 +Audio:UDA1334ATS +Audio:WM8731CLSEFL +Audio:WM8731CSEFL +Audio:WM8731SEDS +Audio:YM2149 +Battery_Management:ADP5063 +Battery_Management:ADP5090ACP +Battery_Management:ADP5091 +Battery_Management:ADP5092 +Battery_Management:AP9101CK +Battery_Management:AP9101CK6 +Battery_Management:APW7261 +Battery_Management:AS8506C +Battery_Management:BQ2003 +Battery_Management:BQ21040DBV +Battery_Management:BQ24004 +Battery_Management:BQ24005 +Battery_Management:BQ24006 +Battery_Management:BQ24012 +Battery_Management:BQ24013 +Battery_Management:BQ24072RGT +Battery_Management:BQ24073RGT +Battery_Management:BQ24074RGT +Battery_Management:BQ24075RGT +Battery_Management:BQ24079RGT +Battery_Management:BQ24090DGQ +Battery_Management:BQ24133RGY +Battery_Management:BQ24166RGE +Battery_Management:BQ24167RGE +Battery_Management:BQ24610 +Battery_Management:BQ24617 +Battery_Management:BQ24650 +Battery_Management:BQ2501x +Battery_Management:BQ25040 +Battery_Management:BQ25173DSG +Battery_Management:BQ25504 +Battery_Management:BQ25570 +Battery_Management:BQ25601 +Battery_Management:BQ25886RGE +Battery_Management:BQ25887 +Battery_Management:BQ25895RTW +Battery_Management:BQ27441-G1 +Battery_Management:BQ27441DRZR-G1A +Battery_Management:BQ27441DRZR-G1B +Battery_Management:BQ27441DRZT-G1A +Battery_Management:BQ27441DRZT-G1B +Battery_Management:BQ27750 +Battery_Management:BQ297xy +Battery_Management:BQ51050BRHL +Battery_Management:BQ51050BYFP +Battery_Management:BQ51051BRHL +Battery_Management:BQ51051BYFP +Battery_Management:BQ51052BYFP +Battery_Management:BQ76200PW +Battery_Management:BQ76920PW +Battery_Management:BQ76930DBT +Battery_Management:BQ76940DBT +Battery_Management:BQ78350DBT +Battery_Management:BQ78350DBT-R1 +Battery_Management:DS2745U +Battery_Management:DW01A +Battery_Management:LC709203FQH-01TWG +Battery_Management:LC709203FQH-02TWG +Battery_Management:LC709203FQH-03TWG +Battery_Management:LC709203FQH-04TWG +Battery_Management:LGS5500EP +Battery_Management:LP3947 +Battery_Management:LT3652EDD +Battery_Management:LT3652EMSE +Battery_Management:LT3652IDD +Battery_Management:LT3652IMSE +Battery_Management:LTC2942 +Battery_Management:LTC2942-1 +Battery_Management:LTC2959 +Battery_Management:LTC3553 +Battery_Management:LTC3555 +Battery_Management:LTC3555-1 +Battery_Management:LTC3555-3 +Battery_Management:LTC4001 +Battery_Management:LTC4001-1 +Battery_Management:LTC4002EDD-4.2 +Battery_Management:LTC4002EDD-8.4 +Battery_Management:LTC4002ES8-4.2 +Battery_Management:LTC4002ES8-8.4 +Battery_Management:LTC4007 +Battery_Management:LTC4011CFE +Battery_Management:LTC4054ES5-4.2 +Battery_Management:LTC4054XES5-4.2 +Battery_Management:LTC4055 +Battery_Management:LTC4055-1 +Battery_Management:LTC4060EDHC +Battery_Management:LTC4060EFE +Battery_Management:LTC4067EDE +Battery_Management:LTC4156 +Battery_Management:LTC6803-2 +Battery_Management:LTC6803-4 +Battery_Management:LTC6804-1 +Battery_Management:MAX1647 +Battery_Management:MAX1648 +Battery_Management:MAX17261xxTD +Battery_Management:MAX17261xxWL +Battery_Management:MAX17263xxTE +Battery_Management:MAX1811 +Battery_Management:MAX1873REEE +Battery_Management:MAX1873SEEE +Battery_Management:MAX1873TEEE +Battery_Management:MAX712CPE +Battery_Management:MAX712CSE +Battery_Management:MAX712EPE +Battery_Management:MAX712ESE +Battery_Management:MAX712MJE +Battery_Management:MAX713CPE +Battery_Management:MAX713CSE +Battery_Management:MAX713EPE +Battery_Management:MAX713ESE +Battery_Management:MAX713MJE +Battery_Management:MC34673 +Battery_Management:MCP73811T-420I-OT +Battery_Management:MCP73811T-435I-OT +Battery_Management:MCP73812T-420I-OT +Battery_Management:MCP73812T-435I-OT +Battery_Management:MCP73831-2-MC +Battery_Management:MCP73831-2-OT +Battery_Management:MCP73831-3-MC +Battery_Management:MCP73831-3-OT +Battery_Management:MCP73831-4-MC +Battery_Management:MCP73831-4-OT +Battery_Management:MCP73831-5-MC +Battery_Management:MCP73831-5-OT +Battery_Management:MCP73832-2-MC +Battery_Management:MCP73832-2-OT +Battery_Management:MCP73832-3-MC +Battery_Management:MCP73832-3-OT +Battery_Management:MCP73832-4-MC +Battery_Management:MCP73832-4-OT +Battery_Management:MCP73832-5-MC +Battery_Management:MCP73832-5-OT +Battery_Management:MCP73833-xxx-MF +Battery_Management:MCP73833-xxx-UN +Battery_Management:MCP73871 +Battery_Management:MCP73871-1AA +Battery_Management:MCP73871-1CA +Battery_Management:MCP73871-1CC +Battery_Management:MCP73871-2AA +Battery_Management:MCP73871-2CA +Battery_Management:MCP73871-2CC +Battery_Management:MCP73871-3CA +Battery_Management:MCP73871-3CC +Battery_Management:MCP73871-4CA +Battery_Management:MCP73871-4CC +Battery_Management:SLM6800 +Battery_Management:TP4056-42-ESOP8 +Battery_Management:TP4057 +Buffer:CDCV304 +Buffer:PI6C5946002ZH +Comparator:AD8561 +Comparator:ADCMP350 +Comparator:ADCMP354 +Comparator:ADCMP356 +Comparator:LM2901 +Comparator:LM2903 +Comparator:LM311 +Comparator:LM319 +Comparator:LM319H +Comparator:LM339 +Comparator:LM393 +Comparator:LM397 +Comparator:LMH7324 +Comparator:LMV331 +Comparator:LMV339 +Comparator:LMV393 +Comparator:LMV7219M5 +Comparator:LMV7219M7 +Comparator:LMV7271 +Comparator:LMV7272 +Comparator:LMV7275 +Comparator:LP2901D +Comparator:LT1011 +Comparator:LT1016 +Comparator:LT1116 +Comparator:LT1711xMS8 +Comparator:LTC6752xMS8-2 +Comparator:LTC6752xS5 +Comparator:LTC6752xSC6-1 +Comparator:LTC6752xSC6-4 +Comparator:LTC6752xUD-3 +Comparator:LTC6754xSC6 +Comparator:LTC6754xUD +Comparator:MAX9031AU +Comparator:MAX9031AX +Comparator:MAX941xPA +Comparator:MAX941xSA +Comparator:MAX941xUA +Comparator:MCP6561-OT +Comparator:MCP6561R +Comparator:MCP6561U +Comparator:MCP6561x-LT +Comparator:MCP6562 +Comparator:MCP6566 +Comparator:MCP6566R +Comparator:MCP6566U +Comparator:MCP6567 +Comparator:MCP6569 +Comparator:MCP65R41 +Comparator:MCP65R46 +Comparator:MIC845H +Comparator:MIC845L +Comparator:MIC845N +Comparator:TL3116 +Comparator:TL331 +Comparator:TLV3501AID +Comparator:TLV3501AIDBV +Comparator:TLV7031DBV +Comparator:TLV7041DBV +Comparator:TLV7041DCK +Comparator:TLV7041SDCK +Connector:4P2C +Connector:4P2C_Shielded +Connector:4P4C +Connector:4P4C_Shielded +Connector:6P2C +Connector:6P2C_Shielded +Connector:6P4C +Connector:6P4C_Shielded +Connector:6P6C +Connector:6P6C_Shielded +Connector:8P4C +Connector:8P4C_Shielded +Connector:8P8C +Connector:8P8C_LED +Connector:8P8C_LED_Shielded +Connector:8P8C_LED_Shielded_x2 +Connector:8P8C_Shielded +Connector:ATX-20 +Connector:ATX-24 +Connector:AVR-ISP-10 +Connector:AVR-ISP-6 +Connector:AVR-JTAG-10 +Connector:AVR-PDI-6 +Connector:AVR-TPI-6 +Connector:AVR-UPDI-6 +Connector:Barrel_Jack +Connector:Barrel_Jack_MountingPin +Connector:Barrel_Jack_Switch +Connector:Barrel_Jack_Switch_MountingPin +Connector:Barrel_Jack_Switch_Pin3Ring +Connector:Bus_ISA_16bit +Connector:Bus_ISA_8bit +Connector:Bus_M.2_Socket_A +Connector:Bus_M.2_Socket_B +Connector:Bus_M.2_Socket_E +Connector:Bus_M.2_Socket_M +Connector:Bus_PCI_32bit_5V +Connector:Bus_PCI_32bit_Universal +Connector:Bus_PCI_Express_Mini +Connector:Bus_PCI_Express_x1 +Connector:Bus_PCI_Express_x16 +Connector:Bus_PCI_Express_x4 +Connector:Bus_PCI_Express_x8 +Connector:CUI_PD-30 +Connector:CUI_PD-30S +Connector:CoaxialSwitch_Testpoint +Connector:Conn_01x01_Pin +Connector:Conn_01x01_Socket +Connector:Conn_01x02_Pin +Connector:Conn_01x02_Socket +Connector:Conn_01x03_Pin +Connector:Conn_01x03_Socket +Connector:Conn_01x04_Pin +Connector:Conn_01x04_Socket +Connector:Conn_01x05_Pin +Connector:Conn_01x05_Socket +Connector:Conn_01x06_Pin +Connector:Conn_01x06_Socket +Connector:Conn_01x07_Pin +Connector:Conn_01x07_Socket +Connector:Conn_01x08_Pin +Connector:Conn_01x08_Socket +Connector:Conn_01x09_Pin +Connector:Conn_01x09_Socket +Connector:Conn_01x10_Pin +Connector:Conn_01x10_Socket +Connector:Conn_01x11_Pin +Connector:Conn_01x11_Socket +Connector:Conn_01x12_Pin +Connector:Conn_01x12_Socket +Connector:Conn_01x13_Pin +Connector:Conn_01x13_Socket +Connector:Conn_01x14_Pin +Connector:Conn_01x14_Socket +Connector:Conn_01x15_Pin +Connector:Conn_01x15_Socket +Connector:Conn_01x16_Pin +Connector:Conn_01x16_Socket +Connector:Conn_01x17_Pin +Connector:Conn_01x17_Socket +Connector:Conn_01x18_Pin +Connector:Conn_01x18_Socket +Connector:Conn_01x19_Pin +Connector:Conn_01x19_Socket +Connector:Conn_01x20_Pin +Connector:Conn_01x20_Socket +Connector:Conn_01x21_Pin +Connector:Conn_01x21_Socket +Connector:Conn_01x22_Pin +Connector:Conn_01x22_Socket +Connector:Conn_01x23_Pin +Connector:Conn_01x23_Socket +Connector:Conn_01x24_Pin +Connector:Conn_01x24_Socket +Connector:Conn_01x25_Pin +Connector:Conn_01x25_Socket +Connector:Conn_01x26_Pin +Connector:Conn_01x26_Socket +Connector:Conn_01x27_Pin +Connector:Conn_01x27_Socket +Connector:Conn_01x28_Pin +Connector:Conn_01x28_Socket +Connector:Conn_01x29_Pin +Connector:Conn_01x29_Socket +Connector:Conn_01x30_Pin +Connector:Conn_01x30_Socket +Connector:Conn_01x31_Pin +Connector:Conn_01x31_Socket +Connector:Conn_01x32_Pin +Connector:Conn_01x32_Socket +Connector:Conn_01x33_Pin +Connector:Conn_01x33_Socket +Connector:Conn_01x34_Pin +Connector:Conn_01x34_Socket +Connector:Conn_01x35_Pin +Connector:Conn_01x35_Socket +Connector:Conn_01x36_Pin +Connector:Conn_01x36_Socket +Connector:Conn_01x37_Pin +Connector:Conn_01x37_Socket +Connector:Conn_01x38_Pin +Connector:Conn_01x38_Socket +Connector:Conn_01x39_Pin +Connector:Conn_01x39_Socket +Connector:Conn_01x40_Pin +Connector:Conn_01x40_Socket +Connector:Conn_01x41_Pin +Connector:Conn_01x41_Socket +Connector:Conn_01x42_Pin +Connector:Conn_01x42_Socket +Connector:Conn_01x43_Pin +Connector:Conn_01x43_Socket +Connector:Conn_01x44_Pin +Connector:Conn_01x44_Socket +Connector:Conn_01x45_Pin +Connector:Conn_01x45_Socket +Connector:Conn_01x46_Pin +Connector:Conn_01x46_Socket +Connector:Conn_01x47_Pin +Connector:Conn_01x47_Socket +Connector:Conn_01x48_Pin +Connector:Conn_01x48_Socket +Connector:Conn_01x49_Pin +Connector:Conn_01x49_Socket +Connector:Conn_01x50_Pin +Connector:Conn_01x50_Socket +Connector:Conn_01x51_Pin +Connector:Conn_01x51_Socket +Connector:Conn_01x52_Pin +Connector:Conn_01x52_Socket +Connector:Conn_01x53_Pin +Connector:Conn_01x53_Socket +Connector:Conn_01x54_Pin +Connector:Conn_01x54_Socket +Connector:Conn_01x55_Pin +Connector:Conn_01x55_Socket +Connector:Conn_01x56_Pin +Connector:Conn_01x56_Socket +Connector:Conn_01x57_Pin +Connector:Conn_01x57_Socket +Connector:Conn_01x58_Pin +Connector:Conn_01x58_Socket +Connector:Conn_01x59_Pin +Connector:Conn_01x59_Socket +Connector:Conn_01x60_Pin +Connector:Conn_01x60_Socket +Connector:Conn_15X4 +Connector:Conn_ARM_Cortex_Debug_ETM_20 +Connector:Conn_ARM_JTAG_SWD_10 +Connector:Conn_ARM_JTAG_SWD_20 +Connector:Conn_ARM_SWD_TagConnect_TC2030 +Connector:Conn_ARM_SWD_TagConnect_TC2030-NL +Connector:Conn_Coaxial +Connector:Conn_Coaxial_Power +Connector:Conn_Coaxial_Small +Connector:Conn_Coaxial_x2 +Connector:Conn_Coaxial_x2_Isolated +Connector:Conn_PIC_ICSP_ICD +Connector:Conn_Plug_2P +Connector:Conn_Plug_3P_Protected +Connector:Conn_Receptacle_2P +Connector:Conn_Receptacle_3P_Protected +Connector:Conn_ST_STDC14 +Connector:Conn_Shielded_Pair +Connector:Conn_Triaxial +Connector:Conn_Triaxial_Same_Side +Connector:DA15_Pins +Connector:DA15_Pins_MountingHoles +Connector:DA15_Socket +Connector:DA15_Socket_MountingHoles +Connector:DB25_Pins +Connector:DB25_Pins_MountingHoles +Connector:DB25_Socket +Connector:DB25_Socket_MountingHoles +Connector:DC37_Pins +Connector:DC37_Pins_MountingHoles +Connector:DC37_Socket +Connector:DC37_Socket_MountingHoles +Connector:DE15_Pins_HighDensity +Connector:DE15_Pins_HighDensity_MountingHoles +Connector:DE15_Socket_HighDensity +Connector:DE15_Socket_HighDensity_MountingHoles +Connector:DE9_Pins +Connector:DE9_Pins_MountingHoles +Connector:DE9_Socket +Connector:DE9_Socket_MountingHoles +Connector:DIN-3 +Connector:DIN-4 +Connector:DIN-5 +Connector:DIN-5_180degree +Connector:DIN-6 +Connector:DIN-7 +Connector:DIN-7_CenterPin7 +Connector:DIN-8 +Connector:DIN41612_01x32_A +Connector:DIN41612_02x05_AB_EvenPins +Connector:DIN41612_02x05_AC_EvenPins +Connector:DIN41612_02x05_AE_EvenPins +Connector:DIN41612_02x05_ZB_EvenPins +Connector:DIN41612_02x08_AB_EvenPins +Connector:DIN41612_02x08_AC_EvenPins +Connector:DIN41612_02x08_AE_EvenPins +Connector:DIN41612_02x08_ZB_EvenPins +Connector:DIN41612_02x10_AB +Connector:DIN41612_02x10_AC +Connector:DIN41612_02x10_AE +Connector:DIN41612_02x10_ZB +Connector:DIN41612_02x16_AB +Connector:DIN41612_02x16_AB_EvenPins +Connector:DIN41612_02x16_AC +Connector:DIN41612_02x16_AC_EvenPins +Connector:DIN41612_02x16_AE +Connector:DIN41612_02x16_AE_EvenPins +Connector:DIN41612_02x16_ZB +Connector:DIN41612_02x16_ZB_EvenPins +Connector:DIN41612_02x32_AB +Connector:DIN41612_02x32_AC +Connector:DIN41612_02x32_AE +Connector:DIN41612_02x32_ZB +Connector:DVI-D_Dual_Link +Connector:DVI-I_Dual_Link +Connector:ExpressCard +Connector:HDMI_A +Connector:HDMI_A_1.4 +Connector:HDMI_B +Connector:HDMI_C_1.3 +Connector:HDMI_C_1.4 +Connector:HDMI_D_1.4 +Connector:HDMI_E +Connector:IEC61076-2_M8_A-coding_01x03_Plug_Shielded +Connector:IEC61076-2_M8_A-coding_01x03_Receptacle_Shielded +Connector:IEC61076-2_M8_A-coding_01x04_Plug_Shielded +Connector:IEC61076-2_M8_A-coding_01x04_Receptacle_Shielded +Connector:IEC_60320_C13_Plug +Connector:IEC_60320_C14_Receptacle +Connector:IEC_60320_C5_Plug +Connector:IEC_60320_C6_Receptacle +Connector:IEC_60320_C7_Plug +Connector:IEC_60320_C8_Receptacle +Connector:IEEE1394a +Connector:JAE_SIM_Card_SF72S006 +Connector:Jack-DC +Connector:LEMO2 +Connector:LEMO4 +Connector:LEMO5 +Connector:LEMO6 +Connector:MXM3.0 +Connector:Micro_SD_Card +Connector:Micro_SD_Card_Det1 +Connector:Micro_SD_Card_Det2 +Connector:Micro_SD_Card_Det_Hirose_DM3AT +Connector:Microsemi_FlashPro-JTAG-10 +Connector:Mini-DIN-3 +Connector:Mini-DIN-4 +Connector:Mini-DIN-5 +Connector:Mini-DIN-6 +Connector:Mini-DIN-7 +Connector:Mini-DIN-8 +Connector:RJ10 +Connector:RJ10_Shielded +Connector:RJ11 +Connector:RJ11_Shielded +Connector:RJ12 +Connector:RJ12_Shielded +Connector:RJ13 +Connector:RJ13_Shielded +Connector:RJ14 +Connector:RJ14_Shielded +Connector:RJ18 +Connector:RJ18_Shielded +Connector:RJ22 +Connector:RJ22_Shielded +Connector:RJ25 +Connector:RJ25_Shielded +Connector:RJ31 +Connector:RJ31_Shielded +Connector:RJ32 +Connector:RJ32_Shielded +Connector:RJ33 +Connector:RJ33_Shielded +Connector:RJ34 +Connector:RJ34_Shielded +Connector:RJ35 +Connector:RJ35_Shielded +Connector:RJ38 +Connector:RJ38_Shielded +Connector:RJ41 +Connector:RJ41_Shielded +Connector:RJ45 +Connector:RJ45_Abracon_ARJP11A-MASA-B-A-EMU2 +Connector:RJ45_Amphenol_RJMG1BD3B8K1ANR +Connector:RJ45_Bel_SI-60062-F +Connector:RJ45_Bel_V895-1001-AW +Connector:RJ45_Halo_HFJ11-x2450E-LxxRL +Connector:RJ45_Halo_HFJ11-x2450ERL +Connector:RJ45_Halo_HFJ11-x2450HRL +Connector:RJ45_Hanrun_HR911105A_Horizontal +Connector:RJ45_JK00177 +Connector:RJ45_JK0654219 +Connector:RJ45_Kycon_G7LX-A88S7-BP-GY +Connector:RJ45_LED +Connector:RJ45_LED_Shielded +Connector:RJ45_LED_Shielded_x2 +Connector:RJ45_Pulse_JXD6-0001NL +Connector:RJ45_RB1-125B8G1A +Connector:RJ45_Shielded +Connector:RJ45_Wuerth_74980111211 +Connector:RJ45_Wuerth_7499010121A +Connector:RJ45_Wuerth_7499010211A +Connector:RJ45_Wuerth_7499151120 +Connector:RJ48 +Connector:RJ48_Shielded +Connector:RJ49 +Connector:RJ49_Shielded +Connector:RJ61 +Connector:RJ61_Shielded +Connector:RJ9 +Connector:RJ9_Shielded +Connector:Raspberry_Pi_4 +Connector:SCART-F +Connector:SD_Card_Device +Connector:SD_Card_Receptacle +Connector:SIM_Card +Connector:SIM_Card_Shielded +Connector:SODIMM-200 +Connector:SODIMM-200_Split +Connector:Samtec_ASP-134486-01 +Connector:Samtec_ASP-134602-01 +Connector:Screw_Terminal_01x01 +Connector:Screw_Terminal_01x02 +Connector:Screw_Terminal_01x03 +Connector:Screw_Terminal_01x04 +Connector:Screw_Terminal_01x05 +Connector:Screw_Terminal_01x06 +Connector:Screw_Terminal_01x07 +Connector:Screw_Terminal_01x08 +Connector:Screw_Terminal_01x09 +Connector:Screw_Terminal_01x10 +Connector:Screw_Terminal_01x11 +Connector:Screw_Terminal_01x12 +Connector:Screw_Terminal_01x13 +Connector:Screw_Terminal_01x14 +Connector:Screw_Terminal_01x15 +Connector:Screw_Terminal_01x16 +Connector:Screw_Terminal_01x17 +Connector:Screw_Terminal_01x18 +Connector:Screw_Terminal_01x19 +Connector:Screw_Terminal_01x20 +Connector:TC2030 +Connector:TC2050 +Connector:TC2070 +Connector:TestPoint +Connector:TestPoint_2Pole +Connector:TestPoint_Alt +Connector:TestPoint_Flag +Connector:TestPoint_Probe +Connector:TestPoint_Small +Connector:UEXT_Host +Connector:UEXT_Slave +Connector:USB3_A +Connector:USB3_A_Stacked +Connector:USB3_B +Connector:USB3_B_Micro +Connector:USB_A +Connector:USB_A_Stacked +Connector:USB_B +Connector:USB_B_Micro +Connector:USB_B_Mini +Connector:USB_C_Plug +Connector:USB_C_Plug_USB2.0 +Connector:USB_C_Receptacle +Connector:USB_C_Receptacle_PowerOnly_24P +Connector:USB_C_Receptacle_PowerOnly_6P +Connector:USB_C_Receptacle_USB2.0_14P +Connector:USB_C_Receptacle_USB2.0_16P +Connector:USB_OTG +Connector_Audio:AudioJack2 +Connector_Audio:AudioJack2_Dual_Ground_Switch +Connector_Audio:AudioJack2_Dual_Switch +Connector_Audio:AudioJack2_Ground +Connector_Audio:AudioJack2_Ground_Switch +Connector_Audio:AudioJack2_Ground_SwitchT +Connector_Audio:AudioJack2_Switch +Connector_Audio:AudioJack2_SwitchT +Connector_Audio:AudioJack3 +Connector_Audio:AudioJack3_Dual_Ground_Switch +Connector_Audio:AudioJack3_Dual_Switch +Connector_Audio:AudioJack3_Ground +Connector_Audio:AudioJack3_Ground_Switch +Connector_Audio:AudioJack3_Ground_SwitchTR +Connector_Audio:AudioJack3_Switch +Connector_Audio:AudioJack3_SwitchT +Connector_Audio:AudioJack3_SwitchTR +Connector_Audio:AudioJack4 +Connector_Audio:AudioJack4_Ground +Connector_Audio:AudioJack4_Ground_SwitchTR1 +Connector_Audio:AudioJack4_SwitchT1 +Connector_Audio:AudioJack4_SwitchTR1 +Connector_Audio:AudioJack5 +Connector_Audio:AudioJack5_Ground +Connector_Audio:AudioPlug2 +Connector_Audio:AudioPlug3 +Connector_Audio:AudioPlug4 +Connector_Audio:NC3FAAH +Connector_Audio:NC3FAAH-0 +Connector_Audio:NC3FAAH1 +Connector_Audio:NC3FAAH1-0 +Connector_Audio:NC3FAAH1-DA +Connector_Audio:NC3FAAH2 +Connector_Audio:NC3FAAH2-0 +Connector_Audio:NC3FAAV +Connector_Audio:NC3FAAV-0 +Connector_Audio:NC3FAAV1 +Connector_Audio:NC3FAAV1-0 +Connector_Audio:NC3FAAV1-DA +Connector_Audio:NC3FAAV2 +Connector_Audio:NC3FAAV2-0 +Connector_Audio:NC3FAH +Connector_Audio:NC3FAH-0 +Connector_Audio:NC3FAH1 +Connector_Audio:NC3FAH1-0 +Connector_Audio:NC3FAH1-DA +Connector_Audio:NC3FAH2 +Connector_Audio:NC3FAH2-0 +Connector_Audio:NC3FAH2-DA +Connector_Audio:NC3FAHL-0 +Connector_Audio:NC3FAHL1 +Connector_Audio:NC3FAHL1-0 +Connector_Audio:NC3FAHR-0 +Connector_Audio:NC3FAHR1 +Connector_Audio:NC3FAHR1-0 +Connector_Audio:NC3FAHR2 +Connector_Audio:NC3FAHR2-0 +Connector_Audio:NC3FAV +Connector_Audio:NC3FAV-0 +Connector_Audio:NC3FAV1 +Connector_Audio:NC3FAV1-0 +Connector_Audio:NC3FAV1-DA +Connector_Audio:NC3FAV2 +Connector_Audio:NC3FAV2-0 +Connector_Audio:NC3FAV2-DA +Connector_Audio:NC3FBH1 +Connector_Audio:NC3FBH1-B +Connector_Audio:NC3FBH1-DA +Connector_Audio:NC3FBH1-E +Connector_Audio:NC3FBH2 +Connector_Audio:NC3FBH2-B +Connector_Audio:NC3FBH2-DA +Connector_Audio:NC3FBH2-E +Connector_Audio:NC3FBHL1 +Connector_Audio:NC3FBV1 +Connector_Audio:NC3FBV1-0 +Connector_Audio:NC3FBV1-B +Connector_Audio:NC3FBV1-DA +Connector_Audio:NC3FBV2 +Connector_Audio:NC3FBV2-B +Connector_Audio:NC3FBV2-DA +Connector_Audio:NC3FBV2-SW +Connector_Audio:NC3MAAH +Connector_Audio:NC3MAAH-0 +Connector_Audio:NC3MAAH-1 +Connector_Audio:NC3MAAV +Connector_Audio:NC3MAAV-0 +Connector_Audio:NC3MAAV-1 +Connector_Audio:NC3MAFH-PH +Connector_Audio:NC3MAH +Connector_Audio:NC3MAH-0 +Connector_Audio:NC3MAHL +Connector_Audio:NC3MAHR +Connector_Audio:NC3MAMH-PH +Connector_Audio:NC3MAV +Connector_Audio:NC3MAV-0 +Connector_Audio:NC3MBH +Connector_Audio:NC3MBH-0 +Connector_Audio:NC3MBH-1 +Connector_Audio:NC3MBH-B +Connector_Audio:NC3MBH-E +Connector_Audio:NC3MBHL +Connector_Audio:NC3MBHL-B +Connector_Audio:NC3MBHR +Connector_Audio:NC3MBHR-B +Connector_Audio:NC3MBV +Connector_Audio:NC3MBV-0 +Connector_Audio:NC3MBV-1 +Connector_Audio:NC3MBV-B +Connector_Audio:NC3MBV-E +Connector_Audio:NC3MBV-SW +Connector_Audio:NC4FAH +Connector_Audio:NC4FAH-0 +Connector_Audio:NC4FAV +Connector_Audio:NC4FAV-0 +Connector_Audio:NC4FBH +Connector_Audio:NC4FBV +Connector_Audio:NC4MAH +Connector_Audio:NC4MAV +Connector_Audio:NC4MBH +Connector_Audio:NC4MBV +Connector_Audio:NC5FAH +Connector_Audio:NC5FAH-0 +Connector_Audio:NC5FAH-DA +Connector_Audio:NC5FAV +Connector_Audio:NC5FAV-DA +Connector_Audio:NC5FAV-SW +Connector_Audio:NC5FBH +Connector_Audio:NC5FBH-B +Connector_Audio:NC5FBV +Connector_Audio:NC5FBV-B +Connector_Audio:NC5FBV-SW +Connector_Audio:NC5MAH +Connector_Audio:NC5MAV +Connector_Audio:NC5MAV-SW +Connector_Audio:NC5MBH +Connector_Audio:NC5MBH-B +Connector_Audio:NC5MBV +Connector_Audio:NC5MBV-B +Connector_Audio:NC5MBV-SW +Connector_Audio:NCJ10FI-H +Connector_Audio:NCJ10FI-H-0 +Connector_Audio:NCJ10FI-V +Connector_Audio:NCJ10FI-V-0 +Connector_Audio:NCJ5FI-H +Connector_Audio:NCJ5FI-H-0 +Connector_Audio:NCJ5FI-V +Connector_Audio:NCJ5FI-V-0 +Connector_Audio:NCJ6FA-H +Connector_Audio:NCJ6FA-H-0 +Connector_Audio:NCJ6FA-H-DA +Connector_Audio:NCJ6FA-V +Connector_Audio:NCJ6FA-V-0 +Connector_Audio:NCJ6FA-V-DA +Connector_Audio:NCJ6FI-H +Connector_Audio:NCJ6FI-H-0 +Connector_Audio:NCJ6FI-V +Connector_Audio:NCJ6FI-V-0 +Connector_Audio:NCJ9FI-H +Connector_Audio:NCJ9FI-H-0 +Connector_Audio:NCJ9FI-V +Connector_Audio:NCJ9FI-V-0 +Connector_Audio:NJ2FD-V +Connector_Audio:NJ3FD-V +Connector_Audio:NJ5FD-V +Connector_Audio:NJ6FD-V +Connector_Audio:NJ6TB-V +Connector_Audio:NL2MDXX-H-3 +Connector_Audio:NL2MDXX-V +Connector_Audio:NL4MDXX-H-2 +Connector_Audio:NL4MDXX-H-3 +Connector_Audio:NL4MDXX-V +Connector_Audio:NL4MDXX-V-2 +Connector_Audio:NL4MDXX-V-3 +Connector_Audio:NL8MDXX-V +Connector_Audio:NL8MDXX-V-3 +Connector_Audio:NLJ2MDXX-H +Connector_Audio:NLJ2MDXX-V +Connector_Audio:NLT4MD-V +Connector_Audio:NMJ4HCD2 +Connector_Audio:NMJ4HFD2 +Connector_Audio:NMJ4HFD3 +Connector_Audio:NMJ4HHD2 +Connector_Audio:NMJ6HCD2 +Connector_Audio:NMJ6HCD3 +Connector_Audio:NMJ6HFD2 +Connector_Audio:NMJ6HFD2-AU +Connector_Audio:NMJ6HFD3 +Connector_Audio:NMJ6HFD4 +Connector_Audio:NMJ6HHD2 +Connector_Audio:NRJ3HF-1 +Connector_Audio:NRJ4HF +Connector_Audio:NRJ4HF-1 +Connector_Audio:NRJ4HH +Connector_Audio:NRJ4HH-1 +Connector_Audio:NRJ6HF +Connector_Audio:NRJ6HF-1 +Connector_Audio:NRJ6HF-1-AU +Connector_Audio:NRJ6HF-AU +Connector_Audio:NRJ6HH +Connector_Audio:NRJ6HH-1 +Connector_Audio:NRJ6HH-AU +Connector_Audio:NRJ6HM-1 +Connector_Audio:NRJ6HM-1-AU +Connector_Audio:NRJ6HM-1-PRE +Connector_Audio:NSJ12HC +Connector_Audio:NSJ12HF-1 +Connector_Audio:NSJ12HH-1 +Connector_Audio:NSJ12HL +Connector_Audio:NSJ8HC +Connector_Audio:NSJ8HL +Connector_Audio:SpeakON_NL2 +Connector_Audio:SpeakON_NL2_AudioJack2_Combo +Connector_Audio:SpeakON_NL4 +Connector_Audio:SpeakON_NL4_Switch +Connector_Audio:SpeakON_NL8 +Connector_Audio:XLR3 +Connector_Audio:XLR3_AudioJack2_Combo +Connector_Audio:XLR3_AudioJack2_Combo_Ground +Connector_Audio:XLR3_AudioJack3_Combo +Connector_Audio:XLR3_AudioJack3_Combo_Ground +Connector_Audio:XLR3_AudioJack3_Combo_GroundSwitch_Switch +Connector_Audio:XLR3_AudioJack3_Combo_Ground_Switch +Connector_Audio:XLR3_AudioJack3_Combo_Switch +Connector_Audio:XLR3_Ground +Connector_Audio:XLR3_Ground_Switched +Connector_Audio:XLR3_Switched +Connector_Audio:XLR4 +Connector_Audio:XLR4_Ground +Connector_Audio:XLR5 +Connector_Audio:XLR5_Ground +Connector_Audio:XLR5_Ground_Switched +Connector_Audio:XLR6 +Connector_Generic:Conn_01x01 +Connector_Generic:Conn_01x02 +Connector_Generic:Conn_01x03 +Connector_Generic:Conn_01x04 +Connector_Generic:Conn_01x05 +Connector_Generic:Conn_01x06 +Connector_Generic:Conn_01x07 +Connector_Generic:Conn_01x08 +Connector_Generic:Conn_01x09 +Connector_Generic:Conn_01x10 +Connector_Generic:Conn_01x11 +Connector_Generic:Conn_01x12 +Connector_Generic:Conn_01x13 +Connector_Generic:Conn_01x14 +Connector_Generic:Conn_01x15 +Connector_Generic:Conn_01x16 +Connector_Generic:Conn_01x17 +Connector_Generic:Conn_01x18 +Connector_Generic:Conn_01x19 +Connector_Generic:Conn_01x20 +Connector_Generic:Conn_01x21 +Connector_Generic:Conn_01x22 +Connector_Generic:Conn_01x23 +Connector_Generic:Conn_01x24 +Connector_Generic:Conn_01x25 +Connector_Generic:Conn_01x26 +Connector_Generic:Conn_01x27 +Connector_Generic:Conn_01x28 +Connector_Generic:Conn_01x29 +Connector_Generic:Conn_01x30 +Connector_Generic:Conn_01x31 +Connector_Generic:Conn_01x32 +Connector_Generic:Conn_01x33 +Connector_Generic:Conn_01x34 +Connector_Generic:Conn_01x35 +Connector_Generic:Conn_01x36 +Connector_Generic:Conn_01x37 +Connector_Generic:Conn_01x38 +Connector_Generic:Conn_01x39 +Connector_Generic:Conn_01x40 +Connector_Generic:Conn_01x41 +Connector_Generic:Conn_01x42 +Connector_Generic:Conn_01x43 +Connector_Generic:Conn_01x44 +Connector_Generic:Conn_01x45 +Connector_Generic:Conn_01x46 +Connector_Generic:Conn_01x47 +Connector_Generic:Conn_01x48 +Connector_Generic:Conn_01x49 +Connector_Generic:Conn_01x50 +Connector_Generic:Conn_01x51 +Connector_Generic:Conn_01x52 +Connector_Generic:Conn_01x53 +Connector_Generic:Conn_01x54 +Connector_Generic:Conn_01x55 +Connector_Generic:Conn_01x56 +Connector_Generic:Conn_01x57 +Connector_Generic:Conn_01x58 +Connector_Generic:Conn_01x59 +Connector_Generic:Conn_01x60 +Connector_Generic:Conn_02x01 +Connector_Generic:Conn_02x01_Row_Letter_First +Connector_Generic:Conn_02x01_Row_Letter_Last +Connector_Generic:Conn_02x02_Counter_Clockwise +Connector_Generic:Conn_02x02_Odd_Even +Connector_Generic:Conn_02x02_Row_Letter_First +Connector_Generic:Conn_02x02_Row_Letter_Last +Connector_Generic:Conn_02x02_Top_Bottom +Connector_Generic:Conn_02x03_Counter_Clockwise +Connector_Generic:Conn_02x03_Odd_Even +Connector_Generic:Conn_02x03_Row_Letter_First +Connector_Generic:Conn_02x03_Row_Letter_Last +Connector_Generic:Conn_02x03_Top_Bottom +Connector_Generic:Conn_02x04_Counter_Clockwise +Connector_Generic:Conn_02x04_Odd_Even +Connector_Generic:Conn_02x04_Row_Letter_First +Connector_Generic:Conn_02x04_Row_Letter_Last +Connector_Generic:Conn_02x04_Top_Bottom +Connector_Generic:Conn_02x05_Counter_Clockwise +Connector_Generic:Conn_02x05_Odd_Even +Connector_Generic:Conn_02x05_Row_Letter_First +Connector_Generic:Conn_02x05_Row_Letter_Last +Connector_Generic:Conn_02x05_Top_Bottom +Connector_Generic:Conn_02x06_Counter_Clockwise +Connector_Generic:Conn_02x06_Odd_Even +Connector_Generic:Conn_02x06_Row_Letter_First +Connector_Generic:Conn_02x06_Row_Letter_Last +Connector_Generic:Conn_02x06_Top_Bottom +Connector_Generic:Conn_02x07_Counter_Clockwise +Connector_Generic:Conn_02x07_Odd_Even +Connector_Generic:Conn_02x07_Row_Letter_First +Connector_Generic:Conn_02x07_Row_Letter_Last +Connector_Generic:Conn_02x07_Top_Bottom +Connector_Generic:Conn_02x08_Counter_Clockwise +Connector_Generic:Conn_02x08_Odd_Even +Connector_Generic:Conn_02x08_Row_Letter_First +Connector_Generic:Conn_02x08_Row_Letter_Last +Connector_Generic:Conn_02x08_Top_Bottom +Connector_Generic:Conn_02x09_Counter_Clockwise +Connector_Generic:Conn_02x09_Odd_Even +Connector_Generic:Conn_02x09_Row_Letter_First +Connector_Generic:Conn_02x09_Row_Letter_Last +Connector_Generic:Conn_02x09_Top_Bottom +Connector_Generic:Conn_02x10_Counter_Clockwise +Connector_Generic:Conn_02x10_Odd_Even +Connector_Generic:Conn_02x10_Row_Letter_First +Connector_Generic:Conn_02x10_Row_Letter_Last +Connector_Generic:Conn_02x10_Top_Bottom +Connector_Generic:Conn_02x11_Counter_Clockwise +Connector_Generic:Conn_02x11_Odd_Even +Connector_Generic:Conn_02x11_Row_Letter_First +Connector_Generic:Conn_02x11_Row_Letter_Last +Connector_Generic:Conn_02x11_Top_Bottom +Connector_Generic:Conn_02x12_Counter_Clockwise +Connector_Generic:Conn_02x12_Odd_Even +Connector_Generic:Conn_02x12_Row_Letter_First +Connector_Generic:Conn_02x12_Row_Letter_Last +Connector_Generic:Conn_02x12_Top_Bottom +Connector_Generic:Conn_02x13_Counter_Clockwise +Connector_Generic:Conn_02x13_Odd_Even +Connector_Generic:Conn_02x13_Row_Letter_First +Connector_Generic:Conn_02x13_Row_Letter_Last +Connector_Generic:Conn_02x13_Top_Bottom +Connector_Generic:Conn_02x14_Counter_Clockwise +Connector_Generic:Conn_02x14_Odd_Even +Connector_Generic:Conn_02x14_Row_Letter_First +Connector_Generic:Conn_02x14_Row_Letter_Last +Connector_Generic:Conn_02x14_Top_Bottom +Connector_Generic:Conn_02x15_Counter_Clockwise +Connector_Generic:Conn_02x15_Odd_Even +Connector_Generic:Conn_02x15_Row_Letter_First +Connector_Generic:Conn_02x15_Row_Letter_Last +Connector_Generic:Conn_02x15_Top_Bottom +Connector_Generic:Conn_02x16_Counter_Clockwise +Connector_Generic:Conn_02x16_Odd_Even +Connector_Generic:Conn_02x16_Row_Letter_First +Connector_Generic:Conn_02x16_Row_Letter_Last +Connector_Generic:Conn_02x16_Top_Bottom +Connector_Generic:Conn_02x17_Counter_Clockwise +Connector_Generic:Conn_02x17_Odd_Even +Connector_Generic:Conn_02x17_Row_Letter_First +Connector_Generic:Conn_02x17_Row_Letter_Last +Connector_Generic:Conn_02x17_Top_Bottom +Connector_Generic:Conn_02x18_Counter_Clockwise +Connector_Generic:Conn_02x18_Odd_Even +Connector_Generic:Conn_02x18_Row_Letter_First +Connector_Generic:Conn_02x18_Row_Letter_Last +Connector_Generic:Conn_02x18_Top_Bottom +Connector_Generic:Conn_02x19_Counter_Clockwise +Connector_Generic:Conn_02x19_Odd_Even +Connector_Generic:Conn_02x19_Row_Letter_First +Connector_Generic:Conn_02x19_Row_Letter_Last +Connector_Generic:Conn_02x19_Top_Bottom +Connector_Generic:Conn_02x20_Counter_Clockwise +Connector_Generic:Conn_02x20_Odd_Even +Connector_Generic:Conn_02x20_Row_Letter_First +Connector_Generic:Conn_02x20_Row_Letter_Last +Connector_Generic:Conn_02x20_Top_Bottom +Connector_Generic:Conn_02x21_Counter_Clockwise +Connector_Generic:Conn_02x21_Odd_Even +Connector_Generic:Conn_02x21_Row_Letter_First +Connector_Generic:Conn_02x21_Row_Letter_Last +Connector_Generic:Conn_02x21_Top_Bottom +Connector_Generic:Conn_02x22_Counter_Clockwise +Connector_Generic:Conn_02x22_Odd_Even +Connector_Generic:Conn_02x22_Row_Letter_First +Connector_Generic:Conn_02x22_Row_Letter_Last +Connector_Generic:Conn_02x22_Top_Bottom +Connector_Generic:Conn_02x23_Counter_Clockwise +Connector_Generic:Conn_02x23_Odd_Even +Connector_Generic:Conn_02x23_Row_Letter_First +Connector_Generic:Conn_02x23_Row_Letter_Last +Connector_Generic:Conn_02x23_Top_Bottom +Connector_Generic:Conn_02x24_Counter_Clockwise +Connector_Generic:Conn_02x24_Odd_Even +Connector_Generic:Conn_02x24_Row_Letter_First +Connector_Generic:Conn_02x24_Row_Letter_Last +Connector_Generic:Conn_02x24_Top_Bottom +Connector_Generic:Conn_02x25_Counter_Clockwise +Connector_Generic:Conn_02x25_Odd_Even +Connector_Generic:Conn_02x25_Row_Letter_First +Connector_Generic:Conn_02x25_Row_Letter_Last +Connector_Generic:Conn_02x25_Top_Bottom +Connector_Generic:Conn_02x26_Counter_Clockwise +Connector_Generic:Conn_02x26_Odd_Even +Connector_Generic:Conn_02x26_Row_Letter_First +Connector_Generic:Conn_02x26_Row_Letter_Last +Connector_Generic:Conn_02x26_Top_Bottom +Connector_Generic:Conn_02x27_Counter_Clockwise +Connector_Generic:Conn_02x27_Odd_Even +Connector_Generic:Conn_02x27_Row_Letter_First +Connector_Generic:Conn_02x27_Row_Letter_Last +Connector_Generic:Conn_02x27_Top_Bottom +Connector_Generic:Conn_02x28_Counter_Clockwise +Connector_Generic:Conn_02x28_Odd_Even +Connector_Generic:Conn_02x28_Row_Letter_First +Connector_Generic:Conn_02x28_Row_Letter_Last +Connector_Generic:Conn_02x28_Top_Bottom +Connector_Generic:Conn_02x29_Counter_Clockwise +Connector_Generic:Conn_02x29_Odd_Even +Connector_Generic:Conn_02x29_Row_Letter_First +Connector_Generic:Conn_02x29_Row_Letter_Last +Connector_Generic:Conn_02x29_Top_Bottom +Connector_Generic:Conn_02x30_Counter_Clockwise +Connector_Generic:Conn_02x30_Odd_Even +Connector_Generic:Conn_02x30_Row_Letter_First +Connector_Generic:Conn_02x30_Row_Letter_Last +Connector_Generic:Conn_02x30_Top_Bottom +Connector_Generic:Conn_02x31_Counter_Clockwise +Connector_Generic:Conn_02x31_Odd_Even +Connector_Generic:Conn_02x31_Row_Letter_First +Connector_Generic:Conn_02x31_Row_Letter_Last +Connector_Generic:Conn_02x31_Top_Bottom +Connector_Generic:Conn_02x32_Counter_Clockwise +Connector_Generic:Conn_02x32_Odd_Even +Connector_Generic:Conn_02x32_Row_Letter_First +Connector_Generic:Conn_02x32_Row_Letter_Last +Connector_Generic:Conn_02x32_Top_Bottom +Connector_Generic:Conn_02x33_Counter_Clockwise +Connector_Generic:Conn_02x33_Odd_Even +Connector_Generic:Conn_02x33_Row_Letter_First +Connector_Generic:Conn_02x33_Row_Letter_Last +Connector_Generic:Conn_02x33_Top_Bottom +Connector_Generic:Conn_02x34_Counter_Clockwise +Connector_Generic:Conn_02x34_Odd_Even +Connector_Generic:Conn_02x34_Row_Letter_First +Connector_Generic:Conn_02x34_Row_Letter_Last +Connector_Generic:Conn_02x34_Top_Bottom +Connector_Generic:Conn_02x35_Counter_Clockwise +Connector_Generic:Conn_02x35_Odd_Even +Connector_Generic:Conn_02x35_Row_Letter_First +Connector_Generic:Conn_02x35_Row_Letter_Last +Connector_Generic:Conn_02x35_Top_Bottom +Connector_Generic:Conn_02x36_Counter_Clockwise +Connector_Generic:Conn_02x36_Odd_Even +Connector_Generic:Conn_02x36_Row_Letter_First +Connector_Generic:Conn_02x36_Row_Letter_Last +Connector_Generic:Conn_02x36_Top_Bottom +Connector_Generic:Conn_02x37_Counter_Clockwise +Connector_Generic:Conn_02x37_Odd_Even +Connector_Generic:Conn_02x37_Row_Letter_First +Connector_Generic:Conn_02x37_Row_Letter_Last +Connector_Generic:Conn_02x37_Top_Bottom +Connector_Generic:Conn_02x38_Counter_Clockwise +Connector_Generic:Conn_02x38_Odd_Even +Connector_Generic:Conn_02x38_Row_Letter_First +Connector_Generic:Conn_02x38_Row_Letter_Last +Connector_Generic:Conn_02x38_Top_Bottom +Connector_Generic:Conn_02x39_Counter_Clockwise +Connector_Generic:Conn_02x39_Odd_Even +Connector_Generic:Conn_02x39_Row_Letter_First +Connector_Generic:Conn_02x39_Row_Letter_Last +Connector_Generic:Conn_02x39_Top_Bottom +Connector_Generic:Conn_02x40_Counter_Clockwise +Connector_Generic:Conn_02x40_Odd_Even +Connector_Generic:Conn_02x40_Row_Letter_First +Connector_Generic:Conn_02x40_Row_Letter_Last +Connector_Generic:Conn_02x40_Top_Bottom +Connector_Generic:Conn_02x41_Row_Letter_First +Connector_Generic:Conn_02x41_Row_Letter_Last +Connector_Generic:Conn_02x42_Row_Letter_First +Connector_Generic:Conn_02x42_Row_Letter_Last +Connector_Generic:Conn_02x43_Row_Letter_First +Connector_Generic:Conn_02x43_Row_Letter_Last +Connector_Generic:Conn_02x44_Row_Letter_First +Connector_Generic:Conn_02x44_Row_Letter_Last +Connector_Generic:Conn_02x45_Row_Letter_First +Connector_Generic:Conn_02x45_Row_Letter_Last +Connector_Generic:Conn_02x46_Row_Letter_First +Connector_Generic:Conn_02x46_Row_Letter_Last +Connector_Generic:Conn_02x47_Row_Letter_First +Connector_Generic:Conn_02x47_Row_Letter_Last +Connector_Generic:Conn_02x48_Row_Letter_First +Connector_Generic:Conn_02x48_Row_Letter_Last +Connector_Generic:Conn_02x49_Row_Letter_First +Connector_Generic:Conn_02x49_Row_Letter_Last +Connector_Generic:Conn_02x50_Row_Letter_First +Connector_Generic:Conn_02x50_Row_Letter_Last +Connector_Generic:Conn_02x51_Row_Letter_First +Connector_Generic:Conn_02x51_Row_Letter_Last +Connector_Generic:Conn_02x52_Row_Letter_First +Connector_Generic:Conn_02x52_Row_Letter_Last +Connector_Generic:Conn_02x53_Row_Letter_First +Connector_Generic:Conn_02x53_Row_Letter_Last +Connector_Generic:Conn_02x54_Row_Letter_First +Connector_Generic:Conn_02x54_Row_Letter_Last +Connector_Generic:Conn_02x55_Row_Letter_First +Connector_Generic:Conn_02x55_Row_Letter_Last +Connector_Generic:Conn_02x56_Row_Letter_First +Connector_Generic:Conn_02x56_Row_Letter_Last +Connector_Generic:Conn_02x57_Row_Letter_First +Connector_Generic:Conn_02x57_Row_Letter_Last +Connector_Generic:Conn_02x58_Row_Letter_First +Connector_Generic:Conn_02x58_Row_Letter_Last +Connector_Generic:Conn_02x59_Row_Letter_First +Connector_Generic:Conn_02x59_Row_Letter_Last +Connector_Generic:Conn_02x60_Row_Letter_First +Connector_Generic:Conn_02x60_Row_Letter_Last +Connector_Generic:Conn_2Rows-05Pins +Connector_Generic:Conn_2Rows-07Pins +Connector_Generic:Conn_2Rows-09Pins +Connector_Generic:Conn_2Rows-11Pins +Connector_Generic:Conn_2Rows-13Pins +Connector_Generic:Conn_2Rows-15Pins +Connector_Generic:Conn_2Rows-17Pins +Connector_Generic:Conn_2Rows-19Pins +Connector_Generic:Conn_2Rows-21Pins +Connector_Generic:Conn_2Rows-23Pins +Connector_Generic:Conn_2Rows-25Pins +Connector_Generic:Conn_2Rows-27Pins +Connector_Generic:Conn_2Rows-29Pins +Connector_Generic:Conn_2Rows-31Pins +Connector_Generic:Conn_2Rows-33Pins +Connector_Generic:Conn_2Rows-35Pins +Connector_Generic:Conn_2Rows-37Pins +Connector_Generic:Conn_2Rows-39Pins +Connector_Generic:Conn_2Rows-41Pins +Connector_Generic:Conn_2Rows-43Pins +Connector_Generic:Conn_2Rows-45Pins +Connector_Generic:Conn_2Rows-47Pins +Connector_Generic:Conn_2Rows-49Pins +Connector_Generic:Conn_2Rows-51Pins +Connector_Generic:Conn_2Rows-53Pins +Connector_Generic:Conn_2Rows-55Pins +Connector_Generic:Conn_2Rows-57Pins +Connector_Generic:Conn_2Rows-59Pins +Connector_Generic:Conn_2Rows-61Pins +Connector_Generic:Conn_2Rows-63Pins +Connector_Generic:Conn_2Rows-65Pins +Connector_Generic:Conn_2Rows-67Pins +Connector_Generic:Conn_2Rows-69Pins +Connector_Generic:Conn_2Rows-71Pins +Connector_Generic:Conn_2Rows-73Pins +Connector_Generic:Conn_2Rows-75Pins +Connector_Generic_MountingPin:Conn_01x01_MountingPin +Connector_Generic_MountingPin:Conn_01x02_MountingPin +Connector_Generic_MountingPin:Conn_01x03_MountingPin +Connector_Generic_MountingPin:Conn_01x04_MountingPin +Connector_Generic_MountingPin:Conn_01x05_MountingPin +Connector_Generic_MountingPin:Conn_01x06_MountingPin +Connector_Generic_MountingPin:Conn_01x07_MountingPin +Connector_Generic_MountingPin:Conn_01x08_MountingPin +Connector_Generic_MountingPin:Conn_01x09_MountingPin +Connector_Generic_MountingPin:Conn_01x10_MountingPin +Connector_Generic_MountingPin:Conn_01x11_MountingPin +Connector_Generic_MountingPin:Conn_01x12_MountingPin +Connector_Generic_MountingPin:Conn_01x13_MountingPin +Connector_Generic_MountingPin:Conn_01x14_MountingPin +Connector_Generic_MountingPin:Conn_01x15_MountingPin +Connector_Generic_MountingPin:Conn_01x16_MountingPin +Connector_Generic_MountingPin:Conn_01x17_MountingPin +Connector_Generic_MountingPin:Conn_01x18_MountingPin +Connector_Generic_MountingPin:Conn_01x19_MountingPin +Connector_Generic_MountingPin:Conn_01x20_MountingPin +Connector_Generic_MountingPin:Conn_01x21_MountingPin +Connector_Generic_MountingPin:Conn_01x22_MountingPin +Connector_Generic_MountingPin:Conn_01x23_MountingPin +Connector_Generic_MountingPin:Conn_01x24_MountingPin +Connector_Generic_MountingPin:Conn_01x25_MountingPin +Connector_Generic_MountingPin:Conn_01x26_MountingPin +Connector_Generic_MountingPin:Conn_01x27_MountingPin +Connector_Generic_MountingPin:Conn_01x28_MountingPin +Connector_Generic_MountingPin:Conn_01x29_MountingPin +Connector_Generic_MountingPin:Conn_01x30_MountingPin +Connector_Generic_MountingPin:Conn_01x31_MountingPin +Connector_Generic_MountingPin:Conn_01x32_MountingPin +Connector_Generic_MountingPin:Conn_01x33_MountingPin +Connector_Generic_MountingPin:Conn_01x34_MountingPin +Connector_Generic_MountingPin:Conn_01x35_MountingPin +Connector_Generic_MountingPin:Conn_01x36_MountingPin +Connector_Generic_MountingPin:Conn_01x37_MountingPin +Connector_Generic_MountingPin:Conn_01x38_MountingPin +Connector_Generic_MountingPin:Conn_01x39_MountingPin +Connector_Generic_MountingPin:Conn_01x40_MountingPin +Connector_Generic_MountingPin:Conn_01x41_MountingPin +Connector_Generic_MountingPin:Conn_01x42_MountingPin +Connector_Generic_MountingPin:Conn_01x43_MountingPin +Connector_Generic_MountingPin:Conn_01x44_MountingPin +Connector_Generic_MountingPin:Conn_01x45_MountingPin +Connector_Generic_MountingPin:Conn_01x46_MountingPin +Connector_Generic_MountingPin:Conn_01x47_MountingPin +Connector_Generic_MountingPin:Conn_01x48_MountingPin +Connector_Generic_MountingPin:Conn_01x49_MountingPin +Connector_Generic_MountingPin:Conn_01x50_MountingPin +Connector_Generic_MountingPin:Conn_01x51_MountingPin +Connector_Generic_MountingPin:Conn_01x52_MountingPin +Connector_Generic_MountingPin:Conn_01x53_MountingPin +Connector_Generic_MountingPin:Conn_01x54_MountingPin +Connector_Generic_MountingPin:Conn_01x55_MountingPin +Connector_Generic_MountingPin:Conn_01x56_MountingPin +Connector_Generic_MountingPin:Conn_01x57_MountingPin +Connector_Generic_MountingPin:Conn_01x58_MountingPin +Connector_Generic_MountingPin:Conn_01x59_MountingPin +Connector_Generic_MountingPin:Conn_01x60_MountingPin +Connector_Generic_MountingPin:Conn_02x01_MountingPin +Connector_Generic_MountingPin:Conn_02x01_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x01_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x02_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x02_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x02_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x02_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x02_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x03_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x03_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x03_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x03_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x03_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x04_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x04_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x04_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x04_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x04_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x05_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x05_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x05_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x05_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x05_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x06_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x06_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x06_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x06_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x06_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x07_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x07_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x07_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x07_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x07_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x08_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x08_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x08_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x08_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x08_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x09_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x09_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x09_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x09_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x09_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x10_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x10_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x10_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x10_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x10_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x11_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x11_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x11_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x11_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x11_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x12_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x12_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x12_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x12_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x12_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x13_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x13_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x13_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x13_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x13_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x14_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x14_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x14_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x14_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x14_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x15_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x15_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x15_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x15_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x15_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x16_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x16_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x16_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x16_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x16_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x17_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x17_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x17_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x17_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x17_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x18_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x18_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x18_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x18_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x18_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x19_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x19_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x19_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x19_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x19_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x20_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x20_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x20_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x20_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x20_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x21_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x21_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x21_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x21_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x21_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x22_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x22_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x22_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x22_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x22_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x23_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x23_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x23_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x23_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x23_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x24_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x24_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x24_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x24_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x24_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x25_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x25_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x25_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x25_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x25_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x26_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x26_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x26_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x26_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x26_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x27_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x27_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x27_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x27_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x27_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x28_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x28_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x28_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x28_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x28_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x29_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x29_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x29_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x29_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x29_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x30_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x30_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x30_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x30_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x30_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x31_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x31_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x31_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x31_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x31_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x32_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x32_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x32_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x32_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x32_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x33_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x33_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x33_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x33_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x33_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x34_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x34_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x34_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x34_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x34_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x35_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x35_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x35_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x35_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x35_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x36_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x36_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x36_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x36_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x36_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x37_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x37_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x37_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x37_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x37_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x38_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x38_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x38_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x38_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x38_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x39_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x39_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x39_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x39_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x39_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x40_Counter_Clockwise_MountingPin +Connector_Generic_MountingPin:Conn_02x40_Odd_Even_MountingPin +Connector_Generic_MountingPin:Conn_02x40_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x40_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x40_Top_Bottom_MountingPin +Connector_Generic_MountingPin:Conn_02x41_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x41_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x42_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x42_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x43_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x43_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x44_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x44_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x45_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x45_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x46_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x46_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x47_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x47_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x48_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x48_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x49_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x49_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x50_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x50_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x51_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x51_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x52_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x52_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x53_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x53_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x54_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x54_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x55_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x55_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x56_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x56_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x57_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x57_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x58_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x58_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x59_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x59_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_02x60_Row_Letter_First_MountingPin +Connector_Generic_MountingPin:Conn_02x60_Row_Letter_Last_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-05Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-07Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-09Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-11Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-13Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-15Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-17Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-19Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-21Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-23Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-25Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-27Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-29Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-31Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-33Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-35Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-37Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-39Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-41Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-43Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-45Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-47Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-49Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-51Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-53Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-55Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-57Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-59Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-61Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-63Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-65Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-67Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-69Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-71Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-73Pins_MountingPin +Connector_Generic_MountingPin:Conn_2Rows-75Pins_MountingPin +Connector_Generic_Shielded:Conn_01x01_Shielded +Connector_Generic_Shielded:Conn_01x02_Shielded +Connector_Generic_Shielded:Conn_01x03_Shielded +Connector_Generic_Shielded:Conn_01x04_Shielded +Connector_Generic_Shielded:Conn_01x05_Shielded +Connector_Generic_Shielded:Conn_01x06_Shielded +Connector_Generic_Shielded:Conn_01x07_Shielded +Connector_Generic_Shielded:Conn_01x08_Shielded +Connector_Generic_Shielded:Conn_01x09_Shielded +Connector_Generic_Shielded:Conn_01x10_Shielded +Connector_Generic_Shielded:Conn_01x11_Shielded +Connector_Generic_Shielded:Conn_01x12_Shielded +Connector_Generic_Shielded:Conn_01x13_Shielded +Connector_Generic_Shielded:Conn_01x14_Shielded +Connector_Generic_Shielded:Conn_01x15_Shielded +Connector_Generic_Shielded:Conn_01x16_Shielded +Connector_Generic_Shielded:Conn_01x17_Shielded +Connector_Generic_Shielded:Conn_01x18_Shielded +Connector_Generic_Shielded:Conn_01x19_Shielded +Connector_Generic_Shielded:Conn_01x20_Shielded +Connector_Generic_Shielded:Conn_01x21_Shielded +Connector_Generic_Shielded:Conn_01x22_Shielded +Connector_Generic_Shielded:Conn_01x23_Shielded +Connector_Generic_Shielded:Conn_01x24_Shielded +Connector_Generic_Shielded:Conn_01x25_Shielded +Connector_Generic_Shielded:Conn_01x26_Shielded +Connector_Generic_Shielded:Conn_01x27_Shielded +Connector_Generic_Shielded:Conn_01x28_Shielded +Connector_Generic_Shielded:Conn_01x29_Shielded +Connector_Generic_Shielded:Conn_01x30_Shielded +Connector_Generic_Shielded:Conn_01x31_Shielded +Connector_Generic_Shielded:Conn_01x32_Shielded +Connector_Generic_Shielded:Conn_01x33_Shielded +Connector_Generic_Shielded:Conn_01x34_Shielded +Connector_Generic_Shielded:Conn_01x35_Shielded +Connector_Generic_Shielded:Conn_01x36_Shielded +Connector_Generic_Shielded:Conn_01x37_Shielded +Connector_Generic_Shielded:Conn_01x38_Shielded +Connector_Generic_Shielded:Conn_01x39_Shielded +Connector_Generic_Shielded:Conn_01x40_Shielded +Connector_Generic_Shielded:Conn_01x41_Shielded +Connector_Generic_Shielded:Conn_01x42_Shielded +Connector_Generic_Shielded:Conn_01x43_Shielded +Connector_Generic_Shielded:Conn_01x44_Shielded +Connector_Generic_Shielded:Conn_01x45_Shielded +Connector_Generic_Shielded:Conn_01x46_Shielded +Connector_Generic_Shielded:Conn_01x47_Shielded +Connector_Generic_Shielded:Conn_01x48_Shielded +Connector_Generic_Shielded:Conn_01x49_Shielded +Connector_Generic_Shielded:Conn_01x50_Shielded +Connector_Generic_Shielded:Conn_01x51_Shielded +Connector_Generic_Shielded:Conn_01x52_Shielded +Connector_Generic_Shielded:Conn_01x53_Shielded +Connector_Generic_Shielded:Conn_01x54_Shielded +Connector_Generic_Shielded:Conn_01x55_Shielded +Connector_Generic_Shielded:Conn_01x56_Shielded +Connector_Generic_Shielded:Conn_01x57_Shielded +Connector_Generic_Shielded:Conn_01x58_Shielded +Connector_Generic_Shielded:Conn_01x59_Shielded +Connector_Generic_Shielded:Conn_01x60_Shielded +Connector_Generic_Shielded:Conn_02x01_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x01_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x01_Shielded +Connector_Generic_Shielded:Conn_02x02_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x02_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x02_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x02_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x02_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x03_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x03_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x03_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x03_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x03_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x04_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x04_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x04_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x04_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x04_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x05_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x05_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x05_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x05_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x05_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x06_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x06_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x06_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x06_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x06_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x07_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x07_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x07_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x07_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x07_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x08_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x08_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x08_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x08_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x08_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x09_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x09_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x09_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x09_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x09_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x10_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x10_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x10_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x10_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x10_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x11_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x11_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x11_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x11_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x11_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x12_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x12_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x12_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x12_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x12_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x13_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x13_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x13_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x13_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x13_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x14_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x14_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x14_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x14_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x14_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x15_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x15_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x15_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x15_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x15_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x16_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x16_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x16_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x16_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x16_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x17_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x17_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x17_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x17_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x17_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x18_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x18_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x18_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x18_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x18_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x19_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x19_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x19_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x19_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x19_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x20_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x20_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x20_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x20_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x20_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x21_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x21_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x21_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x21_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x21_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x22_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x22_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x22_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x22_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x22_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x23_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x23_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x23_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x23_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x23_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x24_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x24_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x24_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x24_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x24_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x25_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x25_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x25_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x25_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x25_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x26_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x26_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x26_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x26_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x26_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x27_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x27_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x27_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x27_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x27_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x28_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x28_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x28_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x28_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x28_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x29_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x29_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x29_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x29_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x29_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x30_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x30_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x30_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x30_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x30_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x31_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x31_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x31_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x31_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x31_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x32_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x32_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x32_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x32_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x32_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x33_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x33_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x33_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x33_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x33_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x34_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x34_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x34_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x34_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x34_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x35_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x35_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x35_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x35_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x35_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x36_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x36_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x36_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x36_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x36_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x37_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x37_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x37_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x37_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x37_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x38_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x38_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x38_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x38_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x38_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x39_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x39_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x39_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x39_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x39_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x40_Counter_Clockwise_Shielded +Connector_Generic_Shielded:Conn_02x40_Odd_Even_Shielded +Connector_Generic_Shielded:Conn_02x40_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x40_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x40_Top_Bottom_Shielded +Connector_Generic_Shielded:Conn_02x40_Top_Bottom_Shielded_1 +Connector_Generic_Shielded:Conn_02x41_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x41_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x42_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x42_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x43_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x43_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x44_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x44_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x45_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x45_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x46_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x46_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x47_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x47_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x48_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x48_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x49_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x49_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x50_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x50_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x51_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x51_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x52_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x52_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x53_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x53_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x54_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x54_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x55_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x55_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x56_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x56_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x57_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x57_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x58_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x58_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x59_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x59_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_02x60_Row_Letter_First_Shielded +Connector_Generic_Shielded:Conn_02x60_Row_Letter_Last_Shielded +Connector_Generic_Shielded:Conn_2Rows-05Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-07Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-09Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-11Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-13Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-15Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-17Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-19Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-21Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-23Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-25Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-27Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-29Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-31Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-33Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-35Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-37Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-39Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-41Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-43Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-45Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-47Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-49Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-51Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-53Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-55Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-57Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-59Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-61Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-63Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-65Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-67Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-69Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-71Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-73Pins_Shielded +Connector_Generic_Shielded:Conn_2Rows-75Pins_Shielded +Converter_ACDC:BAC05S05DC +Converter_ACDC:HLK-10M03 +Converter_ACDC:HLK-10M05 +Converter_ACDC:HLK-10M09 +Converter_ACDC:HLK-10M12 +Converter_ACDC:HLK-10M15 +Converter_ACDC:HLK-10M24 +Converter_ACDC:HLK-12M03A +Converter_ACDC:HLK-12M05A +Converter_ACDC:HLK-12M09A +Converter_ACDC:HLK-12M12A +Converter_ACDC:HLK-12M15A +Converter_ACDC:HLK-12M24A +Converter_ACDC:HLK-20M05 +Converter_ACDC:HLK-20M09 +Converter_ACDC:HLK-20M12 +Converter_ACDC:HLK-20M15 +Converter_ACDC:HLK-20M24 +Converter_ACDC:HLK-2M03 +Converter_ACDC:HLK-2M05 +Converter_ACDC:HLK-2M09 +Converter_ACDC:HLK-2M12 +Converter_ACDC:HLK-2M15 +Converter_ACDC:HLK-2M24 +Converter_ACDC:HLK-30M05 +Converter_ACDC:HLK-30M05C +Converter_ACDC:HLK-30M09 +Converter_ACDC:HLK-30M09C +Converter_ACDC:HLK-30M12 +Converter_ACDC:HLK-30M12C +Converter_ACDC:HLK-30M15 +Converter_ACDC:HLK-30M15C +Converter_ACDC:HLK-30M24 +Converter_ACDC:HLK-30M24C +Converter_ACDC:HLK-5M03 +Converter_ACDC:HLK-5M05 +Converter_ACDC:HLK-5M09 +Converter_ACDC:HLK-5M12 +Converter_ACDC:HLK-5M15 +Converter_ACDC:HLK-5M24 +Converter_ACDC:HLK-PM01 +Converter_ACDC:HLK-PM03 +Converter_ACDC:HLK-PM09 +Converter_ACDC:HLK-PM12 +Converter_ACDC:HLK-PM15 +Converter_ACDC:HLK-PM24 +Converter_ACDC:HS-40003 +Converter_ACDC:HS-40005 +Converter_ACDC:HS-40009 +Converter_ACDC:HS-40012 +Converter_ACDC:HS-40015 +Converter_ACDC:HS-40018 +Converter_ACDC:HS-40024 +Converter_ACDC:IRM-02-12 +Converter_ACDC:IRM-02-12S +Converter_ACDC:IRM-02-15 +Converter_ACDC:IRM-02-15S +Converter_ACDC:IRM-02-24 +Converter_ACDC:IRM-02-24S +Converter_ACDC:IRM-02-3.3 +Converter_ACDC:IRM-02-3.3S +Converter_ACDC:IRM-02-5 +Converter_ACDC:IRM-02-5S +Converter_ACDC:IRM-02-9 +Converter_ACDC:IRM-02-9S +Converter_ACDC:IRM-03-12 +Converter_ACDC:IRM-03-12S +Converter_ACDC:IRM-03-15 +Converter_ACDC:IRM-03-15S +Converter_ACDC:IRM-03-24 +Converter_ACDC:IRM-03-24S +Converter_ACDC:IRM-03-3.3 +Converter_ACDC:IRM-03-3.3S +Converter_ACDC:IRM-03-5 +Converter_ACDC:IRM-03-5S +Converter_ACDC:IRM-03-9 +Converter_ACDC:IRM-03-9S +Converter_ACDC:IRM-05-12 +Converter_ACDC:IRM-05-15 +Converter_ACDC:IRM-05-24 +Converter_ACDC:IRM-05-3.3 +Converter_ACDC:IRM-05-5 +Converter_ACDC:IRM-10-12 +Converter_ACDC:IRM-10-15 +Converter_ACDC:IRM-10-24 +Converter_ACDC:IRM-10-3.3 +Converter_ACDC:IRM-10-5 +Converter_ACDC:IRM-20-12 +Converter_ACDC:IRM-20-15 +Converter_ACDC:IRM-20-24 +Converter_ACDC:IRM-20-3.3 +Converter_ACDC:IRM-20-5 +Converter_ACDC:IRM-60-12 +Converter_ACDC:IRM-60-15 +Converter_ACDC:IRM-60-24 +Converter_ACDC:IRM-60-48 +Converter_ACDC:IRM-60-5 +Converter_ACDC:MFM-10-12 +Converter_ACDC:MFM-10-15 +Converter_ACDC:MFM-10-24 +Converter_ACDC:MFM-10-3.3 +Converter_ACDC:MFM-10-5 +Converter_ACDC:MFM-15-12 +Converter_ACDC:MFM-15-15 +Converter_ACDC:MFM-15-24 +Converter_ACDC:MFM-15-3.3 +Converter_ACDC:MFM-15-5 +Converter_ACDC:PBO-3-S12 +Converter_ACDC:PBO-3-S15 +Converter_ACDC:PBO-3-S24 +Converter_ACDC:PBO-3-S3.3 +Converter_ACDC:PBO-3-S5 +Converter_ACDC:PBO-3-S9 +Converter_ACDC:RAC01-05SGB +Converter_ACDC:RAC01-12SGB +Converter_ACDC:RAC01-24SGB +Converter_ACDC:RAC01-3.3SGB +Converter_ACDC:RAC01-xxSGB +Converter_ACDC:RAC04-05SGA +Converter_ACDC:RAC04-05SGB +Converter_ACDC:RAC04-09SGA +Converter_ACDC:RAC04-09SGB +Converter_ACDC:RAC04-12SGA +Converter_ACDC:RAC04-12SGB +Converter_ACDC:RAC04-15SGA +Converter_ACDC:RAC04-15SGB +Converter_ACDC:RAC04-24SGA +Converter_ACDC:RAC04-24SGB +Converter_ACDC:RAC04-3.3SGA +Converter_ACDC:RAC04-3.3SGB +Converter_ACDC:RAC04-xxSGA +Converter_ACDC:RAC04-xxSGB +Converter_ACDC:RAC05-05SK +Converter_ACDC:RAC05-12SK +Converter_ACDC:RAC05-15SK +Converter_ACDC:RAC05-24SK +Converter_ACDC:RAC05-3.3SK +Converter_ACDC:RAC20-05SK +Converter_ACDC:RAC20-12DK +Converter_ACDC:RAC20-12SK +Converter_ACDC:RAC20-15DK +Converter_ACDC:RAC20-15SK +Converter_ACDC:RAC20-24SK +Converter_ACDC:RAC20-48SK +Converter_ACDC:TMF05105 +Converter_ACDC:TMF05112 +Converter_ACDC:TMF05115 +Converter_ACDC:TMF05124 +Converter_ACDC:TMF10105 +Converter_ACDC:TMF10112 +Converter_ACDC:TMF10115 +Converter_ACDC:TMF10124 +Converter_ACDC:TMF20105 +Converter_ACDC:TMF20112 +Converter_ACDC:TMF20115 +Converter_ACDC:TMF20124 +Converter_ACDC:TMF30105 +Converter_ACDC:TMF30112 +Converter_ACDC:TMF30115 +Converter_ACDC:TMF30124 +Converter_ACDC:TMLM04103 +Converter_ACDC:TMLM04105 +Converter_ACDC:TMLM04109 +Converter_ACDC:TMLM04112 +Converter_ACDC:TMLM04115 +Converter_ACDC:TMLM04124 +Converter_ACDC:TMLM04225 +Converter_ACDC:TMLM04253 +Converter_ACDC:TMLM05103 +Converter_ACDC:TMLM05105 +Converter_ACDC:TMLM05112 +Converter_ACDC:TMLM05115 +Converter_ACDC:TMLM05124 +Converter_ACDC:TMLM10103 +Converter_ACDC:TMLM10105 +Converter_ACDC:TMLM10112 +Converter_ACDC:TMLM10115 +Converter_ACDC:TMLM10124 +Converter_ACDC:TMLM20103 +Converter_ACDC:TMLM20105 +Converter_ACDC:TMLM20112 +Converter_ACDC:TMLM20115 +Converter_ACDC:TMLM20124 +Converter_ACDC:TPP-15-103-D +Converter_ACDC:TPP-15-105-D +Converter_ACDC:TPP-15-109-D +Converter_ACDC:TPP-15-112-D +Converter_ACDC:TPP-15-115-D +Converter_ACDC:TPP-15-124-D +Converter_ACDC:TPP-15-136-D +Converter_ACDC:TPP-15-148-D +Converter_ACDC:VTX-214-010-103 +Converter_ACDC:VTX-214-010-105 +Converter_ACDC:VTX-214-010-106 +Converter_ACDC:VTX-214-010-107 +Converter_ACDC:VTX-214-010-108 +Converter_ACDC:VTX-214-010-109 +Converter_ACDC:VTX-214-010-110 +Converter_ACDC:VTX-214-010-112 +Converter_ACDC:VTX-214-010-115 +Converter_ACDC:VTX-214-010-118 +Converter_ACDC:VTX-214-010-124 +Converter_ACDC:VTX-214-010-148 +Converter_ACDC:VTX-214-015-103 +Converter_ACDC:VTX-214-015-105 +Converter_ACDC:VTX-214-015-106 +Converter_ACDC:VTX-214-015-107 +Converter_ACDC:VTX-214-015-109 +Converter_ACDC:VTX-214-015-112 +Converter_ACDC:VTX-214-015-115 +Converter_ACDC:VTX-214-015-118 +Converter_ACDC:VTX-214-015-124 +Converter_ACDC:VTX-214-015-148 +Converter_DCDC:ATA00A18S-L +Converter_DCDC:ATA00A36S-L +Converter_DCDC:ATA00AA18S-L +Converter_DCDC:ATA00AA36S-L +Converter_DCDC:ATA00B18S-L +Converter_DCDC:ATA00B36S-L +Converter_DCDC:ATA00BB18S-L +Converter_DCDC:ATA00BB36S-L +Converter_DCDC:ATA00C18S-L +Converter_DCDC:ATA00C36S-L +Converter_DCDC:ATA00CC18S-L +Converter_DCDC:ATA00CC36S-L +Converter_DCDC:ATA00F18S-L +Converter_DCDC:ATA00F36S-L +Converter_DCDC:ATA00H18S-L +Converter_DCDC:ATA00H36S-L +Converter_DCDC:Ag9905LP +Converter_DCDC:BD8314NUV +Converter_DCDC:IA0305D +Converter_DCDC:IA0305S +Converter_DCDC:IA0503D +Converter_DCDC:IA0503S +Converter_DCDC:IA0505D +Converter_DCDC:IA0505S +Converter_DCDC:IA0509D +Converter_DCDC:IA0509S +Converter_DCDC:IA0512D +Converter_DCDC:IA0512S +Converter_DCDC:IA0515D +Converter_DCDC:IA0515S +Converter_DCDC:IA0524D +Converter_DCDC:IA0524S +Converter_DCDC:IA1203D +Converter_DCDC:IA1203S +Converter_DCDC:IA1205D +Converter_DCDC:IA1205S +Converter_DCDC:IA1209D +Converter_DCDC:IA1209S +Converter_DCDC:IA1212D +Converter_DCDC:IA1212S +Converter_DCDC:IA1215D +Converter_DCDC:IA1215S +Converter_DCDC:IA1224D +Converter_DCDC:IA1224S +Converter_DCDC:IA2403D +Converter_DCDC:IA2403S +Converter_DCDC:IA2405D +Converter_DCDC:IA2405S +Converter_DCDC:IA2409D +Converter_DCDC:IA2409S +Converter_DCDC:IA2412D +Converter_DCDC:IA2412S +Converter_DCDC:IA2415D +Converter_DCDC:IA2415S +Converter_DCDC:IA2424D +Converter_DCDC:IA2424S +Converter_DCDC:IA4803D +Converter_DCDC:IA4803S +Converter_DCDC:IA4805D +Converter_DCDC:IA4805S +Converter_DCDC:IA4809D +Converter_DCDC:IA4809S +Converter_DCDC:IA4812D +Converter_DCDC:IA4812S +Converter_DCDC:IA4815D +Converter_DCDC:IA4815S +Converter_DCDC:IA4824D +Converter_DCDC:IA4824S +Converter_DCDC:IH0503D +Converter_DCDC:IH0503DH +Converter_DCDC:IH0503S +Converter_DCDC:IH0503SH +Converter_DCDC:IH0505D +Converter_DCDC:IH0505DH +Converter_DCDC:IH0505S +Converter_DCDC:IH0505SH +Converter_DCDC:IH0509D +Converter_DCDC:IH0509DH +Converter_DCDC:IH0509S +Converter_DCDC:IH0509SH +Converter_DCDC:IH0512D +Converter_DCDC:IH0512DH +Converter_DCDC:IH0512S +Converter_DCDC:IH0512SH +Converter_DCDC:IH0515D +Converter_DCDC:IH0515DH +Converter_DCDC:IH0515S +Converter_DCDC:IH0515SH +Converter_DCDC:IH0524D +Converter_DCDC:IH0524DH +Converter_DCDC:IH0524S +Converter_DCDC:IH0524SH +Converter_DCDC:IH1203D +Converter_DCDC:IH1203DH +Converter_DCDC:IH1203S +Converter_DCDC:IH1203SH +Converter_DCDC:IH1205D +Converter_DCDC:IH1205DH +Converter_DCDC:IH1205S +Converter_DCDC:IH1205SH +Converter_DCDC:IH1209D +Converter_DCDC:IH1209DH +Converter_DCDC:IH1209S +Converter_DCDC:IH1209SH +Converter_DCDC:IH1212D +Converter_DCDC:IH1212DH +Converter_DCDC:IH1212S +Converter_DCDC:IH1212SH +Converter_DCDC:IH1215D +Converter_DCDC:IH1215DH +Converter_DCDC:IH1215S +Converter_DCDC:IH1215SH +Converter_DCDC:IH1224D +Converter_DCDC:IH1224DH +Converter_DCDC:IH1224S +Converter_DCDC:IH1224SH +Converter_DCDC:IH2403D +Converter_DCDC:IH2403DH +Converter_DCDC:IH2403S +Converter_DCDC:IH2403SH +Converter_DCDC:IH2405D +Converter_DCDC:IH2405DH +Converter_DCDC:IH2405S +Converter_DCDC:IH2405SH +Converter_DCDC:IH2409D +Converter_DCDC:IH2409DH +Converter_DCDC:IH2409S +Converter_DCDC:IH2409SH +Converter_DCDC:IH2412D +Converter_DCDC:IH2412DH +Converter_DCDC:IH2412S +Converter_DCDC:IH2412SH +Converter_DCDC:IH2415D +Converter_DCDC:IH2415DH +Converter_DCDC:IH2415S +Converter_DCDC:IH2415SH +Converter_DCDC:IH2424D +Converter_DCDC:IH2424DH +Converter_DCDC:IH2424S +Converter_DCDC:IH2424SH +Converter_DCDC:IH4803D +Converter_DCDC:IH4803DH +Converter_DCDC:IH4803S +Converter_DCDC:IH4803SH +Converter_DCDC:IH4805D +Converter_DCDC:IH4805DH +Converter_DCDC:IH4805S +Converter_DCDC:IH4805SH +Converter_DCDC:IH4809D +Converter_DCDC:IH4809DH +Converter_DCDC:IH4809S +Converter_DCDC:IH4809SH +Converter_DCDC:IH4812D +Converter_DCDC:IH4812DH +Converter_DCDC:IH4812S +Converter_DCDC:IH4812SH +Converter_DCDC:IH4815D +Converter_DCDC:IH4815DH +Converter_DCDC:IH4815S +Converter_DCDC:IH4815SH +Converter_DCDC:IH4824D +Converter_DCDC:IH4824DH +Converter_DCDC:IH4824S +Converter_DCDC:IH4824SH +Converter_DCDC:ISU0205D12 +Converter_DCDC:ISU0205D15 +Converter_DCDC:ISU0205S05 +Converter_DCDC:ISU0205S12 +Converter_DCDC:ISU0205S15 +Converter_DCDC:ISU0205S24 +Converter_DCDC:ISU0224D12 +Converter_DCDC:ISU0224D15 +Converter_DCDC:ISU0224S05 +Converter_DCDC:ISU0224S12 +Converter_DCDC:ISU0224S15 +Converter_DCDC:ISU0224S24 +Converter_DCDC:ISU0248D12 +Converter_DCDC:ISU0248D15 +Converter_DCDC:ISU0248S05 +Converter_DCDC:ISU0248S12 +Converter_DCDC:ISU0248S15 +Converter_DCDC:ISU0248S24 +Converter_DCDC:ITQ2403SA +Converter_DCDC:ITQ2403SA-H +Converter_DCDC:ITQ2405S +Converter_DCDC:ITQ2405S-H +Converter_DCDC:ITQ2405SA +Converter_DCDC:ITQ2405SA-H +Converter_DCDC:ITQ2409SA +Converter_DCDC:ITQ2409SA-H +Converter_DCDC:ITQ2412S +Converter_DCDC:ITQ2412S-H +Converter_DCDC:ITQ2412SA +Converter_DCDC:ITQ2412SA-H +Converter_DCDC:ITQ2415S +Converter_DCDC:ITQ2415S-H +Converter_DCDC:ITQ2415SA +Converter_DCDC:ITQ2415SA-H +Converter_DCDC:ITQ2424SA +Converter_DCDC:ITQ2424SA-H +Converter_DCDC:ITQ4803SA +Converter_DCDC:ITQ4803SA-H +Converter_DCDC:ITQ4805S +Converter_DCDC:ITQ4805S-H +Converter_DCDC:ITQ4805SA +Converter_DCDC:ITQ4805SA-H +Converter_DCDC:ITQ4809SA +Converter_DCDC:ITQ4809SA-H +Converter_DCDC:ITQ4812S +Converter_DCDC:ITQ4812S-H +Converter_DCDC:ITQ4812SA +Converter_DCDC:ITQ4812SA-H +Converter_DCDC:ITQ4815S +Converter_DCDC:ITQ4815S-H +Converter_DCDC:ITQ4815SA +Converter_DCDC:ITQ4815SA-H +Converter_DCDC:ITQ4824SA +Converter_DCDC:ITQ4824SA-H +Converter_DCDC:ITX0503SA +Converter_DCDC:ITX0503SA-H +Converter_DCDC:ITX0503SA-HR +Converter_DCDC:ITX0503SA-R +Converter_DCDC:ITX0505S +Converter_DCDC:ITX0505S-H +Converter_DCDC:ITX0505S-HR +Converter_DCDC:ITX0505S-R +Converter_DCDC:ITX0505SA +Converter_DCDC:ITX0505SA-H +Converter_DCDC:ITX0505SA-HR +Converter_DCDC:ITX0505SA-R +Converter_DCDC:ITX0509SA +Converter_DCDC:ITX0509SA-H +Converter_DCDC:ITX0509SA-HR +Converter_DCDC:ITX0509SA-R +Converter_DCDC:ITX0512S +Converter_DCDC:ITX0512S-H +Converter_DCDC:ITX0512S-HR +Converter_DCDC:ITX0512S-R +Converter_DCDC:ITX0512SA +Converter_DCDC:ITX0512SA-H +Converter_DCDC:ITX0512SA-HR +Converter_DCDC:ITX0512SA-R +Converter_DCDC:ITX0515S +Converter_DCDC:ITX0515S-H +Converter_DCDC:ITX0515S-HR +Converter_DCDC:ITX0515S-R +Converter_DCDC:ITX0515SA +Converter_DCDC:ITX0515SA-H +Converter_DCDC:ITX0515SA-HR +Converter_DCDC:ITX0515SA-R +Converter_DCDC:ITX0524SA +Converter_DCDC:ITX0524SA-H +Converter_DCDC:ITX0524SA-HR +Converter_DCDC:ITX0524SA-R +Converter_DCDC:ITX1203SA +Converter_DCDC:ITX1203SA-H +Converter_DCDC:ITX1203SA-HR +Converter_DCDC:ITX1203SA-R +Converter_DCDC:ITX1205S +Converter_DCDC:ITX1205S-H +Converter_DCDC:ITX1205S-HR +Converter_DCDC:ITX1205S-R +Converter_DCDC:ITX1205SA +Converter_DCDC:ITX1205SA-H +Converter_DCDC:ITX1205SA-HR +Converter_DCDC:ITX1205SA-R +Converter_DCDC:ITX1209SA +Converter_DCDC:ITX1209SA-H +Converter_DCDC:ITX1209SA-HR +Converter_DCDC:ITX1209SA-R +Converter_DCDC:ITX1212S +Converter_DCDC:ITX1212S-H +Converter_DCDC:ITX1212S-HR +Converter_DCDC:ITX1212S-R +Converter_DCDC:ITX1212SA +Converter_DCDC:ITX1212SA-H +Converter_DCDC:ITX1212SA-HR +Converter_DCDC:ITX1212SA-R +Converter_DCDC:ITX1215S +Converter_DCDC:ITX1215S-H +Converter_DCDC:ITX1215S-HR +Converter_DCDC:ITX1215S-R +Converter_DCDC:ITX1215SA +Converter_DCDC:ITX1215SA-H +Converter_DCDC:ITX1215SA-HR +Converter_DCDC:ITX1215SA-R +Converter_DCDC:ITX1224SA +Converter_DCDC:ITX1224SA-H +Converter_DCDC:ITX1224SA-HR +Converter_DCDC:ITX1224SA-R +Converter_DCDC:ITX2403SA +Converter_DCDC:ITX2403SA-H +Converter_DCDC:ITX2403SA-HR +Converter_DCDC:ITX2403SA-R +Converter_DCDC:ITX2405S +Converter_DCDC:ITX2405S-H +Converter_DCDC:ITX2405S-HR +Converter_DCDC:ITX2405S-R +Converter_DCDC:ITX2405SA +Converter_DCDC:ITX2405SA-H +Converter_DCDC:ITX2405SA-HR +Converter_DCDC:ITX2405SA-R +Converter_DCDC:ITX2409SA +Converter_DCDC:ITX2409SA-H +Converter_DCDC:ITX2409SA-HR +Converter_DCDC:ITX2409SA-R +Converter_DCDC:ITX2412S +Converter_DCDC:ITX2412S-H +Converter_DCDC:ITX2412S-HR +Converter_DCDC:ITX2412S-R +Converter_DCDC:ITX2412SA +Converter_DCDC:ITX2412SA-H +Converter_DCDC:ITX2412SA-HR +Converter_DCDC:ITX2412SA-R +Converter_DCDC:ITX2415S +Converter_DCDC:ITX2415S-H +Converter_DCDC:ITX2415S-HR +Converter_DCDC:ITX2415S-R +Converter_DCDC:ITX2415SA +Converter_DCDC:ITX2415SA-H +Converter_DCDC:ITX2415SA-HR +Converter_DCDC:ITX2415SA-R +Converter_DCDC:ITX2424SA +Converter_DCDC:ITX2424SA-H +Converter_DCDC:ITX2424SA-HR +Converter_DCDC:ITX2424SA-R +Converter_DCDC:ITX4803SA +Converter_DCDC:ITX4803SA-H +Converter_DCDC:ITX4803SA-HR +Converter_DCDC:ITX4803SA-R +Converter_DCDC:ITX4805S +Converter_DCDC:ITX4805S-H +Converter_DCDC:ITX4805S-HR +Converter_DCDC:ITX4805S-R +Converter_DCDC:ITX4805SA +Converter_DCDC:ITX4805SA-H +Converter_DCDC:ITX4805SA-HR +Converter_DCDC:ITX4805SA-R +Converter_DCDC:ITX4809SA +Converter_DCDC:ITX4809SA-H +Converter_DCDC:ITX4809SA-HR +Converter_DCDC:ITX4809SA-R +Converter_DCDC:ITX4812S +Converter_DCDC:ITX4812S-H +Converter_DCDC:ITX4812S-HR +Converter_DCDC:ITX4812S-R +Converter_DCDC:ITX4812SA +Converter_DCDC:ITX4812SA-H +Converter_DCDC:ITX4812SA-HR +Converter_DCDC:ITX4812SA-R +Converter_DCDC:ITX4815S +Converter_DCDC:ITX4815S-H +Converter_DCDC:ITX4815S-HR +Converter_DCDC:ITX4815S-R +Converter_DCDC:ITX4815SA +Converter_DCDC:ITX4815SA-H +Converter_DCDC:ITX4815SA-HR +Converter_DCDC:ITX4815SA-R +Converter_DCDC:ITX4824SA +Converter_DCDC:ITX4824SA-H +Converter_DCDC:ITX4824SA-HR +Converter_DCDC:ITX4824SA-R +Converter_DCDC:JTD1524D05 +Converter_DCDC:JTD1524D12 +Converter_DCDC:JTD1524D15 +Converter_DCDC:JTD1524S05 +Converter_DCDC:JTD1524S12 +Converter_DCDC:JTD1524S15 +Converter_DCDC:JTD1524S3V3 +Converter_DCDC:JTD1548D05 +Converter_DCDC:JTD1548D12 +Converter_DCDC:JTD1548D15 +Converter_DCDC:JTD1548S05 +Converter_DCDC:JTD1548S12 +Converter_DCDC:JTD1548S15 +Converter_DCDC:JTD1548S3V3 +Converter_DCDC:JTD2024D05 +Converter_DCDC:JTD2024D12 +Converter_DCDC:JTD2024D15 +Converter_DCDC:JTD2024S05 +Converter_DCDC:JTD2024S12 +Converter_DCDC:JTD2024S15 +Converter_DCDC:JTD2024S3V3 +Converter_DCDC:JTD2048D05 +Converter_DCDC:JTD2048D12 +Converter_DCDC:JTD2048D15 +Converter_DCDC:JTD2048S05 +Converter_DCDC:JTD2048S12 +Converter_DCDC:JTD2048S15 +Converter_DCDC:JTD2048S3V3 +Converter_DCDC:JTE0624D03 +Converter_DCDC:JTE0624D05 +Converter_DCDC:JTE0624D12 +Converter_DCDC:JTE0624D15 +Converter_DCDC:JTE0624D24 +Converter_DCDC:LT1026 +Converter_DCDC:MEE1S0303SC +Converter_DCDC:MEE1S0305SC +Converter_DCDC:MEE1S0309SC +Converter_DCDC:MEE1S0312SC +Converter_DCDC:MEE1S0315SC +Converter_DCDC:MEE1S0503SC +Converter_DCDC:MEE1S0505SC +Converter_DCDC:MEE1S0509SC +Converter_DCDC:MEE1S0512SC +Converter_DCDC:MEE1S0515SC +Converter_DCDC:MEE1S1205SC +Converter_DCDC:MEE1S1209SC +Converter_DCDC:MEE1S1212SC +Converter_DCDC:MEE1S1215SC +Converter_DCDC:MEE1S1505SC +Converter_DCDC:MEE1S1509SC +Converter_DCDC:MEE1S1512SC +Converter_DCDC:MEE1S1515SC +Converter_DCDC:MEE1S2405SC +Converter_DCDC:MEE1S2409SC +Converter_DCDC:MEE1S2412SC +Converter_DCDC:MEE1S2415SC +Converter_DCDC:MEE3S0505SC +Converter_DCDC:MEE3S0509SC +Converter_DCDC:MEE3S0512SC +Converter_DCDC:MEE3S0515SC +Converter_DCDC:MEE3S1205SC +Converter_DCDC:MEE3S1209SC +Converter_DCDC:MEE3S1212SC +Converter_DCDC:MEE3S1215SC +Converter_DCDC:MGJ2D051505SC +Converter_DCDC:MGJ2D051509SC +Converter_DCDC:MGJ2D051515SC +Converter_DCDC:MGJ2D051802SC +Converter_DCDC:MGJ2D052003SC +Converter_DCDC:MGJ2D052005SC +Converter_DCDC:MGJ2D121505SC +Converter_DCDC:MGJ2D121509SC +Converter_DCDC:MGJ2D121802SC +Converter_DCDC:MGJ2D122003SC +Converter_DCDC:MGJ2D122005SC +Converter_DCDC:MGJ2D151505SC +Converter_DCDC:MGJ2D151509SC +Converter_DCDC:MGJ2D151515SC +Converter_DCDC:MGJ2D151802SC +Converter_DCDC:MGJ2D152003SC +Converter_DCDC:MGJ2D152005SC +Converter_DCDC:MGJ2D241505SC +Converter_DCDC:MGJ2D241509SC +Converter_DCDC:MGJ2D241709SC +Converter_DCDC:MGJ2D241802SC +Converter_DCDC:MGJ2D242003SC +Converter_DCDC:MGJ2D242005SC +Converter_DCDC:MGJ3T05150505MC +Converter_DCDC:MGJ3T12150505MC +Converter_DCDC:MGJ3T24150505MC +Converter_DCDC:MYRGPxx0060x21RC +Converter_DCDC:MYRGPxx0060x21RF +Converter_DCDC:NCS1S1203SC +Converter_DCDC:NCS1S1205SC +Converter_DCDC:NCS1S1212SC +Converter_DCDC:NCS1S2403SC +Converter_DCDC:NCS1S2405SC +Converter_DCDC:NCS1S2412SC +Converter_DCDC:NSD10-xxDyy +Converter_DCDC:NSD10-xxSyy +Converter_DCDC:OKI-78SR-12_1.0-W36-C +Converter_DCDC:OKI-78SR-12_1.0-W36H-C +Converter_DCDC:OKI-78SR-3.3_1.5-W36-C +Converter_DCDC:OKI-78SR-3.3_1.5-W36H-C +Converter_DCDC:OKI-78SR-5_1.5-W36-C +Converter_DCDC:OKI-78SR-5_1.5-W36H-C +Converter_DCDC:PTN78000H_EUS-5 +Converter_DCDC:PTN78000W_EUS-5 +Converter_DCDC:PTN78020H_EUK-7 +Converter_DCDC:PTN78020W_EUK-7 +Converter_DCDC:PTN78060H_EUW-7 +Converter_DCDC:PTN78060W_EUW-7 +Converter_DCDC:RPA60-2405SFW +Converter_DCDC:RPA60-2412SFW +Converter_DCDC:RPA60-2415SFW +Converter_DCDC:RPA60-2424SFW +Converter_DCDC:RPM3.3-1.0 +Converter_DCDC:RPM3.3-2.0 +Converter_DCDC:RPM3.3-3.0 +Converter_DCDC:RPM3.3-6.0 +Converter_DCDC:RPM5.0-1.0 +Converter_DCDC:RPM5.0-2.0 +Converter_DCDC:RPM5.0-3.0 +Converter_DCDC:RPM5.0-6.0 +Converter_DCDC:RPMH12-1.5 +Converter_DCDC:RPMH15-1.5 +Converter_DCDC:RPMH24-1.5 +Converter_DCDC:RPMH3.3-1.5 +Converter_DCDC:RPMH5.0-1.5 +Converter_DCDC:TBA1-0511E +Converter_DCDC:TBA1-0512E +Converter_DCDC:TBA1-0513E +Converter_DCDC:TBA1-0521E +Converter_DCDC:TBA1-0522E +Converter_DCDC:TBA1-0523E +Converter_DCDC:TBA1-1211E +Converter_DCDC:TBA1-1212E +Converter_DCDC:TBA1-1213E +Converter_DCDC:TBA1-1221E +Converter_DCDC:TBA1-1222E +Converter_DCDC:TBA1-1223E +Converter_DCDC:TBA1-2411E +Converter_DCDC:TBA1-2412E +Converter_DCDC:TBA1-2413E +Converter_DCDC:TBA1-2421E +Converter_DCDC:TBA1-2422E +Converter_DCDC:TBA1-2423E +Converter_DCDC:TBA2-0511 +Converter_DCDC:TBA2-0512 +Converter_DCDC:TBA2-0513 +Converter_DCDC:TBA2-0521 +Converter_DCDC:TBA2-0522 +Converter_DCDC:TBA2-0523 +Converter_DCDC:TBA2-1211 +Converter_DCDC:TBA2-1212 +Converter_DCDC:TBA2-1213 +Converter_DCDC:TBA2-1221 +Converter_DCDC:TBA2-1222 +Converter_DCDC:TBA2-1223 +Converter_DCDC:TBA2-2411 +Converter_DCDC:TBA2-2412 +Converter_DCDC:TBA2-2413 +Converter_DCDC:TBA2-2421 +Converter_DCDC:TBA2-2422 +Converter_DCDC:TBA2-2423 +Converter_DCDC:TC7662AxPA +Converter_DCDC:TC7662Bx0A +Converter_DCDC:TC7662BxPA +Converter_DCDC:TDU1-0511 +Converter_DCDC:TDU1-0512 +Converter_DCDC:TDU1-0513 +Converter_DCDC:TDU1-1211 +Converter_DCDC:TDU1-1212 +Converter_DCDC:TDU1-1213 +Converter_DCDC:TDU1-2411 +Converter_DCDC:TDU1-2412 +Converter_DCDC:TDU1-2413 +Converter_DCDC:TEA1-0505 +Converter_DCDC:TEA1-0505E +Converter_DCDC:TEA1-0505HI +Converter_DCDC:TEC2-1210WI +Converter_DCDC:TEC2-1211WI +Converter_DCDC:TEC2-1212WI +Converter_DCDC:TEC2-1213WI +Converter_DCDC:TEC2-1215WI +Converter_DCDC:TEC2-1219WI +Converter_DCDC:TEC2-2410WI +Converter_DCDC:TEC2-2411WI +Converter_DCDC:TEC2-2412WI +Converter_DCDC:TEC2-2413WI +Converter_DCDC:TEC2-2415WI +Converter_DCDC:TEC2-2419WI +Converter_DCDC:TEC2-4810WI +Converter_DCDC:TEC2-4811WI +Converter_DCDC:TEC2-4812WI +Converter_DCDC:TEC2-4813WI +Converter_DCDC:TEC2-4815WI +Converter_DCDC:TEC2-4819WI +Converter_DCDC:TEC3-2410UI +Converter_DCDC:TEC3-2411UI +Converter_DCDC:TEC3-2412UI +Converter_DCDC:TEC3-2413UI +Converter_DCDC:TEC3-2421UI +Converter_DCDC:TEC3-2422UI +Converter_DCDC:TEC3-2423UI +Converter_DCDC:TEL12-1211 +Converter_DCDC:TEL12-1212 +Converter_DCDC:TEL12-1213 +Converter_DCDC:TEL12-1215 +Converter_DCDC:TEL12-1222 +Converter_DCDC:TEL12-1223 +Converter_DCDC:TEL12-2411 +Converter_DCDC:TEL12-2411WI +Converter_DCDC:TEL12-2412 +Converter_DCDC:TEL12-2412WI +Converter_DCDC:TEL12-2413 +Converter_DCDC:TEL12-2413WI +Converter_DCDC:TEL12-2415 +Converter_DCDC:TEL12-2415WI +Converter_DCDC:TEL12-2422 +Converter_DCDC:TEL12-2422WI +Converter_DCDC:TEL12-2423 +Converter_DCDC:TEL12-2423WI +Converter_DCDC:TEL12-4811 +Converter_DCDC:TEL12-4811WI +Converter_DCDC:TEL12-4812 +Converter_DCDC:TEL12-4812WI +Converter_DCDC:TEL12-4813 +Converter_DCDC:TEL12-4813WI +Converter_DCDC:TEL12-4815 +Converter_DCDC:TEL12-4815WI +Converter_DCDC:TEL12-4822 +Converter_DCDC:TEL12-4822WI +Converter_DCDC:TEL12-4823 +Converter_DCDC:TEL12-4823WI +Converter_DCDC:TEN10-11010WIRH +Converter_DCDC:TEN10-11011WIRH +Converter_DCDC:TEN10-11012WIRH +Converter_DCDC:TEN10-11013WIRH +Converter_DCDC:TEN10-11015WIRH +Converter_DCDC:TEN10-11021WIRH +Converter_DCDC:TEN10-11022WIRH +Converter_DCDC:TEN10-11023WIRH +Converter_DCDC:TEN20-11011WIRH +Converter_DCDC:TEN20-11012WIRH +Converter_DCDC:TEN20-11013WIRH +Converter_DCDC:TEN20-11015WIRH +Converter_DCDC:TEN20-11021WIRH +Converter_DCDC:TEN20-11022WIRH +Converter_DCDC:TEN20-11023WIRH +Converter_DCDC:TEN20-2410WIN +Converter_DCDC:TEN20-2411WIN +Converter_DCDC:TEN20-2412WIN +Converter_DCDC:TEN20-2413WIN +Converter_DCDC:TEN20-2421WIN +Converter_DCDC:TEN20-2422WIN +Converter_DCDC:TEN20-2423WIN +Converter_DCDC:TEN20-4810WIN +Converter_DCDC:TEN20-4811WIN +Converter_DCDC:TEN20-4812WIN +Converter_DCDC:TEN20-4813WIN +Converter_DCDC:TEN20-4821WIN +Converter_DCDC:TEN20-4822WIN +Converter_DCDC:TEN20-4823WIN +Converter_DCDC:TEN3-11010WIRH +Converter_DCDC:TEN3-11011WIRH +Converter_DCDC:TEN3-11012WIRH +Converter_DCDC:TEN3-11013WIRH +Converter_DCDC:TEN3-11015WIRH +Converter_DCDC:TEN3-11021WIRH +Converter_DCDC:TEN3-11022WIRH +Converter_DCDC:TEN3-11023WIRH +Converter_DCDC:TEN40-11011WIRH +Converter_DCDC:TEN40-11012WIRH +Converter_DCDC:TEN40-11013WIRH +Converter_DCDC:TEN40-11015WIRH +Converter_DCDC:TEN40-11022WIRH +Converter_DCDC:TEN40-11023WIRH +Converter_DCDC:TEN6-11010WIRH +Converter_DCDC:TEN6-11011WIRH +Converter_DCDC:TEN6-11012WIRH +Converter_DCDC:TEN6-11013WIRH +Converter_DCDC:TEN6-11015WIRH +Converter_DCDC:TEN6-11021WIRH +Converter_DCDC:TEN6-11022WIRH +Converter_DCDC:TEN6-11023WIRH +Converter_DCDC:THB10-1211 +Converter_DCDC:THB10-1212 +Converter_DCDC:THB10-1222 +Converter_DCDC:THB10-1223 +Converter_DCDC:THB10-2411 +Converter_DCDC:THB10-2412 +Converter_DCDC:THB10-2422 +Converter_DCDC:THB10-2423 +Converter_DCDC:THB10-4811 +Converter_DCDC:THB10-4812 +Converter_DCDC:THB10-4822 +Converter_DCDC:THB10-4823 +Converter_DCDC:THR40-7211WI +Converter_DCDC:THR40-7212WI +Converter_DCDC:THR40-7213WI +Converter_DCDC:THR40-72154WI +Converter_DCDC:THR40-7215WI +Converter_DCDC:THR40-7222WI +Converter_DCDC:THR40-7223WI +Converter_DCDC:TMA-0505D +Converter_DCDC:TMA-0505S +Converter_DCDC:TMA-0512D +Converter_DCDC:TMA-0512S +Converter_DCDC:TMA-0515D +Converter_DCDC:TMA-0515S +Converter_DCDC:TMA-1205D +Converter_DCDC:TMA-1205S +Converter_DCDC:TMA-1212D +Converter_DCDC:TMA-1212S +Converter_DCDC:TMA-1215D +Converter_DCDC:TMA-1215S +Converter_DCDC:TMA-1505D +Converter_DCDC:TMA-1505S +Converter_DCDC:TMA-1512D +Converter_DCDC:TMA-1512S +Converter_DCDC:TMA-1515D +Converter_DCDC:TMA-1515S +Converter_DCDC:TMA-2405D +Converter_DCDC:TMA-2405S +Converter_DCDC:TMA-2412D +Converter_DCDC:TMA-2412S +Converter_DCDC:TMA-2415D +Converter_DCDC:TMA-2415S +Converter_DCDC:TME-0303S +Converter_DCDC:TME-0305S +Converter_DCDC:TME-0503S +Converter_DCDC:TME-0505S +Converter_DCDC:TME-0509S +Converter_DCDC:TME-0512S +Converter_DCDC:TME-0515S +Converter_DCDC:TME-1205S +Converter_DCDC:TME-1209S +Converter_DCDC:TME-1212S +Converter_DCDC:TME-1215S +Converter_DCDC:TME-2405S +Converter_DCDC:TME-2409S +Converter_DCDC:TME-2412S +Converter_DCDC:TME-2415S +Converter_DCDC:TMR-0510 +Converter_DCDC:TMR-0511 +Converter_DCDC:TMR-0512 +Converter_DCDC:TMR-1210 +Converter_DCDC:TMR-1211 +Converter_DCDC:TMR-1212 +Converter_DCDC:TMR-2410 +Converter_DCDC:TMR-2411 +Converter_DCDC:TMR-2412 +Converter_DCDC:TMR-4810 +Converter_DCDC:TMR-4811 +Converter_DCDC:TMR-4812 +Converter_DCDC:TMR2-2410WI +Converter_DCDC:TMR2-2411WI +Converter_DCDC:TMR2-2412WI +Converter_DCDC:TMR2-2413WI +Converter_DCDC:TMR2-4810WI +Converter_DCDC:TMR2-4811WI +Converter_DCDC:TMR2-4812WI +Converter_DCDC:TMR2-4813WI +Converter_DCDC:TMR4-2411WI +Converter_DCDC:TMR4-2412WI +Converter_DCDC:TMR4-2413WI +Converter_DCDC:TMR4-2415WI +Converter_DCDC:TMR4-2422WI +Converter_DCDC:TMR4-2423WI +Converter_DCDC:TMR4-4811WI +Converter_DCDC:TMR4-4812WI +Converter_DCDC:TMR4-4813WI +Converter_DCDC:TMR4-4815WI +Converter_DCDC:TMR4-4822WI +Converter_DCDC:TMR4-4823WI +Converter_DCDC:TMU3-0511 +Converter_DCDC:TMU3-0512 +Converter_DCDC:TMU3-0513 +Converter_DCDC:TMU3-1211 +Converter_DCDC:TMU3-1212 +Converter_DCDC:TMU3-1213 +Converter_DCDC:TMU3-2411 +Converter_DCDC:TMU3-2412 +Converter_DCDC:TMU3-2413 +Converter_DCDC:TPS43060RTE +Converter_DCDC:TPS54240DGQ +Converter_DCDC:TPS54240DRC +Converter_DCDC:TPS61022 +Converter_DCDC:TPSM53602RDA +Converter_DCDC:TPSM53603RDA +Converter_DCDC:TPSM53604RDA +Converter_DCDC:TRA3-0511 +Converter_DCDC:TRA3-0512 +Converter_DCDC:TRA3-0513 +Converter_DCDC:TRA3-0519 +Converter_DCDC:TRA3-1211 +Converter_DCDC:TRA3-1212 +Converter_DCDC:TRA3-1213 +Converter_DCDC:TRA3-1219 +Converter_DCDC:TRA3-2411 +Converter_DCDC:TRA3-2412 +Converter_DCDC:TRA3-2413 +Converter_DCDC:TRA3-2419 +Converter_DCDC:TRI1-0511 +Converter_DCDC:TRI1-0512 +Converter_DCDC:TRI1-0513 +Converter_DCDC:TRI1-1211 +Converter_DCDC:TRI1-1212 +Converter_DCDC:TRI1-1213 +Converter_DCDC:TRI1-2411 +Converter_DCDC:TRI1-2412 +Converter_DCDC:TRI1-2413 +CPLD_Altera:EP1210 +CPLD_Altera:EP1810 +CPLD_Altera:EP300 +CPLD_Altera:EP310 +CPLD_Altera:EP320 +CPLD_Altera:EP600 +CPLD_Altera:EP910 +CPLD_Altera:EPM1270F256 +CPLD_Altera:EPM1270M256 +CPLD_Altera:EPM1270T144 +CPLD_Altera:EPM2210F256 +CPLD_Altera:EPM2210F324 +CPLD_Altera:EPM240F100 +CPLD_Altera:EPM240M100 +CPLD_Altera:EPM240T100 +CPLD_Altera:EPM240ZM100 +CPLD_Altera:EPM240ZM68 +CPLD_Altera:EPM570F100 +CPLD_Altera:EPM570F256 +CPLD_Altera:EPM570M100 +CPLD_Altera:EPM570M256 +CPLD_Altera:EPM570T100 +CPLD_Altera:EPM570T144 +CPLD_Altera:EPM570ZM100 +CPLD_Altera:EPM570ZM144 +CPLD_Altera:EPM570ZM256 +CPLD_Microchip:ATF1502AS-xAx44 +CPLD_Microchip:ATF1502ASL-xAx44 +CPLD_Microchip:ATF1502ASV-xAx44 +CPLD_Microchip:ATF1504AS-xAx44 +CPLD_Microchip:ATF1504ASL-xAx44 +CPLD_Microchip:ATF1504ASV-xAx44 +CPLD_Microchip:ATF1504ASVL-xAx44 +CPLD_Renesas:SLG46826G +CPLD_Xilinx:XC7336 +CPLD_Xilinx:XC95108PC84 +CPLD_Xilinx:XC95108PQ100 +CPLD_Xilinx:XC95144PQ100 +CPLD_Xilinx:XC95144XL-TQ100 +CPLD_Xilinx:XC95144XL-TQ144 +CPLD_Xilinx:XC9536PC44 +CPLD_Xilinx:XC9572XL-TQ100 +CPLD_Xilinx:XC9572XL-VQ64 +CPLD_Xilinx:XCR3064-VQ100 +CPLD_Xilinx:XCR3064-VQ44 +CPLD_Xilinx:XCR3064XL-VQ100 +CPLD_Xilinx:XCR3064XL-VQ44 +CPLD_Xilinx:XCR3128-VQ100 +CPLD_Xilinx:XCR3128XL-VQ100 +CPLD_Xilinx:XCR3256-TQ144 +CPLD_Xilinx:XCR3256XL-TQ144 +CPU:CDP1802ACE +CPU:CDP1802ACEX +CPU:CDP1802BCE +CPU:CDP1802BCEX +CPU:P4080-BGA1295 +CPU:Z80CPU +CPU_NXP_6800:MC6800 +CPU_NXP_6800:MC6802 +CPU_NXP_6800:MC6809 +CPU_NXP_6800:MC6809E +CPU_NXP_6800:MC68A00 +CPU_NXP_6800:MC68A02 +CPU_NXP_6800:MC68A09 +CPU_NXP_6800:MC68A09E +CPU_NXP_6800:MC68B00 +CPU_NXP_6800:MC68B02 +CPU_NXP_6800:MC68B09 +CPU_NXP_6800:MC68B09E +CPU_NXP_68000:68000D +CPU_NXP_68000:68008D +CPU_NXP_68000:68010D +CPU_NXP_68000:MC68000FN +CPU_NXP_68000:MC68332 +CPU_NXP_IMX:MCIMX6D4AVT +CPU_NXP_IMX:MCIMX6D5EYM +CPU_NXP_IMX:MCIMX6D6AVT +CPU_NXP_IMX:MCIMX6D7CVT +CPU_NXP_IMX:MCIMX6DP4AVT +CPU_NXP_IMX:MCIMX6DP5EVT +CPU_NXP_IMX:MCIMX6DP5EYM +CPU_NXP_IMX:MCIMX6DP6AVT +CPU_NXP_IMX:MCIMX6DP7CVT +CPU_NXP_IMX:MCIMX6Q4AVT +CPU_NXP_IMX:MCIMX6Q5EYM +CPU_NXP_IMX:MCIMX6Q6AVT +CPU_NXP_IMX:MCIMX6Q7CVT +CPU_NXP_IMX:MCIMX6QP4AVT +CPU_NXP_IMX:MCIMX6QP5EVT +CPU_NXP_IMX:MCIMX6QP5EYM +CPU_NXP_IMX:MCIMX6QP6AVT +CPU_NXP_IMX:MCIMX6QP7CVT +CPU_PowerPC:MPC8641D +Device:Ammeter_AC +Device:Ammeter_DC +Device:Antenna +Device:Antenna_Chip +Device:Antenna_Dipole +Device:Antenna_Loop +Device:Antenna_Shield +Device:Battery +Device:Battery_Cell +Device:Buzzer +Device:C +Device:C_45deg +Device:C_Feedthrough +Device:C_Network04 +Device:C_Network05 +Device:C_Network06 +Device:C_Network07 +Device:C_Network08 +Device:C_Polarized +Device:C_Polarized_Series_2C +Device:C_Polarized_Small +Device:C_Polarized_Small_Series_2C +Device:C_Polarized_Small_US +Device:C_Polarized_Small_US_Series_2C +Device:C_Polarized_US +Device:C_Polarized_US_Series_2C +Device:C_Small +Device:C_Trim +Device:C_Trim_Differential +Device:C_Trim_Small +Device:C_Variable +Device:CircuitBreaker_1P +Device:CircuitBreaker_1P_US +Device:CircuitBreaker_2P +Device:CircuitBreaker_2P_US +Device:CircuitBreaker_3P +Device:CircuitBreaker_3P_US +Device:Crystal +Device:Crystal_GND2 +Device:Crystal_GND23 +Device:Crystal_GND23_Small +Device:Crystal_GND24 +Device:Crystal_GND24_Small +Device:Crystal_GND2_Small +Device:Crystal_GND3 +Device:Crystal_GND3_Small +Device:Crystal_Small +Device:D +Device:DIAC +Device:DIAC_Filled +Device:D_45deg +Device:D_45deg_Filled +Device:D_AAK +Device:D_Bridge_+-AA +Device:D_Bridge_+A-A +Device:D_Bridge_+AA- +Device:D_Bridge_-A+A +Device:D_Bridge_-AA+ +Device:D_Capacitance +Device:D_Capacitance_Filled +Device:D_Current-regulator +Device:D_Current-regulator_Small +Device:D_Dual_CommonAnode_AKK +Device:D_Dual_CommonAnode_AKK_Parallel +Device:D_Dual_CommonAnode_AKK_Split +Device:D_Dual_CommonAnode_KAK +Device:D_Dual_CommonAnode_KAK_Parallel +Device:D_Dual_CommonAnode_KAK_Split +Device:D_Dual_CommonAnode_KKA +Device:D_Dual_CommonAnode_KKA_Parallel +Device:D_Dual_CommonAnode_KKA_Split +Device:D_Dual_CommonCathode_AAK +Device:D_Dual_CommonCathode_AAK_Parallel +Device:D_Dual_CommonCathode_AAK_Split +Device:D_Dual_CommonCathode_AKA +Device:D_Dual_CommonCathode_AKA_Parallel +Device:D_Dual_CommonCathode_AKA_Split +Device:D_Dual_CommonCathode_KAA +Device:D_Dual_CommonCathode_KAA_Parallel +Device:D_Dual_CommonCathode_KAA_Split +Device:D_Dual_Series_ACK +Device:D_Dual_Series_ACK_Parallel +Device:D_Dual_Series_ACK_Split +Device:D_Dual_Series_AKC +Device:D_Dual_Series_AKC_Parallel +Device:D_Dual_Series_AKC_Split +Device:D_Dual_Series_CAK +Device:D_Dual_Series_CAK_Parallel +Device:D_Dual_Series_CAK_Split +Device:D_Dual_Series_CKA +Device:D_Dual_Series_CKA_Parallel +Device:D_Dual_Series_CKA_Split +Device:D_Dual_Series_KAC +Device:D_Dual_Series_KAC_Parallel +Device:D_Dual_Series_KAC_Split +Device:D_Dual_Series_KCA +Device:D_Dual_Series_KCA_Parallel +Device:D_Dual_Series_KCA_Split +Device:D_Filled +Device:D_KAA +Device:D_KAK +Device:D_KKA +Device:D_Laser_1A3C +Device:D_Laser_1C2A +Device:D_Laser_Photo_MType +Device:D_Laser_Photo_NType +Device:D_Laser_Photo_PType +Device:D_Photo +Device:D_Photo_Filled +Device:D_Radiation +Device:D_Radiation_Filled +Device:D_Schottky +Device:D_Schottky_AAK +Device:D_Schottky_AKA +Device:D_Schottky_AKK +Device:D_Schottky_Dual_CommonAnode_AKK +Device:D_Schottky_Dual_CommonAnode_AKK_Parallel +Device:D_Schottky_Dual_CommonAnode_AKK_Split +Device:D_Schottky_Dual_CommonAnode_KAK +Device:D_Schottky_Dual_CommonAnode_KAK_Parallel +Device:D_Schottky_Dual_CommonAnode_KAK_Split +Device:D_Schottky_Dual_CommonAnode_KKA +Device:D_Schottky_Dual_CommonAnode_KKA_Parallel +Device:D_Schottky_Dual_CommonAnode_KKA_Split +Device:D_Schottky_Dual_CommonCathode_AAK +Device:D_Schottky_Dual_CommonCathode_AAK_Parallel +Device:D_Schottky_Dual_CommonCathode_AAK_Split +Device:D_Schottky_Dual_CommonCathode_AKA +Device:D_Schottky_Dual_CommonCathode_AKA_Parallel +Device:D_Schottky_Dual_CommonCathode_AKA_Split +Device:D_Schottky_Dual_CommonCathode_KAA +Device:D_Schottky_Dual_CommonCathode_KAA_Parallel +Device:D_Schottky_Dual_CommonCathode_KAA_Split +Device:D_Schottky_Dual_Series_ACK +Device:D_Schottky_Dual_Series_ACK_Parallel +Device:D_Schottky_Dual_Series_ACK_Split +Device:D_Schottky_Dual_Series_AKC +Device:D_Schottky_Dual_Series_AKC_Parallel +Device:D_Schottky_Dual_Series_AKC_Split +Device:D_Schottky_Dual_Series_CAK +Device:D_Schottky_Dual_Series_CAK_Parallel +Device:D_Schottky_Dual_Series_CAK_Split +Device:D_Schottky_Dual_Series_CKA +Device:D_Schottky_Dual_Series_CKA_Parallel +Device:D_Schottky_Dual_Series_CKA_Split +Device:D_Schottky_Dual_Series_KAC +Device:D_Schottky_Dual_Series_KAC_Parallel +Device:D_Schottky_Dual_Series_KAC_Split +Device:D_Schottky_Dual_Series_KCA +Device:D_Schottky_Dual_Series_KCA_Parallel +Device:D_Schottky_Dual_Series_KCA_Split +Device:D_Schottky_Filled +Device:D_Schottky_KAA +Device:D_Schottky_KAK +Device:D_Schottky_KKA +Device:D_Schottky_Small +Device:D_Schottky_Small_Filled +Device:D_Shockley +Device:D_SiPM +Device:D_Small +Device:D_Small_Filled +Device:D_TVS +Device:D_TVS_Dual_AAC +Device:D_TVS_Dual_ACA +Device:D_TVS_Dual_CAA +Device:D_TVS_Filled +Device:D_TVS_Small +Device:D_TVS_Small_Filled +Device:D_TemperatureDependent +Device:D_TemperatureDependent_Filled +Device:D_Tunnel +Device:D_Tunnel_Filled +Device:D_Unitunnel +Device:D_Unitunnel_Filled +Device:D_Zener +Device:D_Zener_Dual_CommonAnode_AKK +Device:D_Zener_Dual_CommonAnode_AKK_Parallel +Device:D_Zener_Dual_CommonAnode_AKK_Split +Device:D_Zener_Dual_CommonAnode_KAK +Device:D_Zener_Dual_CommonAnode_KAK_Parallel +Device:D_Zener_Dual_CommonAnode_KAK_Split +Device:D_Zener_Dual_CommonAnode_KKA +Device:D_Zener_Dual_CommonAnode_KKA_Parallel +Device:D_Zener_Dual_CommonAnode_KKA_Split +Device:D_Zener_Dual_CommonCathode_AAK +Device:D_Zener_Dual_CommonCathode_AAK_Parallel +Device:D_Zener_Dual_CommonCathode_AAK_Split +Device:D_Zener_Dual_CommonCathode_AKA +Device:D_Zener_Dual_CommonCathode_AKA_Parallel +Device:D_Zener_Dual_CommonCathode_AKA_Split +Device:D_Zener_Dual_CommonCathode_KAA +Device:D_Zener_Dual_CommonCathode_KAA_Parallel +Device:D_Zener_Dual_CommonCathode_KAA_Split +Device:D_Zener_Filled +Device:D_Zener_Small +Device:D_Zener_Small_Filled +Device:DelayLine +Device:Earphone +Device:ElectromagneticActor +Device:FerriteBead +Device:FerriteBead_Small +Device:Filter_EMI_C +Device:Filter_EMI_CLC +Device:Filter_EMI_CommonMode +Device:Filter_EMI_LCL +Device:Filter_EMI_LL +Device:Filter_EMI_LLL +Device:Filter_EMI_LLLL +Device:Filter_EMI_LLLL_15263748 +Device:Filter_EMI_LLL_162534 +Device:Filter_EMI_LL_1423 +Device:FrequencyCounter +Device:Fuse +Device:Fuse_Polarized +Device:Fuse_Polarized_Small +Device:Fuse_Small +Device:GDT_2Pin +Device:GDT_3Pin +Device:Galvanometer +Device:HallGenerator +Device:Heater +Device:L +Device:LED +Device:LED_45deg +Device:LED_45deg_Filled +Device:LED_ABGR +Device:LED_ABRG +Device:LED_AGBR +Device:LED_AGRB +Device:LED_ARBG +Device:LED_ARGB +Device:LED_BAGR +Device:LED_BARG +Device:LED_BGAR +Device:LED_BGKR +Device:LED_BGRA +Device:LED_BGRK +Device:LED_BKGR +Device:LED_BKRG +Device:LED_BRAG +Device:LED_BRGA +Device:LED_BRGK +Device:LED_BRKG +Device:LED_Dual_AAK +Device:LED_Dual_AAKK +Device:LED_Dual_AKA +Device:LED_Dual_AKAK +Device:LED_Dual_AKKA +Device:LED_Dual_Bidirectional +Device:LED_Dual_KAK +Device:LED_Dual_KAKA +Device:LED_Dual_KKA +Device:LED_Filled +Device:LED_GABR +Device:LED_GARB +Device:LED_GBAR +Device:LED_GBKR +Device:LED_GBRA +Device:LED_GBRK +Device:LED_GKBR +Device:LED_GKRB +Device:LED_GRAB +Device:LED_GRBA +Device:LED_GRBK +Device:LED_GRKB +Device:LED_KBGR +Device:LED_KBRG +Device:LED_KGBR +Device:LED_KGRB +Device:LED_KRBG +Device:LED_KRGB +Device:LED_Pad +Device:LED_RABG +Device:LED_RAGB +Device:LED_RBAG +Device:LED_RBGA +Device:LED_RBGK +Device:LED_RBKG +Device:LED_RGAB +Device:LED_RGB +Device:LED_RGBA +Device:LED_RGBK +Device:LED_RGB_EP +Device:LED_RGKB +Device:LED_RKBG +Device:LED_RKGB +Device:LED_Series +Device:LED_Series_Pad +Device:LED_Small +Device:LED_Small_Filled +Device:L_45deg +Device:L_Coupled +Device:L_Coupled_1243 +Device:L_Coupled_1324 +Device:L_Coupled_1342 +Device:L_Coupled_1423 +Device:L_Coupled_Small +Device:L_Coupled_Small_1243 +Device:L_Coupled_Small_1324 +Device:L_Coupled_Small_1342 +Device:L_Coupled_Small_1423 +Device:L_Ferrite +Device:L_Ferrite_Coupled +Device:L_Ferrite_Coupled_1243 +Device:L_Ferrite_Coupled_1324 +Device:L_Ferrite_Coupled_1342 +Device:L_Ferrite_Coupled_1423 +Device:L_Ferrite_Coupled_Small +Device:L_Ferrite_Coupled_Small_1243 +Device:L_Ferrite_Coupled_Small_1324 +Device:L_Ferrite_Coupled_Small_1342 +Device:L_Ferrite_Coupled_Small_1423 +Device:L_Ferrite_Small +Device:L_Iron +Device:L_Iron_Coupled +Device:L_Iron_Coupled_1243 +Device:L_Iron_Coupled_1324 +Device:L_Iron_Coupled_1342 +Device:L_Iron_Coupled_1423 +Device:L_Iron_Coupled_Small +Device:L_Iron_Coupled_Small_1243 +Device:L_Iron_Coupled_Small_1324 +Device:L_Iron_Coupled_Small_1342 +Device:L_Iron_Coupled_Small_1423 +Device:L_Iron_Small +Device:L_Pack04 +Device:L_Small +Device:L_Trim +Device:Lamp +Device:Lamp_Flash +Device:Lamp_Neon +Device:Memristor +Device:Microphone +Device:Microphone_Condenser +Device:Microphone_Crystal +Device:Microphone_Ultrasound +Device:NetTie_2 +Device:NetTie_3 +Device:NetTie_3_Tee +Device:NetTie_4 +Device:NetTie_4_Cross +Device:Ohmmeter +Device:Opamp_Dual +Device:Opamp_Quad +Device:Oscilloscope +Device:PeltierElement +Device:Polyfuse +Device:Polyfuse_Small +Device:Q_NIGBT_CEG +Device:Q_NIGBT_CGE +Device:Q_NIGBT_ECG +Device:Q_NIGBT_ECGC +Device:Q_NIGBT_EGC +Device:Q_NIGBT_GCE +Device:Q_NIGBT_GCEC +Device:Q_NIGBT_GEC +Device:Q_NJFET_DGS +Device:Q_NJFET_DSG +Device:Q_NJFET_GDS +Device:Q_NJFET_GSD +Device:Q_NJFET_SDG +Device:Q_NJFET_SGD +Device:Q_NMOS +Device:Q_NMOS_Depletion +Device:Q_NPN +Device:Q_NPN_BRT +Device:Q_NPN_BRT_No_R2 +Device:Q_NPN_CurrentMirror +Device:Q_NPN_Darlington +Device:Q_NUJT_BEB +Device:Q_PJFET_DGS +Device:Q_PJFET_DSG +Device:Q_PJFET_GDS +Device:Q_PJFET_GSD +Device:Q_PJFET_SDG +Device:Q_PJFET_SGD +Device:Q_PMOS +Device:Q_PMOS_Depletion +Device:Q_PNP +Device:Q_PNP_BRT +Device:Q_PNP_BRT_No_R2 +Device:Q_PNP_CurrentMirror +Device:Q_PNP_Darlington +Device:Q_PUJT_BEB +Device:Q_Photo_NPN +Device:Q_Photo_NPN_CBE +Device:Q_Photo_NPN_CE +Device:Q_Photo_NPN_EBC +Device:Q_Photo_NPN_EC +Device:Q_SCR_AGK +Device:Q_SCR_AKG +Device:Q_SCR_GAK +Device:Q_SCR_GKA +Device:Q_SCR_KAG +Device:Q_SCR_KGA +Device:Q_Triac +Device:R +Device:RFShield_OnePiece +Device:RFShield_TwoPieces +Device:R_45deg +Device:R_Network03 +Device:R_Network03_Split +Device:R_Network03_US +Device:R_Network04 +Device:R_Network04_Split +Device:R_Network04_US +Device:R_Network05 +Device:R_Network05_Split +Device:R_Network05_US +Device:R_Network06 +Device:R_Network06_Split +Device:R_Network06_US +Device:R_Network07 +Device:R_Network07_Split +Device:R_Network07_US +Device:R_Network08 +Device:R_Network08_Split +Device:R_Network08_US +Device:R_Network09 +Device:R_Network09_Split +Device:R_Network09_US +Device:R_Network10 +Device:R_Network10_Split +Device:R_Network10_US +Device:R_Network11 +Device:R_Network11_Split +Device:R_Network11_US +Device:R_Network12 +Device:R_Network12_Split +Device:R_Network12_US +Device:R_Network13 +Device:R_Network13_Split +Device:R_Network13_US +Device:R_Network_Dividers_x02_SIP +Device:R_Network_Dividers_x03_SIP +Device:R_Network_Dividers_x04_SIP +Device:R_Network_Dividers_x05_SIP +Device:R_Network_Dividers_x06_SIP +Device:R_Network_Dividers_x07_SIP +Device:R_Network_Dividers_x08_SIP +Device:R_Network_Dividers_x09_SIP +Device:R_Network_Dividers_x10_SIP +Device:R_Network_Dividers_x11_SIP +Device:R_Pack02 +Device:R_Pack02_SIP +Device:R_Pack02_SIP_Split +Device:R_Pack02_Split +Device:R_Pack03 +Device:R_Pack03_SIP +Device:R_Pack03_SIP_Split +Device:R_Pack03_Split +Device:R_Pack04 +Device:R_Pack04_SIP +Device:R_Pack04_SIP_Split +Device:R_Pack04_Split +Device:R_Pack05 +Device:R_Pack05_SIP +Device:R_Pack05_SIP_Split +Device:R_Pack05_Split +Device:R_Pack06 +Device:R_Pack06_SIP +Device:R_Pack06_SIP_Split +Device:R_Pack06_Split +Device:R_Pack07 +Device:R_Pack07_SIP +Device:R_Pack07_SIP_Split +Device:R_Pack07_Split +Device:R_Pack08 +Device:R_Pack08_Split +Device:R_Pack09 +Device:R_Pack09_Split +Device:R_Pack10 +Device:R_Pack10_Split +Device:R_Pack11 +Device:R_Pack11_Split +Device:R_Photo +Device:R_Potentiometer +Device:R_Potentiometer_Dual +Device:R_Potentiometer_Dual_MountingPin +Device:R_Potentiometer_Dual_Separate +Device:R_Potentiometer_MountingPin +Device:R_Potentiometer_Small +Device:R_Potentiometer_Trim +Device:R_Potentiometer_Trim_US +Device:R_Potentiometer_US +Device:R_Shunt +Device:R_Shunt_US +Device:R_Small +Device:R_Small_US +Device:R_Trim +Device:R_US +Device:R_Variable +Device:R_Variable_US +Device:Resonator +Device:Resonator_Small +Device:RotaryEncoder +Device:RotaryEncoder_Switch +Device:RotaryEncoder_Switch_MP +Device:Solar_Cell +Device:Solar_Cells +Device:SparkGap +Device:Speaker +Device:Speaker_Crystal +Device:Speaker_Ultrasound +Device:Thermistor +Device:Thermistor_NTC +Device:Thermistor_NTC_3Wire +Device:Thermistor_NTC_4Wire +Device:Thermistor_NTC_US +Device:Thermistor_PTC +Device:Thermistor_PTC_3Wire +Device:Thermistor_PTC_4Wire +Device:Thermistor_PTC_US +Device:Thermistor_US +Device:Thermocouple +Device:Thermocouple_Alt +Device:Thermocouple_Block +Device:Transformer_1P_1S +Device:Transformer_1P_1S_SO8 +Device:Transformer_1P_2S +Device:Transformer_1P_SS +Device:Transformer_Audio +Device:Transformer_SP_1S +Device:Transformer_SP_2S +Device:Varistor +Device:Varistor_US +Device:VoltageDivider +Device:VoltageDivider_CenterPin1 +Device:VoltageDivider_CenterPin3 +Device:Voltmeter_AC +Device:Voltmeter_DC +Diode:1.5KExxA +Diode:1.5KExxCA +Diode:1.5SMCxxA +Diode:1.5SMCxxCA +Diode:1N4001 +Diode:1N4002 +Diode:1N4003 +Diode:1N4004 +Diode:1N4005 +Diode:1N4006 +Diode:1N4007 +Diode:1N4148 +Diode:1N4148W +Diode:1N4148WS +Diode:1N4148WT +Diode:1N4149 +Diode:1N4151 +Diode:1N4448 +Diode:1N4448W +Diode:1N4448WS +Diode:1N4448WT +Diode:1N47xxA +Diode:1N4933 +Diode:1N4934 +Diode:1N4935 +Diode:1N4936 +Diode:1N4937 +Diode:1N53xxB +Diode:1N5400 +Diode:1N5401 +Diode:1N5402 +Diode:1N5403 +Diode:1N5404 +Diode:1N5405 +Diode:1N5406 +Diode:1N5407 +Diode:1N5408 +Diode:1N5711 +Diode:1N5711UR +Diode:1N5712 +Diode:1N5712UR +Diode:1N5817 +Diode:1N5818 +Diode:1N5819 +Diode:1N5819WS +Diode:1N5820 +Diode:1N5821 +Diode:1N5822 +Diode:1N5908 +Diode:1N6263 +Diode:1N62xxA +Diode:1N62xxCA +Diode:1N630xA +Diode:1N630xCA +Diode:1N6857 +Diode:1N6857UR +Diode:1N6858 +Diode:1N6858UR +Diode:1N914 +Diode:1N914WT +Diode:1SS355VM +Diode:2BZX84Cxx +Diode:5KPxxA +Diode:5KPxxCA +Diode:B120-E3 +Diode:B130-E3 +Diode:B140-E3 +Diode:B150-E3 +Diode:B160-E3 +Diode:B220 +Diode:B230 +Diode:B240 +Diode:B250 +Diode:B260 +Diode:B320 +Diode:B330 +Diode:B340 +Diode:B350 +Diode:B360 +Diode:BA157 +Diode:BA158 +Diode:BA159 +Diode:BA243 +Diode:BA244 +Diode:BA282 +Diode:BA283 +Diode:BAR42FILM +Diode:BAR43FILM +Diode:BAS16TW +Diode:BAS16VY +Diode:BAS16W +Diode:BAS19 +Diode:BAS20 +Diode:BAS21 +Diode:BAS316 +Diode:BAS40-04 +Diode:BAS516 +Diode:BAT160A +Diode:BAT160C +Diode:BAT160S +Diode:BAT41 +Diode:BAT42 +Diode:BAT42W-V +Diode:BAT43 +Diode:BAT43W-V +Diode:BAT46 +Diode:BAT48JFILM +Diode:BAT48RL +Diode:BAT48ZFILM +Diode:BAT54A +Diode:BAT54ADW +Diode:BAT54AW +Diode:BAT54C +Diode:BAT54CW +Diode:BAT54J +Diode:BAT54S +Diode:BAT54SDW +Diode:BAT54SW +Diode:BAT54W +Diode:BAT60A +Diode:BAT85 +Diode:BAT86 +Diode:BAT86S +Diode:BAV16W +Diode:BAV17 +Diode:BAV18 +Diode:BAV19 +Diode:BAV199DW +Diode:BAV20 +Diode:BAV21 +Diode:BAV300 +Diode:BAV301 +Diode:BAV302 +Diode:BAV303 +Diode:BAV70 +Diode:BAV70M +Diode:BAV70S +Diode:BAV70T +Diode:BAV70W +Diode:BAV756S +Diode:BAV99 +Diode:BAV99S +Diode:BAW56DW +Diode:BAW56S +Diode:BAW75 +Diode:BAW76 +Diode:BAY93 +Diode:BYV79-100 +Diode:BYV79-150 +Diode:BYV79-200 +Diode:BZD27Cxx +Diode:BZM55Bxx +Diode:BZM55Cxx +Diode:BZT52Bxx +Diode:BZV55B10 +Diode:BZV55B11 +Diode:BZV55B12 +Diode:BZV55B13 +Diode:BZV55B15 +Diode:BZV55B16 +Diode:BZV55B18 +Diode:BZV55B20 +Diode:BZV55B22 +Diode:BZV55B24 +Diode:BZV55B27 +Diode:BZV55B2V4 +Diode:BZV55B2V7 +Diode:BZV55B30 +Diode:BZV55B33 +Diode:BZV55B36 +Diode:BZV55B39 +Diode:BZV55B3V0 +Diode:BZV55B3V3 +Diode:BZV55B3V6 +Diode:BZV55B3V9 +Diode:BZV55B43 +Diode:BZV55B47 +Diode:BZV55B4V3 +Diode:BZV55B4V7 +Diode:BZV55B51 +Diode:BZV55B56 +Diode:BZV55B5V1 +Diode:BZV55B5V6 +Diode:BZV55B62 +Diode:BZV55B68 +Diode:BZV55B6V2 +Diode:BZV55B6V8 +Diode:BZV55B75 +Diode:BZV55B7V5 +Diode:BZV55B8V2 +Diode:BZV55B9V1 +Diode:BZV55C10 +Diode:BZV55C11 +Diode:BZV55C12 +Diode:BZV55C13 +Diode:BZV55C15 +Diode:BZV55C16 +Diode:BZV55C18 +Diode:BZV55C20 +Diode:BZV55C22 +Diode:BZV55C24 +Diode:BZV55C27 +Diode:BZV55C2V4 +Diode:BZV55C2V7 +Diode:BZV55C30 +Diode:BZV55C33 +Diode:BZV55C36 +Diode:BZV55C39 +Diode:BZV55C3V0 +Diode:BZV55C3V3 +Diode:BZV55C3V6 +Diode:BZV55C3V9 +Diode:BZV55C43 +Diode:BZV55C47 +Diode:BZV55C4V3 +Diode:BZV55C4V7 +Diode:BZV55C51 +Diode:BZV55C56 +Diode:BZV55C5V1 +Diode:BZV55C5V6 +Diode:BZV55C62 +Diode:BZV55C68 +Diode:BZV55C6V2 +Diode:BZV55C6V8 +Diode:BZV55C75 +Diode:BZV55C7V5 +Diode:BZV55C8V2 +Diode:BZV55C9V1 +Diode:BZX384xxxx +Diode:BZX84Cxx +Diode:C3D02060A +Diode:C3D02060E +Diode:C3D02060F +Diode:C3D02065E +Diode:C3D03060A +Diode:C3D03060E +Diode:C3D03060F +Diode:C3D03065E +Diode:C3D04060A +Diode:C3D04060E +Diode:C3D04060F +Diode:C3D04065A +Diode:C3D04065E +Diode:C3D06060A +Diode:C3D06060F +Diode:C3D06060G +Diode:C3D06065A +Diode:C3D06065E +Diode:C3D06065I +Diode:C3D08060A +Diode:C3D08060G +Diode:C3D08065A +Diode:C3D08065E +Diode:C3D08065I +Diode:C3D10060A +Diode:C3D10060G +Diode:C3D10065A +Diode:C3D10065E +Diode:C3D10065I +Diode:C3D10170H +Diode:C3D12065A +Diode:C3D16060D +Diode:C3D16065A +Diode:C3D16065D +Diode:C3D1P7060Q +Diode:C3D20060D +Diode:C3D20065D +Diode:C3D25170H +Diode:C3D30065D +Diode:C4D02120A +Diode:C4D02120E +Diode:C4D05120A +Diode:C4D05120E +Diode:C4D08120A +Diode:C4D08120E +Diode:C4D10120A +Diode:C4D10120D +Diode:C4D10120E +Diode:C4D10120H +Diode:C4D15120A +Diode:C4D15120D +Diode:C4D15120H +Diode:C4D20120A +Diode:C4D20120D +Diode:C4D20120H +Diode:C4D30120D +Diode:C4D40120D +Diode:C5D50065D +Diode:CD4148W +Diode:CDBA3100-HF +Diode:CDBA340-HF +Diode:CDBA360-HF +Diode:CDBU40-HF +Diode:CSD01060A +Diode:CSD01060E +Diode:CVFD20065A +Diode:Central_Semi_CMKD4448 +Diode:Central_Semi_CMKD6001 +Diode:Comchip_ACDSV6-4448TI-G +Diode:Comchip_CDSV6-4148-G +Diode:Comchip_CDSV6-4448TI-G +Diode:DB3 +Diode:DB4 +Diode:DC34 +Diode:DSB2810 +Diode:DSB5712 +Diode:DZ2S030X0L +Diode:DZ2S033X0L +Diode:DZ2S036X0L +Diode:DZ2S039X0L +Diode:DZ2S047X0L +Diode:DZ2S051X0L +Diode:DZ2S056X0L +Diode:DZ2S068X0L +Diode:DZ2S082X0L +Diode:DZ2S100X0L +Diode:DZ2S110X0L +Diode:DZ2S120X0L +Diode:DZ2S130X0L +Diode:DZ2S150X0L +Diode:DZ2S160X0L +Diode:DZ2S180X0L +Diode:DZ2S200X0L +Diode:DZ2S360X0L +Diode:ESD131-B1-W0201 +Diode:ESD5Zxx +Diode:ESD9B3.3ST5G +Diode:ESD9B5.0ST5G +Diode:ESH2PB +Diode:ESH2PC +Diode:ESH2PD +Diode:HN2D02FU +Diode:IDDD04G65C6 +Diode:IDDD06G65C6 +Diode:IDDD08G65C6 +Diode:IDDD10G65C6 +Diode:IDDD12G65C6 +Diode:IDDD16G65C6 +Diode:IDDD20G65C6 +Diode:LL41 +Diode:LL4148 +Diode:LL42 +Diode:LL43 +Diode:LL4448 +Diode:MBR0520 +Diode:MBR0520LT +Diode:MBR0530 +Diode:MBR0540 +Diode:MBR0550 +Diode:MBR0560 +Diode:MBR0570 +Diode:MBR0580 +Diode:MBR1020VL +Diode:MBR340 +Diode:MBR735 +Diode:MBR745 +Diode:MBRA340 +Diode:MBRS340 +Diode:MCL4148 +Diode:MCL4448 +Diode:MM3Zxx +Diode:MM5Zxx +Diode:MMBD4148TW +Diode:MMBD4448HADW +Diode:MMBD4448HCQW +Diode:MMBD4448HTW +Diode:MMBZxx +Diode:MMSD4148 +Diode:MMSD914 +Diode:MRA4003T3G +Diode:MRA4004T3G +Diode:MRA4005T3G +Diode:MRA4006T3G +Diode:MRA4007T3G +Diode:NRVA4003T3G +Diode:NRVA4004T3G +Diode:NRVA4005T3G +Diode:NRVA4006T3G +Diode:NRVA4007T3G +Diode:NSR0340HT1G +Diode:PMEG030V030EPD +Diode:PMEG030V050EPD +Diode:PMEG040V030EPD +Diode:PMEG040V050EPD +Diode:PMEG045T050EPD +Diode:PMEG045T100EPD +Diode:PMEG045T150EIPD +Diode:PMEG045T150EPD +Diode:PMEG045V050EPD +Diode:PMEG045V100EPD +Diode:PMEG045V150EPD +Diode:PMEG050T150EPD +Diode:PMEG050V030EPD +Diode:PMEG050V150EPD +Diode:PMEG060V030EPD +Diode:PMEG060V050EPD +Diode:PMEG060V100EPD +Diode:PMEG10010ELR +Diode:PMEG10020AELR +Diode:PMEG10020ELR +Diode:PMEG100V060ELPD +Diode:PMEG100V080ELPD +Diode:PMEG100V100ELPD +Diode:PMEG1020EH +Diode:PMEG1020EJ +Diode:PMEG1030EH +Diode:PMEG1030EJ +Diode:PMEG2005EH +Diode:PMEG2005EJ +Diode:PMEG2010AEH +Diode:PMEG2010AEJ +Diode:PMEG2010AET +Diode:PMEG2010BER +Diode:PMEG2010EH +Diode:PMEG2010EJ +Diode:PMEG2010ER +Diode:PMEG2010ET +Diode:PMEG2015EH +Diode:PMEG2015EJ +Diode:PMEG2020EH +Diode:PMEG2020EJ +Diode:PMEG3002EJ +Diode:PMEG3005EH +Diode:PMEG3005EJ +Diode:PMEG3010BER +Diode:PMEG3010CEH +Diode:PMEG3010CEJ +Diode:PMEG3010EH +Diode:PMEG3010EJ +Diode:PMEG3010ER +Diode:PMEG3010ET +Diode:PMEG3015EH +Diode:PMEG3015EJ +Diode:PMEG3020BER +Diode:PMEG3020EH +Diode:PMEG3020EJ +Diode:PMEG3020ER +Diode:PMEG4002EJ +Diode:PMEG4005CEJ +Diode:PMEG4005EH +Diode:PMEG4005EJ +Diode:PMEG4010CEH +Diode:PMEG4010CEJ +Diode:PMEG4010EH +Diode:PMEG4010EJ +Diode:PMEG4010ER +Diode:PMEG4010ET +Diode:PMEG4020ER +Diode:PMEG4030ER +Diode:PMEG4050EP +Diode:PMEG40T10ER +Diode:PMEG40T20ER +Diode:PMEG40T30ER +Diode:PMEG45A10EPD +Diode:PMEG45T15EPD +Diode:PMEG45U10EPD +Diode:PMEG6002EJ +Diode:PMEG6010CEH +Diode:PMEG6010CEJ +Diode:PMEG6010ELR +Diode:PMEG6010ER +Diode:PMEG6020AELR +Diode:PMEG6020ELR +Diode:PMEG6020ER +Diode:PMEG6030EP +Diode:PMEG6045ETP +Diode:PMEG60T10ELR +Diode:PMEG60T20ELR +Diode:PMEG60T30ELR +Diode:PTVS10VZ1USK +Diode:PTVS12VZ1USK +Diode:PTVS15VZ1USK +Diode:PTVS18VZ1USK +Diode:PTVS20VZ1USK +Diode:PTVS22VZ1USK +Diode:PTVS26VZ1USK +Diode:PTVS5V0Z1USK +Diode:PTVS5V0Z1USKP +Diode:PTVS7V5Z1USK +Diode:Panasonic_MA5J002E +Diode:RF01VM2S +Diode:Rohm_UMN1N +Diode:Rohm_UMP11N +Diode:S2JTR +Diode:SB120 +Diode:SB130 +Diode:SB140 +Diode:SB150 +Diode:SB160 +Diode:SB5H100 +Diode:SB5H90 +Diode:SD05_SOD323 +Diode:SD103ATW +Diode:SD12_SOD323 +Diode:SD15_SOD323 +Diode:SD24_SOD323 +Diode:SD36_SOD323 +Diode:SM15T36A +Diode:SM15T36CA +Diode:SM15T6V8A +Diode:SM15T6V8CA +Diode:SM15T7V5A +Diode:SM15T7V5CA +Diode:SM2000 +Diode:SM4001 +Diode:SM4002 +Diode:SM4003 +Diode:SM4004 +Diode:SM4005 +Diode:SM4006 +Diode:SM4007 +Diode:SM5059 +Diode:SM5060 +Diode:SM5061 +Diode:SM5062 +Diode:SM5063 +Diode:SM513 +Diode:SM516 +Diode:SM518 +Diode:SM5908 +Diode:SM6T100A +Diode:SM6T10A +Diode:SM6T12A +Diode:SM6T150A +Diode:SM6T15A +Diode:SM6T18A +Diode:SM6T200A +Diode:SM6T220A +Diode:SM6T22A +Diode:SM6T24A +Diode:SM6T27A +Diode:SM6T30A +Diode:SM6T33A +Diode:SM6T36A +Diode:SM6T39A +Diode:SM6T56A +Diode:SM6T68A +Diode:SM6T6V8A +Diode:SM6T75A +Diode:SM6T7V5A +Diode:SM712_SOT23 +Diode:SMAJ100A +Diode:SMAJ100CA +Diode:SMAJ10A +Diode:SMAJ10CA +Diode:SMAJ110A +Diode:SMAJ110CA +Diode:SMAJ11A +Diode:SMAJ11CA +Diode:SMAJ120A +Diode:SMAJ120CA +Diode:SMAJ12A +Diode:SMAJ12CA +Diode:SMAJ130A +Diode:SMAJ130CA +Diode:SMAJ13A +Diode:SMAJ13CA +Diode:SMAJ14A +Diode:SMAJ14CA +Diode:SMAJ150A +Diode:SMAJ150CA +Diode:SMAJ15A +Diode:SMAJ15CA +Diode:SMAJ160A +Diode:SMAJ160CA +Diode:SMAJ16A +Diode:SMAJ16CA +Diode:SMAJ170A +Diode:SMAJ170CA +Diode:SMAJ17A +Diode:SMAJ17CA +Diode:SMAJ180A +Diode:SMAJ180CA +Diode:SMAJ188A +Diode:SMAJ188CA +Diode:SMAJ18A +Diode:SMAJ18CA +Diode:SMAJ200A +Diode:SMAJ200CA +Diode:SMAJ20A +Diode:SMAJ20CA +Diode:SMAJ220A +Diode:SMAJ220CA +Diode:SMAJ22A +Diode:SMAJ22CA +Diode:SMAJ24A +Diode:SMAJ24CA +Diode:SMAJ250A +Diode:SMAJ250CA +Diode:SMAJ26A +Diode:SMAJ26CA +Diode:SMAJ28A +Diode:SMAJ28CA +Diode:SMAJ300A +Diode:SMAJ300CA +Diode:SMAJ30A +Diode:SMAJ30CA +Diode:SMAJ33A +Diode:SMAJ33CA +Diode:SMAJ350A +Diode:SMAJ350CA +Diode:SMAJ36A +Diode:SMAJ36CA +Diode:SMAJ400A +Diode:SMAJ400CA +Diode:SMAJ40A +Diode:SMAJ40CA +Diode:SMAJ43A +Diode:SMAJ43CA +Diode:SMAJ440A +Diode:SMAJ440CA +Diode:SMAJ45A +Diode:SMAJ45CA +Diode:SMAJ48A +Diode:SMAJ48CA +Diode:SMAJ5.0A +Diode:SMAJ5.0CA +Diode:SMAJ51A +Diode:SMAJ51CA +Diode:SMAJ54A +Diode:SMAJ54CA +Diode:SMAJ58A +Diode:SMAJ58CA +Diode:SMAJ6.0A +Diode:SMAJ6.0CA +Diode:SMAJ6.5A +Diode:SMAJ6.5CA +Diode:SMAJ60A +Diode:SMAJ60CA +Diode:SMAJ64A +Diode:SMAJ64CA +Diode:SMAJ7.0A +Diode:SMAJ7.0CA +Diode:SMAJ7.5A +Diode:SMAJ7.5CA +Diode:SMAJ70A +Diode:SMAJ70CA +Diode:SMAJ75A +Diode:SMAJ75CA +Diode:SMAJ78A +Diode:SMAJ78CA +Diode:SMAJ8.0A +Diode:SMAJ8.0CA +Diode:SMAJ8.5A +Diode:SMAJ8.5CA +Diode:SMAJ85A +Diode:SMAJ85CA +Diode:SMAJ9.0A +Diode:SMAJ9.0CA +Diode:SMAJ90A +Diode:SMAJ90CA +Diode:SMF10A +Diode:SMF11A +Diode:SMF12A +Diode:SMF13A +Diode:SMF14A +Diode:SMF15A +Diode:SMF16A +Diode:SMF17A +Diode:SMF18A +Diode:SMF20A +Diode:SMF22A +Diode:SMF24A +Diode:SMF26A +Diode:SMF28A +Diode:SMF30A +Diode:SMF33A +Diode:SMF36A +Diode:SMF40A +Diode:SMF43A +Diode:SMF45A +Diode:SMF48A +Diode:SMF51A +Diode:SMF54A +Diode:SMF58A +Diode:SMF5V0A +Diode:SMF6V0A +Diode:SMF6V5A +Diode:SMF7V0A +Diode:SMF7V5A +Diode:SMF8V0A +Diode:SMF8V5A +Diode:SMF9V0A +Diode:SMZxxx +Diode:SS110 +Diode:SS1150 +Diode:SS12 +Diode:SS1200 +Diode:SS13 +Diode:SS14 +Diode:SS15 +Diode:SS16 +Diode:SS18 +Diode:SS210 +Diode:SS2150 +Diode:SS22 +Diode:SS2200 +Diode:SS23 +Diode:SS24 +Diode:SS25 +Diode:SS26 +Diode:SS28 +Diode:SS310 +Diode:SS3150 +Diode:SS32 +Diode:SS3200 +Diode:SS33 +Diode:SS34 +Diode:SS35 +Diode:SS36 +Diode:SS38 +Diode:STBR3008WY +Diode:STBR3012WY +Diode:STBR6008WY +Diode:STBR6012WY +Diode:STTH2002D +Diode:STTH2002G +Diode:STTH212S +Diode:STTH212U +Diode:SZESD9B5.0ST5G +Diode:Toshiba_HN1D01FU +Diode:UF5400 +Diode:UF5401 +Diode:UF5402 +Diode:UF5403 +Diode:UF5404 +Diode:UF5405 +Diode:UF5406 +Diode:UF5407 +Diode:UF5408 +Diode:US1A +Diode:US1B +Diode:US1D +Diode:US1G +Diode:US1J +Diode:US1K +Diode:US1M +Diode:US2AA +Diode:US2BA +Diode:US2DA +Diode:US2FA +Diode:US2GA +Diode:US2JA +Diode:US2KA +Diode:US2MA +Diode:VS-6ESU06 +Diode:Z1SMAxxx +Diode:Z2SMBxxx +Diode:Z3SMCxxx +Diode:ZMDxx +Diode:ZMMxx +Diode:ZMYxx +Diode:ZPDxx +Diode:ZPYxx +Diode:ZYxxx +Diode_Bridge:ABS10 +Diode_Bridge:ABS2 +Diode_Bridge:ABS4 +Diode_Bridge:ABS6 +Diode_Bridge:ABS8 +Diode_Bridge:B125C1500G +Diode_Bridge:B125C2300-1500A +Diode_Bridge:B125C2300-1500B +Diode_Bridge:B125C3x00-2200A +Diode_Bridge:B125C5000-3x00A +Diode_Bridge:B125C800DM +Diode_Bridge:B125R +Diode_Bridge:B250C1500G +Diode_Bridge:B250C2300-1500A +Diode_Bridge:B250C2300-1500B +Diode_Bridge:B250C3x00-2200A +Diode_Bridge:B250C5000-3x00A +Diode_Bridge:B250C800DM +Diode_Bridge:B250R +Diode_Bridge:B380C1500G +Diode_Bridge:B380C2300-1500A +Diode_Bridge:B380C2300-1500B +Diode_Bridge:B380C3x00-2200A +Diode_Bridge:B380C5000-3x00A +Diode_Bridge:B380C800DM +Diode_Bridge:B380R +Diode_Bridge:B40C1500G +Diode_Bridge:B40C2300-1500A +Diode_Bridge:B40C2300-1500B +Diode_Bridge:B40C3x00-2200A +Diode_Bridge:B40C5000-3x00A +Diode_Bridge:B40C800DM +Diode_Bridge:B40R +Diode_Bridge:B500C2300-1500A +Diode_Bridge:B500C3x00-2200A +Diode_Bridge:B500C5000-3x00A +Diode_Bridge:B500R +Diode_Bridge:B700C2300-1500B +Diode_Bridge:B80C1500G +Diode_Bridge:B80C2300-1500A +Diode_Bridge:B80C2300-1500B +Diode_Bridge:B80C3x00-2200A +Diode_Bridge:B80C5000-3x00A +Diode_Bridge:B80C800DM +Diode_Bridge:B80R +Diode_Bridge:DF005M +Diode_Bridge:DF005S +Diode_Bridge:DF01M +Diode_Bridge:DF01S +Diode_Bridge:DF01S1 +Diode_Bridge:DF02M +Diode_Bridge:DF02S +Diode_Bridge:DF04M +Diode_Bridge:DF04S +Diode_Bridge:DF06M +Diode_Bridge:DF06S +Diode_Bridge:DF08M +Diode_Bridge:DF08S +Diode_Bridge:DF10M +Diode_Bridge:DF10S +Diode_Bridge:DMA40U1800GU +Diode_Bridge:DNA40U2200GU +Diode_Bridge:GBU4A +Diode_Bridge:GBU4B +Diode_Bridge:GBU4D +Diode_Bridge:GBU4G +Diode_Bridge:GBU4J +Diode_Bridge:GBU4K +Diode_Bridge:GBU4M +Diode_Bridge:GBU6A +Diode_Bridge:GBU6B +Diode_Bridge:GBU6D +Diode_Bridge:GBU6G +Diode_Bridge:GBU6J +Diode_Bridge:GBU6K +Diode_Bridge:GBU6M +Diode_Bridge:GBU8A +Diode_Bridge:GBU8B +Diode_Bridge:GBU8D +Diode_Bridge:GBU8G +Diode_Bridge:GBU8J +Diode_Bridge:GBU8K +Diode_Bridge:GBU8M +Diode_Bridge:GUO40-08NO1 +Diode_Bridge:GUO40-12NO1 +Diode_Bridge:GUO40-16NO1 +Diode_Bridge:KBPC15005T +Diode_Bridge:KBPC15005W +Diode_Bridge:KBPC1501T +Diode_Bridge:KBPC1501W +Diode_Bridge:KBPC1502T +Diode_Bridge:KBPC1502W +Diode_Bridge:KBPC1504T +Diode_Bridge:KBPC1504W +Diode_Bridge:KBPC1506T +Diode_Bridge:KBPC1506W +Diode_Bridge:KBPC1508T +Diode_Bridge:KBPC1508W +Diode_Bridge:KBPC1510T +Diode_Bridge:KBPC1510W +Diode_Bridge:KBPC25005T +Diode_Bridge:KBPC25005W +Diode_Bridge:KBPC2501T +Diode_Bridge:KBPC2501W +Diode_Bridge:KBPC2502T +Diode_Bridge:KBPC2502W +Diode_Bridge:KBPC2504T +Diode_Bridge:KBPC2504W +Diode_Bridge:KBPC2506T +Diode_Bridge:KBPC2506W +Diode_Bridge:KBPC2508T +Diode_Bridge:KBPC2508W +Diode_Bridge:KBPC2510T +Diode_Bridge:KBPC2510W +Diode_Bridge:KBPC35005T +Diode_Bridge:KBPC35005W +Diode_Bridge:KBPC3501T +Diode_Bridge:KBPC3501W +Diode_Bridge:KBPC3502T +Diode_Bridge:KBPC3502W +Diode_Bridge:KBPC3504T +Diode_Bridge:KBPC3504W +Diode_Bridge:KBPC3506T +Diode_Bridge:KBPC3506W +Diode_Bridge:KBPC3508T +Diode_Bridge:KBPC3508W +Diode_Bridge:KBPC3510T +Diode_Bridge:KBPC3510W +Diode_Bridge:KBPC50005T +Diode_Bridge:KBPC50005W +Diode_Bridge:KBPC5001T +Diode_Bridge:KBPC5001W +Diode_Bridge:KBPC5002T +Diode_Bridge:KBPC5002W +Diode_Bridge:KBPC5004T +Diode_Bridge:KBPC5004W +Diode_Bridge:KBPC5006T +Diode_Bridge:KBPC5006W +Diode_Bridge:KBPC5008T +Diode_Bridge:KBPC5008W +Diode_Bridge:KBPC5010T +Diode_Bridge:KBPC5010W +Diode_Bridge:KBU4A +Diode_Bridge:KBU4B +Diode_Bridge:KBU4D +Diode_Bridge:KBU4G +Diode_Bridge:KBU4J +Diode_Bridge:KBU4K +Diode_Bridge:KBU4M +Diode_Bridge:KBU6A +Diode_Bridge:KBU6B +Diode_Bridge:KBU6D +Diode_Bridge:KBU6G +Diode_Bridge:KBU6J +Diode_Bridge:KBU6K +Diode_Bridge:KBU6M +Diode_Bridge:KBU8A +Diode_Bridge:KBU8B +Diode_Bridge:KBU8D +Diode_Bridge:KBU8G +Diode_Bridge:KBU8J +Diode_Bridge:KBU8K +Diode_Bridge:KBU8M +Diode_Bridge:MB2S +Diode_Bridge:MB4S +Diode_Bridge:MB6S +Diode_Bridge:MBL104S +Diode_Bridge:MBL106S +Diode_Bridge:MBL108S +Diode_Bridge:MBL110S +Diode_Bridge:MDB10S +Diode_Bridge:MDB6S +Diode_Bridge:MDB8S +Diode_Bridge:RB151 +Diode_Bridge:RB152 +Diode_Bridge:RB153 +Diode_Bridge:RB154 +Diode_Bridge:RB155 +Diode_Bridge:RB156 +Diode_Bridge:RB157 +Diode_Bridge:RMB2S +Diode_Bridge:RMB4S +Diode_Bridge:SC35VB160S-G +Diode_Bridge:SC35VB80S-G +Diode_Bridge:VS-KBPC1005 +Diode_Bridge:VS-KBPC101 +Diode_Bridge:VS-KBPC102 +Diode_Bridge:VS-KBPC104 +Diode_Bridge:VS-KBPC106 +Diode_Bridge:VS-KBPC108 +Diode_Bridge:VS-KBPC110 +Diode_Bridge:VS-KBPC6005 +Diode_Bridge:VS-KBPC601 +Diode_Bridge:VS-KBPC602 +Diode_Bridge:VS-KBPC604 +Diode_Bridge:VS-KBPC606 +Diode_Bridge:VS-KBPC608 +Diode_Bridge:VS-KBPC610 +Diode_Bridge:VS-KBPC8005 +Diode_Bridge:VS-KBPC801 +Diode_Bridge:VS-KBPC802 +Diode_Bridge:VS-KBPC804 +Diode_Bridge:VS-KBPC806 +Diode_Bridge:VS-KBPC808 +Diode_Bridge:VS-KBPC810 +Diode_Bridge:W005G +Diode_Bridge:W01G +Diode_Bridge:W02G +Diode_Bridge:W04G +Diode_Bridge:W06G +Diode_Bridge:W08G +Diode_Bridge:W10G +Diode_Laser:PL520 +Diode_Laser:PLT5_450B +Diode_Laser:PLT5_488 +Diode_Laser:PLT5_510 +Diode_Laser:SPL_PL90 +Display_Character:AD-121F2 +Display_Character:CA56-12CGKWA +Display_Character:CA56-12EWA +Display_Character:CA56-12SEKWA +Display_Character:CA56-12SRWA +Display_Character:CA56-12SURKWA +Display_Character:CA56-12SYKWA +Display_Character:CC56-12CGKWA +Display_Character:CC56-12EWA +Display_Character:CC56-12GWA +Display_Character:CC56-12SEKWA +Display_Character:CC56-12SRWA +Display_Character:CC56-12SURKWA +Display_Character:CC56-12SYKWA +Display_Character:CC56-12YWA +Display_Character:D148K +Display_Character:D168K +Display_Character:D198K +Display_Character:D1X8K-14BL +Display_Character:DA04-11CGKWA +Display_Character:DA04-11EWA +Display_Character:DA04-11GWA +Display_Character:DA04-11SEKWA +Display_Character:DA04-11SRWA +Display_Character:DA04-11SURKWA +Display_Character:DA04-11SYKWA +Display_Character:DA56-11CGKWA +Display_Character:DA56-11EWA +Display_Character:DA56-11GWA +Display_Character:DA56-11SEKWA +Display_Character:DA56-11SRWA +Display_Character:DA56-11SURKWA +Display_Character:DA56-11SYKWA +Display_Character:DA56-11YWA +Display_Character:DC56-11CGKWA +Display_Character:DC56-11EWA +Display_Character:DC56-11GWA +Display_Character:DC56-11SEKWA +Display_Character:DC56-11SRWA +Display_Character:DC56-11SURKWA +Display_Character:DC56-11SYKWA +Display_Character:DC56-11YWA +Display_Character:DE113-XX-XX +Display_Character:DE114-RS-20 +Display_Character:DE122-XX-XX +Display_Character:DE170-XX-XX +Display_Character:EA_T123X-I2C +Display_Character:ELD-426SYGWA +Display_Character:HDSM-441B +Display_Character:HDSM-443B +Display_Character:HDSM-541B +Display_Character:HDSM-543B +Display_Character:HDSP-7401 +Display_Character:HDSP-7403 +Display_Character:HDSP-7501 +Display_Character:HDSP-7503 +Display_Character:HDSP-7507 +Display_Character:HDSP-7508 +Display_Character:HDSP-7801 +Display_Character:HDSP-7803 +Display_Character:HDSP-7807 +Display_Character:HDSP-7808 +Display_Character:HDSP-A151 +Display_Character:HDSP-A153 +Display_Character:HDSP-A401 +Display_Character:HDSP-A403 +Display_Character:HY1602E +Display_Character:KCSA02-105 +Display_Character:KCSA02-106 +Display_Character:KCSA02-107 +Display_Character:KCSA02-123 +Display_Character:KCSA02-136 +Display_Character:KCSC02-105 +Display_Character:KCSC02-106 +Display_Character:KCSC02-107 +Display_Character:KCSC02-123 +Display_Character:KCSC02-136 +Display_Character:LCD-016N002L +Display_Character:LM16255K +Display_Character:LTC-4627JD +Display_Character:LTC-4627JD-01 +Display_Character:LTC-4627JF +Display_Character:LTC-4627JG +Display_Character:LTC-4627JR +Display_Character:LTC-4627JS +Display_Character:LTS-6960HR +Display_Character:LTS-6980HR +Display_Character:MAN3410A +Display_Character:MAN3420A +Display_Character:MAN3440A +Display_Character:MAN3610A +Display_Character:MAN3620A +Display_Character:MAN3630A +Display_Character:MAN3640A +Display_Character:MAN3810A +Display_Character:MAN3820A +Display_Character:MAN3840A +Display_Character:MAN71A +Display_Character:MAN72A +Display_Character:MAN73A +Display_Character:MAN74A +Display_Character:NHD-0420H1Z +Display_Character:NHD-C0220BIZ +Display_Character:NHD-C0220BIZ-FSRGB +Display_Character:RC1602A +Display_Character:RC1602A-GHW-ESX +Display_Character:SA15-11EWA +Display_Character:SA15-11GWA +Display_Character:SA15-11SRWA +Display_Character:SA39-11EWA +Display_Character:SA39-11GWA +Display_Character:SA39-11SRWA +Display_Character:SA39-11YWA +Display_Character:SA39-12EWA +Display_Character:SA39-12GWA +Display_Character:SA39-12SRWA +Display_Character:SA39-12YWA +Display_Character:SBC18-11EGWA +Display_Character:SBC18-11SURKCGKWA +Display_Character:SC39-11EWA +Display_Character:SC39-11GWA +Display_Character:SC39-11SRWA +Display_Character:SC39-11YWA +Display_Character:SC39-12EWA +Display_Character:SC39-12GWA +Display_Character:SC39-12SRWA +Display_Character:SC39-12YWA +Display_Character:SM420561N +Display_Character:WC1602A +Display_Graphic:AG12864E +Display_Graphic:EA_DOGL128X-6 +Display_Graphic:EA_DOGM128X-6 +Display_Graphic:EA_DOGS104B-A +Display_Graphic:EA_DOGXL160-7 +Display_Graphic:EA_eDIP128B-6LW +Display_Graphic:EA_eDIP128B-6LWTP +Display_Graphic:EA_eDIP128W-6LW +Display_Graphic:EA_eDIP128W-6LWTP +Display_Graphic:EA_eDIP160B-7LW +Display_Graphic:EA_eDIP160B-7LWTP +Display_Graphic:EA_eDIP160W-7LW +Display_Graphic:EA_eDIP160W-7LWTP +Display_Graphic:EA_eDIP240B-7LW +Display_Graphic:EA_eDIP240B-7LWTP +Display_Graphic:EA_eDIP240J-7LA +Display_Graphic:EA_eDIP240J-7LATP +Display_Graphic:EA_eDIP240J-7LW +Display_Graphic:EA_eDIP240J-7LWTP +Display_Graphic:EA_eDIP320B-8LW +Display_Graphic:EA_eDIP320B-8LWTP +Display_Graphic:EA_eDIP320J-8LA +Display_Graphic:EA_eDIP320J-8LATP +Display_Graphic:EA_eDIP320J-8LW +Display_Graphic:EA_eDIP320J-8LWTP +Display_Graphic:EA_eDIPTFT32-A +Display_Graphic:EA_eDIPTFT32-ATP +Display_Graphic:EA_eDIPTFT43-A +Display_Graphic:EA_eDIPTFT43-ATC +Display_Graphic:EA_eDIPTFT43-ATP +Display_Graphic:EA_eDIPTFT43-ATS +Display_Graphic:EA_eDIPTFT57-A +Display_Graphic:EA_eDIPTFT57-ATP +Display_Graphic:EA_eDIPTFT70-A +Display_Graphic:EA_eDIPTFT70-ATC +Display_Graphic:EA_eDIPTFT70-ATP +Display_Graphic:ERM19264 +Display_Graphic:NHD-C12832A1Z-FSRGB +Display_Graphic:OLED-128O064D +Driver_Display:82720 +Driver_Display:ADS7843E +Driver_Display:ADS7843E-2K5 +Driver_Display:ADS7843EG4 +Driver_Display:ADS7843IDBQRQ1 +Driver_Display:AY0438X-L +Driver_Display:AY0438X-P +Driver_Display:CR2013-MI2120 +Driver_Display:XPT2046QF +Driver_Display:XPT2046TS +Driver_FET:1EDN7550B +Driver_FET:1EDN8550B +Driver_FET:2ED1323S12P +Driver_FET:2ED1324S12P +Driver_FET:2ED21824S06J +Driver_FET:2EDL23N06PJXUMA1 +Driver_FET:ACPL-336J +Driver_FET:ACPL-P343 +Driver_FET:ACPL-W343 +Driver_FET:AN34092B +Driver_FET:BSP75N +Driver_FET:BSP76 +Driver_FET:BTS4140N +Driver_FET:EL7202CN +Driver_FET:EL7202CS +Driver_FET:EL7212CN +Driver_FET:EL7212CS +Driver_FET:EL7222CN +Driver_FET:EL7222CS +Driver_FET:FAN3111C +Driver_FET:FAN3111E +Driver_FET:FAN3268 +Driver_FET:FAN3278 +Driver_FET:FAN7371 +Driver_FET:FAN7388 +Driver_FET:FAN7842 +Driver_FET:FAN7888 +Driver_FET:FL5150MX +Driver_FET:FL5160MX +Driver_FET:HCPL-3120 +Driver_FET:HCPL-314J +Driver_FET:HIP2100_DFN +Driver_FET:HIP2100_EPSOIC +Driver_FET:HIP2100_QFN +Driver_FET:HIP2100_SOIC +Driver_FET:HIP2101_DFN +Driver_FET:HIP2101_EPSOIC +Driver_FET:HIP2101_QFN +Driver_FET:HIP2101_SOIC +Driver_FET:HIP4080A +Driver_FET:HIP4081A +Driver_FET:HIP4082xB +Driver_FET:HIP4082xP +Driver_FET:ICL7667 +Driver_FET:IR2010 +Driver_FET:IR2010S +Driver_FET:IR2011 +Driver_FET:IR2085S +Driver_FET:IR2101 +Driver_FET:IR2102 +Driver_FET:IR2103 +Driver_FET:IR2104 +Driver_FET:IR2106 +Driver_FET:IR21064 +Driver_FET:IR2108 +Driver_FET:IR21084 +Driver_FET:IR2109 +Driver_FET:IR21091 +Driver_FET:IR21094 +Driver_FET:IR2110 +Driver_FET:IR2110S +Driver_FET:IR2111 +Driver_FET:IR2112 +Driver_FET:IR2112S +Driver_FET:IR2113 +Driver_FET:IR2113S +Driver_FET:IR2114S +Driver_FET:IR2133 +Driver_FET:IR2133S +Driver_FET:IR2135 +Driver_FET:IR2135S +Driver_FET:IR2153 +Driver_FET:IR21531 +Driver_FET:IR2155 +Driver_FET:IR2181 +Driver_FET:IR21814 +Driver_FET:IR2183 +Driver_FET:IR21834 +Driver_FET:IR2184 +Driver_FET:IR21844 +Driver_FET:IR2213 +Driver_FET:IR2213S +Driver_FET:IR2214S +Driver_FET:IR2233 +Driver_FET:IR2233S +Driver_FET:IR2235 +Driver_FET:IR2235S +Driver_FET:IR2301 +Driver_FET:IR2302 +Driver_FET:IR2304 +Driver_FET:IR2308 +Driver_FET:IR25602S +Driver_FET:IR25603 +Driver_FET:IR25604S +Driver_FET:IR25607S +Driver_FET:IR7106S +Driver_FET:IR7184S +Driver_FET:IR7304S +Driver_FET:IRS2001 +Driver_FET:IRS2001M +Driver_FET:IRS2003 +Driver_FET:IRS2004 +Driver_FET:IRS2005M +Driver_FET:IRS2005S +Driver_FET:IRS2008S +Driver_FET:IRS2011 +Driver_FET:IRS2101 +Driver_FET:IRS2103 +Driver_FET:IRS2104 +Driver_FET:IRS2106 +Driver_FET:IRS21064 +Driver_FET:IRS2108 +Driver_FET:IRS21084 +Driver_FET:IRS2109 +Driver_FET:IRS21091 +Driver_FET:IRS21094 +Driver_FET:IRS2110 +Driver_FET:IRS2110S +Driver_FET:IRS2111 +Driver_FET:IRS2112 +Driver_FET:IRS2112S +Driver_FET:IRS2113 +Driver_FET:IRS2113M +Driver_FET:IRS2113S +Driver_FET:IRS21531D +Driver_FET:IRS2153D +Driver_FET:IRS2181 +Driver_FET:IRS21814 +Driver_FET:IRS21814M +Driver_FET:IRS2183 +Driver_FET:IRS21834 +Driver_FET:IRS2184 +Driver_FET:IRS21844 +Driver_FET:IRS21844M +Driver_FET:IRS2186 +Driver_FET:IRS21864 +Driver_FET:IRS21867S +Driver_FET:IRS2301S +Driver_FET:IRS2302S +Driver_FET:IRS2304 +Driver_FET:IRS2308 +Driver_FET:IRS25606S +Driver_FET:IRS2890DS +Driver_FET:ITS711L1 +Driver_FET:ITS716G +Driver_FET:ITS724G +Driver_FET:L6491 +Driver_FET:LF2190N +Driver_FET:LM2105D +Driver_FET:LM2105DSG +Driver_FET:LM5109AMA +Driver_FET:LM5109ASD +Driver_FET:LM5109BMA +Driver_FET:LM5109BSD +Driver_FET:LM5109MA +Driver_FET:LMG1020YFF +Driver_FET:LTC4440EMS8 +Driver_FET:LTC4440ES6 +Driver_FET:LTC4440IMS8 +Driver_FET:LTC4440IS6 +Driver_FET:MAX15012AxSA +Driver_FET:MAX15012BxSA +Driver_FET:MAX15012CxSA +Driver_FET:MAX15012DxSA +Driver_FET:MAX15013AxSA +Driver_FET:MAX15013BxSA +Driver_FET:MAX15013CxSA +Driver_FET:MAX15013DxSA +Driver_FET:MC33152 +Driver_FET:MC34152 +Driver_FET:MCP1415 +Driver_FET:MCP1415R +Driver_FET:MCP1416 +Driver_FET:MCP1416R +Driver_FET:MCP14A0303xMNY +Driver_FET:MCP14A0304xMNY +Driver_FET:MCP14A0305xMNY +Driver_FET:MCP14A0901xMNY +Driver_FET:MCP14A0902xMNY +Driver_FET:MCP14A1201xMNY +Driver_FET:MCP14A1202xMNY +Driver_FET:MIC4426 +Driver_FET:MIC4427 +Driver_FET:MIC4428 +Driver_FET:MIC4604YM +Driver_FET:NCD5702 +Driver_FET:NCV8402xST +Driver_FET:PE29101 +Driver_FET:PE29102 +Driver_FET:PM8834 +Driver_FET:PM8834M +Driver_FET:SM72295MA +Driver_FET:STGAP1AS +Driver_FET:STGAP2SCM +Driver_FET:STGAP2SM +Driver_FET:TC4421 +Driver_FET:TC4422 +Driver_FET:TC4426xOA +Driver_FET:TC4427xOA +Driver_FET:TC4428xOA +Driver_FET:TLP250 +Driver_FET:UCC21520ADW +Driver_FET:UCC21520DW +Driver_FET:UCC27511ADBV +Driver_FET:UCC27714D +Driver_FET:ZXGD3001E6 +Driver_FET:ZXGD3002E6 +Driver_FET:ZXGD3003E6 +Driver_FET:ZXGD3004E6 +Driver_FET:ZXGD3006E6 +Driver_FET:ZXGD3009E6 +Driver_Haptic:DRV2510-Q1 +Driver_Haptic:DRV2605LDGS +Driver_LED:AL8860MP +Driver_LED:AL8860WT +Driver_LED:AP3019AKTR +Driver_LED:AP3019AKTTR +Driver_LED:BCR430UW6 +Driver_LED:CH455G +Driver_LED:CH455H +Driver_LED:CH455K +Driver_LED:CL220K4-G +Driver_LED:CL220N5-G +Driver_LED:DIO5661CD6 +Driver_LED:DIO5661ST6 +Driver_LED:DIO5661TST6 +Driver_LED:HT1632C-52LQFP +Driver_LED:HV9921N8-G +Driver_LED:HV9922N8-G +Driver_LED:HV9923N8-G +Driver_LED:HV9925SG-G +Driver_LED:HV9930LG-G +Driver_LED:HV9931LG-G +Driver_LED:HV9961LG-G +Driver_LED:HV9961NG-G +Driver_LED:HV9967BK7-G +Driver_LED:HV9967BMG-G +Driver_LED:HV9972LG-G +Driver_LED:IS31FL3216 +Driver_LED:IS31FL3216A +Driver_LED:IS31FL3218-GR +Driver_LED:IS31FL3218-QF +Driver_LED:IS31FL3236-TQ +Driver_LED:IS31FL3236A-TQ +Driver_LED:IS31FL3731-QF +Driver_LED:IS31FL3731-SA +Driver_LED:IS31FL3733-QF +Driver_LED:IS31FL3733-TQ +Driver_LED:IS31FL3736 +Driver_LED:IS31FL3737 +Driver_LED:IS31LT3360 +Driver_LED:KTD2026 +Driver_LED:KTD2027 +Driver_LED:KTD2061xxUAC +Driver_LED:LED1642GWPTR +Driver_LED:LED1642GWQTR +Driver_LED:LED1642GWTTR +Driver_LED:LED1642GWXTTR +Driver_LED:LED5000 +Driver_LED:LM3914N +Driver_LED:LM3914V +Driver_LED:LP5036 +Driver_LED:LP8868XQDMT +Driver_LED:LT3465 +Driver_LED:LT3465A +Driver_LED:LT3755xMSE +Driver_LED:LT3755xMSE-1 +Driver_LED:LT3755xMSE-2 +Driver_LED:LT3755xUD +Driver_LED:LT3755xUD-1 +Driver_LED:LT3755xUD-2 +Driver_LED:LT3756xMSE +Driver_LED:LT3756xMSE-1 +Driver_LED:LT3756xMSE-2 +Driver_LED:LT3756xUD +Driver_LED:LT3756xUD-1 +Driver_LED:LT3756xUD-2 +Driver_LED:LT8391xFE +Driver_LED:MAX7219 +Driver_LED:MAX7221xNG +Driver_LED:MAX7221xRG +Driver_LED:MAX7221xWG +Driver_LED:MBI5252GFN +Driver_LED:MBI5252GP +Driver_LED:MC14495P +Driver_LED:MCP1643xMS +Driver_LED:MCP1662-xOT +Driver_LED:MP3362GJ +Driver_LED:MPQ2483DQ +Driver_LED:MPQ3362GJ-AEC1 +Driver_LED:NCP5623DTBR2G +Driver_LED:NCR401U +Driver_LED:PCA9531PW +Driver_LED:PCA9635 +Driver_LED:PCA9685BS +Driver_LED:PCA9685PW +Driver_LED:PCA9745BTW +Driver_LED:RCD-24 +Driver_LED:ST1CC40DR +Driver_LED:ST1CC40PUR +Driver_LED:STP08CP05B +Driver_LED:STP08CP05M +Driver_LED:STP08CP05T +Driver_LED:STP08CP05XT +Driver_LED:STP16CP05M +Driver_LED:STP16CP05P +Driver_LED:STP16CP05T +Driver_LED:STP16CP05XT +Driver_LED:STP16CPC26M +Driver_LED:STP16CPC26P +Driver_LED:STP16CPC26T +Driver_LED:STP16CPC26X +Driver_LED:TCA6507RUE +Driver_LED:TLC59108xPW +Driver_LED:TLC5916 +Driver_LED:TLC5917 +Driver_LED:TLC5940NT +Driver_LED:TLC5940PWP +Driver_LED:TLC5947DAP +Driver_LED:TLC5947RHB +Driver_LED:TLC5949PWP +Driver_LED:TLC5951DAP +Driver_LED:TLC5951RHA +Driver_LED:TLC5951RTA +Driver_LED:TLC5957RTQ +Driver_LED:TLC5971PWP +Driver_LED:TLC5971RGE +Driver_LED:TLC5973 +Driver_LED:TPS61165DBV +Driver_LED:TPS92692PWP +Driver_LED:WS2811 +Driver_LED:iC-HTG +Driver_Motor:A4950E +Driver_Motor:A4950K +Driver_Motor:A4952_LY +Driver_Motor:A4953_LJ +Driver_Motor:A4954 +Driver_Motor:AMT49413 +Driver_Motor:DRV8212P +Driver_Motor:DRV8308 +Driver_Motor:DRV8412 +Driver_Motor:DRV8432 +Driver_Motor:DRV8461SPWP +Driver_Motor:DRV8662 +Driver_Motor:DRV8711 +Driver_Motor:DRV8800PWP +Driver_Motor:DRV8800RTY +Driver_Motor:DRV8801PWP +Driver_Motor:DRV8801RTY +Driver_Motor:DRV8833PW +Driver_Motor:DRV8833PWP +Driver_Motor:DRV8833RTY +Driver_Motor:DRV8837 +Driver_Motor:DRV8837C +Driver_Motor:DRV8838 +Driver_Motor:DRV8847PWP +Driver_Motor:DRV8847PWR +Driver_Motor:DRV8847RTE +Driver_Motor:DRV8847SPWR +Driver_Motor:DRV8848 +Driver_Motor:DRV8870DDA +Driver_Motor:DRV8871DDA +Driver_Motor:DRV8872DDA +Driver_Motor:EMC2301-x-ACZL +Driver_Motor:EMC2302-x-AIZL +Driver_Motor:EMC2303-x-KP +Driver_Motor:EMC2305-x-AP +Driver_Motor:L293 +Driver_Motor:L293D +Driver_Motor:L293E +Driver_Motor:L297 +Driver_Motor:L298HN +Driver_Motor:L298N +Driver_Motor:L298P +Driver_Motor:LMD18200 +Driver_Motor:MAX22201 +Driver_Motor:MAX22202 +Driver_Motor:MAX22207 +Driver_Motor:MP6536DU +Driver_Motor:PAC5527QM +Driver_Motor:PG001M +Driver_Motor:Pololu_Breakout_A4988 +Driver_Motor:Pololu_Breakout_DRV8825 +Driver_Motor:SLA7042M +Driver_Motor:SLA7044M +Driver_Motor:SLA7070MPRT +Driver_Motor:SLA7071MPRT +Driver_Motor:SLA7072MPRT +Driver_Motor:SLA7073MPRT +Driver_Motor:SLA7075MPRT +Driver_Motor:SLA7076MPRT +Driver_Motor:SLA7077MPRT +Driver_Motor:SLA7078MPRT +Driver_Motor:SN754410NE +Driver_Motor:STK672-040-E +Driver_Motor:STK672-080-E +Driver_Motor:STSPIN220 +Driver_Motor:STSPIN230 +Driver_Motor:STSPIN233 +Driver_Motor:STSPIN240 +Driver_Motor:TB6612FNG +Driver_Motor:TC78H670FTG +Driver_Motor:TMC2041-LA +Driver_Motor:TMC2100-LA +Driver_Motor:TMC2100-TA +Driver_Motor:TMC2130-LA +Driver_Motor:TMC2130-TA +Driver_Motor:TMC2160 +Driver_Motor:TMC2202-WA +Driver_Motor:TMC2208-LA +Driver_Motor:TMC2224-LA +Driver_Motor:TMC2226-SA +Driver_Motor:TMC262 +Driver_Motor:TMC2660 +Driver_Motor:TMC5130A-TA +Driver_Motor:TMC5160A-TA +Driver_Motor:VNH2SP30 +Driver_Motor:VNH5019A-E +Driver_Motor:ZXBM5210-S +Driver_Motor:ZXBM5210-SP +Driver_Relay:DRV8860 +Driver_Relay:DRV8860_PWPR +Driver_Relay:MAX4820xUP +Driver_Relay:MAX4821xUP +Driver_Relay:TPL9201_TSSOP +Driver_TEC:MAX1968xUI +Driver_TEC:MAX1969xUI +DSP_AnalogDevices:ADAU1450 +DSP_AnalogDevices:ADAU1451 +DSP_AnalogDevices:ADAU1452 +DSP_AnalogDevices:ADAU1701 +DSP_AnalogDevices:ADAU1702 +DSP_Freescale:DSP96002 +DSP_Microchip_DSPIC33:DSPIC33EP256MU810-xPT +DSP_Microchip_DSPIC33:DSPIC33FJ128GP204 +DSP_Microchip_DSPIC33:DSPIC33FJ128GP804 +DSP_Microchip_DSPIC33:DSPIC33FJ128MC204 +DSP_Microchip_DSPIC33:DSPIC33FJ128MC510A +DSP_Microchip_DSPIC33:DSPIC33FJ128MC710A +DSP_Microchip_DSPIC33:DSPIC33FJ128MC804 +DSP_Microchip_DSPIC33:DSPIC33FJ256MC510A +DSP_Microchip_DSPIC33:DSPIC33FJ256MC710A +DSP_Microchip_DSPIC33:DSPIC33FJ32GP304 +DSP_Microchip_DSPIC33:DSPIC33FJ32MC304 +DSP_Microchip_DSPIC33:DSPIC33FJ64GP204 +DSP_Microchip_DSPIC33:DSPIC33FJ64GP306A-IMR +DSP_Microchip_DSPIC33:DSPIC33FJ64GP804 +DSP_Microchip_DSPIC33:DSPIC33FJ64MC204 +DSP_Microchip_DSPIC33:DSPIC33FJ64MC510A +DSP_Microchip_DSPIC33:DSPIC33FJ64MC710A +DSP_Microchip_DSPIC33:DSPIC33FJ64MC802-xSP +DSP_Microchip_DSPIC33:DSPIC33FJ64MC804 +DSP_Motorola:DSP56301 +DSP_Texas:TMS320LF2406PZ +Fiber_Optic:AFBR-1624Z +Fiber_Optic:AFBR-2624Z +Filter:0603USB-142 +Filter:0603USB-222 +Filter:0603USB-251 +Filter:0603USB-601 +Filter:0603USB-951 +Filter:0850BM14E0016 +Filter:0900FM15K0039 +Filter:1FP41-4R +Filter:1FP42-3R +Filter:1FP44-2R +Filter:1FP45-0R +Filter:1FP45-1R +Filter:1FP61-4R +Filter:1FP62-3R +Filter:1FP64-2R +Filter:1FP65-0R +Filter:1FP65-1R +Filter:B39162B8813P810 +Filter:BNX025 +Filter:Choke_CommonMode_FerriteCore_1234 +Filter:Choke_CommonMode_FerriteCore_1243 +Filter:Choke_CommonMode_FerriteCore_1324 +Filter:Choke_CommonMode_FerriteCore_1342 +Filter:Choke_CommonMode_FerriteCore_1423 +Filter:Choke_CommonMode_PulseElectronics_PH9455x105NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x155NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x156NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x205NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x356NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x405NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x705NL +Filter:Choke_CommonMode_PulseElectronics_PH9455x826NL +Filter:Choke_Schaffner_RN102-0.3-02-12M +Filter:Choke_Schaffner_RN102-0.3-02-22M +Filter:Choke_Schaffner_RN102-0.6-02-4M4 +Filter:Choke_Schaffner_RN102-1-02-3M0 +Filter:Choke_Schaffner_RN102-1.5-02-1M6 +Filter:Choke_Schaffner_RN102-2-02-1M1 +Filter:Choke_Wurth_WE-CNSW_744232090 +Filter:FN405-0.5-02 +Filter:FN405-1-02 +Filter:FN405-10-02 +Filter:FN405-3-02 +Filter:FN405-6-02 +Filter:FN406-0.5-02 +Filter:FN406-1-02 +Filter:FN406-3-02 +Filter:FN406-6-02 +Filter:FN406-8.4-02 +Filter:FN406B-0.5-02 +Filter:FN406B-1-02 +Filter:FN406B-3-02 +Filter:FN406B-6-02 +Filter:FN406B-8.4-02 +Filter:LTC1562xG-2 +Filter:LTC1562xxG +Filter:P300PL104M275xC222 +Filter:P300PL104M275xC332 +Filter:P300PL104M275xC472 +Filter:P300PL154M275xC222 +Filter:P300PL154M275xC332 +Filter:P300PL154M275xC472 +Filter:SAFFA1G58KA0F0A +Filter:SAFFA1G96FN0F0A +Filter:SAFFA2G14FA0F0A +Filter:SAFFA881MFL0F0A +Filter:SAFFA942MFM0F0A +Filter:SAFFB1G58KA0F0A +Filter:SAFFB1G96FN0F0A +Filter:SAFFB2G14FA0F0A +Filter:SAFFB881MFL0F0A +Filter:SAFFB942MFM0F0A +Filter:SF14-1575F5UUA1 +Filter:SF14-1575F5UUC1 +FPGA_CologneChip_GateMate:CCGM1A1 +FPGA_Efinix_Trion:T8Q144xx +FPGA_Lattice:ICE40HX1K-TQ144 +FPGA_Lattice:ICE40HX4K-BG121 +FPGA_Lattice:ICE40HX4K-TQ144 +FPGA_Lattice:ICE40HX8K-BG121 +FPGA_Lattice:ICE40UL1K-SWG16 +FPGA_Lattice:ICE40UP5K-SG48ITR +FPGA_Lattice:ICE5LP1K-SG48 +FPGA_Lattice:LFE5U-85F-6BG381x +FPGA_Lattice:LFE5U-85F-6BG756x +FPGA_Lattice:LFE5U-85F-7BG381x +FPGA_Lattice:LFE5U-85F-7BG756x +FPGA_Lattice:LFE5U-85F-8BG381x +FPGA_Lattice:LFE5U-85F-8BG756x +FPGA_Lattice:LFE5UM-85F-6BG381x +FPGA_Lattice:LFE5UM-85F-6BG756x +FPGA_Lattice:LFE5UM-85F-7BG381x +FPGA_Lattice:LFE5UM-85F-7BG756x +FPGA_Lattice:LFE5UM-85F-8BG381x +FPGA_Lattice:LFE5UM-85F-8BG756x +FPGA_Lattice:LFE5UM5G-85F-8BG381x +FPGA_Lattice:LFE5UM5G-85F-8BG756x +FPGA_Lattice:LFXP2-5E-5TN144 +FPGA_Lattice:LFXP2-5E-6TN144 +FPGA_Lattice:LFXP2-5E-7TN144 +FPGA_Microsemi:A3P030-VQG100 +FPGA_Microsemi:A3P060-VQG100 +FPGA_Microsemi:A3P1000-PQG208 +FPGA_Microsemi:A3P125-PQG208 +FPGA_Microsemi:A3P125-VQG100 +FPGA_Microsemi:A3P250-PQG208 +FPGA_Microsemi:A3P250-VQG100 +FPGA_Microsemi:A3P400-PQG208 +FPGA_Microsemi:A3P600-PQG208 +FPGA_Microsemi:ACT1020PL44 +FPGA_Microsemi:ACT1020PL68 +FPGA_Microsemi:ACT1225PL84 +FPGA_Microsemi:EX128-TQ100 +FPGA_Microsemi:EX128-TQ64 +FPGA_Microsemi:EX256-TQ100 +FPGA_Microsemi:EX64-TQ100 +FPGA_Microsemi:EX64-TQ64 +FPGA_Microsemi:M2GL090T-FG484 +FPGA_Xilinx:XC2018-PC68 +FPGA_Xilinx:XC2018-PC84 +FPGA_Xilinx:XC2064-PC68 +FPGA_Xilinx:XC2C256-TQ144 +FPGA_Xilinx:XC2C256-VQ100 +FPGA_Xilinx:XC2S100TQ144 +FPGA_Xilinx:XC2S150PQ208 +FPGA_Xilinx:XC2S200PQ208 +FPGA_Xilinx:XC2S300PQ208 +FPGA_Xilinx:XC2S400FT256 +FPGA_Xilinx:XC2S50-PQ208 +FPGA_Xilinx:XC2S64A-xQFG48 +FPGA_Xilinx:XC3020-PC68 +FPGA_Xilinx:XC3030-PC44 +FPGA_Xilinx:XC3030-PC68 +FPGA_Xilinx:XC3030-PC84 +FPGA_Xilinx:XC3030-VQ100 +FPGA_Xilinx:XC3042-PC84 +FPGA_Xilinx:XC3042-VQ100 +FPGA_Xilinx:XC3S1400A-FG484 +FPGA_Xilinx:XC3S200AN-FT256 +FPGA_Xilinx:XC3S400-FG320 +FPGA_Xilinx:XC3S400-PQ208 +FPGA_Xilinx:XC3S50-VQ100 +FPGA_Xilinx:XC3S50AN-TQG144 +FPGA_Xilinx:XC4003-PC84 +FPGA_Xilinx:XC4003-VQ100 +FPGA_Xilinx:XC4004-PQ160 +FPGA_Xilinx:XC4005-PC84 +FPGA_Xilinx:XC4005-PG156 +FPGA_Xilinx:XC4005-PQ100 +FPGA_Xilinx:XC4005-PQ160 +FPGA_Xilinx:XC6SLX25T-BG484 +FPGA_Xilinx:XCV150_BG352 +FPGA_Xilinx_Artix7:XC7A100T-CSG324 +FPGA_Xilinx_Artix7:XC7A100T-FGG484 +FPGA_Xilinx_Artix7:XC7A100T-FGG676 +FPGA_Xilinx_Artix7:XC7A100T-FTG256 +FPGA_Xilinx_Artix7:XC7A15T-CPG236 +FPGA_Xilinx_Artix7:XC7A15T-CSG324 +FPGA_Xilinx_Artix7:XC7A15T-CSG325 +FPGA_Xilinx_Artix7:XC7A15T-FGG484 +FPGA_Xilinx_Artix7:XC7A15T-FTG256 +FPGA_Xilinx_Artix7:XC7A200T-FBG484 +FPGA_Xilinx_Artix7:XC7A200T-FBG676 +FPGA_Xilinx_Artix7:XC7A200T-FFG1156 +FPGA_Xilinx_Artix7:XC7A200T-SBG484 +FPGA_Xilinx_Artix7:XC7A35T-CPG236 +FPGA_Xilinx_Artix7:XC7A35T-CSG324 +FPGA_Xilinx_Artix7:XC7A35T-CSG325 +FPGA_Xilinx_Artix7:XC7A35T-FGG484 +FPGA_Xilinx_Artix7:XC7A35T-FTG256 +FPGA_Xilinx_Artix7:XC7A50T-CPG236 +FPGA_Xilinx_Artix7:XC7A50T-CSG324 +FPGA_Xilinx_Artix7:XC7A50T-CSG325 +FPGA_Xilinx_Artix7:XC7A50T-FGG484 +FPGA_Xilinx_Artix7:XC7A50T-FTG256 +FPGA_Xilinx_Artix7:XC7A75T-CSG324 +FPGA_Xilinx_Artix7:XC7A75T-FGG484 +FPGA_Xilinx_Artix7:XC7A75T-FGG676 +FPGA_Xilinx_Artix7:XC7A75T-FTG256 +FPGA_Xilinx_Kintex7:XC7K160T-FBG484 +FPGA_Xilinx_Kintex7:XC7K160T-FBG676 +FPGA_Xilinx_Kintex7:XC7K160T-FFG676 +FPGA_Xilinx_Kintex7:XC7K325T-FBG676 +FPGA_Xilinx_Kintex7:XC7K325T-FBG900 +FPGA_Xilinx_Kintex7:XC7K325T-FFG676 +FPGA_Xilinx_Kintex7:XC7K325T-FFG900 +FPGA_Xilinx_Kintex7:XC7K355T-FFG901 +FPGA_Xilinx_Kintex7:XC7K410T-FBG676 +FPGA_Xilinx_Kintex7:XC7K410T-FBG900 +FPGA_Xilinx_Kintex7:XC7K410T-FFG676 +FPGA_Xilinx_Kintex7:XC7K410T-FFG900 +FPGA_Xilinx_Kintex7:XC7K420T-FFG1156 +FPGA_Xilinx_Kintex7:XC7K420T-FFG901 +FPGA_Xilinx_Kintex7:XC7K480T-FFG1156 +FPGA_Xilinx_Kintex7:XC7K480T-FFG901 +FPGA_Xilinx_Kintex7:XC7K70T-FBG484 +FPGA_Xilinx_Kintex7:XC7K70T-FBG676 +FPGA_Xilinx_Spartan6:XC6SLX100-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX100-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX100-FGG676 +FPGA_Xilinx_Spartan6:XC6SLX100T-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX100T-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX100T-FGG676 +FPGA_Xilinx_Spartan6:XC6SLX100T-FGG900 +FPGA_Xilinx_Spartan6:XC6SLX150-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX150-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX150-FGG676 +FPGA_Xilinx_Spartan6:XC6SLX150-FGG900 +FPGA_Xilinx_Spartan6:XC6SLX150T-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX150T-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX150T-FGG676 +FPGA_Xilinx_Spartan6:XC6SLX150T-FGG900 +FPGA_Xilinx_Spartan6:XC6SLX16-CPG196 +FPGA_Xilinx_Spartan6:XC6SLX16-CSG225 +FPGA_Xilinx_Spartan6:XC6SLX16-CSG324 +FPGA_Xilinx_Spartan6:XC6SLX16-FTG256 +FPGA_Xilinx_Spartan6:XC6SLX25-CSG324 +FPGA_Xilinx_Spartan6:XC6SLX25-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX25-FTG256 +FPGA_Xilinx_Spartan6:XC6SLX25T-CSG324 +FPGA_Xilinx_Spartan6:XC6SLX25T-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX4-CPG196 +FPGA_Xilinx_Spartan6:XC6SLX4-CSG225 +FPGA_Xilinx_Spartan6:XC6SLX4-TQG144 +FPGA_Xilinx_Spartan6:XC6SLX45-CSG324 +FPGA_Xilinx_Spartan6:XC6SLX45-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX45-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX45-FGG676 +FPGA_Xilinx_Spartan6:XC6SLX45T-CSG324 +FPGA_Xilinx_Spartan6:XC6SLX45T-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX45T-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX75-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX75-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX75-FGG676 +FPGA_Xilinx_Spartan6:XC6SLX75T-CSG484 +FPGA_Xilinx_Spartan6:XC6SLX75T-FGG484 +FPGA_Xilinx_Spartan6:XC6SLX75T-FGG676 +FPGA_Xilinx_Spartan6:XC6SLX9-CPG196 +FPGA_Xilinx_Spartan6:XC6SLX9-CSG225 +FPGA_Xilinx_Spartan6:XC6SLX9-CSG324 +FPGA_Xilinx_Spartan6:XC6SLX9-FTG256 +FPGA_Xilinx_Spartan6:XC6SLX9-TQG144 +FPGA_Xilinx_Virtex5:XC5VFX100T-FF1136 +FPGA_Xilinx_Virtex5:XC5VFX100T-FF1738 +FPGA_Xilinx_Virtex5:XC5VFX130T-FF1738 +FPGA_Xilinx_Virtex5:XC5VFX200T-FF1738 +FPGA_Xilinx_Virtex5:XC5VFX30T-FF665 +FPGA_Xilinx_Virtex5:XC5VFX70T-FF1136 +FPGA_Xilinx_Virtex5:XC5VFX70T-FF665 +FPGA_Xilinx_Virtex5:XC5VLX110-FF1153 +FPGA_Xilinx_Virtex5:XC5VLX110-FF1760 +FPGA_Xilinx_Virtex5:XC5VLX110-FF676 +FPGA_Xilinx_Virtex5:XC5VLX110T-FF1136 +FPGA_Xilinx_Virtex5:XC5VLX110T-FF1738 +FPGA_Xilinx_Virtex5:XC5VLX155-FF1153 +FPGA_Xilinx_Virtex5:XC5VLX155-FF1760 +FPGA_Xilinx_Virtex5:XC5VLX155T-FF1136 +FPGA_Xilinx_Virtex5:XC5VLX155T-FF1738 +FPGA_Xilinx_Virtex5:XC5VLX20T-FF323 +FPGA_Xilinx_Virtex5:XC5VLX220-FF1760 +FPGA_Xilinx_Virtex5:XC5VLX220T-FF1738 +FPGA_Xilinx_Virtex5:XC5VLX30-FF324 +FPGA_Xilinx_Virtex5:XC5VLX30-FF676 +FPGA_Xilinx_Virtex5:XC5VLX30T-FF323 +FPGA_Xilinx_Virtex5:XC5VLX30T-FF665 +FPGA_Xilinx_Virtex5:XC5VLX330-FF1760 +FPGA_Xilinx_Virtex5:XC5VLX330T-FF1738 +FPGA_Xilinx_Virtex5:XC5VLX50-FF1153 +FPGA_Xilinx_Virtex5:XC5VLX50-FF324 +FPGA_Xilinx_Virtex5:XC5VLX50-FF676 +FPGA_Xilinx_Virtex5:XC5VLX50T-FF1136 +FPGA_Xilinx_Virtex5:XC5VLX50T-FF665 +FPGA_Xilinx_Virtex5:XC5VLX85-FF1153 +FPGA_Xilinx_Virtex5:XC5VLX85-FF676 +FPGA_Xilinx_Virtex5:XC5VLX85T-FF1136 +FPGA_Xilinx_Virtex5:XC5VSX240T-FF1738 +FPGA_Xilinx_Virtex5:XC5VSX35T-FF665 +FPGA_Xilinx_Virtex5:XC5VSX50T-FF1136 +FPGA_Xilinx_Virtex5:XC5VSX50T-FF665 +FPGA_Xilinx_Virtex5:XC5VSX95T-FF1136 +FPGA_Xilinx_Virtex5:XC5VTX150T-FF1156 +FPGA_Xilinx_Virtex5:XC5VTX150T-FF1759 +FPGA_Xilinx_Virtex5:XC5VTX240T-FF1759 +FPGA_Xilinx_Virtex6:XC6VHX250T-FF1154 +FPGA_Xilinx_Virtex6:XC6VHX255T-FF1155 +FPGA_Xilinx_Virtex6:XC6VHX255T-FF1923 +FPGA_Xilinx_Virtex6:XC6VHX380T-FF1154 +FPGA_Xilinx_Virtex6:XC6VHX380T-FF1155 +FPGA_Xilinx_Virtex6:XC6VHX380T-FF1923 +FPGA_Xilinx_Virtex6:XC6VHX380T-FF1924 +FPGA_Xilinx_Virtex6:XC6VHX565T-FF1923 +FPGA_Xilinx_Virtex6:XC6VHX565T-FF1924 +FPGA_Xilinx_Virtex6:XC6VLX130T-FF1156 +FPGA_Xilinx_Virtex6:XC6VLX130T-FF484 +FPGA_Xilinx_Virtex6:XC6VLX130T-FF784 +FPGA_Xilinx_Virtex6:XC6VLX195T-FF1156 +FPGA_Xilinx_Virtex6:XC6VLX195T-FF784 +FPGA_Xilinx_Virtex6:XC6VLX240T-FF1156 +FPGA_Xilinx_Virtex6:XC6VLX240T-FF1759 +FPGA_Xilinx_Virtex6:XC6VLX240T-FF784 +FPGA_Xilinx_Virtex6:XC6VLX365T-FF1156 +FPGA_Xilinx_Virtex6:XC6VLX365T-FF1759 +FPGA_Xilinx_Virtex6:XC6VLX550T-FF1759 +FPGA_Xilinx_Virtex6:XC6VLX550T-FF1760 +FPGA_Xilinx_Virtex6:XC6VLX75T-FF484 +FPGA_Xilinx_Virtex6:XC6VLX75T-FF784 +FPGA_Xilinx_Virtex6:XC6VLX760-FF1760 +FPGA_Xilinx_Virtex6:XC6VSX315T-FF1156 +FPGA_Xilinx_Virtex6:XC6VSX315T-FF1759 +FPGA_Xilinx_Virtex6:XC6VSX475T-FF1156 +FPGA_Xilinx_Virtex6:XC6VSX475T-FF1759 +FPGA_Xilinx_Virtex7:XC7V2000T-FHG1761 +FPGA_Xilinx_Virtex7:XC7V2000T-FLG1925 +FPGA_Xilinx_Virtex7:XC7V585T-FFG1157 +FPGA_Xilinx_Virtex7:XC7V585T-FFG1761 +FPGA_Xilinx_Virtex7:XC7VH580T-FLG1155 +FPGA_Xilinx_Virtex7:XC7VH580T-FLG1931 +FPGA_Xilinx_Virtex7:XC7VH580T-HCG1155 +FPGA_Xilinx_Virtex7:XC7VH580T-HCG1931 +FPGA_Xilinx_Virtex7:XC7VH870T-FLG1932 +FPGA_Xilinx_Virtex7:XC7VH870T-HCG1932 +FPGA_Xilinx_Virtex7:XC7VX1140T-FLG1926 +FPGA_Xilinx_Virtex7:XC7VX1140T-FLG1928 +FPGA_Xilinx_Virtex7:XC7VX1140T-FLG1930 +FPGA_Xilinx_Virtex7:XC7VX330T-FFG1157 +FPGA_Xilinx_Virtex7:XC7VX330T-FFG1761 +FPGA_Xilinx_Virtex7:XC7VX415T-FFG1157 +FPGA_Xilinx_Virtex7:XC7VX415T-FFG1158 +FPGA_Xilinx_Virtex7:XC7VX415T-FFG1927 +FPGA_Xilinx_Virtex7:XC7VX485T-FFG1157 +FPGA_Xilinx_Virtex7:XC7VX485T-FFG1158 +FPGA_Xilinx_Virtex7:XC7VX485T-FFG1761 +FPGA_Xilinx_Virtex7:XC7VX485T-FFG1927 +FPGA_Xilinx_Virtex7:XC7VX485T-FFG1930 +FPGA_Xilinx_Virtex7:XC7VX550T-FFG1158 +FPGA_Xilinx_Virtex7:XC7VX550T-FFG1927 +FPGA_Xilinx_Virtex7:XC7VX690T-FFG1157 +FPGA_Xilinx_Virtex7:XC7VX690T-FFG1158 +FPGA_Xilinx_Virtex7:XC7VX690T-FFG1761 +FPGA_Xilinx_Virtex7:XC7VX690T-FFG1926 +FPGA_Xilinx_Virtex7:XC7VX690T-FFG1927 +FPGA_Xilinx_Virtex7:XC7VX690T-FFG1930 +FPGA_Xilinx_Virtex7:XC7VX980T-FFG1926 +FPGA_Xilinx_Virtex7:XC7VX980T-FFG1928 +FPGA_Xilinx_Virtex7:XC7VX980T-FFG1930 +GPU:MC6845 +GPU:MC68A45 +GPU:MC68B45 +Graphic:Logo_Open_Hardware_Large +Graphic:Logo_Open_Hardware_Small +Graphic:SYM_Arrow45_Large +Graphic:SYM_Arrow45_Normal +Graphic:SYM_Arrow45_Small +Graphic:SYM_Arrow45_Tiny +Graphic:SYM_Arrow45_XLarge +Graphic:SYM_Arrow_Large +Graphic:SYM_Arrow_Normal +Graphic:SYM_Arrow_Small +Graphic:SYM_Arrow_Tiny +Graphic:SYM_Arrow_XLarge +Graphic:SYM_ESD_Large +Graphic:SYM_ESD_Small +Graphic:SYM_Earth_Protective_Large +Graphic:SYM_Earth_Protective_Small +Graphic:SYM_EasterEgg_42x60mm +Graphic:SYM_Flash_Large +Graphic:SYM_Flash_Small +Graphic:SYM_Flash_XLarge +Graphic:SYM_Hot_Large +Graphic:SYM_Hot_Small +Graphic:SYM_LASER_Large +Graphic:SYM_LASER_Small +Graphic:SYM_Magnet_Large +Graphic:SYM_Magnet_Small +Graphic:SYM_Radio_Waves_Large +Graphic:SYM_Radio_Waves_Small +Graphic:SYM_Radioactive_Large +Graphic:SYM_Radioactive_Radiation_Small +Interface:5PB1108PGxx +Interface:6821 +Interface:6822 +Interface:68230 +Interface:68681 +Interface:68901_PLCC +Interface:8237 +Interface:8255 +Interface:8255A +Interface:8259 +Interface:8259A +Interface:8259A-2 +Interface:8288 +Interface:82C55A +Interface:82C55A_PLCC +Interface:88SE9125C0-NAA +Interface:AD9833xRM +Interface:AD9834 +Interface:AD9850 +Interface:AD9851 +Interface:AD9910 +Interface:AD9912 +Interface:AD9951 +Interface:AD9954 +Interface:AM26LS31CD +Interface:AM26LS31CDB +Interface:AM26LS31CN +Interface:AM26LS31MJ +Interface:AM26LS31xNS +Interface:AM26LV32xD +Interface:AM26LV32xNS +Interface:CDCLVP1102RGT +Interface:CH376T +Interface:DS90C124 +Interface:DS90C241 +Interface:DS90C402 +Interface:DS90LV011A +Interface:DS90LV027A +Interface:FD1771 +Interface:FIN1019M +Interface:FIN1019MTC +Interface:HT12D +Interface:HT12E +Interface:LTC1518 +Interface:LTC1519 +Interface:LTC1688 +Interface:LTC1689 +Interface:LTC6957xDD-1 +Interface:LTC6957xDD-2 +Interface:LTC6957xDD-3 +Interface:LTC6957xDD-4 +Interface:LTC6957xMS-1 +Interface:LTC6957xMS-2 +Interface:LTC6957xMS-3 +Interface:LTC6957xMS-4 +Interface:MAX6816 +Interface:MC100EPT22D +Interface:MC100EPT22DT +Interface:MC100LVELT22D +Interface:MC100LVELT22DT +Interface:MC6840 +Interface:MC6843 +Interface:MC6844 +Interface:MC68A21 +Interface:MC68A40 +Interface:MC68A44 +Interface:MC68B21 +Interface:MC68B40 +Interface:MC68B44 +Interface:NB3N551MN +Interface:ONET1191PRGT +Interface:PCA9306 +Interface:PCA9306D +Interface:PCA9306DC +Interface:PCA9306DC1 +Interface:PCA9306DP +Interface:PCA9600D +Interface:PCA9600DP +Interface:PCA9615DP +Interface:PCI9030-PQFP176 +Interface:S5933_PQ160 +Interface:SI9986 +Interface:SLB9660xT +Interface:SLB9665xT +Interface:SN65LVDS047D +Interface:SN65LVDS047PW +Interface:SN65LVDS1D +Interface:SN65LVDS1DBV +Interface:SN65LVDS2D +Interface:SN65LVDS2DBV +Interface:SN65LVDT2D +Interface:SN65LVDT2DBV +Interface:SN74LV8153N +Interface:SN74LV8153PW +Interface:SN75160BDW +Interface:SN75160BN +Interface:TB5D1MD +Interface:TB5D1MDW +Interface:TB5D2H +Interface:TB5D2HDW +Interface:TB5R1D +Interface:TB5R1DW +Interface:TB5R2D +Interface:TB5R2DW +Interface:TCA9406DC +Interface:TCA9800 +Interface:TCA9801 +Interface:TCA9802 +Interface:TCA9803 +Interface:U2270B +Interface:WD2791 +Interface:WD2793 +Interface:WD2795 +Interface:WD2797 +Interface:Z8420 +Interface:Z84C20 +Interface_CAN_LIN:ADM3053 +Interface_CAN_LIN:ADM3057ExRW +Interface_CAN_LIN:CA-IF1042LVS +Interface_CAN_LIN:ISO1044BD +Interface_CAN_LIN:ISO1050DUB +Interface_CAN_LIN:ISOW1044 +Interface_CAN_LIN:LTC2875-DD +Interface_CAN_LIN:LTC2875-S8 +Interface_CAN_LIN:MCP2021A-xxxxMD +Interface_CAN_LIN:MCP2021A-xxxxP +Interface_CAN_LIN:MCP2021A-xxxxSN +Interface_CAN_LIN:MCP2022A-xxxxP +Interface_CAN_LIN:MCP2022A-xxxxSL +Interface_CAN_LIN:MCP2022A-xxxxST +Interface_CAN_LIN:MCP2050-330-EMQ +Interface_CAN_LIN:MCP2050-330-EP +Interface_CAN_LIN:MCP2050-330-ESL +Interface_CAN_LIN:MCP2050-500-EMQ +Interface_CAN_LIN:MCP2050-500-EP +Interface_CAN_LIN:MCP2050-500-ESL +Interface_CAN_LIN:MCP2515-xSO +Interface_CAN_LIN:MCP2515-xST +Interface_CAN_LIN:MCP2517FD-xJHA +Interface_CAN_LIN:MCP2517FD-xSL +Interface_CAN_LIN:MCP251863T-E-9PX +Interface_CAN_LIN:MCP251863T-H-SS +Interface_CAN_LIN:MCP2518FD-xQBB +Interface_CAN_LIN:MCP2542FDxMF +Interface_CAN_LIN:MCP2542WFDxMF +Interface_CAN_LIN:MCP2551-I-P +Interface_CAN_LIN:MCP2551-I-SN +Interface_CAN_LIN:MCP2557FD-xMF +Interface_CAN_LIN:MCP2557FD-xMNY +Interface_CAN_LIN:MCP2557FD-xSN +Interface_CAN_LIN:MCP2558FD-xMF +Interface_CAN_LIN:MCP2558FD-xMNY +Interface_CAN_LIN:MCP2558FD-xSN +Interface_CAN_LIN:MCP2561-E-MF +Interface_CAN_LIN:MCP2561-E-P +Interface_CAN_LIN:MCP2561-E-SN +Interface_CAN_LIN:MCP2561-H-MF +Interface_CAN_LIN:MCP2561-H-P +Interface_CAN_LIN:MCP2561-H-SN +Interface_CAN_LIN:MCP2562-E-MF +Interface_CAN_LIN:MCP2562-E-P +Interface_CAN_LIN:MCP2562-E-SN +Interface_CAN_LIN:MCP2562-H-MF +Interface_CAN_LIN:MCP2562-H-P +Interface_CAN_LIN:MCP2562-H-SN +Interface_CAN_LIN:MCP25625-x-SS +Interface_CAN_LIN:PCA82C251 +Interface_CAN_LIN:SN65HVD1050D +Interface_CAN_LIN:SN65HVD230 +Interface_CAN_LIN:SN65HVD231 +Interface_CAN_LIN:SN65HVD232 +Interface_CAN_LIN:SN65HVD233 +Interface_CAN_LIN:SN65HVD234 +Interface_CAN_LIN:SN65HVD235 +Interface_CAN_LIN:SN65HVD255D +Interface_CAN_LIN:SN65HVD256D +Interface_CAN_LIN:SN65HVD257D +Interface_CAN_LIN:TCAN1043xDxQ1 +Interface_CAN_LIN:TCAN330 +Interface_CAN_LIN:TCAN330G +Interface_CAN_LIN:TCAN332 +Interface_CAN_LIN:TCAN332G +Interface_CAN_LIN:TCAN334 +Interface_CAN_LIN:TCAN334G +Interface_CAN_LIN:TCAN337 +Interface_CAN_LIN:TCAN337G +Interface_CAN_LIN:TCAN4550RGY +Interface_CAN_LIN:TCAN4551RGYRQ1 +Interface_CAN_LIN:TJA1021T +Interface_CAN_LIN:TJA1021TK +Interface_CAN_LIN:TJA1029T +Interface_CAN_LIN:TJA1029TK +Interface_CAN_LIN:TJA1042T +Interface_CAN_LIN:TJA1042T-3 +Interface_CAN_LIN:TJA1042TK-3 +Interface_CAN_LIN:TJA1043T +Interface_CAN_LIN:TJA1043TK +Interface_CAN_LIN:TJA1049T +Interface_CAN_LIN:TJA1049T-3 +Interface_CAN_LIN:TJA1049TK +Interface_CAN_LIN:TJA1049TK-3 +Interface_CAN_LIN:TJA1051T +Interface_CAN_LIN:TJA1051T-3 +Interface_CAN_LIN:TJA1051T-E +Interface_CAN_LIN:TJA1051TK-3 +Interface_CAN_LIN:TJA1052i-1 +Interface_CAN_LIN:TJA1052i-2 +Interface_CAN_LIN:TJA1052i-5 +Interface_CAN_LIN:TJA1145T +Interface_CAN_LIN:TJA1145T-FD +Interface_CAN_LIN:TJA1145TK +Interface_CAN_LIN:TJA1145TK-FD +Interface_CurrentLoop:XTR111AxDGQ +Interface_CurrentLoop:XTR115U +Interface_CurrentLoop:XTR116U +Interface_Ethernet:DP83848C +Interface_Ethernet:DP83848I +Interface_Ethernet:ENC28J60x-ML +Interface_Ethernet:ENC28J60x-SO +Interface_Ethernet:ENC28J60x-SP +Interface_Ethernet:ENC28J60x-SS +Interface_Ethernet:ENC424J600-ML +Interface_Ethernet:ENC424J600-PT +Interface_Ethernet:KSZ8081MLX +Interface_Ethernet:KSZ8081RNA +Interface_Ethernet:KSZ8081RND +Interface_Ethernet:KSZ9031RNXCA +Interface_Ethernet:KSZ9563RNX +Interface_Ethernet:KSZ9893RNX +Interface_Ethernet:LAN7500-ABJZ +Interface_Ethernet:LAN8710A +Interface_Ethernet:LAN8720A +Interface_Ethernet:LAN8742A +Interface_Ethernet:LAN9303 +Interface_Ethernet:LAN9303i +Interface_Ethernet:LAN9512 +Interface_Ethernet:LAN9512i +Interface_Ethernet:LAN9513 +Interface_Ethernet:LAN9513i +Interface_Ethernet:LAN9514 +Interface_Ethernet:LAN9514i +Interface_Ethernet:RTL8211EG-VB-CG +Interface_Ethernet:VSC8541XMV-0x +Interface_Ethernet:W5100 +Interface_Ethernet:W5100S-L +Interface_Ethernet:W5100S-Q +Interface_Ethernet:W5500 +Interface_Ethernet:W6100-L +Interface_Ethernet:W6100-Q +Interface_Ethernet:WGI210AT +Interface_Expansion:AS1115-BQFT +Interface_Expansion:AS1115-BSST +Interface_Expansion:AW9523B +Interface_Expansion:LTC4314xGN +Interface_Expansion:LTC4314xUDC +Interface_Expansion:LTC4316xDD +Interface_Expansion:LTC4317 +Interface_Expansion:MAX31910xUI +Interface_Expansion:MAX31911xUI +Interface_Expansion:MAX31912xUI +Interface_Expansion:MAX31913xUI +Interface_Expansion:MAX7325AEG+ +Interface_Expansion:MCP23008-xML +Interface_Expansion:MCP23008-xP +Interface_Expansion:MCP23008-xSO +Interface_Expansion:MCP23008-xSS +Interface_Expansion:MCP23017_ML +Interface_Expansion:MCP23017_SO +Interface_Expansion:MCP23017_SP +Interface_Expansion:MCP23017_SS +Interface_Expansion:MCP23S17_ML +Interface_Expansion:MCP23S17_SO +Interface_Expansion:MCP23S17_SP +Interface_Expansion:MCP23S17_SS +Interface_Expansion:P82B96 +Interface_Expansion:PCA9506BS +Interface_Expansion:PCA9516 +Interface_Expansion:PCA9536D +Interface_Expansion:PCA9536DP +Interface_Expansion:PCA9537 +Interface_Expansion:PCA9544AD +Interface_Expansion:PCA9544APW +Interface_Expansion:PCA9547BS +Interface_Expansion:PCA9547D +Interface_Expansion:PCA9547PW +Interface_Expansion:PCA9548ADB +Interface_Expansion:PCA9548ADW +Interface_Expansion:PCA9548APW +Interface_Expansion:PCA9548ARGE +Interface_Expansion:PCA9555D +Interface_Expansion:PCA9555DB +Interface_Expansion:PCA9555PW +Interface_Expansion:PCA9557BS +Interface_Expansion:PCA9557D +Interface_Expansion:PCA9557PW +Interface_Expansion:PCA9847PW +Interface_Expansion:PCAL6416AHF +Interface_Expansion:PCAL6416APW +Interface_Expansion:PCAL6534EV +Interface_Expansion:PCF8574AP +Interface_Expansion:PCF8574AT +Interface_Expansion:PCF8574ATS +Interface_Expansion:PCF8574P +Interface_Expansion:PCF8574T +Interface_Expansion:PCF8574TS +Interface_Expansion:PCF8575DBR +Interface_Expansion:PCF8584 +Interface_Expansion:PCF8591 +Interface_Expansion:STMPE1600 +Interface_Expansion:TCA9534 +Interface_Expansion:TCA9535DBR +Interface_Expansion:TCA9535DBT +Interface_Expansion:TCA9535MRGER +Interface_Expansion:TCA9535PWR +Interface_Expansion:TCA9535RGER +Interface_Expansion:TCA9535RTWR +Interface_Expansion:TCA9544A +Interface_Expansion:TCA9548AMRGER +Interface_Expansion:TCA9548APWR +Interface_Expansion:TCA9548ARGER +Interface_Expansion:TCA9554DB +Interface_Expansion:TCA9554DBQ +Interface_Expansion:TCA9554DW +Interface_Expansion:TCA9554PW +Interface_Expansion:TCA9555DBR +Interface_Expansion:TCA9555DBT +Interface_Expansion:TCA9555PWR +Interface_Expansion:TCA9555RGER +Interface_Expansion:TCA9555RTWR +Interface_Expansion:TPIC6595 +Interface_Expansion:XRA1201IG24 +Interface_Expansion:XRA1201IL24 +Interface_Expansion:XRA1201PIG24 +Interface_Expansion:XRA1201PIL24 +Interface_HDMI:ADV7611 +Interface_HDMI:TPD12S520DBT +Interface_HID:JoyWarrior24A10L +Interface_HID:JoyWarrior24A8L +Interface_HID:SpinWarrior24A3 +Interface_HID:SpinWarrior24R4 +Interface_HID:SpinWarrior24R6 +Interface_LineDriver:DS7820 +Interface_LineDriver:DS7830 +Interface_LineDriver:DS8830 +Interface_LineDriver:DS89C21 +Interface_LineDriver:EL7242C +Interface_LineDriver:MC3486N +Interface_LineDriver:MC3487DX +Interface_LineDriver:MC3487N +Interface_LineDriver:UA9637 +Interface_LineDriver:UA9638CD +Interface_LineDriver:UA9638CDE4 +Interface_LineDriver:UA9638CDG4 +Interface_LineDriver:UA9638CDR +Interface_LineDriver:UA9638CDRG4 +Interface_LineDriver:UA9638CP +Interface_LineDriver:UA9638CPE4 +Interface_Optical:IRM-H6xxT +Interface_Optical:IS471F +Interface_Optical:IS485 +Interface_Optical:IS486 +Interface_Optical:QSE159 +Interface_Optical:SFP +Interface_Optical:SFP+ +Interface_Optical:TSDP341xx +Interface_Optical:TSDP343xx +Interface_Optical:TSMP58000 +Interface_Optical:TSMP58138 +Interface_Optical:TSOP17xx +Interface_Optical:TSOP21xx +Interface_Optical:TSOP23xx +Interface_Optical:TSOP25xx +Interface_Optical:TSOP312xx +Interface_Optical:TSOP314xx +Interface_Optical:TSOP321xx +Interface_Optical:TSOP323xx +Interface_Optical:TSOP325xx +Interface_Optical:TSOP32S40F +Interface_Optical:TSOP331xx +Interface_Optical:TSOP333xx +Interface_Optical:TSOP335xx +Interface_Optical:TSOP341xx +Interface_Optical:TSOP343xx +Interface_Optical:TSOP345xx +Interface_Optical:TSOP348xx +Interface_Optical:TSOP34S40F +Interface_Optical:TSOP382xx +Interface_Optical:TSOP384xx +Interface_Optical:TSOP38G36 +Interface_Optical:TSOP41xx +Interface_Optical:TSOP43xx +Interface_Optical:TSOP45xx +Interface_Optical:TSOP531xx +Interface_Optical:TSOP533xx +Interface_Optical:TSOP535xx +Interface_Optical:TSOP581xx +Interface_Optical:TSOP582xx +Interface_Optical:TSOP583xx +Interface_Optical:TSOP584xx +Interface_Optical:TSOP585xx +Interface_Telecom:FX614 +Interface_Telecom:HT9170D +Interface_Telecom:Si3210 +Interface_UART:16450 +Interface_UART:16550 +Interface_UART:68C681 +Interface_UART:8250 +Interface_UART:8252 +Interface_UART:ADM101E +Interface_UART:ADM1491EBR +Interface_UART:ADM205 +Interface_UART:ADM206 +Interface_UART:ADM207 +Interface_UART:ADM208 +Interface_UART:ADM209 +Interface_UART:ADM211 +Interface_UART:ADM213 +Interface_UART:ADM222 +Interface_UART:ADM232A +Interface_UART:ADM242 +Interface_UART:ADM2481xRW +Interface_UART:ADM2483xRW +Interface_UART:ADM2484E +Interface_UART:ADM2582E +Interface_UART:ADM2587E +Interface_UART:ADM2682E +Interface_UART:ADM2687E +Interface_UART:ADM3488ExR +Interface_UART:ADM3490ExR +Interface_UART:ADM3491ExR +Interface_UART:AZ75232G +Interface_UART:AZ75232GS +Interface_UART:AZ75232M +Interface_UART:GD65232DB +Interface_UART:GD65232DW +Interface_UART:GD65232PW +Interface_UART:GD75232DB +Interface_UART:GD75232DW +Interface_UART:GD75232N +Interface_UART:GD75232PW +Interface_UART:ICL3232 +Interface_UART:ISL3172E +Interface_UART:ISL3175E +Interface_UART:ISL3178E +Interface_UART:ISL3280ExHZ +Interface_UART:ISL3281ExHZ +Interface_UART:ISL3282ExRHZ +Interface_UART:ISL3283ExHZ +Interface_UART:ISL3284ExHZ +Interface_UART:ISL3295xxH +Interface_UART:ISL3298xxRT +Interface_UART:ISL83491 +Interface_UART:ISO1500 +Interface_UART:ISO3082DW +Interface_UART:ISO3088DW +Interface_UART:LT1080 +Interface_UART:LT1785AxN8 +Interface_UART:LT1785AxS8 +Interface_UART:LT1785xN8 +Interface_UART:LT1785xS8 +Interface_UART:LT1791AxN8 +Interface_UART:LT1791AxS +Interface_UART:LT1791xN8 +Interface_UART:LT1791xS +Interface_UART:LTC2850xDD +Interface_UART:LTC2850xMS8 +Interface_UART:LTC2850xS8 +Interface_UART:LTC2851xDD +Interface_UART:LTC2851xMS8 +Interface_UART:LTC2851xS8 +Interface_UART:LTC2852xDD +Interface_UART:LTC2852xMS +Interface_UART:LTC2852xS +Interface_UART:LTC2856xDD-1 +Interface_UART:LTC2856xDD-2 +Interface_UART:LTC2856xMS8-1 +Interface_UART:LTC2856xMS8-2 +Interface_UART:LTC2857xDD-1 +Interface_UART:LTC2857xDD-2 +Interface_UART:LTC2857xMS8-1 +Interface_UART:LTC2857xMS8-2 +Interface_UART:LTC2858xDD-1 +Interface_UART:LTC2858xDD-2 +Interface_UART:LTC2858xMS-1 +Interface_UART:LTC2858xMS-2 +Interface_UART:LTC2861 +Interface_UART:MAX13432EESD +Interface_UART:MAX13432EETD +Interface_UART:MAX13433EESD +Interface_UART:MAX13433EETD +Interface_UART:MAX14783ExS +Interface_UART:MAX14830 +Interface_UART:MAX1487E +Interface_UART:MAX202 +Interface_UART:MAX232 +Interface_UART:MAX232I +Interface_UART:MAX238xNG+ +Interface_UART:MAX238xWG+ +Interface_UART:MAX3051 +Interface_UART:MAX3072E +Interface_UART:MAX3075E +Interface_UART:MAX3078E +Interface_UART:MAX3218 +Interface_UART:MAX3221 +Interface_UART:MAX3226 +Interface_UART:MAX3227 +Interface_UART:MAX3232 +Interface_UART:MAX3284E +Interface_UART:MAX3483 +Interface_UART:MAX3485 +Interface_UART:MAX3486 +Interface_UART:MAX3488xPA +Interface_UART:MAX3488xSA +Interface_UART:MAX3490xPA +Interface_UART:MAX3490xSA +Interface_UART:MAX481E +Interface_UART:MAX483E +Interface_UART:MAX485E +Interface_UART:MAX487E +Interface_UART:MAX488E +Interface_UART:MAX489E +Interface_UART:MAX490E +Interface_UART:MAX491E +Interface_UART:MC6850 +Interface_UART:MC68A50 +Interface_UART:MC68B50 +Interface_UART:SC16IS740 +Interface_UART:SC16IS750xBS +Interface_UART:SC16IS750xPW +Interface_UART:SC16IS752IBS +Interface_UART:SC16IS752IPW +Interface_UART:SC16IS760xBS +Interface_UART:SC16IS760xPW +Interface_UART:SC16IS762IBS +Interface_UART:SC16IS762IPW +Interface_UART:SN65HVD11HD +Interface_UART:SN65LBC176D +Interface_UART:SN65LBC176P +Interface_UART:SN65LBC176QD +Interface_UART:SN65LBC176QDR +Interface_UART:SN75176AD +Interface_UART:SN75176AP +Interface_UART:SN75LBC176D +Interface_UART:SN75LBC176P +Interface_UART:SNJ55LBC176JG +Interface_UART:SP3481CN +Interface_UART:SP3481CP +Interface_UART:SP3481EN +Interface_UART:SP3481EP +Interface_UART:SP3485CN +Interface_UART:SP3485CP +Interface_UART:SP3485EN +Interface_UART:SP3485EP +Interface_UART:SSP3085 +Interface_UART:ST202ExD +Interface_UART:ST232ExD +Interface_UART:ST485E +Interface_UART:THVD1400D +Interface_UART:THVD1420D +Interface_UART:THVD1450D +Interface_UART:THVD1450DR +Interface_UART:THVD1451D +Interface_UART:THVD1500 +Interface_UART:THVD8000 +Interface_UART:Z8530 +Interface_USB:ADUM3160 +Interface_USB:ADUM4160 +Interface_USB:AP33771 +Interface_USB:BQ24392 +Interface_USB:CH224K +Interface_USB:CH236D +Interface_USB:CH246D +Interface_USB:CH330N +Interface_USB:CH334R +Interface_USB:CH340C +Interface_USB:CH340E +Interface_USB:CH340G +Interface_USB:CH340K +Interface_USB:CH340N +Interface_USB:CH340T +Interface_USB:CH340X +Interface_USB:CH343G +Interface_USB:CH343P +Interface_USB:CH344Q +Interface_USB:CH9102F +Interface_USB:CP2102N-Axx-xQFN20 +Interface_USB:CP2102N-Axx-xQFN24 +Interface_USB:CP2102N-Axx-xQFN28 +Interface_USB:CP2104 +Interface_USB:CP2108-xxx-xM +Interface_USB:CP2112 +Interface_USB:CP2615-xx-xM +Interface_USB:CY7C65211-24LTXI +Interface_USB:CY7C65211A-24LTXI +Interface_USB:CY7C65213-28PVXI +Interface_USB:CY7C65213-32LTXI +Interface_USB:CY7C65213A-28PVXI +Interface_USB:CY7C65213A-32LTXI +Interface_USB:CY7C65215-32LTXI +Interface_USB:CY7C65215A-32LTXI +Interface_USB:CYPD3171-24LQXQ +Interface_USB:CYPD3174-16SXQ +Interface_USB:CYPD3174-24LQXQ +Interface_USB:CYPD3175-24LQXQ +Interface_USB:CYPD3177-24LQ +Interface_USB:FE1.1s +Interface_USB:FSUSB30MUX +Interface_USB:FSUSB42MUX +Interface_USB:FT200XD +Interface_USB:FT201XQ +Interface_USB:FT201XS +Interface_USB:FT220XQ +Interface_USB:FT220XS +Interface_USB:FT221XQ +Interface_USB:FT221XS +Interface_USB:FT2232D +Interface_USB:FT2232HL +Interface_USB:FT2232HQ +Interface_USB:FT230XQ +Interface_USB:FT230XS +Interface_USB:FT231XQ +Interface_USB:FT231XS +Interface_USB:FT232BM +Interface_USB:FT232H +Interface_USB:FT232RL +Interface_USB:FT234XD +Interface_USB:FT240XQ +Interface_USB:FT240XS +Interface_USB:FT245BM +Interface_USB:FT4222HQ +Interface_USB:FT4232H +Interface_USB:FT601Q +Interface_USB:FUSB302B01MPX +Interface_USB:FUSB302B10MPX +Interface_USB:FUSB302B11MPX +Interface_USB:FUSB302BMPX +Interface_USB:FUSB303BTMX +Interface_USB:FUSB307BMPX +Interface_USB:IP2721 +Interface_USB:MA8601 +Interface_USB:MCP2200-E-SS +Interface_USB:MCP2200-I-MQ +Interface_USB:MCP2200-I-SO +Interface_USB:MCP2200-I-SS +Interface_USB:MCP2200T-E-SS +Interface_USB:MCP2200T-I-MQ +Interface_USB:MCP2200T-I-SO +Interface_USB:MCP2200T-I-SS +Interface_USB:MCP2210x-MQ +Interface_USB:MCP2210x-SO +Interface_USB:MCP2210x-SS +Interface_USB:MCP2221AxML +Interface_USB:MCP2221AxP +Interface_USB:MCP2221AxSL +Interface_USB:MCP2221AxST +Interface_USB:MP5034GJ +Interface_USB:STULPI01A +Interface_USB:STULPI01B +Interface_USB:STUSB4500QTR +Interface_USB:TPS2500DRC +Interface_USB:TPS2501DRC +Interface_USB:TPS2513 +Interface_USB:TPS2513A +Interface_USB:TPS2514 +Interface_USB:TPS2514A +Interface_USB:TPS2560 +Interface_USB:TPS2561 +Interface_USB:TPS25730D +Interface_USB:TS3USB30EDGSR +Interface_USB:TS3USB30ERSWR +Interface_USB:TS3USBCA410 +Interface_USB:TS3USBCA420 +Interface_USB:TUSB2036 +Interface_USB:TUSB320 +Interface_USB:TUSB320I +Interface_USB:TUSB321 +Interface_USB:TUSB322I +Interface_USB:TUSB4041I +Interface_USB:TUSB7340 +Interface_USB:TUSB8041 +Interface_USB:UPD720202K8-7x1-BAA +Interface_USB:USB2504 +Interface_USB:USB2514B_Bi +Interface_USB:USB3250-ABZJ +Interface_USB:USB3300-EZK +Interface_USB:USB3341 +Interface_USB:USB3343 +Interface_USB:USB3346 +Interface_USB:USB3347 +Interface_USB:USB3740B-AI2 +Interface_USB:USB3740B-AI9 +Interface_USB:XR21B1424 +Isolator:4N25 +Isolator:4N26 +Isolator:4N27 +Isolator:4N28 +Isolator:4N35 +Isolator:4N36 +Isolator:4N37 +Isolator:6N135 +Isolator:6N135S +Isolator:6N136 +Isolator:6N136S +Isolator:6N137 +Isolator:6N138 +Isolator:6N139 +Isolator:ACPL-214-500E +Isolator:ADN4650 +Isolator:ADN4651 +Isolator:ADN4652 +Isolator:ADuM1200AR +Isolator:ADuM1200BR +Isolator:ADuM1200CR +Isolator:ADuM1200WS +Isolator:ADuM1200WT +Isolator:ADuM1200WU +Isolator:ADuM1201AR +Isolator:ADuM1201BR +Isolator:ADuM1201CR +Isolator:ADuM1201WS +Isolator:ADuM1201WT +Isolator:ADuM1201WU +Isolator:ADuM120N +Isolator:ADuM121N +Isolator:ADuM1250 +Isolator:ADuM1281 +Isolator:ADuM1300xRW +Isolator:ADuM1400xRW +Isolator:ADuM1401xRW +Isolator:ADuM1402xRW +Isolator:ADuM1410 +Isolator:ADuM1411 +Isolator:ADuM1412 +Isolator:ADuM260N +Isolator:ADuM261N +Isolator:ADuM262N +Isolator:ADuM263N +Isolator:ADuM3151 +Isolator:ADuM3152 +Isolator:ADuM3153 +Isolator:ADuM5211 +Isolator:ADuM5401 +Isolator:ADuM5402 +Isolator:ADuM5403 +Isolator:ADuM5404 +Isolator:ADuM5410 +Isolator:ADuM5411 +Isolator:ADuM5412 +Isolator:ADuM7640A +Isolator:ADuM7640C +Isolator:ADuM7641A +Isolator:ADuM7641C +Isolator:ADuM7642A +Isolator:ADuM7642C +Isolator:ADuM7643A +Isolator:ADuM7643C +Isolator:CNY17-1 +Isolator:CNY17-2 +Isolator:CNY17-3 +Isolator:CNY17-4 +Isolator:CPC-5002 +Isolator:EL814 +Isolator:EL817 +Isolator:FODM214 +Isolator:FODM214A +Isolator:FODM217A +Isolator:FODM217B +Isolator:FODM217C +Isolator:FODM217D +Isolator:H11AA1 +Isolator:H11L1 +Isolator:H11L2 +Isolator:H11L3 +Isolator:HCNW2201 +Isolator:HCNW2211 +Isolator:HCPL-0201 +Isolator:HCPL-0211 +Isolator:HCPL-0600 +Isolator:HCPL-0601 +Isolator:HCPL-0611 +Isolator:HCPL-061A +Isolator:HCPL-061N +Isolator:HCPL-0630 +Isolator:HCPL-0631 +Isolator:HCPL-063A +Isolator:HCPL-063N +Isolator:HCPL-0661 +Isolator:HCPL-2201 +Isolator:HCPL-2202 +Isolator:HCPL-2211 +Isolator:HCPL-2212 +Isolator:HCPL-2601 +Isolator:HCPL-2611 +Isolator:HCPL-261A +Isolator:HCPL-261N +Isolator:HCPL-2630 +Isolator:HCPL-2631 +Isolator:HCPL-263A +Isolator:HCPL-263N +Isolator:HCPL-4661 +Isolator:HCPL-9000 +Isolator:HCPL2730 +Isolator:HCPL2731 +Isolator:ILD74 +Isolator:ILQ74 +Isolator:ISO1211 +Isolator:ISO1212 +Isolator:ISO1540 +Isolator:ISO1541 +Isolator:ISO1642DWR +Isolator:ISO1643DWR +Isolator:ISO1644DWR +Isolator:ISO6731 +Isolator:ISO6740 +Isolator:ISO6741 +Isolator:ISO6742 +Isolator:ISO7320C +Isolator:ISO7320FC +Isolator:ISO7321C +Isolator:ISO7321FC +Isolator:ISO7330C +Isolator:ISO7330FC +Isolator:ISO7331C +Isolator:ISO7331FC +Isolator:ISO7340C +Isolator:ISO7340FC +Isolator:ISO7341C +Isolator:ISO7341FC +Isolator:ISO7342C +Isolator:ISO7342FC +Isolator:ISO7760DBQ +Isolator:ISO7760DW +Isolator:ISO7760FDBQ +Isolator:ISO7760FDW +Isolator:ISO7761DBQ +Isolator:ISO7761DW +Isolator:ISO7761FDBQ +Isolator:ISO7761FDW +Isolator:ISO7762DBQ +Isolator:ISO7762DW +Isolator:ISO7762FDBQ +Isolator:ISO7762FDW +Isolator:ISO7763DBQ +Isolator:ISO7763DW +Isolator:ISO7763FDBQ +Isolator:ISO7763FDW +Isolator:ISOW7740 +Isolator:ISOW7741 +Isolator:ISOW7742 +Isolator:ISOW7743 +Isolator:ISOW7744 +Isolator:LTV-247 +Isolator:LTV-352T +Isolator:LTV-354T +Isolator:LTV-355T +Isolator:LTV-356T +Isolator:LTV-357T +Isolator:LTV-358T +Isolator:LTV-814 +Isolator:LTV-817 +Isolator:LTV-817M +Isolator:LTV-817S +Isolator:LTV-824 +Isolator:LTV-827 +Isolator:LTV-827M +Isolator:LTV-827S +Isolator:LTV-844 +Isolator:LTV-847 +Isolator:LTV-847M +Isolator:LTV-847S +Isolator:MAX14850AEE+ +Isolator:MAX14850ASE+ +Isolator:MID400 +Isolator:MOCD207M +Isolator:MOCD208M +Isolator:MOCD211M +Isolator:MOCD213M +Isolator:MOCD217M +Isolator:NSL-32 +Isolator:PC3H4 +Isolator:PC3H4A +Isolator:PC817 +Isolator:PC827 +Isolator:PC837 +Isolator:PC847 +Isolator:PS8802-1 +Isolator:PS8802-2 +Isolator:SFH617A-1 +Isolator:SFH617A-1X001 +Isolator:SFH617A-1X006 +Isolator:SFH617A-1X007T +Isolator:SFH617A-1X016 +Isolator:SFH617A-2 +Isolator:SFH617A-2X001 +Isolator:SFH617A-2X006 +Isolator:SFH617A-2X009T +Isolator:SFH617A-2X016 +Isolator:SFH617A-2X017T +Isolator:SFH617A-2X019T +Isolator:SFH617A-3 +Isolator:SFH617A-3X001 +Isolator:SFH617A-3X006 +Isolator:SFH617A-3X007T +Isolator:SFH617A-3X016 +Isolator:SFH617A-3X017T +Isolator:SFH617A-4 +Isolator:SFH617A-4X001 +Isolator:SFH617A-4X006 +Isolator:SFH617A-4X016 +Isolator:SFH6206-1T +Isolator:SFH6206-2T +Isolator:SFH6206-2X001T +Isolator:SFH6206-3T +Isolator:SFH6206-3X001T +Isolator:SFH620A-1 +Isolator:SFH620A-1X001 +Isolator:SFH620A-1X006 +Isolator:SFH620A-2 +Isolator:SFH620A-2X001 +Isolator:SFH620A-2X006 +Isolator:SFH620A-2X007T +Isolator:SFH620A-2X016 +Isolator:SFH620A-2X017T +Isolator:SFH620A-3 +Isolator:SFH620A-3X001 +Isolator:SFH620A-3X006 +Isolator:SFH620A-3X016 +Isolator:Si8640BA-B-IU +Isolator:Si8640BB-B-IS +Isolator:Si8640BB-B-IS1 +Isolator:Si8640BB-B-IU +Isolator:Si8640BC-B-IS1 +Isolator:Si8640BD-B-IS +Isolator:Si8640BD-B-IS2 +Isolator:Si8640EB-B-IU +Isolator:Si8640EC-B-IS1 +Isolator:Si8640ED-B-IS +Isolator:Si8640ED-B-IS2 +Isolator:Si8641BA-B-IU +Isolator:Si8641BB-B-IS +Isolator:Si8641BB-B-IS1 +Isolator:Si8641BB-B-IU +Isolator:Si8641BC-B-IS1 +Isolator:Si8641BD-B-IS +Isolator:Si8641BD-B-IS2 +Isolator:Si8641EB-B-IU +Isolator:Si8641EC-B-IS1 +Isolator:Si8641ED-B-IS +Isolator:Si8641ED-B-IS2 +Isolator:Si8642BA-B-IU +Isolator:Si8642BB-B-IS +Isolator:Si8642BB-B-IS1 +Isolator:Si8642BB-B-IU +Isolator:Si8642BC-B-IS1 +Isolator:Si8642BD-B-IS +Isolator:Si8642BD-B-IS2 +Isolator:Si8642EA-B-IU +Isolator:Si8642EB-B-IU +Isolator:Si8642EC-B-IS1 +Isolator:Si8642ED-B-IS +Isolator:Si8642ED-B-IS2 +Isolator:Si8645BA-B-IU +Isolator:Si8645BB-B-IS +Isolator:Si8645BB-B-IS1 +Isolator:Si8645BB-B-IU +Isolator:Si8645BC-B-IS1 +Isolator:Si8645BD-B-IS +Isolator:Si8660BA-AS1 +Isolator:Si8660BA-B-IS1 +Isolator:Si8660BB-AS1 +Isolator:Si8660BB-AU +Isolator:Si8660BB-B-IS1 +Isolator:Si8660BB-B-IU +Isolator:Si8660BC-AS1 +Isolator:Si8660BC-B-IS1 +Isolator:Si8660BD-AS +Isolator:Si8660BD-B-IS +Isolator:Si8660EB-AU +Isolator:Si8660EB-B-IU +Isolator:Si8660EC-AS1 +Isolator:Si8660EC-B-IS1 +Isolator:Si8660ED-AS +Isolator:Si8660ED-B-IS +Isolator:Si8661BB-AS1 +Isolator:Si8661BB-AU +Isolator:Si8661BB-B-IS1 +Isolator:Si8661BB-B-IU +Isolator:Si8661BC-AS1 +Isolator:Si8661BC-B-IS1 +Isolator:Si8661BD-AS +Isolator:Si8661BD-AS2 +Isolator:Si8661BD-B-IS +Isolator:Si8661BD-B-IS2 +Isolator:Si8661EB-AU +Isolator:Si8661EB-B-IU +Isolator:Si8661EC-AS1 +Isolator:Si8661EC-B-IS1 +Isolator:Si8661ED-AS +Isolator:Si8661ED-B-IS +Isolator:Si8662BB-AS1 +Isolator:Si8662BB-AU +Isolator:Si8662BB-B-IS1 +Isolator:Si8662BB-B-IU +Isolator:Si8662BC-AS1 +Isolator:Si8662BC-B-IS1 +Isolator:Si8662BD-AS +Isolator:Si8662BD-B-IS +Isolator:Si8662EB-AU +Isolator:Si8662EB-B-IU +Isolator:Si8662EC-AS1 +Isolator:Si8662EC-B-IS1 +Isolator:Si8662ED-AS +Isolator:Si8662ED-B-IS +Isolator:Si8663BB-AS1 +Isolator:Si8663BB-AU +Isolator:Si8663BB-B-IS1 +Isolator:Si8663BB-B-IU +Isolator:Si8663BC-AS1 +Isolator:Si8663BC-B-IS1 +Isolator:Si8663BD-AS +Isolator:Si8663BD-B-IS +Isolator:Si8663EB-AU +Isolator:Si8663EB-B-IU +Isolator:Si8663EC-AS1 +Isolator:Si8663EC-B-IS1 +Isolator:Si8663ED-AS +Isolator:Si8663ED-B-IS +Isolator:TCMT1100 +Isolator:TCMT1101 +Isolator:TCMT1102 +Isolator:TCMT1103 +Isolator:TCMT1104 +Isolator:TCMT1105 +Isolator:TCMT1106 +Isolator:TCMT1107 +Isolator:TCMT1108 +Isolator:TCMT1109 +Isolator:TCMT1600 +Isolator:TCMT4100 +Isolator:TCMT4106 +Isolator:TCMT4600 +Isolator:TCMT4606 +Isolator:TIL111 +Isolator:TLP127 +Isolator:TLP130 +Isolator:TLP131 +Isolator:TLP137 +Isolator:TLP184 +Isolator:TLP184xSE +Isolator:TLP185 +Isolator:TLP185xSE +Isolator:TLP2310 +Isolator:TLP2703 +Isolator:TLP2745 +Isolator:TLP2748 +Isolator:TLP2761 +Isolator:TLP2767 +Isolator:TLP2768A +Isolator:TLP2770 +Isolator:TLP290 +Isolator:TLP290-4 +Isolator:TLP291 +Isolator:TLP291-4 +Isolator:TLP292 +Isolator:TLP292-4 +Isolator:TLP293 +Isolator:TLP293-4 +Isolator:TLP3021 +Isolator:TLP3022 +Isolator:TLP3023 +Isolator:TLP627 +Isolator:TLP627-2 +Isolator:TLP627-4 +Isolator:TLP785 +Isolator:TLP785F +Isolator:VO0600T +Isolator:VO0601T +Isolator:VO0611T +Isolator:VO0630T +Isolator:VO0631T +Isolator:VO0661T +Isolator:VO2601 +Isolator:VO2611 +Isolator:VO2630 +Isolator:VO2631 +Isolator:VO4661 +Isolator:VO615A +Isolator:VO615A-1 +Isolator:VO615A-2 +Isolator:VO615A-3 +Isolator:VO615A-4 +Isolator:VO615A-5 +Isolator:VO615A-6 +Isolator:VO615A-7 +Isolator:VO615A-8 +Isolator:VO615A-9 +Isolator:VOA300 +Isolator:VOS618A +Isolator:VTL5C +Isolator:VTL5Cx2 +Isolator:π120U30 +Isolator:π120U31 +Isolator_Analog:ACPL-C790 +Isolator_Analog:ACPL-C79A +Isolator_Analog:ACPL-C79B +Isolator_Analog:ACPL-C870 +Isolator_Analog:ACPL-C87A +Isolator_Analog:ACPL-C87B +Isolator_Analog:AMC3330 +Isolator_Analog:IL300 +Isolator_Analog:LOC112 +Isolator_Analog:LOC112P +Isolator_Analog:LOC112S +Jumper:Jumper_2_Bridged +Jumper:Jumper_2_Open +Jumper:Jumper_2_Small_Bridged +Jumper:Jumper_2_Small_Open +Jumper:Jumper_3_Bridged12 +Jumper:Jumper_3_Open +Jumper:SolderJumper_2_Bridged +Jumper:SolderJumper_2_Open +Jumper:SolderJumper_3_Bridged12 +Jumper:SolderJumper_3_Bridged123 +Jumper:SolderJumper_3_Open +LED:APA-106-F5 +LED:APA102 +LED:APA102-2020 +LED:APFA3010 +LED:ASMB-MTB0-0A3A2 +LED:ASMB-MTB1-0A3A2 +LED:ASMT-YTB7-0AA02 +LED:ASMT-YTC2-0AA02 +LED:CLS6B-FKW +LED:CLV1L-FKB +LED:CLX6F-FKC +LED:CQY99 +LED:HDSP-4830 +LED:HDSP-4830_2 +LED:HDSP-4832 +LED:HDSP-4832_2 +LED:HDSP-4836 +LED:HDSP-4836_2 +LED:HDSP-4840 +LED:HDSP-4840_2 +LED:HDSP-4850 +LED:HDSP-4850_2 +LED:HLCP-J100 +LED:HLCP-J100_2 +LED:IR204A +LED:IR26-21C_L110_TR8 +LED:IRL81A +LED:Inolux_IN-P55TATRGB +LED:Inolux_IN-PI554FCH +LED:Inolux_IN-PI556FCH +LED:LD271 +LED:LD274 +LED:LED_Cree_XHP50_12V +LED:LED_Cree_XHP50_6V +LED:LED_Cree_XHP70_12V +LED:LED_Cree_XHP70_6V +LED:LTST-C235KGKRKT +LED:LiteOn_LTST-E563C +LED:NeoPixel_THT +LED:QLS6A-FKW +LED:QLS6B-FKW +LED:SFH4346 +LED:SFH4356P +LED:SFH4546 +LED:SFH4550 +LED:SFH460 +LED:SFH480 +LED:SFH482 +LED:SK6805 +LED:SK6812 +LED:SK6812MINI +LED:SMLVN6RGB +LED:TSAL4400 +LED:WS2812 +LED:WS2812B +LED:WS2812B-2020 +LED:WS2812S +LED:WS2813 +LED:WS2822S +Logic_LevelTranslator:74LVC2T45DC +Logic_LevelTranslator:74LVCH2T45DC +Logic_LevelTranslator:FXMA108 +Logic_LevelTranslator:NCN4555MN +Logic_LevelTranslator:NLSV2T244D +Logic_LevelTranslator:NLSV2T244DM +Logic_LevelTranslator:NLSV2T244MU +Logic_LevelTranslator:SN74AUP1T34DCK +Logic_LevelTranslator:SN74AVC4T245PW +Logic_LevelTranslator:SN74AVC8T245PW +Logic_LevelTranslator:SN74LV1T125DBV +Logic_LevelTranslator:SN74LV1T125DCK +Logic_LevelTranslator:SN74LV1T34DBV +Logic_LevelTranslator:SN74LV1T34DCK +Logic_LevelTranslator:SN74LVC1T45DBV +Logic_LevelTranslator:SN74LVC1T45DCK +Logic_LevelTranslator:SN74LVC1T45DRL +Logic_LevelTranslator:SN74LVC245APW +Logic_LevelTranslator:SN74LVC2T45DCUR +Logic_LevelTranslator:SN74LVC2T45YZP +Logic_LevelTranslator:SN74LVC8T245 +Logic_LevelTranslator:TCA9517ADGK +Logic_LevelTranslator:TCA9517D +Logic_LevelTranslator:TXB0101DBV +Logic_LevelTranslator:TXB0101DCK +Logic_LevelTranslator:TXB0101DRL +Logic_LevelTranslator:TXB0101YZP +Logic_LevelTranslator:TXB0102DCT +Logic_LevelTranslator:TXB0102DCU +Logic_LevelTranslator:TXB0102YZP +Logic_LevelTranslator:TXB0104D +Logic_LevelTranslator:TXB0104PW +Logic_LevelTranslator:TXB0104RGY +Logic_LevelTranslator:TXB0104RUT +Logic_LevelTranslator:TXB0104YZT +Logic_LevelTranslator:TXB0104ZXU +Logic_LevelTranslator:TXB0106PW +Logic_LevelTranslator:TXB0106RGY +Logic_LevelTranslator:TXB0108DQSR +Logic_LevelTranslator:TXB0108PW +Logic_LevelTranslator:TXB0108RGY +Logic_LevelTranslator:TXB0304RUT +Logic_LevelTranslator:TXBN0304RUT +Logic_LevelTranslator:TXS0101DBV +Logic_LevelTranslator:TXS0101DCK +Logic_LevelTranslator:TXS0101DRL +Logic_LevelTranslator:TXS0101YZP +Logic_LevelTranslator:TXS0102DCT +Logic_LevelTranslator:TXS0102DCU +Logic_LevelTranslator:TXS0102DQE +Logic_LevelTranslator:TXS0102YZP +Logic_LevelTranslator:TXS0104ED +Logic_LevelTranslator:TXS0104EPW +Logic_LevelTranslator:TXS0108EPW +Logic_LevelTranslator:TXS02612RTW +Logic_Programmable:GAL16V8 +Logic_Programmable:PAL16L8 +Logic_Programmable:PAL20 +Logic_Programmable:PAL20L8 +Logic_Programmable:PAL20RS10 +Logic_Programmable:PAL24 +Logic_Programmable:PEEL22CV10AP +Logic_Programmable:PEEL22CV10AS +MCU_AnalogDevices:ADUC816 +MCU_AnalogDevices:MAX32660GTP +MCU_AnalogDevices:MAX32670GTL +MCU_Cypress:CY7C68013A-56LTX +MCU_Cypress:CY7C68013A-56PVX +MCU_Cypress:CY7C68014A-56LTX +MCU_Cypress:CY7C68014A-56PVX +MCU_Cypress:CY8C4127LQI-BL453 +MCU_Cypress:CY8C4127LQI-BL473 +MCU_Cypress:CY8C4127LQI-BL483 +MCU_Cypress:CY8C4127LQI-BL493 +MCU_Cypress:CY8C4245AXI-M445 +MCU_Cypress:CY8C4245AZI-M445 +MCU_Cypress:CY8C4246AXI-M445 +MCU_Cypress:CY8C4246AZI-M445 +MCU_Cypress:CY8C4246AZI-M475 +MCU_Cypress:CY8C4247AXI-M485 +MCU_Cypress:CY8C4247AZI-M475 +MCU_Cypress:CY8C4247AZI-M485 +MCU_Cypress:CY8C4247LQI-BL453 +MCU_Cypress:CY8C4247LQI-BL463 +MCU_Cypress:CY8C4247LQI-BL473 +MCU_Cypress:CY8C4247LQI-BL483 +MCU_Cypress:CY8C4247LQI-BL493 +MCU_Cypress:CY8C4247LQQ-BL483 +MCU_Cypress:CY8C4xx7LQI-4xx +MCU_Cypress:CYBL10161-56LQXI +MCU_Cypress:CYBL10162-56LQXI +MCU_Cypress:CYBL10163-56LQXI +MCU_Cypress:CYBL10461-56LQXI +MCU_Cypress:CYBL10462-56LQXI +MCU_Cypress:CYBL10463-56LQXI +MCU_Cypress:CYBL10561-56LQXI +MCU_Cypress:CYBL10562-56LQXI +MCU_Cypress:CYBL10563-56LQXI +MCU_Cypress:CYBL10563-56LQXQ +MCU_Cypress:CYBL10563-68FLXIT +MCU_Cypress:CYBL10563-68FNXIT +MCU_Cypress:CYBL10x6x-56LQxx +MCU_Dialog:DA14691 +MCU_Dialog:DA14695 +MCU_Espressif:ESP32-C3 +MCU_Espressif:ESP32-PICO-D4 +MCU_Espressif:ESP32-S2 +MCU_Espressif:ESP32-S3 +MCU_Espressif:ESP8266EX +MCU_Intel:80186 +MCU_Intel:80188 +MCU_Intel:8035 +MCU_Intel:8039 +MCU_Intel:8040 +MCU_Intel:8048 +MCU_Intel:8049 +MCU_Intel:8050 +MCU_Intel:8080 +MCU_Intel:8080A +MCU_Intel:8086_Max_Mode +MCU_Intel:8086_Min_Mode +MCU_Intel:8087 +MCU_Intel:8088 +MCU_Intel:8088_Max_Mode +MCU_Intel:8088_Min_Mode +MCU_Intel:80C186XL +MCU_Intel:80C188 +MCU_Intel:80C188XL +MCU_Intel:8748 +MCU_Intel:8749 +MCU_Intel:I386EX_PQFP +MCU_Intel:IA186XLPLC68IR2 +MCU_Intel:IA188XLPLC68IR2 +MCU_Intel:M80C186 +MCU_Intel:M80C186XL +MCU_Intel:P8031AH +MCU_Intel:P8051AH +MCU_Intel:P8052AH +MCU_Intel:P8751BH +MCU_Intel:P8752BH +MCU_Microchip_8051:AT89C2051-12P +MCU_Microchip_8051:AT89C2051-12S +MCU_Microchip_8051:AT89C2051-24P +MCU_Microchip_8051:AT89C2051-24S +MCU_Microchip_8051:AT89C4051-12P +MCU_Microchip_8051:AT89C4051-12S +MCU_Microchip_8051:AT89C4051-24P +MCU_Microchip_8051:AT89C4051-24S +MCU_Microchip_8051:AT89S2051-24P +MCU_Microchip_8051:AT89S2051-24S +MCU_Microchip_8051:AT89S4051-24P +MCU_Microchip_8051:AT89S4051-24S +MCU_Microchip_8051:AT89x51xxA +MCU_Microchip_8051:AT89x51xxJ +MCU_Microchip_8051:AT89x51xxP +MCU_Microchip_ATmega:ATmega128-16A +MCU_Microchip_ATmega:ATmega128-16M +MCU_Microchip_ATmega:ATmega1280-16A +MCU_Microchip_ATmega:ATmega1280-16C +MCU_Microchip_ATmega:ATmega1280V-8A +MCU_Microchip_ATmega:ATmega1280V-8C +MCU_Microchip_ATmega:ATmega1281-16A +MCU_Microchip_ATmega:ATmega1281-16M +MCU_Microchip_ATmega:ATmega1281V-8A +MCU_Microchip_ATmega:ATmega1281V-8M +MCU_Microchip_ATmega:ATmega1284-A +MCU_Microchip_ATmega:ATmega1284-M +MCU_Microchip_ATmega:ATmega1284-P +MCU_Microchip_ATmega:ATmega1284P-A +MCU_Microchip_ATmega:ATmega1284P-M +MCU_Microchip_ATmega:ATmega1284P-P +MCU_Microchip_ATmega:ATmega128A-A +MCU_Microchip_ATmega:ATmega128A-M +MCU_Microchip_ATmega:ATmega128L-8A +MCU_Microchip_ATmega:ATmega128L-8M +MCU_Microchip_ATmega:ATmega16-16A +MCU_Microchip_ATmega:ATmega16-16M +MCU_Microchip_ATmega:ATmega16-16P +MCU_Microchip_ATmega:ATmega162-16A +MCU_Microchip_ATmega:ATmega162-16M +MCU_Microchip_ATmega:ATmega162-16P +MCU_Microchip_ATmega:ATmega162V-8A +MCU_Microchip_ATmega:ATmega162V-8M +MCU_Microchip_ATmega:ATmega162V-8P +MCU_Microchip_ATmega:ATmega164A-A +MCU_Microchip_ATmega:ATmega164A-C +MCU_Microchip_ATmega:ATmega164A-M +MCU_Microchip_ATmega:ATmega164A-MC +MCU_Microchip_ATmega:ATmega164A-P +MCU_Microchip_ATmega:ATmega164P-20A +MCU_Microchip_ATmega:ATmega164P-20M +MCU_Microchip_ATmega:ATmega164P-20P +MCU_Microchip_ATmega:ATmega164PA-A +MCU_Microchip_ATmega:ATmega164PA-C +MCU_Microchip_ATmega:ATmega164PA-M +MCU_Microchip_ATmega:ATmega164PA-MC +MCU_Microchip_ATmega:ATmega164PA-P +MCU_Microchip_ATmega:ATmega164PV-10A +MCU_Microchip_ATmega:ATmega164PV-10M +MCU_Microchip_ATmega:ATmega164PV-10P +MCU_Microchip_ATmega:ATmega165A-A +MCU_Microchip_ATmega:ATmega165A-M +MCU_Microchip_ATmega:ATmega165P-16A +MCU_Microchip_ATmega:ATmega165P-16M +MCU_Microchip_ATmega:ATmega165PA-A +MCU_Microchip_ATmega:ATmega165PA-M +MCU_Microchip_ATmega:ATmega165PV-8A +MCU_Microchip_ATmega:ATmega165PV-8M +MCU_Microchip_ATmega:ATmega168-20A +MCU_Microchip_ATmega:ATmega168-20M +MCU_Microchip_ATmega:ATmega168-20P +MCU_Microchip_ATmega:ATmega168A-A +MCU_Microchip_ATmega:ATmega168A-CC +MCU_Microchip_ATmega:ATmega168A-M +MCU_Microchip_ATmega:ATmega168A-MM +MCU_Microchip_ATmega:ATmega168A-P +MCU_Microchip_ATmega:ATmega168P-20A +MCU_Microchip_ATmega:ATmega168P-20M +MCU_Microchip_ATmega:ATmega168P-20P +MCU_Microchip_ATmega:ATmega168PA-A +MCU_Microchip_ATmega:ATmega168PA-CC +MCU_Microchip_ATmega:ATmega168PA-M +MCU_Microchip_ATmega:ATmega168PA-MM +MCU_Microchip_ATmega:ATmega168PA-P +MCU_Microchip_ATmega:ATmega168PB-A +MCU_Microchip_ATmega:ATmega168PB-M +MCU_Microchip_ATmega:ATmega168PV-10A +MCU_Microchip_ATmega:ATmega168PV-10M +MCU_Microchip_ATmega:ATmega168PV-10P +MCU_Microchip_ATmega:ATmega168V-10A +MCU_Microchip_ATmega:ATmega168V-10M +MCU_Microchip_ATmega:ATmega168V-10P +MCU_Microchip_ATmega:ATmega169A-A +MCU_Microchip_ATmega:ATmega169A-M +MCU_Microchip_ATmega:ATmega169A-MC +MCU_Microchip_ATmega:ATmega169P-16A +MCU_Microchip_ATmega:ATmega169P-16M +MCU_Microchip_ATmega:ATmega169P-16MC +MCU_Microchip_ATmega:ATmega169PA-A +MCU_Microchip_ATmega:ATmega169PA-M +MCU_Microchip_ATmega:ATmega169PA-MC +MCU_Microchip_ATmega:ATmega169PV-8A +MCU_Microchip_ATmega:ATmega169PV-8M +MCU_Microchip_ATmega:ATmega169PV-8MC +MCU_Microchip_ATmega:ATmega16A-A +MCU_Microchip_ATmega:ATmega16A-M +MCU_Microchip_ATmega:ATmega16A-P +MCU_Microchip_ATmega:ATmega16L-8A +MCU_Microchip_ATmega:ATmega16L-8M +MCU_Microchip_ATmega:ATmega16L-8P +MCU_Microchip_ATmega:ATmega16M1-A +MCU_Microchip_ATmega:ATmega16M1-M +MCU_Microchip_ATmega:ATmega16U2-A +MCU_Microchip_ATmega:ATmega16U2-M +MCU_Microchip_ATmega:ATmega16U4-A +MCU_Microchip_ATmega:ATmega16U4-M +MCU_Microchip_ATmega:ATmega16U4RC-A +MCU_Microchip_ATmega:ATmega16U4RC-M +MCU_Microchip_ATmega:ATmega2560-16A +MCU_Microchip_ATmega:ATmega2560-16C +MCU_Microchip_ATmega:ATmega2560V-8A +MCU_Microchip_ATmega:ATmega2560V-8C +MCU_Microchip_ATmega:ATmega2561-16A +MCU_Microchip_ATmega:ATmega2561-16M +MCU_Microchip_ATmega:ATmega2561V-8A +MCU_Microchip_ATmega:ATmega2561V-8M +MCU_Microchip_ATmega:ATmega32-16A +MCU_Microchip_ATmega:ATmega32-16M +MCU_Microchip_ATmega:ATmega32-16P +MCU_Microchip_ATmega:ATmega3208-A +MCU_Microchip_ATmega:ATmega3208-M +MCU_Microchip_ATmega:ATmega3208-X +MCU_Microchip_ATmega:ATmega3209-A +MCU_Microchip_ATmega:ATmega3209-M +MCU_Microchip_ATmega:ATmega324A-A +MCU_Microchip_ATmega:ATmega324A-C +MCU_Microchip_ATmega:ATmega324A-M +MCU_Microchip_ATmega:ATmega324A-MC +MCU_Microchip_ATmega:ATmega324A-P +MCU_Microchip_ATmega:ATmega324P-20A +MCU_Microchip_ATmega:ATmega324P-20M +MCU_Microchip_ATmega:ATmega324P-20P +MCU_Microchip_ATmega:ATmega324PA-A +MCU_Microchip_ATmega:ATmega324PA-C +MCU_Microchip_ATmega:ATmega324PA-M +MCU_Microchip_ATmega:ATmega324PA-MC +MCU_Microchip_ATmega:ATmega324PA-P +MCU_Microchip_ATmega:ATmega324PB-A +MCU_Microchip_ATmega:ATmega324PB-M +MCU_Microchip_ATmega:ATmega324PV-10A +MCU_Microchip_ATmega:ATmega324PV-10M +MCU_Microchip_ATmega:ATmega324PV-10P +MCU_Microchip_ATmega:ATmega325-16A +MCU_Microchip_ATmega:ATmega325-16M +MCU_Microchip_ATmega:ATmega3250-16A +MCU_Microchip_ATmega:ATmega3250A-A +MCU_Microchip_ATmega:ATmega3250P-20A +MCU_Microchip_ATmega:ATmega3250PA-A +MCU_Microchip_ATmega:ATmega3250PV-10A +MCU_Microchip_ATmega:ATmega3250V-8A +MCU_Microchip_ATmega:ATmega325A-A +MCU_Microchip_ATmega:ATmega325A-M +MCU_Microchip_ATmega:ATmega325P-20A +MCU_Microchip_ATmega:ATmega325P-20M +MCU_Microchip_ATmega:ATmega325PA-A +MCU_Microchip_ATmega:ATmega325PA-M +MCU_Microchip_ATmega:ATmega325PV-10A +MCU_Microchip_ATmega:ATmega325PV-10M +MCU_Microchip_ATmega:ATmega325V-8A +MCU_Microchip_ATmega:ATmega325V-8M +MCU_Microchip_ATmega:ATmega328-A +MCU_Microchip_ATmega:ATmega328-M +MCU_Microchip_ATmega:ATmega328-MM +MCU_Microchip_ATmega:ATmega328-P +MCU_Microchip_ATmega:ATmega328P-A +MCU_Microchip_ATmega:ATmega328P-M +MCU_Microchip_ATmega:ATmega328P-MM +MCU_Microchip_ATmega:ATmega328P-P +MCU_Microchip_ATmega:ATmega328PB-A +MCU_Microchip_ATmega:ATmega328PB-M +MCU_Microchip_ATmega:ATmega329-16A +MCU_Microchip_ATmega:ATmega329-16M +MCU_Microchip_ATmega:ATmega3290-16A +MCU_Microchip_ATmega:ATmega3290A-A +MCU_Microchip_ATmega:ATmega3290P-20A +MCU_Microchip_ATmega:ATmega3290PA-A +MCU_Microchip_ATmega:ATmega3290PV-10A +MCU_Microchip_ATmega:ATmega3290V-8A +MCU_Microchip_ATmega:ATmega329A-A +MCU_Microchip_ATmega:ATmega329A-M +MCU_Microchip_ATmega:ATmega329P-20A +MCU_Microchip_ATmega:ATmega329P-20M +MCU_Microchip_ATmega:ATmega329PA-A +MCU_Microchip_ATmega:ATmega329PA-M +MCU_Microchip_ATmega:ATmega329PV-10A +MCU_Microchip_ATmega:ATmega329PV-10M +MCU_Microchip_ATmega:ATmega329V-8A +MCU_Microchip_ATmega:ATmega329V-8M +MCU_Microchip_ATmega:ATmega32A-A +MCU_Microchip_ATmega:ATmega32A-M +MCU_Microchip_ATmega:ATmega32A-P +MCU_Microchip_ATmega:ATmega32L-8A +MCU_Microchip_ATmega:ATmega32L-8M +MCU_Microchip_ATmega:ATmega32L-8P +MCU_Microchip_ATmega:ATmega32M1-A +MCU_Microchip_ATmega:ATmega32M1-M +MCU_Microchip_ATmega:ATmega32U2-A +MCU_Microchip_ATmega:ATmega32U2-M +MCU_Microchip_ATmega:ATmega32U4-A +MCU_Microchip_ATmega:ATmega32U4-M +MCU_Microchip_ATmega:ATmega32U4RC-A +MCU_Microchip_ATmega:ATmega32U4RC-M +MCU_Microchip_ATmega:ATmega406-1AA +MCU_Microchip_ATmega:ATmega48-20A +MCU_Microchip_ATmega:ATmega48-20M +MCU_Microchip_ATmega:ATmega48-20MM +MCU_Microchip_ATmega:ATmega48-20P +MCU_Microchip_ATmega:ATmega4808-A +MCU_Microchip_ATmega:ATmega4808-M +MCU_Microchip_ATmega:ATmega4808-X +MCU_Microchip_ATmega:ATmega4809-A +MCU_Microchip_ATmega:ATmega4809-M +MCU_Microchip_ATmega:ATmega48A-A +MCU_Microchip_ATmega:ATmega48A-CC +MCU_Microchip_ATmega:ATmega48A-M +MCU_Microchip_ATmega:ATmega48A-MM +MCU_Microchip_ATmega:ATmega48A-P +MCU_Microchip_ATmega:ATmega48P-20A +MCU_Microchip_ATmega:ATmega48P-20M +MCU_Microchip_ATmega:ATmega48P-20MM +MCU_Microchip_ATmega:ATmega48P-20P +MCU_Microchip_ATmega:ATmega48PA-A +MCU_Microchip_ATmega:ATmega48PA-CC +MCU_Microchip_ATmega:ATmega48PA-M +MCU_Microchip_ATmega:ATmega48PA-MM +MCU_Microchip_ATmega:ATmega48PA-P +MCU_Microchip_ATmega:ATmega48PB-A +MCU_Microchip_ATmega:ATmega48PB-M +MCU_Microchip_ATmega:ATmega48PV-10A +MCU_Microchip_ATmega:ATmega48PV-10M +MCU_Microchip_ATmega:ATmega48PV-10MM +MCU_Microchip_ATmega:ATmega48PV-10P +MCU_Microchip_ATmega:ATmega48V-10A +MCU_Microchip_ATmega:ATmega48V-10M +MCU_Microchip_ATmega:ATmega48V-10MM +MCU_Microchip_ATmega:ATmega48V-10P +MCU_Microchip_ATmega:ATmega64-16A +MCU_Microchip_ATmega:ATmega64-16M +MCU_Microchip_ATmega:ATmega640-16A +MCU_Microchip_ATmega:ATmega640-16C +MCU_Microchip_ATmega:ATmega640V-8A +MCU_Microchip_ATmega:ATmega640V-8C +MCU_Microchip_ATmega:ATmega644-20A +MCU_Microchip_ATmega:ATmega644-20M +MCU_Microchip_ATmega:ATmega644-20P +MCU_Microchip_ATmega:ATmega644A-A +MCU_Microchip_ATmega:ATmega644A-M +MCU_Microchip_ATmega:ATmega644A-P +MCU_Microchip_ATmega:ATmega644P-20A +MCU_Microchip_ATmega:ATmega644P-20M +MCU_Microchip_ATmega:ATmega644P-20P +MCU_Microchip_ATmega:ATmega644PA-A +MCU_Microchip_ATmega:ATmega644PA-M +MCU_Microchip_ATmega:ATmega644PA-P +MCU_Microchip_ATmega:ATmega644PV-10A +MCU_Microchip_ATmega:ATmega644PV-10M +MCU_Microchip_ATmega:ATmega644PV-10P +MCU_Microchip_ATmega:ATmega644V-10A +MCU_Microchip_ATmega:ATmega644V-10M +MCU_Microchip_ATmega:ATmega644V-10P +MCU_Microchip_ATmega:ATmega645-16A +MCU_Microchip_ATmega:ATmega645-16M +MCU_Microchip_ATmega:ATmega6450-16A +MCU_Microchip_ATmega:ATmega6450A-A +MCU_Microchip_ATmega:ATmega6450P-A +MCU_Microchip_ATmega:ATmega6450V-8A +MCU_Microchip_ATmega:ATmega645A-A +MCU_Microchip_ATmega:ATmega645A-M +MCU_Microchip_ATmega:ATmega645P-A +MCU_Microchip_ATmega:ATmega645P-M +MCU_Microchip_ATmega:ATmega645V-8A +MCU_Microchip_ATmega:ATmega645V-8M +MCU_Microchip_ATmega:ATmega649-16A +MCU_Microchip_ATmega:ATmega649-16M +MCU_Microchip_ATmega:ATmega6490-16A +MCU_Microchip_ATmega:ATmega6490A-A +MCU_Microchip_ATmega:ATmega6490P-A +MCU_Microchip_ATmega:ATmega6490V-8A +MCU_Microchip_ATmega:ATmega649A-A +MCU_Microchip_ATmega:ATmega649A-M +MCU_Microchip_ATmega:ATmega649P-A +MCU_Microchip_ATmega:ATmega649P-M +MCU_Microchip_ATmega:ATmega649V-8A +MCU_Microchip_ATmega:ATmega649V-8M +MCU_Microchip_ATmega:ATmega64A-A +MCU_Microchip_ATmega:ATmega64A-M +MCU_Microchip_ATmega:ATmega64L-8A +MCU_Microchip_ATmega:ATmega64L-8M +MCU_Microchip_ATmega:ATmega64M1-A +MCU_Microchip_ATmega:ATmega64M1-M +MCU_Microchip_ATmega:ATmega8-16A +MCU_Microchip_ATmega:ATmega8-16M +MCU_Microchip_ATmega:ATmega8-16P +MCU_Microchip_ATmega:ATmega8515-16A +MCU_Microchip_ATmega:ATmega8515-16J +MCU_Microchip_ATmega:ATmega8515-16M +MCU_Microchip_ATmega:ATmega8515-16P +MCU_Microchip_ATmega:ATmega8515L-8A +MCU_Microchip_ATmega:ATmega8515L-8J +MCU_Microchip_ATmega:ATmega8515L-8M +MCU_Microchip_ATmega:ATmega8515L-8P +MCU_Microchip_ATmega:ATmega8535-16A +MCU_Microchip_ATmega:ATmega8535-16J +MCU_Microchip_ATmega:ATmega8535-16M +MCU_Microchip_ATmega:ATmega8535-16P +MCU_Microchip_ATmega:ATmega8535L-8A +MCU_Microchip_ATmega:ATmega8535L-8J +MCU_Microchip_ATmega:ATmega8535L-8M +MCU_Microchip_ATmega:ATmega8535L-8P +MCU_Microchip_ATmega:ATmega88-20A +MCU_Microchip_ATmega:ATmega88-20M +MCU_Microchip_ATmega:ATmega88-20P +MCU_Microchip_ATmega:ATmega88A-A +MCU_Microchip_ATmega:ATmega88A-CC +MCU_Microchip_ATmega:ATmega88A-M +MCU_Microchip_ATmega:ATmega88A-MM +MCU_Microchip_ATmega:ATmega88A-P +MCU_Microchip_ATmega:ATmega88P-20A +MCU_Microchip_ATmega:ATmega88P-20M +MCU_Microchip_ATmega:ATmega88P-20P +MCU_Microchip_ATmega:ATmega88PA-A +MCU_Microchip_ATmega:ATmega88PA-CC +MCU_Microchip_ATmega:ATmega88PA-M +MCU_Microchip_ATmega:ATmega88PA-MM +MCU_Microchip_ATmega:ATmega88PA-P +MCU_Microchip_ATmega:ATmega88PB-A +MCU_Microchip_ATmega:ATmega88PB-M +MCU_Microchip_ATmega:ATmega88PV-10A +MCU_Microchip_ATmega:ATmega88PV-10M +MCU_Microchip_ATmega:ATmega88PV-10P +MCU_Microchip_ATmega:ATmega88V-10A +MCU_Microchip_ATmega:ATmega88V-10M +MCU_Microchip_ATmega:ATmega88V-10P +MCU_Microchip_ATmega:ATmega8A-A +MCU_Microchip_ATmega:ATmega8A-M +MCU_Microchip_ATmega:ATmega8A-P +MCU_Microchip_ATmega:ATmega8L-8A +MCU_Microchip_ATmega:ATmega8L-8M +MCU_Microchip_ATmega:ATmega8L-8P +MCU_Microchip_ATmega:ATmega8U2-A +MCU_Microchip_ATmega:ATmega8U2-M +MCU_Microchip_ATmega:ATxmega128A1-A +MCU_Microchip_ATmega:ATxmega128A1-C +MCU_Microchip_ATmega:ATxmega128A1-C7 +MCU_Microchip_ATmega:ATxmega128A1U-A +MCU_Microchip_ATmega:ATxmega128A1U-C +MCU_Microchip_ATmega:ATxmega128A1U-C7 +MCU_Microchip_ATmega:ATxmega128A3-A +MCU_Microchip_ATmega:ATxmega128A3-M +MCU_Microchip_ATmega:ATxmega128A3U-A +MCU_Microchip_ATmega:ATxmega128A3U-M +MCU_Microchip_ATmega:ATxmega128A4U-A +MCU_Microchip_ATmega:ATxmega128A4U-C +MCU_Microchip_ATmega:ATxmega128A4U-M +MCU_Microchip_ATmega:ATxmega128B1-A +MCU_Microchip_ATmega:ATxmega128B1-C +MCU_Microchip_ATmega:ATxmega128B3-A +MCU_Microchip_ATmega:ATxmega128B3-M +MCU_Microchip_ATmega:ATxmega128B3-MC +MCU_Microchip_ATmega:ATxmega128C3-A +MCU_Microchip_ATmega:ATxmega128C3-M +MCU_Microchip_ATmega:ATxmega128D3-A +MCU_Microchip_ATmega:ATxmega128D3-M +MCU_Microchip_ATmega:ATxmega128D4-A +MCU_Microchip_ATmega:ATxmega128D4-C +MCU_Microchip_ATmega:ATxmega128D4-M +MCU_Microchip_ATmega:ATxmega16A4U-A +MCU_Microchip_ATmega:ATxmega16A4U-C +MCU_Microchip_ATmega:ATxmega16A4U-M +MCU_Microchip_ATmega:ATxmega16C4-A +MCU_Microchip_ATmega:ATxmega16C4-C +MCU_Microchip_ATmega:ATxmega16C4-M +MCU_Microchip_ATmega:ATxmega16D4-A +MCU_Microchip_ATmega:ATxmega16D4-C +MCU_Microchip_ATmega:ATxmega16D4-M +MCU_Microchip_ATmega:ATxmega16E5-A +MCU_Microchip_ATmega:ATxmega16E5-M +MCU_Microchip_ATmega:ATxmega16E5-M4 +MCU_Microchip_ATmega:ATxmega192A3-A +MCU_Microchip_ATmega:ATxmega192A3-M +MCU_Microchip_ATmega:ATxmega192A3U-A +MCU_Microchip_ATmega:ATxmega192A3U-M +MCU_Microchip_ATmega:ATxmega192C3-A +MCU_Microchip_ATmega:ATxmega192C3-M +MCU_Microchip_ATmega:ATxmega192D3-A +MCU_Microchip_ATmega:ATxmega192D3-M +MCU_Microchip_ATmega:ATxmega256A3-A +MCU_Microchip_ATmega:ATxmega256A3-M +MCU_Microchip_ATmega:ATxmega256A3B-A +MCU_Microchip_ATmega:ATxmega256A3B-M +MCU_Microchip_ATmega:ATxmega256A3BU-A +MCU_Microchip_ATmega:ATxmega256A3BU-M +MCU_Microchip_ATmega:ATxmega256A3U-A +MCU_Microchip_ATmega:ATxmega256A3U-M +MCU_Microchip_ATmega:ATxmega256C3-A +MCU_Microchip_ATmega:ATxmega256C3-M +MCU_Microchip_ATmega:ATxmega256D3-A +MCU_Microchip_ATmega:ATxmega256D3-M +MCU_Microchip_ATmega:ATxmega32A4U-A +MCU_Microchip_ATmega:ATxmega32A4U-C +MCU_Microchip_ATmega:ATxmega32A4U-M +MCU_Microchip_ATmega:ATxmega32C3-A +MCU_Microchip_ATmega:ATxmega32C3-M +MCU_Microchip_ATmega:ATxmega32C4-A +MCU_Microchip_ATmega:ATxmega32C4-C +MCU_Microchip_ATmega:ATxmega32C4-M +MCU_Microchip_ATmega:ATxmega32D3-A +MCU_Microchip_ATmega:ATxmega32D3-M +MCU_Microchip_ATmega:ATxmega32D4-A +MCU_Microchip_ATmega:ATxmega32D4-C +MCU_Microchip_ATmega:ATxmega32D4-M +MCU_Microchip_ATmega:ATxmega32E5-A +MCU_Microchip_ATmega:ATxmega32E5-M +MCU_Microchip_ATmega:ATxmega32E5-M4 +MCU_Microchip_ATmega:ATxmega384C3-A +MCU_Microchip_ATmega:ATxmega384C3-M +MCU_Microchip_ATmega:ATxmega384D3-A +MCU_Microchip_ATmega:ATxmega384D3-M +MCU_Microchip_ATmega:ATxmega64A1-A +MCU_Microchip_ATmega:ATxmega64A1-C +MCU_Microchip_ATmega:ATxmega64A1-C7 +MCU_Microchip_ATmega:ATxmega64A1U-A +MCU_Microchip_ATmega:ATxmega64A1U-C +MCU_Microchip_ATmega:ATxmega64A1U-C7 +MCU_Microchip_ATmega:ATxmega64A3-A +MCU_Microchip_ATmega:ATxmega64A3-M +MCU_Microchip_ATmega:ATxmega64A3U-A +MCU_Microchip_ATmega:ATxmega64A3U-M +MCU_Microchip_ATmega:ATxmega64A4U-A +MCU_Microchip_ATmega:ATxmega64A4U-C +MCU_Microchip_ATmega:ATxmega64A4U-M +MCU_Microchip_ATmega:ATxmega64B1-A +MCU_Microchip_ATmega:ATxmega64B1-C +MCU_Microchip_ATmega:ATxmega64B3-A +MCU_Microchip_ATmega:ATxmega64B3-M +MCU_Microchip_ATmega:ATxmega64C3-A +MCU_Microchip_ATmega:ATxmega64C3-M +MCU_Microchip_ATmega:ATxmega64D3-A +MCU_Microchip_ATmega:ATxmega64D3-M +MCU_Microchip_ATmega:ATxmega64D4-A +MCU_Microchip_ATmega:ATxmega64D4-C +MCU_Microchip_ATmega:ATxmega64D4-M +MCU_Microchip_ATmega:ATxmega8E5-A +MCU_Microchip_ATmega:ATxmega8E5-M +MCU_Microchip_ATmega:ATxmega8E5-M4 +MCU_Microchip_ATtiny:ATtiny10-MA +MCU_Microchip_ATtiny:ATtiny10-TS +MCU_Microchip_ATtiny:ATtiny102-M +MCU_Microchip_ATtiny:ATtiny102-SS +MCU_Microchip_ATtiny:ATtiny104-SS +MCU_Microchip_ATtiny:ATtiny13-20M +MCU_Microchip_ATtiny:ATtiny13-20MM +MCU_Microchip_ATtiny:ATtiny13-20P +MCU_Microchip_ATtiny:ATtiny13-20S +MCU_Microchip_ATtiny:ATtiny13-20SS +MCU_Microchip_ATtiny:ATtiny13A-M +MCU_Microchip_ATtiny:ATtiny13A-MM +MCU_Microchip_ATtiny:ATtiny13A-P +MCU_Microchip_ATtiny:ATtiny13A-S +MCU_Microchip_ATtiny:ATtiny13A-SS +MCU_Microchip_ATtiny:ATtiny13V-10M +MCU_Microchip_ATtiny:ATtiny13V-10MM +MCU_Microchip_ATtiny:ATtiny13V-10P +MCU_Microchip_ATtiny:ATtiny13V-10S +MCU_Microchip_ATtiny:ATtiny13V-10SS +MCU_Microchip_ATtiny:ATtiny1604-SS +MCU_Microchip_ATtiny:ATtiny1606-M +MCU_Microchip_ATtiny:ATtiny1606-S +MCU_Microchip_ATtiny:ATtiny1607-M +MCU_Microchip_ATtiny:ATtiny1614-SS +MCU_Microchip_ATtiny:ATtiny1616-M +MCU_Microchip_ATtiny:ATtiny1616-S +MCU_Microchip_ATtiny:ATtiny1617-M +MCU_Microchip_ATtiny:ATtiny1624-SS +MCU_Microchip_ATtiny:ATtiny1624-X +MCU_Microchip_ATtiny:ATtiny1626-M +MCU_Microchip_ATtiny:ATtiny1626-S +MCU_Microchip_ATtiny:ATtiny1626-X +MCU_Microchip_ATtiny:ATtiny1627-M +MCU_Microchip_ATtiny:ATtiny1634-M +MCU_Microchip_ATtiny:ATtiny1634-S +MCU_Microchip_ATtiny:ATtiny167-M +MCU_Microchip_ATtiny:ATtiny167-S +MCU_Microchip_ATtiny:ATtiny167-X +MCU_Microchip_ATtiny:ATtiny20-CC +MCU_Microchip_ATtiny:ATtiny20-MM +MCU_Microchip_ATtiny:ATtiny20-SS +MCU_Microchip_ATtiny:ATtiny20-U +MCU_Microchip_ATtiny:ATtiny20-X +MCU_Microchip_ATtiny:ATtiny202-SS +MCU_Microchip_ATtiny:ATtiny204-SS +MCU_Microchip_ATtiny:ATtiny212-SS +MCU_Microchip_ATtiny:ATtiny214-SS +MCU_Microchip_ATtiny:ATtiny2313-20M +MCU_Microchip_ATtiny:ATtiny2313-20P +MCU_Microchip_ATtiny:ATtiny2313-20S +MCU_Microchip_ATtiny:ATtiny2313A-M +MCU_Microchip_ATtiny:ATtiny2313A-MM +MCU_Microchip_ATtiny:ATtiny2313A-P +MCU_Microchip_ATtiny:ATtiny2313A-S +MCU_Microchip_ATtiny:ATtiny2313V-10M +MCU_Microchip_ATtiny:ATtiny2313V-10P +MCU_Microchip_ATtiny:ATtiny2313V-10S +MCU_Microchip_ATtiny:ATtiny24-20M +MCU_Microchip_ATtiny:ATtiny24-20P +MCU_Microchip_ATtiny:ATtiny24-20SS +MCU_Microchip_ATtiny:ATtiny24A-CC +MCU_Microchip_ATtiny:ATtiny24A-M +MCU_Microchip_ATtiny:ATtiny24A-MM +MCU_Microchip_ATtiny:ATtiny24A-P +MCU_Microchip_ATtiny:ATtiny24A-SS +MCU_Microchip_ATtiny:ATtiny24V-10M +MCU_Microchip_ATtiny:ATtiny24V-10P +MCU_Microchip_ATtiny:ATtiny24V-10SS +MCU_Microchip_ATtiny:ATtiny25-20M +MCU_Microchip_ATtiny:ATtiny25-20P +MCU_Microchip_ATtiny:ATtiny25-20S +MCU_Microchip_ATtiny:ATtiny25-20SS +MCU_Microchip_ATtiny:ATtiny25V-10M +MCU_Microchip_ATtiny:ATtiny25V-10P +MCU_Microchip_ATtiny:ATtiny25V-10S +MCU_Microchip_ATtiny:ATtiny25V-10SS +MCU_Microchip_ATtiny:ATtiny26-16M +MCU_Microchip_ATtiny:ATtiny26-16P +MCU_Microchip_ATtiny:ATtiny26-16S +MCU_Microchip_ATtiny:ATtiny261A-M +MCU_Microchip_ATtiny:ATtiny261A-P +MCU_Microchip_ATtiny:ATtiny261A-S +MCU_Microchip_ATtiny:ATtiny261A-X +MCU_Microchip_ATtiny:ATtiny26L-8M +MCU_Microchip_ATtiny:ATtiny26L-8P +MCU_Microchip_ATtiny:ATtiny26L-8S +MCU_Microchip_ATtiny:ATtiny28L-4A +MCU_Microchip_ATtiny:ATtiny28L-4M +MCU_Microchip_ATtiny:ATtiny28L-4P +MCU_Microchip_ATtiny:ATtiny28V-1A +MCU_Microchip_ATtiny:ATtiny28V-1M +MCU_Microchip_ATtiny:ATtiny28V-1P +MCU_Microchip_ATtiny:ATtiny3216-M +MCU_Microchip_ATtiny:ATtiny3216-S +MCU_Microchip_ATtiny:ATtiny3217-M +MCU_Microchip_ATtiny:ATtiny3224-SS +MCU_Microchip_ATtiny:ATtiny3224-X +MCU_Microchip_ATtiny:ATtiny3226-M +MCU_Microchip_ATtiny:ATtiny3226-S +MCU_Microchip_ATtiny:ATtiny3226-X +MCU_Microchip_ATtiny:ATtiny3227-M +MCU_Microchip_ATtiny:ATtiny4-MA +MCU_Microchip_ATtiny:ATtiny4-TS +MCU_Microchip_ATtiny:ATtiny40-MM +MCU_Microchip_ATtiny:ATtiny40-S +MCU_Microchip_ATtiny:ATtiny40-X +MCU_Microchip_ATtiny:ATtiny402-SS +MCU_Microchip_ATtiny:ATtiny404-SS +MCU_Microchip_ATtiny:ATtiny406-M +MCU_Microchip_ATtiny:ATtiny406-S +MCU_Microchip_ATtiny:ATtiny412-SS +MCU_Microchip_ATtiny:ATtiny414-SS +MCU_Microchip_ATtiny:ATtiny416-M +MCU_Microchip_ATtiny:ATtiny416-S +MCU_Microchip_ATtiny:ATtiny417-M +MCU_Microchip_ATtiny:ATtiny424-SS +MCU_Microchip_ATtiny:ATtiny424-X +MCU_Microchip_ATtiny:ATtiny426-M +MCU_Microchip_ATtiny:ATtiny426-S +MCU_Microchip_ATtiny:ATtiny426-X +MCU_Microchip_ATtiny:ATtiny427-M +MCU_Microchip_ATtiny:ATtiny4313-M +MCU_Microchip_ATtiny:ATtiny4313-MM +MCU_Microchip_ATtiny:ATtiny4313-P +MCU_Microchip_ATtiny:ATtiny4313-S +MCU_Microchip_ATtiny:ATtiny43U-M +MCU_Microchip_ATtiny:ATtiny43U-S +MCU_Microchip_ATtiny:ATtiny44-20M +MCU_Microchip_ATtiny:ATtiny44-20P +MCU_Microchip_ATtiny:ATtiny44-20SS +MCU_Microchip_ATtiny:ATtiny441-M +MCU_Microchip_ATtiny:ATtiny441-MM +MCU_Microchip_ATtiny:ATtiny441-SS +MCU_Microchip_ATtiny:ATtiny44A-CC +MCU_Microchip_ATtiny:ATtiny44A-M +MCU_Microchip_ATtiny:ATtiny44A-MM +MCU_Microchip_ATtiny:ATtiny44A-P +MCU_Microchip_ATtiny:ATtiny44A-SS +MCU_Microchip_ATtiny:ATtiny44V-10M +MCU_Microchip_ATtiny:ATtiny44V-10P +MCU_Microchip_ATtiny:ATtiny44V-10SS +MCU_Microchip_ATtiny:ATtiny45-20M +MCU_Microchip_ATtiny:ATtiny45-20P +MCU_Microchip_ATtiny:ATtiny45-20S +MCU_Microchip_ATtiny:ATtiny45-20X +MCU_Microchip_ATtiny:ATtiny45V-10M +MCU_Microchip_ATtiny:ATtiny45V-10P +MCU_Microchip_ATtiny:ATtiny45V-10S +MCU_Microchip_ATtiny:ATtiny45V-10X +MCU_Microchip_ATtiny:ATtiny461-20M +MCU_Microchip_ATtiny:ATtiny461-20P +MCU_Microchip_ATtiny:ATtiny461-20S +MCU_Microchip_ATtiny:ATtiny461A-M +MCU_Microchip_ATtiny:ATtiny461A-P +MCU_Microchip_ATtiny:ATtiny461A-S +MCU_Microchip_ATtiny:ATtiny461A-X +MCU_Microchip_ATtiny:ATtiny461V-10M +MCU_Microchip_ATtiny:ATtiny461V-10P +MCU_Microchip_ATtiny:ATtiny461V-10S +MCU_Microchip_ATtiny:ATtiny48-A +MCU_Microchip_ATtiny:ATtiny48-CC +MCU_Microchip_ATtiny:ATtiny48-M +MCU_Microchip_ATtiny:ATtiny48-MM +MCU_Microchip_ATtiny:ATtiny48-P +MCU_Microchip_ATtiny:ATtiny5-MA +MCU_Microchip_ATtiny:ATtiny5-TS +MCU_Microchip_ATtiny:ATtiny804-SS +MCU_Microchip_ATtiny:ATtiny806-M +MCU_Microchip_ATtiny:ATtiny806-S +MCU_Microchip_ATtiny:ATtiny807-M +MCU_Microchip_ATtiny:ATtiny814-SS +MCU_Microchip_ATtiny:ATtiny816-M +MCU_Microchip_ATtiny:ATtiny816-S +MCU_Microchip_ATtiny:ATtiny817-M +MCU_Microchip_ATtiny:ATtiny824-SS +MCU_Microchip_ATtiny:ATtiny824-X +MCU_Microchip_ATtiny:ATtiny826-M +MCU_Microchip_ATtiny:ATtiny826-S +MCU_Microchip_ATtiny:ATtiny826-X +MCU_Microchip_ATtiny:ATtiny827-M +MCU_Microchip_ATtiny:ATtiny828-A +MCU_Microchip_ATtiny:ATtiny828-M +MCU_Microchip_ATtiny:ATtiny84-20M +MCU_Microchip_ATtiny:ATtiny84-20P +MCU_Microchip_ATtiny:ATtiny84-20SS +MCU_Microchip_ATtiny:ATtiny841-M +MCU_Microchip_ATtiny:ATtiny841-MM +MCU_Microchip_ATtiny:ATtiny841-SS +MCU_Microchip_ATtiny:ATtiny84A-CC +MCU_Microchip_ATtiny:ATtiny84A-M +MCU_Microchip_ATtiny:ATtiny84A-MM +MCU_Microchip_ATtiny:ATtiny84A-P +MCU_Microchip_ATtiny:ATtiny84A-SS +MCU_Microchip_ATtiny:ATtiny84V-10M +MCU_Microchip_ATtiny:ATtiny84V-10P +MCU_Microchip_ATtiny:ATtiny84V-10SS +MCU_Microchip_ATtiny:ATtiny85-20M +MCU_Microchip_ATtiny:ATtiny85-20P +MCU_Microchip_ATtiny:ATtiny85-20S +MCU_Microchip_ATtiny:ATtiny85V-10M +MCU_Microchip_ATtiny:ATtiny85V-10P +MCU_Microchip_ATtiny:ATtiny85V-10S +MCU_Microchip_ATtiny:ATtiny861-20M +MCU_Microchip_ATtiny:ATtiny861-20P +MCU_Microchip_ATtiny:ATtiny861-20S +MCU_Microchip_ATtiny:ATtiny861A-M +MCU_Microchip_ATtiny:ATtiny861A-P +MCU_Microchip_ATtiny:ATtiny861A-S +MCU_Microchip_ATtiny:ATtiny861A-X +MCU_Microchip_ATtiny:ATtiny861V-10M +MCU_Microchip_ATtiny:ATtiny861V-10P +MCU_Microchip_ATtiny:ATtiny861V-10S +MCU_Microchip_ATtiny:ATtiny87-M +MCU_Microchip_ATtiny:ATtiny87-S +MCU_Microchip_ATtiny:ATtiny87-X +MCU_Microchip_ATtiny:ATtiny88-A +MCU_Microchip_ATtiny:ATtiny88-CC +MCU_Microchip_ATtiny:ATtiny88-M +MCU_Microchip_ATtiny:ATtiny88-MM +MCU_Microchip_ATtiny:ATtiny88-P +MCU_Microchip_ATtiny:ATtiny9-MA +MCU_Microchip_ATtiny:ATtiny9-TS +MCU_Microchip_AVR:AT90CAN128-16A +MCU_Microchip_AVR:AT90CAN128-16M +MCU_Microchip_AVR:AT90CAN32-16A +MCU_Microchip_AVR:AT90CAN32-16M +MCU_Microchip_AVR:AT90CAN64-16A +MCU_Microchip_AVR:AT90CAN64-16M +MCU_Microchip_AVR:AT90PWM1-16M +MCU_Microchip_AVR:AT90PWM1-16S +MCU_Microchip_AVR:AT90USB1286-A +MCU_Microchip_AVR:AT90USB1286-M +MCU_Microchip_AVR:AT90USB1287-A +MCU_Microchip_AVR:AT90USB1287-M +MCU_Microchip_AVR:AT90USB162-16A +MCU_Microchip_AVR:AT90USB162-16M +MCU_Microchip_AVR:AT90USB646-A +MCU_Microchip_AVR:AT90USB646-M +MCU_Microchip_AVR:AT90USB647-A +MCU_Microchip_AVR:AT90USB647-M +MCU_Microchip_AVR:AT90USB82-16M +MCU_Microchip_AVR_Dx:AVR128DA28x-xSO +MCU_Microchip_AVR_Dx:AVR128DA28x-xSP +MCU_Microchip_AVR_Dx:AVR128DA28x-xSS +MCU_Microchip_AVR_Dx:AVR128DA32x-xPT +MCU_Microchip_AVR_Dx:AVR128DA32x-xRXB +MCU_Microchip_AVR_Dx:AVR128DA48x-x6LX +MCU_Microchip_AVR_Dx:AVR128DA48x-xPT +MCU_Microchip_AVR_Dx:AVR128DA64x-xMR +MCU_Microchip_AVR_Dx:AVR128DA64x-xPT +MCU_Microchip_AVR_Dx:AVR128DB28x-xSO +MCU_Microchip_AVR_Dx:AVR128DB28x-xSP +MCU_Microchip_AVR_Dx:AVR128DB28x-xSS +MCU_Microchip_AVR_Dx:AVR128DB32x-xPT +MCU_Microchip_AVR_Dx:AVR128DB32x-xRXB +MCU_Microchip_AVR_Dx:AVR128DB48x-x6LX +MCU_Microchip_AVR_Dx:AVR128DB48x-xPT +MCU_Microchip_AVR_Dx:AVR128DB64x-xMR +MCU_Microchip_AVR_Dx:AVR128DB64x-xPT +MCU_Microchip_AVR_Dx:AVR32DA28x-xSO +MCU_Microchip_AVR_Dx:AVR32DA28x-xSP +MCU_Microchip_AVR_Dx:AVR32DA28x-xSS +MCU_Microchip_AVR_Dx:AVR32DA32x-xPT +MCU_Microchip_AVR_Dx:AVR32DA32x-xRXB +MCU_Microchip_AVR_Dx:AVR32DA48x-x6LX +MCU_Microchip_AVR_Dx:AVR32DA48x-xPT +MCU_Microchip_AVR_Dx:AVR32DB28x-xSO +MCU_Microchip_AVR_Dx:AVR32DB28x-xSP +MCU_Microchip_AVR_Dx:AVR32DB28x-xSS +MCU_Microchip_AVR_Dx:AVR32DB32x-xPT +MCU_Microchip_AVR_Dx:AVR32DB32x-xRXB +MCU_Microchip_AVR_Dx:AVR32DB48x-x6LX +MCU_Microchip_AVR_Dx:AVR32DB48x-xPT +MCU_Microchip_AVR_Dx:AVR64DA28x-xSO +MCU_Microchip_AVR_Dx:AVR64DA28x-xSP +MCU_Microchip_AVR_Dx:AVR64DA28x-xSS +MCU_Microchip_AVR_Dx:AVR64DA32x-xPT +MCU_Microchip_AVR_Dx:AVR64DA32x-xRXB +MCU_Microchip_AVR_Dx:AVR64DA48x-x6LX +MCU_Microchip_AVR_Dx:AVR64DA48x-xPT +MCU_Microchip_AVR_Dx:AVR64DA64x-xMR +MCU_Microchip_AVR_Dx:AVR64DA64x-xPT +MCU_Microchip_AVR_Dx:AVR64DB28x-xSO +MCU_Microchip_AVR_Dx:AVR64DB28x-xSP +MCU_Microchip_AVR_Dx:AVR64DB28x-xSS +MCU_Microchip_AVR_Dx:AVR64DB32x-xPT +MCU_Microchip_AVR_Dx:AVR64DB32x-xRXB +MCU_Microchip_AVR_Dx:AVR64DB48x-x6LX +MCU_Microchip_AVR_Dx:AVR64DB48x-xPT +MCU_Microchip_AVR_Dx:AVR64DB64x-xMR +MCU_Microchip_AVR_Dx:AVR64DB64x-xPT +MCU_Microchip_PIC10:PIC10F200-IMC +MCU_Microchip_PIC10:PIC10F200-IOT +MCU_Microchip_PIC10:PIC10F200-IP +MCU_Microchip_PIC10:PIC10F202-IMC +MCU_Microchip_PIC10:PIC10F202-IOT +MCU_Microchip_PIC10:PIC10F202-IP +MCU_Microchip_PIC10:PIC10F204-IMC +MCU_Microchip_PIC10:PIC10F204-IOT +MCU_Microchip_PIC10:PIC10F204-IP +MCU_Microchip_PIC10:PIC10F206-IMC +MCU_Microchip_PIC10:PIC10F206-IOT +MCU_Microchip_PIC10:PIC10F206-IP +MCU_Microchip_PIC10:PIC10F220-IMC +MCU_Microchip_PIC10:PIC10F220-IOT +MCU_Microchip_PIC10:PIC10F220-IP +MCU_Microchip_PIC10:PIC10F222-IMC +MCU_Microchip_PIC10:PIC10F222-IOT +MCU_Microchip_PIC10:PIC10F222-IP +MCU_Microchip_PIC10:PIC10F320-IMC +MCU_Microchip_PIC10:PIC10F320-IOT +MCU_Microchip_PIC10:PIC10F320-IP +MCU_Microchip_PIC10:PIC10F322-IMC +MCU_Microchip_PIC10:PIC10F322-IOT +MCU_Microchip_PIC10:PIC10F322-IP +MCU_Microchip_PIC12:PIC12C508-xJW +MCU_Microchip_PIC12:PIC12C508-xP +MCU_Microchip_PIC12:PIC12C508-xSM +MCU_Microchip_PIC12:PIC12C508A-xJW +MCU_Microchip_PIC12:PIC12C508A-xP +MCU_Microchip_PIC12:PIC12C508A-xSM +MCU_Microchip_PIC12:PIC12C508A-xSN +MCU_Microchip_PIC12:PIC12C509-xJW +MCU_Microchip_PIC12:PIC12C509-xP +MCU_Microchip_PIC12:PIC12C509-xSM +MCU_Microchip_PIC12:PIC12C509A-xJW +MCU_Microchip_PIC12:PIC12C509A-xP +MCU_Microchip_PIC12:PIC12C509A-xSM +MCU_Microchip_PIC12:PIC12C509A-xSN +MCU_Microchip_PIC12:PIC12C671-xJW +MCU_Microchip_PIC12:PIC12C671-xP +MCU_Microchip_PIC12:PIC12C671-xSN +MCU_Microchip_PIC12:PIC12C672-xJW +MCU_Microchip_PIC12:PIC12C672-xP +MCU_Microchip_PIC12:PIC12C672-xSN +MCU_Microchip_PIC12:PIC12CE518-xJW +MCU_Microchip_PIC12:PIC12CE518-xP +MCU_Microchip_PIC12:PIC12CE518-xSM +MCU_Microchip_PIC12:PIC12CE518-xSN +MCU_Microchip_PIC12:PIC12CE519-xJW +MCU_Microchip_PIC12:PIC12CE519-xP +MCU_Microchip_PIC12:PIC12CE519-xSM +MCU_Microchip_PIC12:PIC12CE519-xSN +MCU_Microchip_PIC12:PIC12CE673-xJW +MCU_Microchip_PIC12:PIC12CE673-xP +MCU_Microchip_PIC12:PIC12CE674-xJW +MCU_Microchip_PIC12:PIC12CE674-xP +MCU_Microchip_PIC12:PIC12CR509A-xP +MCU_Microchip_PIC12:PIC12CR509A-xSM +MCU_Microchip_PIC12:PIC12CR509A-xSN +MCU_Microchip_PIC12:PIC12F1501-xMC +MCU_Microchip_PIC12:PIC12F1501-xMS +MCU_Microchip_PIC12:PIC12F1501-xP +MCU_Microchip_PIC12:PIC12F1501-xSN +MCU_Microchip_PIC12:PIC12F1822-xMC +MCU_Microchip_PIC12:PIC12F1822-xP +MCU_Microchip_PIC12:PIC12F1822-xSN +MCU_Microchip_PIC12:PIC12F1840-xMC +MCU_Microchip_PIC12:PIC12F1840-xP +MCU_Microchip_PIC12:PIC12F1840-xSN +MCU_Microchip_PIC12:PIC12F508-xMC +MCU_Microchip_PIC12:PIC12F508-xMS +MCU_Microchip_PIC12:PIC12F508-xP +MCU_Microchip_PIC12:PIC12F508-xSN +MCU_Microchip_PIC12:PIC12F509-xMC +MCU_Microchip_PIC12:PIC12F509-xMS +MCU_Microchip_PIC12:PIC12F509-xP +MCU_Microchip_PIC12:PIC12F509-xSN +MCU_Microchip_PIC12:PIC12F510-xMC +MCU_Microchip_PIC12:PIC12F510-xMS +MCU_Microchip_PIC12:PIC12F510-xP +MCU_Microchip_PIC12:PIC12F510-xSN +MCU_Microchip_PIC12:PIC12F519-xMC +MCU_Microchip_PIC12:PIC12F519-xMS +MCU_Microchip_PIC12:PIC12F519-xP +MCU_Microchip_PIC12:PIC12F519-xSN +MCU_Microchip_PIC12:PIC12F609-xMC +MCU_Microchip_PIC12:PIC12F609-xMS +MCU_Microchip_PIC12:PIC12F609-xP +MCU_Microchip_PIC12:PIC12F609-xSN +MCU_Microchip_PIC12:PIC12F615-xMC +MCU_Microchip_PIC12:PIC12F615-xMS +MCU_Microchip_PIC12:PIC12F615-xP +MCU_Microchip_PIC12:PIC12F615-xSN +MCU_Microchip_PIC12:PIC12F617-xMC +MCU_Microchip_PIC12:PIC12F617-xMS +MCU_Microchip_PIC12:PIC12F617-xP +MCU_Microchip_PIC12:PIC12F617-xSN +MCU_Microchip_PIC12:PIC12F629-xMC +MCU_Microchip_PIC12:PIC12F629-xMS +MCU_Microchip_PIC12:PIC12F629-xP +MCU_Microchip_PIC12:PIC12F629-xSN +MCU_Microchip_PIC12:PIC12F635-xMC +MCU_Microchip_PIC12:PIC12F635-xMS +MCU_Microchip_PIC12:PIC12F635-xP +MCU_Microchip_PIC12:PIC12F635-xSN +MCU_Microchip_PIC12:PIC12F675-xMC +MCU_Microchip_PIC12:PIC12F675-xMS +MCU_Microchip_PIC12:PIC12F675-xP +MCU_Microchip_PIC12:PIC12F675-xSN +MCU_Microchip_PIC12:PIC12F683-xMC +MCU_Microchip_PIC12:PIC12F683-xMS +MCU_Microchip_PIC12:PIC12F683-xP +MCU_Microchip_PIC12:PIC12F683-xSN +MCU_Microchip_PIC12:PIC12F752-xMC +MCU_Microchip_PIC12:PIC12F752-xP +MCU_Microchip_PIC12:PIC12F752-xSN +MCU_Microchip_PIC12:PIC12HV609-xMC +MCU_Microchip_PIC12:PIC12HV609-xMS +MCU_Microchip_PIC12:PIC12HV609-xP +MCU_Microchip_PIC12:PIC12HV609-xSN +MCU_Microchip_PIC12:PIC12HV615-xMC +MCU_Microchip_PIC12:PIC12HV615-xMS +MCU_Microchip_PIC12:PIC12HV615-xP +MCU_Microchip_PIC12:PIC12HV615-xSN +MCU_Microchip_PIC12:PIC12HV752-xMC +MCU_Microchip_PIC12:PIC12HV752-xP +MCU_Microchip_PIC12:PIC12HV752-xSN +MCU_Microchip_PIC12:PIC12LF1501-xMC +MCU_Microchip_PIC12:PIC12LF1501-xMS +MCU_Microchip_PIC12:PIC12LF1501-xP +MCU_Microchip_PIC12:PIC12LF1501-xSN +MCU_Microchip_PIC12:PIC12LF1822-xMC +MCU_Microchip_PIC12:PIC12LF1822-xP +MCU_Microchip_PIC12:PIC12LF1822-xSN +MCU_Microchip_PIC12:PIC12LF1840-xMC +MCU_Microchip_PIC12:PIC12LF1840-xP +MCU_Microchip_PIC12:PIC12LF1840-xSN +MCU_Microchip_PIC12:PIC12LF1840T48-xST +MCU_Microchip_PIC16:PIC16C505-IP +MCU_Microchip_PIC16:PIC16C505-ISL +MCU_Microchip_PIC16:PIC16C505-IST +MCU_Microchip_PIC16:PIC16F1454-IML +MCU_Microchip_PIC16:PIC16F1454-IP +MCU_Microchip_PIC16:PIC16F1454-ISL +MCU_Microchip_PIC16:PIC16F1454-ISS +MCU_Microchip_PIC16:PIC16F1454-IST +MCU_Microchip_PIC16:PIC16F1455-IML +MCU_Microchip_PIC16:PIC16F1455-IP +MCU_Microchip_PIC16:PIC16F1455-ISL +MCU_Microchip_PIC16:PIC16F1455-ISS +MCU_Microchip_PIC16:PIC16F1455-IST +MCU_Microchip_PIC16:PIC16F1459-IML +MCU_Microchip_PIC16:PIC16F1459-IP +MCU_Microchip_PIC16:PIC16F1459-ISO +MCU_Microchip_PIC16:PIC16F1459-ISS +MCU_Microchip_PIC16:PIC16F1459-IST +MCU_Microchip_PIC16:PIC16F1503-IMG +MCU_Microchip_PIC16:PIC16F1503-IP +MCU_Microchip_PIC16:PIC16F1503-ISL +MCU_Microchip_PIC16:PIC16F1503-IST +MCU_Microchip_PIC16:PIC16F1507-IML +MCU_Microchip_PIC16:PIC16F1507-IP +MCU_Microchip_PIC16:PIC16F1507-ISO +MCU_Microchip_PIC16:PIC16F1507-ISS +MCU_Microchip_PIC16:PIC16F1508-IML +MCU_Microchip_PIC16:PIC16F1508-IP +MCU_Microchip_PIC16:PIC16F1508-ISO +MCU_Microchip_PIC16:PIC16F1508-ISS +MCU_Microchip_PIC16:PIC16F1509-IML +MCU_Microchip_PIC16:PIC16F1509-IP +MCU_Microchip_PIC16:PIC16F1509-ISO +MCU_Microchip_PIC16:PIC16F1509-ISS +MCU_Microchip_PIC16:PIC16F1512-IMV +MCU_Microchip_PIC16:PIC16F1512-ISO +MCU_Microchip_PIC16:PIC16F1512-ISP +MCU_Microchip_PIC16:PIC16F1512-ISS +MCU_Microchip_PIC16:PIC16F1513-IMV +MCU_Microchip_PIC16:PIC16F1513-ISO +MCU_Microchip_PIC16:PIC16F1513-ISP +MCU_Microchip_PIC16:PIC16F1513-ISS +MCU_Microchip_PIC16:PIC16F1516-IMV +MCU_Microchip_PIC16:PIC16F1516-ISO +MCU_Microchip_PIC16:PIC16F1516-ISP +MCU_Microchip_PIC16:PIC16F1516-ISS +MCU_Microchip_PIC16:PIC16F1517-IMV +MCU_Microchip_PIC16:PIC16F1517-IP +MCU_Microchip_PIC16:PIC16F1517-IPT +MCU_Microchip_PIC16:PIC16F1518-IMV +MCU_Microchip_PIC16:PIC16F1518-ISO +MCU_Microchip_PIC16:PIC16F1518-ISP +MCU_Microchip_PIC16:PIC16F1518-ISS +MCU_Microchip_PIC16:PIC16F1519-IMV +MCU_Microchip_PIC16:PIC16F1519-IP +MCU_Microchip_PIC16:PIC16F1519-IPT +MCU_Microchip_PIC16:PIC16F1526-IMR +MCU_Microchip_PIC16:PIC16F1526-IPT +MCU_Microchip_PIC16:PIC16F1527-IMR +MCU_Microchip_PIC16:PIC16F1527-IPT +MCU_Microchip_PIC16:PIC16F15323-xSL +MCU_Microchip_PIC16:PIC16F15356-xML +MCU_Microchip_PIC16:PIC16F15356-xMV +MCU_Microchip_PIC16:PIC16F15356-xSO +MCU_Microchip_PIC16:PIC16F15356-xSP +MCU_Microchip_PIC16:PIC16F15356-xSS +MCU_Microchip_PIC16:PIC16F15375-xML +MCU_Microchip_PIC16:PIC16F15375-xMV +MCU_Microchip_PIC16:PIC16F15375-xP +MCU_Microchip_PIC16:PIC16F15375-xPT +MCU_Microchip_PIC16:PIC16F15376-xML +MCU_Microchip_PIC16:PIC16F15376-xMV +MCU_Microchip_PIC16:PIC16F15376-xP +MCU_Microchip_PIC16:PIC16F15376-xPT +MCU_Microchip_PIC16:PIC16F15385-xMV +MCU_Microchip_PIC16:PIC16F15385-xPT +MCU_Microchip_PIC16:PIC16F15386-xMV +MCU_Microchip_PIC16:PIC16F15386-xPT +MCU_Microchip_PIC16:PIC16F1619-xGZ +MCU_Microchip_PIC16:PIC16F1619-xML +MCU_Microchip_PIC16:PIC16F1619-xP +MCU_Microchip_PIC16:PIC16F1619-xSO +MCU_Microchip_PIC16:PIC16F1619-xSS +MCU_Microchip_PIC16:PIC16F1786-xML +MCU_Microchip_PIC16:PIC16F1786-xP +MCU_Microchip_PIC16:PIC16F1786-xSO +MCU_Microchip_PIC16:PIC16F1786-xSP +MCU_Microchip_PIC16:PIC16F1786-xSS +MCU_Microchip_PIC16:PIC16F1829-IML +MCU_Microchip_PIC16:PIC16F1829-IP +MCU_Microchip_PIC16:PIC16F1829-ISL +MCU_Microchip_PIC16:PIC16F1829-ISO +MCU_Microchip_PIC16:PIC16F1829-ISS +MCU_Microchip_PIC16:PIC16F1829-IST +MCU_Microchip_PIC16:PIC16F1829LIN-ESS +MCU_Microchip_PIC16:PIC16F18324-xSL +MCU_Microchip_PIC16:PIC16F18325-ISL +MCU_Microchip_PIC16:PIC16F18325-xGZ +MCU_Microchip_PIC16:PIC16F18325-xJQ +MCU_Microchip_PIC16:PIC16F18344-GZ +MCU_Microchip_PIC16:PIC16F18344-P +MCU_Microchip_PIC16:PIC16F18344-SO +MCU_Microchip_PIC16:PIC16F18344-SS +MCU_Microchip_PIC16:PIC16F18344-xSL +MCU_Microchip_PIC16:PIC16F18346-GZ +MCU_Microchip_PIC16:PIC16F18346-P +MCU_Microchip_PIC16:PIC16F18346-SO +MCU_Microchip_PIC16:PIC16F18346-SS_0 +MCU_Microchip_PIC16:PIC16F18854-xSO +MCU_Microchip_PIC16:PIC16F18855-xMV +MCU_Microchip_PIC16:PIC16F18855-xSO +MCU_Microchip_PIC16:PIC16F19195-x5LX +MCU_Microchip_PIC16:PIC16F19195-xMR +MCU_Microchip_PIC16:PIC16F19195-xPT +MCU_Microchip_PIC16:PIC16F19196-x5LX +MCU_Microchip_PIC16:PIC16F19196-xMR +MCU_Microchip_PIC16:PIC16F19196-xPT +MCU_Microchip_PIC16:PIC16F19197-x5LX +MCU_Microchip_PIC16:PIC16F19197-xMR +MCU_Microchip_PIC16:PIC16F19197-xPT +MCU_Microchip_PIC16:PIC16F1934-IML +MCU_Microchip_PIC16:PIC16F1934-IPT +MCU_Microchip_PIC16:PIC16F1937-IML +MCU_Microchip_PIC16:PIC16F1937-IPT +MCU_Microchip_PIC16:PIC16F1939-IML +MCU_Microchip_PIC16:PIC16F1939-IPT +MCU_Microchip_PIC16:PIC16F505-IMG +MCU_Microchip_PIC16:PIC16F505-IP +MCU_Microchip_PIC16:PIC16F505-ISL +MCU_Microchip_PIC16:PIC16F505-IST +MCU_Microchip_PIC16:PIC16F54-IP +MCU_Microchip_PIC16:PIC16F54-ISO +MCU_Microchip_PIC16:PIC16F54-ISS +MCU_Microchip_PIC16:PIC16F610-IML +MCU_Microchip_PIC16:PIC16F610-IP +MCU_Microchip_PIC16:PIC16F616-IML +MCU_Microchip_PIC16:PIC16F616-IP +MCU_Microchip_PIC16:PIC16F627-xxIP +MCU_Microchip_PIC16:PIC16F627-xxISO +MCU_Microchip_PIC16:PIC16F627-xxISS +MCU_Microchip_PIC16:PIC16F627A-IP +MCU_Microchip_PIC16:PIC16F627A-ISO +MCU_Microchip_PIC16:PIC16F627A-ISS +MCU_Microchip_PIC16:PIC16F628-xxIP +MCU_Microchip_PIC16:PIC16F628-xxISO +MCU_Microchip_PIC16:PIC16F628-xxISS +MCU_Microchip_PIC16:PIC16F628A-IP +MCU_Microchip_PIC16:PIC16F628A-ISO +MCU_Microchip_PIC16:PIC16F628A-ISS +MCU_Microchip_PIC16:PIC16F631-IP +MCU_Microchip_PIC16:PIC16F631-ISO +MCU_Microchip_PIC16:PIC16F631-ISS +MCU_Microchip_PIC16:PIC16F648A-IP +MCU_Microchip_PIC16:PIC16F648A-ISO +MCU_Microchip_PIC16:PIC16F648A-ISS +MCU_Microchip_PIC16:PIC16F677-IP +MCU_Microchip_PIC16:PIC16F677-ISO +MCU_Microchip_PIC16:PIC16F677-ISS +MCU_Microchip_PIC16:PIC16F684-IML +MCU_Microchip_PIC16:PIC16F684-IP +MCU_Microchip_PIC16:PIC16F685-IP +MCU_Microchip_PIC16:PIC16F685-ISO +MCU_Microchip_PIC16:PIC16F685-ISS +MCU_Microchip_PIC16:PIC16F687-IP +MCU_Microchip_PIC16:PIC16F687-ISO +MCU_Microchip_PIC16:PIC16F687-ISS +MCU_Microchip_PIC16:PIC16F689-IP +MCU_Microchip_PIC16:PIC16F689-ISO +MCU_Microchip_PIC16:PIC16F689-ISS +MCU_Microchip_PIC16:PIC16F690-IP +MCU_Microchip_PIC16:PIC16F690-ISO +MCU_Microchip_PIC16:PIC16F690-ISS +MCU_Microchip_PIC16:PIC16F716-IP +MCU_Microchip_PIC16:PIC16F716-ISO +MCU_Microchip_PIC16:PIC16F716-ISS +MCU_Microchip_PIC16:PIC16F73-IML +MCU_Microchip_PIC16:PIC16F73-ISO +MCU_Microchip_PIC16:PIC16F73-ISP +MCU_Microchip_PIC16:PIC16F73-ISS +MCU_Microchip_PIC16:PIC16F74-IP +MCU_Microchip_PIC16:PIC16F76-IML +MCU_Microchip_PIC16:PIC16F76-ISO +MCU_Microchip_PIC16:PIC16F76-ISP +MCU_Microchip_PIC16:PIC16F76-ISS +MCU_Microchip_PIC16:PIC16F77-IP +MCU_Microchip_PIC16:PIC16F818-IML +MCU_Microchip_PIC16:PIC16F818-IP +MCU_Microchip_PIC16:PIC16F818-ISO +MCU_Microchip_PIC16:PIC16F818-ISS +MCU_Microchip_PIC16:PIC16F819-IML +MCU_Microchip_PIC16:PIC16F819-IP +MCU_Microchip_PIC16:PIC16F819-ISO +MCU_Microchip_PIC16:PIC16F819-ISS +MCU_Microchip_PIC16:PIC16F83-XXP +MCU_Microchip_PIC16:PIC16F83-XXSO +MCU_Microchip_PIC16:PIC16F84-XXP +MCU_Microchip_PIC16:PIC16F84-XXSO +MCU_Microchip_PIC16:PIC16F84A-XXP +MCU_Microchip_PIC16:PIC16F84A-XXSO +MCU_Microchip_PIC16:PIC16F84A-XXSS +MCU_Microchip_PIC16:PIC16F870-ISO +MCU_Microchip_PIC16:PIC16F870-ISP +MCU_Microchip_PIC16:PIC16F870-ISS +MCU_Microchip_PIC16:PIC16F871-IL +MCU_Microchip_PIC16:PIC16F871-IP +MCU_Microchip_PIC16:PIC16F871-IPT +MCU_Microchip_PIC16:PIC16F873-XXISO +MCU_Microchip_PIC16:PIC16F873-XXISP +MCU_Microchip_PIC16:PIC16F874-XXIP +MCU_Microchip_PIC16:PIC16F874A-IP +MCU_Microchip_PIC16:PIC16F874A-IPT +MCU_Microchip_PIC16:PIC16F876-XXISO +MCU_Microchip_PIC16:PIC16F876-XXISP +MCU_Microchip_PIC16:PIC16F877-XXIP +MCU_Microchip_PIC16:PIC16F877A-IP +MCU_Microchip_PIC16:PIC16F877A-IPT +MCU_Microchip_PIC16:PIC16F88-IML +MCU_Microchip_PIC16:PIC16F88-IP +MCU_Microchip_PIC16:PIC16F882-IP +MCU_Microchip_PIC16:PIC16F883-IP +MCU_Microchip_PIC16:PIC16F884-IP +MCU_Microchip_PIC16:PIC16F886-IP +MCU_Microchip_PIC16:PIC16F887-IP +MCU_Microchip_PIC16:PIC16LF15356-xML +MCU_Microchip_PIC16:PIC16LF15356-xMV +MCU_Microchip_PIC16:PIC16LF15356-xSO +MCU_Microchip_PIC16:PIC16LF15356-xSP +MCU_Microchip_PIC16:PIC16LF15356-xSS +MCU_Microchip_PIC16:PIC16LF15375-xML +MCU_Microchip_PIC16:PIC16LF15375-xMV +MCU_Microchip_PIC16:PIC16LF15375-xP +MCU_Microchip_PIC16:PIC16LF15375-xPT +MCU_Microchip_PIC16:PIC16LF15376-xML +MCU_Microchip_PIC16:PIC16LF15376-xMV +MCU_Microchip_PIC16:PIC16LF15376-xP +MCU_Microchip_PIC16:PIC16LF15376-xPT +MCU_Microchip_PIC16:PIC16LF15385-xMV +MCU_Microchip_PIC16:PIC16LF15385-xPT +MCU_Microchip_PIC16:PIC16LF15386-xMV +MCU_Microchip_PIC16:PIC16LF15386-xPT +MCU_Microchip_PIC16:PIC16LF1786-xML +MCU_Microchip_PIC16:PIC16LF1786-xP +MCU_Microchip_PIC16:PIC16LF1786-xSO +MCU_Microchip_PIC16:PIC16LF1786-xSP +MCU_Microchip_PIC16:PIC16LF1786-xSS +MCU_Microchip_PIC16:PIC16LF18325-ISL +MCU_Microchip_PIC16:PIC16LF18325-xGZ +MCU_Microchip_PIC16:PIC16LF18325-xJQ +MCU_Microchip_PIC16:PIC16LF1904-IP +MCU_Microchip_PIC16:PIC16LF1907-IP +MCU_Microchip_PIC16:PIC16LF19195-x5LX +MCU_Microchip_PIC16:PIC16LF19195-xMR +MCU_Microchip_PIC16:PIC16LF19195-xPT +MCU_Microchip_PIC16:PIC16LF19196-x5LX +MCU_Microchip_PIC16:PIC16LF19196-xMR +MCU_Microchip_PIC16:PIC16LF19196-xPT +MCU_Microchip_PIC16:PIC16LF19197-x5LX +MCU_Microchip_PIC16:PIC16LF19197-xMR +MCU_Microchip_PIC16:PIC16LF19197-xPT +MCU_Microchip_PIC18:PIC18F1220-SO +MCU_Microchip_PIC18:PIC18F1320-SO +MCU_Microchip_PIC18:PIC18F13K50-EP +MCU_Microchip_PIC18:PIC18F13K50-ESO +MCU_Microchip_PIC18:PIC18F13K50-ESS +MCU_Microchip_PIC18:PIC18F14K50-EP +MCU_Microchip_PIC18:PIC18F14K50-ESO +MCU_Microchip_PIC18:PIC18F14K50-ESS +MCU_Microchip_PIC18:PIC18F2331-IML +MCU_Microchip_PIC18:PIC18F2331-ISO +MCU_Microchip_PIC18:PIC18F2331-ISP +MCU_Microchip_PIC18:PIC18F23K20_ISS +MCU_Microchip_PIC18:PIC18F23K22-xSO +MCU_Microchip_PIC18:PIC18F23K22-xSP +MCU_Microchip_PIC18:PIC18F2420-xML +MCU_Microchip_PIC18:PIC18F2420-xSP +MCU_Microchip_PIC18:PIC18F2431-IML +MCU_Microchip_PIC18:PIC18F2431-ISO +MCU_Microchip_PIC18:PIC18F2431-ISP +MCU_Microchip_PIC18:PIC18F2450-IML +MCU_Microchip_PIC18:PIC18F2450-ISO +MCU_Microchip_PIC18:PIC18F2450-ISP +MCU_Microchip_PIC18:PIC18F2455-ISO +MCU_Microchip_PIC18:PIC18F2455-ISP +MCU_Microchip_PIC18:PIC18F24K20_ISS +MCU_Microchip_PIC18:PIC18F24K22-xSO +MCU_Microchip_PIC18:PIC18F24K22-xSP +MCU_Microchip_PIC18:PIC18F24K50-xML +MCU_Microchip_PIC18:PIC18F24K50-xSO +MCU_Microchip_PIC18:PIC18F24K50-xSP +MCU_Microchip_PIC18:PIC18F24K50-xSS +MCU_Microchip_PIC18:PIC18F2520-xML +MCU_Microchip_PIC18:PIC18F2520-xSP +MCU_Microchip_PIC18:PIC18F2550-ISO +MCU_Microchip_PIC18:PIC18F2550-ISP +MCU_Microchip_PIC18:PIC18F25K20_ISS +MCU_Microchip_PIC18:PIC18F25K22-xSO +MCU_Microchip_PIC18:PIC18F25K22-xSP +MCU_Microchip_PIC18:PIC18F25K50-xML +MCU_Microchip_PIC18:PIC18F25K50-xSO +MCU_Microchip_PIC18:PIC18F25K50-xSP +MCU_Microchip_PIC18:PIC18F25K50-xSS +MCU_Microchip_PIC18:PIC18F25K80_IML +MCU_Microchip_PIC18:PIC18F25K80_ISS +MCU_Microchip_PIC18:PIC18F25K83-xSP +MCU_Microchip_PIC18:PIC18F26K20_ISS +MCU_Microchip_PIC18:PIC18F26K22-xSO +MCU_Microchip_PIC18:PIC18F26K22-xSP +MCU_Microchip_PIC18:PIC18F26K80_IML +MCU_Microchip_PIC18:PIC18F26K80_ISS +MCU_Microchip_PIC18:PIC18F26K83-xSP +MCU_Microchip_PIC18:PIC18F27J53_ISS +MCU_Microchip_PIC18:PIC18F4331-IML +MCU_Microchip_PIC18:PIC18F4331-IP +MCU_Microchip_PIC18:PIC18F4331-IPT +MCU_Microchip_PIC18:PIC18F442-IP +MCU_Microchip_PIC18:PIC18F442-IPT +MCU_Microchip_PIC18:PIC18F4420-xP +MCU_Microchip_PIC18:PIC18F4431-IML +MCU_Microchip_PIC18:PIC18F4431-IP +MCU_Microchip_PIC18:PIC18F4431-IPT +MCU_Microchip_PIC18:PIC18F4450-IML +MCU_Microchip_PIC18:PIC18F4450-IP +MCU_Microchip_PIC18:PIC18F4450-IPT +MCU_Microchip_PIC18:PIC18F4455-IML +MCU_Microchip_PIC18:PIC18F4455-IP +MCU_Microchip_PIC18:PIC18F4455-IPT +MCU_Microchip_PIC18:PIC18F4458-IML +MCU_Microchip_PIC18:PIC18F4458-IP +MCU_Microchip_PIC18:PIC18F4458-IPT +MCU_Microchip_PIC18:PIC18F448-IP +MCU_Microchip_PIC18:PIC18F4480-IP +MCU_Microchip_PIC18:PIC18F44J10-IP +MCU_Microchip_PIC18:PIC18F452-IP +MCU_Microchip_PIC18:PIC18F452-IPT +MCU_Microchip_PIC18:PIC18F4520-xP +MCU_Microchip_PIC18:PIC18F4550-IML +MCU_Microchip_PIC18:PIC18F4550-IP +MCU_Microchip_PIC18:PIC18F4550-IPT +MCU_Microchip_PIC18:PIC18F4553-IML +MCU_Microchip_PIC18:PIC18F4553-IP +MCU_Microchip_PIC18:PIC18F4553-IPT +MCU_Microchip_PIC18:PIC18F458-IP +MCU_Microchip_PIC18:PIC18F4580-IP +MCU_Microchip_PIC18:PIC18F45J10-IP +MCU_Microchip_PIC18:PIC18F45K50_QFP +MCU_Microchip_PIC18:PIC18F45K80-IML +MCU_Microchip_PIC18:PIC18F45K80-IPT +MCU_Microchip_PIC18:PIC18F46K22-xPT +MCU_Microchip_PIC18:PIC18F46K80-IML +MCU_Microchip_PIC18:PIC18F46K80-IPT +MCU_Microchip_PIC18:PIC18F66J60-IPT +MCU_Microchip_PIC18:PIC18F66J65-IPT +MCU_Microchip_PIC18:PIC18F67J60-IPT +MCU_Microchip_PIC18:PIC18F87K22-xPT +MCU_Microchip_PIC18:PIC18F96J60-IPF +MCU_Microchip_PIC18:PIC18F96J60-IPT +MCU_Microchip_PIC18:PIC18F96J65-IPF +MCU_Microchip_PIC18:PIC18F96J65-IPT +MCU_Microchip_PIC18:PIC18F97J60-IPF +MCU_Microchip_PIC18:PIC18F97J60-IPT +MCU_Microchip_PIC18:PIC18LF1220-SO +MCU_Microchip_PIC18:PIC18LF1320-SO +MCU_Microchip_PIC18:PIC18LF13K50-EP +MCU_Microchip_PIC18:PIC18LF13K50-ESO +MCU_Microchip_PIC18:PIC18LF13K50-ESS +MCU_Microchip_PIC18:PIC18LF14K50-EP +MCU_Microchip_PIC18:PIC18LF14K50-ESO +MCU_Microchip_PIC18:PIC18LF14K50-ESS +MCU_Microchip_PIC18:PIC18LF2331-IML +MCU_Microchip_PIC18:PIC18LF2331-ISO +MCU_Microchip_PIC18:PIC18LF2331-ISP +MCU_Microchip_PIC18:PIC18LF2431-IML +MCU_Microchip_PIC18:PIC18LF2431-ISO +MCU_Microchip_PIC18:PIC18LF2431-ISP +MCU_Microchip_PIC18:PIC18LF2450-IML +MCU_Microchip_PIC18:PIC18LF2450-ISO +MCU_Microchip_PIC18:PIC18LF2450-ISP +MCU_Microchip_PIC18:PIC18LF2455-ISO +MCU_Microchip_PIC18:PIC18LF2455-ISP +MCU_Microchip_PIC18:PIC18LF24K50-xML +MCU_Microchip_PIC18:PIC18LF24K50-xSO +MCU_Microchip_PIC18:PIC18LF24K50-xSP +MCU_Microchip_PIC18:PIC18LF24K50-xSS +MCU_Microchip_PIC18:PIC18LF2550-ISO +MCU_Microchip_PIC18:PIC18LF2550-ISP +MCU_Microchip_PIC18:PIC18LF25K50-xML +MCU_Microchip_PIC18:PIC18LF25K50-xSO +MCU_Microchip_PIC18:PIC18LF25K50-xSP +MCU_Microchip_PIC18:PIC18LF25K50-xSS +MCU_Microchip_PIC18:PIC18LF25K80_IML +MCU_Microchip_PIC18:PIC18LF25K80_ISS +MCU_Microchip_PIC18:PIC18LF25K83-xSP +MCU_Microchip_PIC18:PIC18LF26K80_IML +MCU_Microchip_PIC18:PIC18LF26K80_ISS +MCU_Microchip_PIC18:PIC18LF26K83-xSP +MCU_Microchip_PIC18:PIC18LF4331-IML +MCU_Microchip_PIC18:PIC18LF4331-IP +MCU_Microchip_PIC18:PIC18LF4331-IPT +MCU_Microchip_PIC18:PIC18LF442-IP +MCU_Microchip_PIC18:PIC18LF442-IPT +MCU_Microchip_PIC18:PIC18LF4431-IML +MCU_Microchip_PIC18:PIC18LF4431-IP +MCU_Microchip_PIC18:PIC18LF4431-IPT +MCU_Microchip_PIC18:PIC18LF4450-IML +MCU_Microchip_PIC18:PIC18LF4450-IP +MCU_Microchip_PIC18:PIC18LF4450-IPT +MCU_Microchip_PIC18:PIC18LF4455-IML +MCU_Microchip_PIC18:PIC18LF4455-IP +MCU_Microchip_PIC18:PIC18LF4455-IPT +MCU_Microchip_PIC18:PIC18LF4458-IML +MCU_Microchip_PIC18:PIC18LF4458-IP +MCU_Microchip_PIC18:PIC18LF4458-IPT +MCU_Microchip_PIC18:PIC18LF448-IP +MCU_Microchip_PIC18:PIC18LF4480-IP +MCU_Microchip_PIC18:PIC18LF44J10-IP +MCU_Microchip_PIC18:PIC18LF452-IP +MCU_Microchip_PIC18:PIC18LF452-IPT +MCU_Microchip_PIC18:PIC18LF4550-IML +MCU_Microchip_PIC18:PIC18LF4550-IP +MCU_Microchip_PIC18:PIC18LF4550-IPT +MCU_Microchip_PIC18:PIC18LF4553-IML +MCU_Microchip_PIC18:PIC18LF4553-IP +MCU_Microchip_PIC18:PIC18LF4553-IPT +MCU_Microchip_PIC18:PIC18LF458-IP +MCU_Microchip_PIC18:PIC18LF4580-IP +MCU_Microchip_PIC18:PIC18LF45J10-IP +MCU_Microchip_PIC18:PIC18LF45K50_QFP +MCU_Microchip_PIC18:PIC18LF45K80-IML +MCU_Microchip_PIC18:PIC18LF45K80-IPT +MCU_Microchip_PIC18:PIC18LF46K80-IML +MCU_Microchip_PIC18:PIC18LF46K80-IPT +MCU_Microchip_PIC24:PIC24FJ128GA306-xMR +MCU_Microchip_PIC24:PIC24FJ128GA306-xPT +MCU_Microchip_PIC24:PIC24FJ256DA210-xBG +MCU_Microchip_PIC24:PIC24FJ256DA210-xPT +MCU_Microchip_PIC24:PIC24FJ64GA306-xMR +MCU_Microchip_PIC24:PIC24FJ64GA306-xPT +MCU_Microchip_PIC24:PIC24FV32KA304-IPT +MCU_Microchip_PIC32:PIC32MK1024GPD100-xPT +MCU_Microchip_PIC32:PIC32MK1024GPE100-xPT +MCU_Microchip_PIC32:PIC32MM0064GPL028x-ML +MCU_Microchip_PIC32:PIC32MX110F016D-IPT +MCU_Microchip_PIC32:PIC32MX120F032D-IPT +MCU_Microchip_PIC32:PIC32MX130F064D-IPT +MCU_Microchip_PIC32:PIC32MX150F128D-IPT +MCU_Microchip_PIC32:PIC32MX170F256D-IPT +MCU_Microchip_PIC32:PIC32MX210F016D-IPT +MCU_Microchip_PIC32:PIC32MX220F032D-IPT +MCU_Microchip_PIC32:PIC32MX230F064D-IPT +MCU_Microchip_PIC32:PIC32MX250F128D-IPT +MCU_Microchip_PIC32:PIC32MX270F256D-IPT +MCU_Microchip_PIC32:PIC32MX575F256H +MCU_Microchip_PIC32:PIC32MX575F512H +MCU_Microchip_PIC32:PIC32MX795F512L-80x-PF +MCU_Microchip_PIC32:PIC32MX795F512L-80x-PT +MCU_Microchip_SAMA:ATSAMA5D21 +MCU_Microchip_SAMD:ATSAMD09C13A-SS +MCU_Microchip_SAMD:ATSAMD09D14A-M +MCU_Microchip_SAMD:ATSAMD10C13A-SS +MCU_Microchip_SAMD:ATSAMD10C14A-SS +MCU_Microchip_SAMD:ATSAMD10D13A-M +MCU_Microchip_SAMD:ATSAMD10D13A-SS +MCU_Microchip_SAMD:ATSAMD10D14A-M +MCU_Microchip_SAMD:ATSAMD10D14A-SS +MCU_Microchip_SAMD:ATSAMD10D14A-U +MCU_Microchip_SAMD:ATSAMD11C14A-SS +MCU_Microchip_SAMD:ATSAMD11D14A-M +MCU_Microchip_SAMD:ATSAMD11D14A-SS +MCU_Microchip_SAMD:ATSAMD11D14A-U +MCU_Microchip_SAMD:ATSAMD21E15A-A +MCU_Microchip_SAMD:ATSAMD21E15A-M +MCU_Microchip_SAMD:ATSAMD21E15B-A +MCU_Microchip_SAMD:ATSAMD21E15B-M +MCU_Microchip_SAMD:ATSAMD21E15L-A +MCU_Microchip_SAMD:ATSAMD21E15L-M +MCU_Microchip_SAMD:ATSAMD21E16A-A +MCU_Microchip_SAMD:ATSAMD21E16A-M +MCU_Microchip_SAMD:ATSAMD21E16B-A +MCU_Microchip_SAMD:ATSAMD21E16B-M +MCU_Microchip_SAMD:ATSAMD21E16L-A +MCU_Microchip_SAMD:ATSAMD21E16L-M +MCU_Microchip_SAMD:ATSAMD21E17A-A +MCU_Microchip_SAMD:ATSAMD21E17A-M +MCU_Microchip_SAMD:ATSAMD21E17D-A +MCU_Microchip_SAMD:ATSAMD21E17D-M +MCU_Microchip_SAMD:ATSAMD21E17L-A +MCU_Microchip_SAMD:ATSAMD21E17L-M +MCU_Microchip_SAMD:ATSAMD21E18A-A +MCU_Microchip_SAMD:ATSAMD21E18A-M +MCU_Microchip_SAMD:ATSAMD21G15A-A +MCU_Microchip_SAMD:ATSAMD21G15A-M +MCU_Microchip_SAMD:ATSAMD21G15B-A +MCU_Microchip_SAMD:ATSAMD21G15B-M +MCU_Microchip_SAMD:ATSAMD21G16A-A +MCU_Microchip_SAMD:ATSAMD21G16A-M +MCU_Microchip_SAMD:ATSAMD21G16B-A +MCU_Microchip_SAMD:ATSAMD21G16B-M +MCU_Microchip_SAMD:ATSAMD21G16L-M +MCU_Microchip_SAMD:ATSAMD21G17A-A +MCU_Microchip_SAMD:ATSAMD21G17A-M +MCU_Microchip_SAMD:ATSAMD21G17D-A +MCU_Microchip_SAMD:ATSAMD21G17D-M +MCU_Microchip_SAMD:ATSAMD21G17L-M +MCU_Microchip_SAMD:ATSAMD21G18A-A +MCU_Microchip_SAMD:ATSAMD21G18A-M +MCU_Microchip_SAMD:ATSAMD21J15A-A +MCU_Microchip_SAMD:ATSAMD21J15A-M +MCU_Microchip_SAMD:ATSAMD21J15B-A +MCU_Microchip_SAMD:ATSAMD21J15B-C +MCU_Microchip_SAMD:ATSAMD21J15B-M +MCU_Microchip_SAMD:ATSAMD21J16A-A +MCU_Microchip_SAMD:ATSAMD21J16A-M +MCU_Microchip_SAMD:ATSAMD21J16B-A +MCU_Microchip_SAMD:ATSAMD21J16B-C +MCU_Microchip_SAMD:ATSAMD21J16B-M +MCU_Microchip_SAMD:ATSAMD21J17A-A +MCU_Microchip_SAMD:ATSAMD21J17A-M +MCU_Microchip_SAMD:ATSAMD21J17D-A +MCU_Microchip_SAMD:ATSAMD21J17D-C +MCU_Microchip_SAMD:ATSAMD21J17D-M +MCU_Microchip_SAMD:ATSAMD21J18A-A +MCU_Microchip_SAMD:ATSAMD21J18A-M +MCU_Microchip_SAMD:ATSAMD51J18A-A +MCU_Microchip_SAMD:ATSAMD51J18A-M +MCU_Microchip_SAMD:ATSAMD51J19A-A +MCU_Microchip_SAMD:ATSAMD51J19A-M +MCU_Microchip_SAMD:ATSAMD51J20A-A +MCU_Microchip_SAMD:ATSAMD51J20A-M +MCU_Microchip_SAMD:ATSAMDA1E14B-A +MCU_Microchip_SAMD:ATSAMDA1E14B-M +MCU_Microchip_SAMD:ATSAMDA1E15B-A +MCU_Microchip_SAMD:ATSAMDA1E15B-M +MCU_Microchip_SAMD:ATSAMDA1E16B-A +MCU_Microchip_SAMD:ATSAMDA1E16B-M +MCU_Microchip_SAMD:ATSAMDA1G14B-A +MCU_Microchip_SAMD:ATSAMDA1G14B-M +MCU_Microchip_SAMD:ATSAMDA1G15B-A +MCU_Microchip_SAMD:ATSAMDA1G15B-M +MCU_Microchip_SAMD:ATSAMDA1G16B-A +MCU_Microchip_SAMD:ATSAMDA1G16B-M +MCU_Microchip_SAMD:ATSAMDA1J14B-A +MCU_Microchip_SAMD:ATSAMDA1J15B-A +MCU_Microchip_SAMD:ATSAMDA1J16B-A +MCU_Microchip_SAME:ATSAME51J18A-A +MCU_Microchip_SAME:ATSAME51J19A-A +MCU_Microchip_SAME:ATSAME51J20A-A +MCU_Microchip_SAME:ATSAME53J18A-M +MCU_Microchip_SAME:ATSAME53J19A-M +MCU_Microchip_SAME:ATSAME53J20A-M +MCU_Microchip_SAME:ATSAME54N19A-A +MCU_Microchip_SAME:ATSAME70J19A-AN +MCU_Microchip_SAME:ATSAME70J20A-AN +MCU_Microchip_SAME:ATSAME70J21A-AN +MCU_Microchip_SAME:ATSAME70N19A-AN +MCU_Microchip_SAME:ATSAME70N20A-AN +MCU_Microchip_SAME:ATSAME70N21A-AN +MCU_Microchip_SAME:ATSAME70Q19A-AN +MCU_Microchip_SAME:ATSAME70Q20A-AN +MCU_Microchip_SAME:ATSAME70Q21A-AN +MCU_Microchip_SAML:ATSAML21E15B-AUT +MCU_Microchip_SAML:ATSAML21E15B-MUT +MCU_Microchip_SAML:ATSAML21E16B-AUT +MCU_Microchip_SAML:ATSAML21E16B-MUT +MCU_Microchip_SAML:ATSAML21E17B-AUT +MCU_Microchip_SAML:ATSAML21E17B-MUT +MCU_Microchip_SAML:ATSAML21E18B-AUT +MCU_Microchip_SAML:ATSAML21E18B-MUT +MCU_Microchip_SAML:ATSAML21G16B-AUT +MCU_Microchip_SAML:ATSAML21G16B-MUT +MCU_Microchip_SAML:ATSAML21G17B-AUT +MCU_Microchip_SAML:ATSAML21G17B-MUT +MCU_Microchip_SAML:ATSAML21G18B-AUT +MCU_Microchip_SAML:ATSAML21G18B-MUT +MCU_Microchip_SAML:ATSAML21J16B-AUT +MCU_Microchip_SAML:ATSAML21J16B-MUT +MCU_Microchip_SAML:ATSAML21J17B-AUT +MCU_Microchip_SAML:ATSAML21J17B-MUT +MCU_Microchip_SAML:ATSAML21J18B-AUT +MCU_Microchip_SAML:ATSAML21J18B-MUT +MCU_Microchip_SAMV:ATSAMV71Q19B-A +MCU_Microchip_SAMV:ATSAMV71Q20B-A +MCU_Microchip_SAMV:ATSAMV71Q21B-A +MCU_Module:Adafruit_Feather_32u4_BluefruitLE +MCU_Module:Adafruit_Feather_Generic +MCU_Module:Adafruit_Feather_HUZZAH32_ESP32 +MCU_Module:Adafruit_Feather_HUZZAH_ESP8266 +MCU_Module:Adafruit_Feather_M0_Adalogger +MCU_Module:Adafruit_Feather_M0_Basic_Proto +MCU_Module:Adafruit_Feather_M0_BluefruitLE +MCU_Module:Adafruit_Feather_M0_Express +MCU_Module:Adafruit_Feather_M0_RFM69HCW_Packet_Radio +MCU_Module:Adafruit_Feather_M0_RFM9x_LoRa_Radio +MCU_Module:Adafruit_Feather_M0_Wifi +MCU_Module:Adafruit_Feather_WICED_Wifi +MCU_Module:Adafruit_HUZZAH_ESP8266_breakout +MCU_Module:Arduino_Leonardo +MCU_Module:Arduino_Nano_ESP32 +MCU_Module:Arduino_Nano_Every +MCU_Module:Arduino_Nano_RP2040_Connect +MCU_Module:Arduino_Nano_v2.x +MCU_Module:Arduino_Nano_v3.x +MCU_Module:Arduino_UNO_R2 +MCU_Module:Arduino_UNO_R3 +MCU_Module:CHIP +MCU_Module:CHIP-PRO +MCU_Module:Carambola2 +MCU_Module:Electrosmith_Daisy_Seed_Rev4 +MCU_Module:Google_Coral +MCU_Module:Maple_Mini +MCU_Module:NUCLEO144-F207ZG +MCU_Module:NUCLEO144-F412ZG +MCU_Module:NUCLEO144-F413ZH +MCU_Module:NUCLEO144-F429ZI +MCU_Module:NUCLEO144-F439ZI +MCU_Module:NUCLEO144-F446ZE +MCU_Module:NUCLEO144-F722ZE +MCU_Module:NUCLEO144-F746ZG +MCU_Module:NUCLEO144-F756ZG +MCU_Module:NUCLEO144-F767ZI +MCU_Module:NUCLEO144-H743ZI +MCU_Module:NUCLEO64-F411RE +MCU_Module:OPOS6UL +MCU_Module:OPOS6UL_NANO +MCU_Module:Olimex_MOD-WIFI-ESP8266-DEV +MCU_Module:Omega2+ +MCU_Module:Omega2S +MCU_Module:Omega2S+ +MCU_Module:PocketBeagle +MCU_Module:RaspberryPi-CM1 +MCU_Module:RaspberryPi-CM3 +MCU_Module:RaspberryPi-CM3+ +MCU_Module:RaspberryPi-CM3+L +MCU_Module:RaspberryPi-CM3-L +MCU_Module:RaspberryPi_Pico +MCU_Module:RaspberryPi_Pico_Debug +MCU_Module:RaspberryPi_Pico_Extensive +MCU_Module:RaspberryPi_Pico_W +MCU_Module:RaspberryPi_Pico_W_Debug +MCU_Module:RaspberryPi_Pico_W_Extensive +MCU_Module:Sipeed-M1 +MCU_Module:Sipeed-M1W +MCU_Module:VisionSOM-6UL +MCU_Module:VisionSOM-6ULL +MCU_Module:VisionSOM-RT +MCU_Module:VisionSOM-STM32MP1 +MCU_Nordic:nRF51x22-QFxx +MCU_Nordic:nRF52810-QCxx +MCU_Nordic:nRF52810-QFxx +MCU_Nordic:nRF52811-QCxx +MCU_Nordic:nRF52820-QDxx +MCU_Nordic:nRF52832-QFxx +MCU_Nordic:nRF52833_QDxx +MCU_Nordic:nRF52833_QIxx +MCU_Nordic:nRF52840 +MCU_Nordic:nRF5340-QKxx +MCU_Nordic:nRF9160-SIxA +MCU_NXP_ColdFire:MCF5211CAE66 +MCU_NXP_ColdFire:MCF5212CAE66 +MCU_NXP_ColdFire:MCF5213-LQFP100 +MCU_NXP_ColdFire:MCF5282 +MCU_NXP_ColdFire:MCF5328-BGA256 +MCU_NXP_ColdFire:MCF5407 +MCU_NXP_HC11:68HC11 +MCU_NXP_HC11:68HC11A8 +MCU_NXP_HC11:68HC11F1 +MCU_NXP_HC11:68HC11_PLCC +MCU_NXP_HC11:68HC711_PLCC +MCU_NXP_HC11:MC68HC11A0CC +MCU_NXP_HC11:MC68HC11A1CC +MCU_NXP_HC11:MC68HC11A7CC +MCU_NXP_HC11:MC68HC11A8CC +MCU_NXP_HC11:MC68HC11F1CC +MCU_NXP_HC12:MC68HC812A4 +MCU_NXP_HC12:MC68HC912 +MCU_NXP_HCS12:MC9S12DT256 +MCU_NXP_Kinetis:MK20DN128VFM5 +MCU_NXP_Kinetis:MK20DN32VFM5 +MCU_NXP_Kinetis:MK20DN64VFM5 +MCU_NXP_Kinetis:MK20DX128VFM5 +MCU_NXP_Kinetis:MK20DX32VFM5 +MCU_NXP_Kinetis:MK20DX64VFM5 +MCU_NXP_Kinetis:MK20FN1M0VMD12 +MCU_NXP_Kinetis:MK20FX512VMD12 +MCU_NXP_Kinetis:MK26FN2M0VMD18 +MCU_NXP_Kinetis:MKE02Z16VLC4 +MCU_NXP_Kinetis:MKE02Z16VLD4 +MCU_NXP_Kinetis:MKE02Z32VLC4 +MCU_NXP_Kinetis:MKE02Z32VLD4 +MCU_NXP_Kinetis:MKE02Z32VLH4 +MCU_NXP_Kinetis:MKE02Z32VQH4 +MCU_NXP_Kinetis:MKE02Z64VLC4 +MCU_NXP_Kinetis:MKE02Z64VLD4 +MCU_NXP_Kinetis:MKE02Z64VLH4 +MCU_NXP_Kinetis:MKE02Z64VQH4 +MCU_NXP_Kinetis:MKE16Z64VLF4 +MCU_NXP_Kinetis:MKL02Z16VFG4 +MCU_NXP_Kinetis:MKL02Z16VFK4 +MCU_NXP_Kinetis:MKL02Z16VFM4 +MCU_NXP_Kinetis:MKL02Z32CAF4 +MCU_NXP_Kinetis:MKL02Z32VFG4 +MCU_NXP_Kinetis:MKL02Z32VFK4 +MCU_NXP_Kinetis:MKL02Z32VFM4 +MCU_NXP_Kinetis:MKL02Z8VFG4 +MCU_NXP_Kinetis:MKL03Z16VFG4 +MCU_NXP_Kinetis:MKL03Z16VFK4 +MCU_NXP_Kinetis:MKL03Z32CAF4 +MCU_NXP_Kinetis:MKL03Z32CBF4 +MCU_NXP_Kinetis:MKL03Z32VFG4 +MCU_NXP_Kinetis:MKL03Z32VFK4 +MCU_NXP_Kinetis:MKL03Z8VFG4 +MCU_NXP_Kinetis:MKL03Z8VFK4 +MCU_NXP_Kinetis:MKL04Z16VFK4 +MCU_NXP_Kinetis:MKL04Z16VFM4 +MCU_NXP_Kinetis:MKL04Z16VLC4 +MCU_NXP_Kinetis:MKL04Z16VLF4 +MCU_NXP_Kinetis:MKL04Z32VFK4 +MCU_NXP_Kinetis:MKL04Z32VFM4 +MCU_NXP_Kinetis:MKL04Z32VLC4 +MCU_NXP_Kinetis:MKL04Z32VLF4 +MCU_NXP_Kinetis:MKL04Z8VFK4 +MCU_NXP_Kinetis:MKL04Z8VFM4 +MCU_NXP_Kinetis:MKL04Z8VLC4 +MCU_NXP_Kinetis:MKL05Z16VFK4 +MCU_NXP_Kinetis:MKL05Z16VFM4 +MCU_NXP_Kinetis:MKL05Z16VLC4 +MCU_NXP_Kinetis:MKL05Z16VLF4 +MCU_NXP_Kinetis:MKL05Z32VFK4 +MCU_NXP_Kinetis:MKL05Z32VFM4 +MCU_NXP_Kinetis:MKL05Z32VLC4 +MCU_NXP_Kinetis:MKL05Z32VLF4 +MCU_NXP_Kinetis:MKL05Z8VFK4 +MCU_NXP_Kinetis:MKL05Z8VFM4 +MCU_NXP_Kinetis:MKL05Z8VLC4 +MCU_NXP_Kinetis:MKL16Z128VFM4 +MCU_NXP_Kinetis:MKL16Z128VFT4 +MCU_NXP_Kinetis:MKL16Z128VLH4 +MCU_NXP_Kinetis:MKL16Z256VLH4 +MCU_NXP_Kinetis:MKL16Z256VMP4 +MCU_NXP_Kinetis:MKL16Z32VFM4 +MCU_NXP_Kinetis:MKL16Z32VFT4 +MCU_NXP_Kinetis:MKL16Z32VLH4 +MCU_NXP_Kinetis:MKL16Z64VFM4 +MCU_NXP_Kinetis:MKL16Z64VFT4 +MCU_NXP_Kinetis:MKL16Z64VLH4 +MCU_NXP_Kinetis:MKL17Z128VFM4 +MCU_NXP_Kinetis:MKL17Z128VFT4 +MCU_NXP_Kinetis:MKL17Z128VLH4 +MCU_NXP_Kinetis:MKL17Z128VMP4 +MCU_NXP_Kinetis:MKL17Z256CAL4 +MCU_NXP_Kinetis:MKL17Z256VFM4 +MCU_NXP_Kinetis:MKL17Z256VFT4 +MCU_NXP_Kinetis:MKL17Z256VLH4 +MCU_NXP_Kinetis:MKL17Z256VMP4 +MCU_NXP_Kinetis:MKL17Z32VDA4 +MCU_NXP_Kinetis:MKL17Z32VFM4 +MCU_NXP_Kinetis:MKL17Z32VFT4 +MCU_NXP_Kinetis:MKL17Z32VLH4 +MCU_NXP_Kinetis:MKL17Z32VMP4 +MCU_NXP_Kinetis:MKL17Z64VDA4 +MCU_NXP_Kinetis:MKL17Z64VFM4 +MCU_NXP_Kinetis:MKL17Z64VFT4 +MCU_NXP_Kinetis:MKL17Z64VLH4 +MCU_NXP_Kinetis:MKL17Z64VMP4 +MCU_NXP_Kinetis:MKL24Z32VFM4 +MCU_NXP_Kinetis:MKL24Z32VFT4 +MCU_NXP_Kinetis:MKL24Z32VLH4 +MCU_NXP_Kinetis:MKL24Z32VLK4 +MCU_NXP_Kinetis:MKL24Z64VFM4 +MCU_NXP_Kinetis:MKL24Z64VFT4 +MCU_NXP_Kinetis:MKL24Z64VLH4 +MCU_NXP_Kinetis:MKL24Z64VLK4 +MCU_NXP_Kinetis:MKL25Z128VFM4 +MCU_NXP_Kinetis:MKL25Z128VFT4 +MCU_NXP_Kinetis:MKL25Z128VLH4 +MCU_NXP_Kinetis:MKL25Z128VLK4 +MCU_NXP_Kinetis:MKL25Z32VFM4 +MCU_NXP_Kinetis:MKL25Z32VFT4 +MCU_NXP_Kinetis:MKL25Z32VLH4 +MCU_NXP_Kinetis:MKL25Z32VLK4 +MCU_NXP_Kinetis:MKL25Z64VFM4 +MCU_NXP_Kinetis:MKL25Z64VFT4 +MCU_NXP_Kinetis:MKL25Z64VLH4 +MCU_NXP_Kinetis:MKL25Z64VLK4 +MCU_NXP_Kinetis:MKL26Z128CAL4 +MCU_NXP_Kinetis:MKL26Z128VFM4 +MCU_NXP_Kinetis:MKL26Z128VFT4 +MCU_NXP_Kinetis:MKL26Z128VLH4 +MCU_NXP_Kinetis:MKL26Z128VLL4 +MCU_NXP_Kinetis:MKL26Z128VMC4 +MCU_NXP_Kinetis:MKL26Z256VLH4 +MCU_NXP_Kinetis:MKL26Z256VLL4 +MCU_NXP_Kinetis:MKL26Z256VMC4 +MCU_NXP_Kinetis:MKL26Z256VMP4 +MCU_NXP_Kinetis:MKL26Z32VFM4 +MCU_NXP_Kinetis:MKL26Z32VFT4 +MCU_NXP_Kinetis:MKL26Z32VLH4 +MCU_NXP_Kinetis:MKL26Z64VFM4 +MCU_NXP_Kinetis:MKL26Z64VFT4 +MCU_NXP_Kinetis:MKL26Z64VLH4 +MCU_NXP_Kinetis:MKL27Z128VFT4 +MCU_NXP_Kinetis:MKL27Z128VLH4 +MCU_NXP_Kinetis:MKL27Z256VFT4 +MCU_NXP_Kinetis:MKL27Z256VLH4 +MCU_NXP_Kinetis:MKL27Z32VFM4 +MCU_NXP_Kinetis:MKL27Z32VFT4 +MCU_NXP_Kinetis:MKL27Z32VLH4 +MCU_NXP_Kinetis:MKL27Z64VFM4 +MCU_NXP_Kinetis:MKL27Z64VFT4 +MCU_NXP_Kinetis:MKL27Z64VLH4 +MCU_NXP_Kinetis:MKL28Z512VDC7 +MCU_NXP_Kinetis:MKL28Z512VLL7 +MCU_NXP_Kinetis:MKL43Z128VLH4 +MCU_NXP_Kinetis:MKL43Z128VMP4 +MCU_NXP_Kinetis:MKL43Z256VLH4 +MCU_NXP_Kinetis:MKL43Z256VMP4 +MCU_NXP_Kinetis:MKL46Z128VLH4 +MCU_NXP_Kinetis:MKL46Z128VLL4 +MCU_NXP_Kinetis:MKL46Z128VMC4 +MCU_NXP_Kinetis:MKL46Z256VLH4 +MCU_NXP_Kinetis:MKL46Z256VLL4 +MCU_NXP_Kinetis:MKL46Z256VMC4 +MCU_NXP_Kinetis:MKL46Z256VMP4 +MCU_NXP_Kinetis:MKV11Z128VLF7 +MCU_NXP_Kinetis:MKV11Z128VLH7 +MCU_NXP_Kinetis:MKW21Z256VHT +MCU_NXP_Kinetis:MKW21Z512VHT +MCU_NXP_Kinetis:MKW31Z256VHT +MCU_NXP_Kinetis:MKW31Z512VHT +MCU_NXP_Kinetis:MKW41Z256VHT +MCU_NXP_Kinetis:MKW41Z512VHT +MCU_NXP_LPC:LPC1102UK +MCU_NXP_LPC:LPC1104UK +MCU_NXP_LPC:LPC1111FHN33-101 +MCU_NXP_LPC:LPC1111FHN33-102 +MCU_NXP_LPC:LPC1111FHN33-103 +MCU_NXP_LPC:LPC1111FHN33-201 +MCU_NXP_LPC:LPC1111FHN33-202 +MCU_NXP_LPC:LPC1111FHN33-203 +MCU_NXP_LPC:LPC1111JHN33-103 +MCU_NXP_LPC:LPC1111JHN33-203 +MCU_NXP_LPC:LPC1112FHI33-102 +MCU_NXP_LPC:LPC1112FHI33-202 +MCU_NXP_LPC:LPC1112FHI33-203 +MCU_NXP_LPC:LPC1112FHN33-101 +MCU_NXP_LPC:LPC1112FHN33-102 +MCU_NXP_LPC:LPC1112FHN33-103 +MCU_NXP_LPC:LPC1112FHN33-201 +MCU_NXP_LPC:LPC1112FHN33-202 +MCU_NXP_LPC:LPC1112FHN33-203 +MCU_NXP_LPC:LPC1112JHI33-203 +MCU_NXP_LPC:LPC1112JHN33-103 +MCU_NXP_LPC:LPC1112JHN33-203 +MCU_NXP_LPC:LPC1113FBD48-301 +MCU_NXP_LPC:LPC1113FBD48-302 +MCU_NXP_LPC:LPC1113FBD48-303 +MCU_NXP_LPC:LPC1113FHN33-201 +MCU_NXP_LPC:LPC1113FHN33-202 +MCU_NXP_LPC:LPC1113FHN33-203 +MCU_NXP_LPC:LPC1113FHN33-301 +MCU_NXP_LPC:LPC1113FHN33-302 +MCU_NXP_LPC:LPC1113FHN33-303 +MCU_NXP_LPC:LPC1113JBD48-303 +MCU_NXP_LPC:LPC1113JHN33-203 +MCU_NXP_LPC:LPC1113JHN33-303 +MCU_NXP_LPC:LPC1114FBD48-301 +MCU_NXP_LPC:LPC1114FBD48-302 +MCU_NXP_LPC:LPC1114FBD48-303 +MCU_NXP_LPC:LPC1114FBD48-323 +MCU_NXP_LPC:LPC1114FBD48-333 +MCU_NXP_LPC:LPC1114FHI33-302 +MCU_NXP_LPC:LPC1114FHI33-303 +MCU_NXP_LPC:LPC1114FHN33-201 +MCU_NXP_LPC:LPC1114FHN33-202 +MCU_NXP_LPC:LPC1114FHN33-203 +MCU_NXP_LPC:LPC1114FHN33-301 +MCU_NXP_LPC:LPC1114FHN33-302 +MCU_NXP_LPC:LPC1114FHN33-303 +MCU_NXP_LPC:LPC1114FHN33-333 +MCU_NXP_LPC:LPC1114JBD48-303 +MCU_NXP_LPC:LPC1114JBD48-323 +MCU_NXP_LPC:LPC1114JBD48-333 +MCU_NXP_LPC:LPC1114JHI33-303 +MCU_NXP_LPC:LPC1114JHN33-203 +MCU_NXP_LPC:LPC1115FBD48-303 +MCU_NXP_LPC:LPC1115JBD48-303 +MCU_NXP_LPC:LPC11E12FBD48-201 +MCU_NXP_LPC:LPC11E13FBD48-301 +MCU_NXP_LPC:LPC11E14FBD48-401 +MCU_NXP_LPC:LPC11U12FBD48-201 +MCU_NXP_LPC:LPC11U13FBD48-201 +MCU_NXP_LPC:LPC11U14FBD48-201 +MCU_NXP_LPC:LPC11U22FBD48-301 +MCU_NXP_LPC:LPC11U23FBD48-301 +MCU_NXP_LPC:LPC11U24FBD48-301 +MCU_NXP_LPC:LPC11U24FBD48-401 +MCU_NXP_LPC:LPC11U34FBD48-311 +MCU_NXP_LPC:LPC11U34FBD48-421 +MCU_NXP_LPC:LPC11U35FBD48-401 +MCU_NXP_LPC:LPC11U36FBD48-401 +MCU_NXP_LPC:LPC11U37FBD48-401_ +MCU_NXP_LPC:LPC1224FBD48-101 +MCU_NXP_LPC:LPC1224FBD48-121 +MCU_NXP_LPC:LPC1224FBD64-101 +MCU_NXP_LPC:LPC1224FBD64-121 +MCU_NXP_LPC:LPC1225FBD48-301 +MCU_NXP_LPC:LPC1225FBD48-321 +MCU_NXP_LPC:LPC1225FBD64-301 +MCU_NXP_LPC:LPC1225FBD64-321 +MCU_NXP_LPC:LPC1226FBD48-301 +MCU_NXP_LPC:LPC1226FBD64-301 +MCU_NXP_LPC:LPC1227FBD48-301 +MCU_NXP_LPC:LPC1227FBD64-301 +MCU_NXP_LPC:LPC1763FBD100 +MCU_NXP_LPC:LPC1764FBD100 +MCU_NXP_LPC:LPC1765FBD100 +MCU_NXP_LPC:LPC1766FBD100 +MCU_NXP_LPC:LPC1767FBD100 +MCU_NXP_LPC:LPC1768FBD100 +MCU_NXP_LPC:LPC1769FBD100 +MCU_NXP_LPC:LPC2141FBD64 +MCU_NXP_LPC:LPC2142FBD64 +MCU_NXP_LPC:LPC2144FBD64 +MCU_NXP_LPC:LPC2146FBD64 +MCU_NXP_LPC:LPC2148FBD64 +MCU_NXP_LPC:LPC811M001JDH16 +MCU_NXP_LPC:LPC812M001JDH16 +MCU_NXP_LPC:LPC812M101JD20 +MCU_NXP_LPC:LPC812M101JDH20 +MCU_NXP_LPC:LPC812M101JTB16 +MCU_NXP_LPC:LPC822M101JDH20 +MCU_NXP_LPC:LPC822M101JHI33 +MCU_NXP_LPC:LPC824M201JDH20 +MCU_NXP_LPC:LPC824M201JHI33 +MCU_NXP_LPC:LPC832M101FDH20 +MCU_NXP_LPC:LPC834M101FHI33 +MCU_NXP_MAC7100:MAC7101 +MCU_NXP_MAC7100:MAC7111 +MCU_NXP_MCore:MMC2114CFCPU +MCU_NXP_NTAG:NHS3100 +MCU_NXP_S08:MC9S08AC128xFDE +MCU_NXP_S08:MC9S08AC128xFGE +MCU_NXP_S08:MC9S08AC128xFUE +MCU_NXP_S08:MC9S08AC128xLKE +MCU_NXP_S08:MC9S08AC128xPUE +MCU_NXP_S08:MC9S08AC16xFDE +MCU_NXP_S08:MC9S08AC16xFGE +MCU_NXP_S08:MC9S08AC16xFJE +MCU_NXP_S08:MC9S08AC32xFDE +MCU_NXP_S08:MC9S08AC32xFGE +MCU_NXP_S08:MC9S08AC32xFJE +MCU_NXP_S08:MC9S08AC32xFUE +MCU_NXP_S08:MC9S08AC32xPUE +MCU_NXP_S08:MC9S08AC48xFDE +MCU_NXP_S08:MC9S08AC48xFGE +MCU_NXP_S08:MC9S08AC48xFJE +MCU_NXP_S08:MC9S08AC48xFUE +MCU_NXP_S08:MC9S08AC48xPUE +MCU_NXP_S08:MC9S08AC60xFDE +MCU_NXP_S08:MC9S08AC60xFGE +MCU_NXP_S08:MC9S08AC60xFJE +MCU_NXP_S08:MC9S08AC60xFUE +MCU_NXP_S08:MC9S08AC60xPUE +MCU_NXP_S08:MC9S08AC8xFDE +MCU_NXP_S08:MC9S08AC8xFGE +MCU_NXP_S08:MC9S08AC8xFJE +MCU_NXP_S08:MC9S08AC96xFDE +MCU_NXP_S08:MC9S08AC96xFGE +MCU_NXP_S08:MC9S08AC96xFUE +MCU_NXP_S08:MC9S08AC96xLKE +MCU_NXP_S08:MC9S08AC96xPUE +MCU_NXP_S08:MC9S08AW16AE0xLC +MCU_NXP_S08:MC9S08AW16xFDE +MCU_NXP_S08:MC9S08AW16xFGE +MCU_NXP_S08:MC9S08AW16xFUE +MCU_NXP_S08:MC9S08AW16xPUE +MCU_NXP_S08:MC9S08AW32xFDE +MCU_NXP_S08:MC9S08AW32xFGE +MCU_NXP_S08:MC9S08AW32xFUE +MCU_NXP_S08:MC9S08AW32xPUE +MCU_NXP_S08:MC9S08AW48xFDE +MCU_NXP_S08:MC9S08AW48xFGE +MCU_NXP_S08:MC9S08AW48xFUE +MCU_NXP_S08:MC9S08AW48xPUE +MCU_NXP_S08:MC9S08AW60xFDE +MCU_NXP_S08:MC9S08AW60xFGE +MCU_NXP_S08:MC9S08AW60xFUE +MCU_NXP_S08:MC9S08AW60xPUE +MCU_NXP_S08:MC9S08AW8AE0xLC +MCU_NXP_S08:MC9S08DN16xLC +MCU_NXP_S08:MC9S08DN16xLF +MCU_NXP_S08:MC9S08DN32xLC +MCU_NXP_S08:MC9S08DN32xLF +MCU_NXP_S08:MC9S08DN32xLH +MCU_NXP_S08:MC9S08DN48xLC +MCU_NXP_S08:MC9S08DN48xLF +MCU_NXP_S08:MC9S08DN48xLH +MCU_NXP_S08:MC9S08DN60xLC +MCU_NXP_S08:MC9S08DN60xLF +MCU_NXP_S08:MC9S08DN60xLH +MCU_NXP_S08:MC9S08DV16xLC +MCU_NXP_S08:MC9S08DV16xLF +MCU_NXP_S08:MC9S08DV32xLC +MCU_NXP_S08:MC9S08DV32xLF +MCU_NXP_S08:MC9S08DV32xLH +MCU_NXP_S08:MC9S08DV48xLC +MCU_NXP_S08:MC9S08DV48xLF +MCU_NXP_S08:MC9S08DV48xLH +MCU_NXP_S08:MC9S08DV60xLC +MCU_NXP_S08:MC9S08DV60xLF +MCU_NXP_S08:MC9S08DV60xLH +MCU_NXP_S08:MC9S08DZ128xLF +MCU_NXP_S08:MC9S08DZ128xLH +MCU_NXP_S08:MC9S08DZ128xLL +MCU_NXP_S08:MC9S08DZ16xLC +MCU_NXP_S08:MC9S08DZ16xLF +MCU_NXP_S08:MC9S08DZ32xLC +MCU_NXP_S08:MC9S08DZ32xLF +MCU_NXP_S08:MC9S08DZ32xLH +MCU_NXP_S08:MC9S08DZ48xLC +MCU_NXP_S08:MC9S08DZ48xLF +MCU_NXP_S08:MC9S08DZ48xLH +MCU_NXP_S08:MC9S08DZ60xLC +MCU_NXP_S08:MC9S08DZ60xLF +MCU_NXP_S08:MC9S08DZ60xLH +MCU_NXP_S08:MC9S08DZ96xLF +MCU_NXP_S08:MC9S08DZ96xLH +MCU_NXP_S08:MC9S08DZ96xLL +MCU_NXP_S08:MC9S08EL16xTJ +MCU_NXP_S08:MC9S08EL16xTL +MCU_NXP_S08:MC9S08EL32xTJ +MCU_NXP_S08:MC9S08EL32xTL +MCU_NXP_S08:MC9S08FL16xLC +MCU_NXP_S08:MC9S08FL8xLC +MCU_NXP_S08:MC9S08JM16xGT +MCU_NXP_S08:MC9S08JM16xLC +MCU_NXP_S08:MC9S08JM16xLD +MCU_NXP_S08:MC9S08JM32xGT +MCU_NXP_S08:MC9S08JM32xLD +MCU_NXP_S08:MC9S08JM32xLH +MCU_NXP_S08:MC9S08JM60xGT +MCU_NXP_S08:MC9S08JM60xLD +MCU_NXP_S08:MC9S08JM60xLH +MCU_NXP_S08:MC9S08JM8xGT +MCU_NXP_S08:MC9S08JM8xLC +MCU_NXP_S08:MC9S08JM8xLD +MCU_NXP_S08:MC9S08JS16CFK +MCU_NXP_S08:MC9S08JS16CWJ +MCU_NXP_S08:MC9S08JS8CFK +MCU_NXP_S08:MC9S08JS8CWJ +MCU_NXP_S08:MC9S08LG32J0xLF +MCU_NXP_S08:MC9S08LG32J0xLH +MCU_NXP_S08:MC9S08LG32J0xLK +MCU_NXP_S08:MC9S08MP16xLC +MCU_NXP_S08:MC9S08MP16xLF +MCU_NXP_S08:MC9S08MP16xWL +MCU_NXP_S08:MC9S08QA2CDNE +MCU_NXP_S08:MC9S08QA2CFQE +MCU_NXP_S08:MC9S08QA2CPAE +MCU_NXP_S08:MC9S08QA4CDNE +MCU_NXP_S08:MC9S08QA4CFQE +MCU_NXP_S08:MC9S08QA4CPAE +MCU_NXP_S08:MC9S08QB4xGK +MCU_NXP_S08:MC9S08QB4xTG +MCU_NXP_S08:MC9S08QB4xWL +MCU_NXP_S08:MC9S08QB8xGK +MCU_NXP_S08:MC9S08QB8xTG +MCU_NXP_S08:MC9S08QB8xWL +MCU_NXP_S08:MC9S08QD2xPC +MCU_NXP_S08:MC9S08QD2xSC +MCU_NXP_S08:MC9S08QD4xPC +MCU_NXP_S08:MC9S08QD4xSC +MCU_NXP_S08:MC9S08QG4xDNE +MCU_NXP_S08:MC9S08QG4xDTE +MCU_NXP_S08:MC9S08QG4xFKE +MCU_NXP_S08:MC9S08QG4xFQE +MCU_NXP_S08:MC9S08QG4xPAE +MCU_NXP_S08:MC9S08QG8xDNE +MCU_NXP_S08:MC9S08QG8xDTE +MCU_NXP_S08:MC9S08QG8xFKE +MCU_NXP_S08:MC9S08QG8xFQE +MCU_NXP_S08:MC9S08QG8xPBE +MCU_NXP_S08:MC9S08SC4xTG +MCU_NXP_S08:MC9S08SE4xRL +MCU_NXP_S08:MC9S08SE4xTG +MCU_NXP_S08:MC9S08SE4xWL +MCU_NXP_S08:MC9S08SE8xRL +MCU_NXP_S08:MC9S08SE8xTG +MCU_NXP_S08:MC9S08SE8xWL +MCU_NXP_S08:MC9S08SF4xTG +MCU_NXP_S08:MC9S08SF4xTJ +MCU_NXP_S08:MC9S08SG16xTG +MCU_NXP_S08:MC9S08SG16xTJ +MCU_NXP_S08:MC9S08SG16xTL +MCU_NXP_S08:MC9S08SG32xTG +MCU_NXP_S08:MC9S08SG32xTJ +MCU_NXP_S08:MC9S08SG32xTL +MCU_NXP_S08:MC9S08SG4xSC +MCU_NXP_S08:MC9S08SG4xTG +MCU_NXP_S08:MC9S08SG4xTJ +MCU_NXP_S08:MC9S08SG8xSC +MCU_NXP_S08:MC9S08SG8xTG +MCU_NXP_S08:MC9S08SG8xTJ +MCU_NXP_S08:MC9S08SH16xTG +MCU_NXP_S08:MC9S08SH16xTJ +MCU_NXP_S08:MC9S08SH16xTL +MCU_NXP_S08:MC9S08SH16xWL +MCU_NXP_S08:MC9S08SH32xTG +MCU_NXP_S08:MC9S08SH32xTJ +MCU_NXP_S08:MC9S08SH32xTL +MCU_NXP_S08:MC9S08SH32xWL +MCU_NXP_S08:MC9S08SH4xFK +MCU_NXP_S08:MC9S08SH4xPJ +MCU_NXP_S08:MC9S08SH4xSC +MCU_NXP_S08:MC9S08SH4xTG +MCU_NXP_S08:MC9S08SH4xTJ +MCU_NXP_S08:MC9S08SH4xWJ +MCU_NXP_S08:MC9S08SH8xFK +MCU_NXP_S08:MC9S08SH8xPJ +MCU_NXP_S08:MC9S08SH8xSC +MCU_NXP_S08:MC9S08SH8xTG +MCU_NXP_S08:MC9S08SH8xTJ +MCU_NXP_S08:MC9S08SH8xWJ +MCU_NXP_S08:MC9S08SL16xTJ +MCU_NXP_S08:MC9S08SL16xTL +MCU_NXP_S08:MC9S08SL32xTJ +MCU_NXP_S08:MC9S08SL32xTL +MCU_NXP_S08:MC9S08SV16CLC +MCU_NXP_S08:MC9S08SV8CLC +MCU_Parallax:P8X32A-D40 +MCU_Parallax:P8X32A-M44 +MCU_Parallax:P8X32A-Q44 +MCU_Puya:PY32F002AF15P +MCU_RaspberryPi:RP2040 +MCU_Renesas_Synergy_S1:R7FS12878xA01CFL +MCU_SiFive:FE310-G000 +MCU_SiFive:FE310-G002 +MCU_SiFive:FU540-C000 +MCU_SiliconLabs:C8051F320-GQ +MCU_SiliconLabs:C8051F321-GM +MCU_SiliconLabs:C8051F380-GQ +MCU_SiliconLabs:C8051F381-GM +MCU_SiliconLabs:C8051F381-GQ +MCU_SiliconLabs:C8051F382-GQ +MCU_SiliconLabs:C8051F383-GM +MCU_SiliconLabs:C8051F383-GQ +MCU_SiliconLabs:C8051F384-GQ +MCU_SiliconLabs:C8051F385-GM +MCU_SiliconLabs:C8051F385-GQ +MCU_SiliconLabs:C8051F386-GQ +MCU_SiliconLabs:C8051F387-GM +MCU_SiliconLabs:C8051F387-GQ +MCU_SiliconLabs:C8051F38C-GM +MCU_SiliconLabs:C8051F38C-GQ +MCU_SiliconLabs:EFM32G230F128G-E-QFN64 +MCU_SiliconLabs:EFM32HG108F32G-C-QFN24 +MCU_SiliconLabs:EFM32HG108F64G-C-QFN24 +MCU_SiliconLabs:EFM32HG308F32G-C-QFN24 +MCU_SiliconLabs:EFM32HG308F64G-C-QFN24 +MCU_SiliconLabs:EFM32ZG108F16-B-QFN24 +MCU_SiliconLabs:EFM32ZG108F32-B-QFN24 +MCU_SiliconLabs:EFM32ZG108F4-B-QFN24 +MCU_SiliconLabs:EFM32ZG108F8-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F16-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F32-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F4-B-QFN24 +MCU_SiliconLabs:EFM32ZG110F8-B-QFN24 +MCU_SiliconLabs:EFM8BB10F2A-A-QFN20 +MCU_SiliconLabs:EFM8BB10F2G-A-QFN20 +MCU_SiliconLabs:EFM8BB10F2I-A-QFN20 +MCU_SiliconLabs:EFM8BB10F4A-A-QFN20 +MCU_SiliconLabs:EFM8BB10F4G-A-QFN20 +MCU_SiliconLabs:EFM8BB10F4I-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8A-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8G-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8G-A-QSOP24 +MCU_SiliconLabs:EFM8BB10F8G-A-SOIC16 +MCU_SiliconLabs:EFM8BB10F8I-A-QFN20 +MCU_SiliconLabs:EFM8BB10F8I-A-QSOP24 +MCU_SiliconLabs:EFM8BB10F8I-A-SOIC16 +MCU_SiliconLabs:EFM8LB12F32E-C-QFP32 +MCU_SiliconLabs:EFM8LB12F64E-C-QFP32 +MCU_SiliconLabs:EFM8UB30F40G-A-QFN20 +MCU_SiliconLabs:EFM8UB31F40G-A-QFN24 +MCU_SiliconLabs:EFM8UB31F40G-A-QSOP24 +MCU_SiliconLabs:EFR32xG23xxxxF512xM48 +MCU_STC:IAP15W205S-35x-SOP16 +MCU_STC:IRC15W207S-35x-SOP16 +MCU_STC:STC15W201S-35x-SOP16 +MCU_STC:STC15W202S-35x-SOP16 +MCU_STC:STC15W203S-35x-SOP16 +MCU_STC:STC15W204S-35x-SOP16 +MCU_STC:STC8G1K04-38I-TSSOP20 +MCU_STC:STC8G1K08-38I-TSSOP20 +MCU_STC:STC8G1K08A-36I-DFN8 +MCU_STC:STC8G1K17-38I-TSSOP20 +MCU_ST_STM32C0:STM32C011D6Yx +MCU_ST_STM32C0:STM32C011F4Px +MCU_ST_STM32C0:STM32C011F4Ux +MCU_ST_STM32C0:STM32C011F6Px +MCU_ST_STM32C0:STM32C011F6Ux +MCU_ST_STM32C0:STM32C011F_4-6_Px +MCU_ST_STM32C0:STM32C011F_4-6_Ux +MCU_ST_STM32C0:STM32C011J4Mx +MCU_ST_STM32C0:STM32C011J6Mx +MCU_ST_STM32C0:STM32C011J_4-6_Mx +MCU_ST_STM32C0:STM32C031C4Tx +MCU_ST_STM32C0:STM32C031C4Ux +MCU_ST_STM32C0:STM32C031C6Tx +MCU_ST_STM32C0:STM32C031C6Ux +MCU_ST_STM32C0:STM32C031C_4-6_Tx +MCU_ST_STM32C0:STM32C031C_4-6_Ux +MCU_ST_STM32C0:STM32C031F4Px +MCU_ST_STM32C0:STM32C031F6Px +MCU_ST_STM32C0:STM32C031F_4-6_Px +MCU_ST_STM32C0:STM32C031G4Ux +MCU_ST_STM32C0:STM32C031G6Ux +MCU_ST_STM32C0:STM32C031G_4-6_Ux +MCU_ST_STM32C0:STM32C031K4Tx +MCU_ST_STM32C0:STM32C031K4Ux +MCU_ST_STM32C0:STM32C031K6Tx +MCU_ST_STM32C0:STM32C031K6Ux +MCU_ST_STM32C0:STM32C031K_4-6_Tx +MCU_ST_STM32C0:STM32C031K_4-6_Ux +MCU_ST_STM32C0:STM32C071C8Tx +MCU_ST_STM32C0:STM32C071C8TxN +MCU_ST_STM32C0:STM32C071C8Ux +MCU_ST_STM32C0:STM32C071C8UxN +MCU_ST_STM32C0:STM32C071CBTx +MCU_ST_STM32C0:STM32C071CBTxN +MCU_ST_STM32C0:STM32C071CBUx +MCU_ST_STM32C0:STM32C071CBUxN +MCU_ST_STM32C0:STM32C071F8Px +MCU_ST_STM32C0:STM32C071F8PxN +MCU_ST_STM32C0:STM32C071FBPx +MCU_ST_STM32C0:STM32C071FBPxN +MCU_ST_STM32C0:STM32C071G8Ux +MCU_ST_STM32C0:STM32C071G8UxN +MCU_ST_STM32C0:STM32C071GBUx +MCU_ST_STM32C0:STM32C071GBUxN +MCU_ST_STM32C0:STM32C071K8Tx +MCU_ST_STM32C0:STM32C071K8TxN +MCU_ST_STM32C0:STM32C071K8Ux +MCU_ST_STM32C0:STM32C071K8UxN +MCU_ST_STM32C0:STM32C071KBTx +MCU_ST_STM32C0:STM32C071KBTxN +MCU_ST_STM32C0:STM32C071KBUx +MCU_ST_STM32C0:STM32C071KBUxN +MCU_ST_STM32C0:STM32C071R8Tx +MCU_ST_STM32C0:STM32C071R8TxN +MCU_ST_STM32C0:STM32C071RBIxN +MCU_ST_STM32C0:STM32C071RBTx +MCU_ST_STM32C0:STM32C071RBTxN +MCU_ST_STM32F0:STM32F030C6Tx +MCU_ST_STM32F0:STM32F030C8Tx +MCU_ST_STM32F0:STM32F030CCTx +MCU_ST_STM32F0:STM32F030F4Px +MCU_ST_STM32F0:STM32F030K6Tx +MCU_ST_STM32F0:STM32F030R8Tx +MCU_ST_STM32F0:STM32F030RCTx +MCU_ST_STM32F0:STM32F031C4Tx +MCU_ST_STM32F0:STM32F031C6Tx +MCU_ST_STM32F0:STM32F031C_4-6_Tx +MCU_ST_STM32F0:STM32F031E6Yx +MCU_ST_STM32F0:STM32F031F4Px +MCU_ST_STM32F0:STM32F031F6Px +MCU_ST_STM32F0:STM32F031F_4-6_Px +MCU_ST_STM32F0:STM32F031G4Ux +MCU_ST_STM32F0:STM32F031G6Ux +MCU_ST_STM32F0:STM32F031G_4-6_Ux +MCU_ST_STM32F0:STM32F031K4Ux +MCU_ST_STM32F0:STM32F031K6Tx +MCU_ST_STM32F0:STM32F031K6Ux +MCU_ST_STM32F0:STM32F031K_4-6_Ux +MCU_ST_STM32F0:STM32F038C6Tx +MCU_ST_STM32F0:STM32F038E6Yx +MCU_ST_STM32F0:STM32F038F6Px +MCU_ST_STM32F0:STM32F038G6Ux +MCU_ST_STM32F0:STM32F038K6Ux +MCU_ST_STM32F0:STM32F042C4Tx +MCU_ST_STM32F0:STM32F042C4Ux +MCU_ST_STM32F0:STM32F042C6Tx +MCU_ST_STM32F0:STM32F042C6Ux +MCU_ST_STM32F0:STM32F042C_4-6_Tx +MCU_ST_STM32F0:STM32F042C_4-6_Ux +MCU_ST_STM32F0:STM32F042F4Px +MCU_ST_STM32F0:STM32F042F6Px +MCU_ST_STM32F0:STM32F042G4Ux +MCU_ST_STM32F0:STM32F042G6Ux +MCU_ST_STM32F0:STM32F042G_4-6_Ux +MCU_ST_STM32F0:STM32F042K4Tx +MCU_ST_STM32F0:STM32F042K4Ux +MCU_ST_STM32F0:STM32F042K6Tx +MCU_ST_STM32F0:STM32F042K6Ux +MCU_ST_STM32F0:STM32F042K_4-6_Tx +MCU_ST_STM32F0:STM32F042K_4-6_Ux +MCU_ST_STM32F0:STM32F042T6Yx +MCU_ST_STM32F0:STM32F048C6Ux +MCU_ST_STM32F0:STM32F048G6Ux +MCU_ST_STM32F0:STM32F048T6Yx +MCU_ST_STM32F0:STM32F051C4Tx +MCU_ST_STM32F0:STM32F051C4Ux +MCU_ST_STM32F0:STM32F051C6Tx +MCU_ST_STM32F0:STM32F051C6Ux +MCU_ST_STM32F0:STM32F051C8Tx +MCU_ST_STM32F0:STM32F051C8Ux +MCU_ST_STM32F0:STM32F051K4Tx +MCU_ST_STM32F0:STM32F051K4Ux +MCU_ST_STM32F0:STM32F051K6Tx +MCU_ST_STM32F0:STM32F051K6Ux +MCU_ST_STM32F0:STM32F051K8Tx +MCU_ST_STM32F0:STM32F051K8Ux +MCU_ST_STM32F0:STM32F051R4Tx +MCU_ST_STM32F0:STM32F051R6Tx +MCU_ST_STM32F0:STM32F051R8Hx +MCU_ST_STM32F0:STM32F051R8Tx +MCU_ST_STM32F0:STM32F051T8Yx +MCU_ST_STM32F0:STM32F058C8Ux +MCU_ST_STM32F0:STM32F058R8Hx +MCU_ST_STM32F0:STM32F058R8Tx +MCU_ST_STM32F0:STM32F058T8Yx +MCU_ST_STM32F0:STM32F070C6Tx +MCU_ST_STM32F0:STM32F070CBTx +MCU_ST_STM32F0:STM32F070F6Px +MCU_ST_STM32F0:STM32F070RBTx +MCU_ST_STM32F0:STM32F071C8Tx +MCU_ST_STM32F0:STM32F071C8Ux +MCU_ST_STM32F0:STM32F071CBTx +MCU_ST_STM32F0:STM32F071CBUx +MCU_ST_STM32F0:STM32F071CBYx +MCU_ST_STM32F0:STM32F071C_8-B_Tx +MCU_ST_STM32F0:STM32F071C_8-B_Ux +MCU_ST_STM32F0:STM32F071RBTx +MCU_ST_STM32F0:STM32F071V8Hx +MCU_ST_STM32F0:STM32F071V8Tx +MCU_ST_STM32F0:STM32F071VBHx +MCU_ST_STM32F0:STM32F071VBTx +MCU_ST_STM32F0:STM32F071V_8-B_Hx +MCU_ST_STM32F0:STM32F071V_8-B_Tx +MCU_ST_STM32F0:STM32F072C8Tx +MCU_ST_STM32F0:STM32F072C8Ux +MCU_ST_STM32F0:STM32F072CBTx +MCU_ST_STM32F0:STM32F072CBUx +MCU_ST_STM32F0:STM32F072CBYx +MCU_ST_STM32F0:STM32F072C_8-B_Tx +MCU_ST_STM32F0:STM32F072C_8-B_Ux +MCU_ST_STM32F0:STM32F072R8Tx +MCU_ST_STM32F0:STM32F072RBHx +MCU_ST_STM32F0:STM32F072RBIx +MCU_ST_STM32F0:STM32F072RBTx +MCU_ST_STM32F0:STM32F072R_8-B_Tx +MCU_ST_STM32F0:STM32F072V8Hx +MCU_ST_STM32F0:STM32F072V8Tx +MCU_ST_STM32F0:STM32F072VBHx +MCU_ST_STM32F0:STM32F072VBTx +MCU_ST_STM32F0:STM32F072V_8-B_Hx +MCU_ST_STM32F0:STM32F072V_8-B_Tx +MCU_ST_STM32F0:STM32F078CBTx +MCU_ST_STM32F0:STM32F078CBUx +MCU_ST_STM32F0:STM32F078CBYx +MCU_ST_STM32F0:STM32F078RBHx +MCU_ST_STM32F0:STM32F078RBTx +MCU_ST_STM32F0:STM32F078VBHx +MCU_ST_STM32F0:STM32F078VBTx +MCU_ST_STM32F0:STM32F091CBTx +MCU_ST_STM32F0:STM32F091CBUx +MCU_ST_STM32F0:STM32F091CCTx +MCU_ST_STM32F0:STM32F091CCUx +MCU_ST_STM32F0:STM32F091C_B-C_Tx +MCU_ST_STM32F0:STM32F091C_B-C_Ux +MCU_ST_STM32F0:STM32F091RBTx +MCU_ST_STM32F0:STM32F091RCHx +MCU_ST_STM32F0:STM32F091RCTx +MCU_ST_STM32F0:STM32F091RCYx +MCU_ST_STM32F0:STM32F091R_B-C_Tx +MCU_ST_STM32F0:STM32F091VBTx +MCU_ST_STM32F0:STM32F091VCHx +MCU_ST_STM32F0:STM32F091VCTx +MCU_ST_STM32F0:STM32F091V_B-C_Tx +MCU_ST_STM32F0:STM32F098CCTx +MCU_ST_STM32F0:STM32F098CCUx +MCU_ST_STM32F0:STM32F098RCHx +MCU_ST_STM32F0:STM32F098RCTx +MCU_ST_STM32F0:STM32F098RCYx +MCU_ST_STM32F0:STM32F098VCHx +MCU_ST_STM32F0:STM32F098VCTx +MCU_ST_STM32F1:STM32F100C4Tx +MCU_ST_STM32F1:STM32F100C6Tx +MCU_ST_STM32F1:STM32F100C8Tx +MCU_ST_STM32F1:STM32F100CBTx +MCU_ST_STM32F1:STM32F100C_4-6_Tx +MCU_ST_STM32F1:STM32F100C_8-B_Tx +MCU_ST_STM32F1:STM32F100R4Hx +MCU_ST_STM32F1:STM32F100R4Tx +MCU_ST_STM32F1:STM32F100R6Hx +MCU_ST_STM32F1:STM32F100R6Tx +MCU_ST_STM32F1:STM32F100R8Hx +MCU_ST_STM32F1:STM32F100R8Tx +MCU_ST_STM32F1:STM32F100RBHx +MCU_ST_STM32F1:STM32F100RBTx +MCU_ST_STM32F1:STM32F100RCTx +MCU_ST_STM32F1:STM32F100RDTx +MCU_ST_STM32F1:STM32F100RETx +MCU_ST_STM32F1:STM32F100R_4-6_Hx +MCU_ST_STM32F1:STM32F100R_4-6_Tx +MCU_ST_STM32F1:STM32F100R_8-B_Hx +MCU_ST_STM32F1:STM32F100R_8-B_Tx +MCU_ST_STM32F1:STM32F100R_C-D-E_Tx +MCU_ST_STM32F1:STM32F100V8Tx +MCU_ST_STM32F1:STM32F100VBTx +MCU_ST_STM32F1:STM32F100VCTx +MCU_ST_STM32F1:STM32F100VDTx +MCU_ST_STM32F1:STM32F100VETx +MCU_ST_STM32F1:STM32F100V_8-B_Tx +MCU_ST_STM32F1:STM32F100V_C-D-E_Tx +MCU_ST_STM32F1:STM32F100ZCTx +MCU_ST_STM32F1:STM32F100ZDTx +MCU_ST_STM32F1:STM32F100ZETx +MCU_ST_STM32F1:STM32F100Z_C-D-E_Tx +MCU_ST_STM32F1:STM32F101C4Tx +MCU_ST_STM32F1:STM32F101C6Tx +MCU_ST_STM32F1:STM32F101C8Tx +MCU_ST_STM32F1:STM32F101C8Ux +MCU_ST_STM32F1:STM32F101CBTx +MCU_ST_STM32F1:STM32F101CBUx +MCU_ST_STM32F1:STM32F101C_4-6_Tx +MCU_ST_STM32F1:STM32F101C_8-B_Tx +MCU_ST_STM32F1:STM32F101C_8-B_Ux +MCU_ST_STM32F1:STM32F101R4Tx +MCU_ST_STM32F1:STM32F101R6Tx +MCU_ST_STM32F1:STM32F101R8Tx +MCU_ST_STM32F1:STM32F101RBHx +MCU_ST_STM32F1:STM32F101RBTx +MCU_ST_STM32F1:STM32F101RCTx +MCU_ST_STM32F1:STM32F101RDTx +MCU_ST_STM32F1:STM32F101RETx +MCU_ST_STM32F1:STM32F101RFTx +MCU_ST_STM32F1:STM32F101RGTx +MCU_ST_STM32F1:STM32F101R_4-6_Tx +MCU_ST_STM32F1:STM32F101R_8-B_Tx +MCU_ST_STM32F1:STM32F101R_C-D-E_Tx +MCU_ST_STM32F1:STM32F101R_F-G_Tx +MCU_ST_STM32F1:STM32F101T4Ux +MCU_ST_STM32F1:STM32F101T6Ux +MCU_ST_STM32F1:STM32F101T8Ux +MCU_ST_STM32F1:STM32F101TBUx +MCU_ST_STM32F1:STM32F101T_4-6_Ux +MCU_ST_STM32F1:STM32F101T_8-B_Ux +MCU_ST_STM32F1:STM32F101V8Tx +MCU_ST_STM32F1:STM32F101VBTx +MCU_ST_STM32F1:STM32F101VCTx +MCU_ST_STM32F1:STM32F101VDTx +MCU_ST_STM32F1:STM32F101VETx +MCU_ST_STM32F1:STM32F101VFTx +MCU_ST_STM32F1:STM32F101VGTx +MCU_ST_STM32F1:STM32F101V_8-B_Tx +MCU_ST_STM32F1:STM32F101V_C-D-E_Tx +MCU_ST_STM32F1:STM32F101V_F-G_Tx +MCU_ST_STM32F1:STM32F101ZCTx +MCU_ST_STM32F1:STM32F101ZDTx +MCU_ST_STM32F1:STM32F101ZETx +MCU_ST_STM32F1:STM32F101ZFTx +MCU_ST_STM32F1:STM32F101ZGTx +MCU_ST_STM32F1:STM32F101Z_C-D-E_Tx +MCU_ST_STM32F1:STM32F101Z_F-G_Tx +MCU_ST_STM32F1:STM32F102C4Tx +MCU_ST_STM32F1:STM32F102C6Tx +MCU_ST_STM32F1:STM32F102C8Tx +MCU_ST_STM32F1:STM32F102CBTx +MCU_ST_STM32F1:STM32F102C_4-6_Tx +MCU_ST_STM32F1:STM32F102C_8-B_Tx +MCU_ST_STM32F1:STM32F102R4Tx +MCU_ST_STM32F1:STM32F102R6Tx +MCU_ST_STM32F1:STM32F102R8Tx +MCU_ST_STM32F1:STM32F102RBTx +MCU_ST_STM32F1:STM32F102R_4-6_Tx +MCU_ST_STM32F1:STM32F102R_8-B_Tx +MCU_ST_STM32F1:STM32F103C4Tx +MCU_ST_STM32F1:STM32F103C6Tx +MCU_ST_STM32F1:STM32F103C6Ux +MCU_ST_STM32F1:STM32F103C8Tx +MCU_ST_STM32F1:STM32F103CBTx +MCU_ST_STM32F1:STM32F103CBUx +MCU_ST_STM32F1:STM32F103C_4-6_Tx +MCU_ST_STM32F1:STM32F103C_8-B_Tx +MCU_ST_STM32F1:STM32F103R4Hx +MCU_ST_STM32F1:STM32F103R4Tx +MCU_ST_STM32F1:STM32F103R6Hx +MCU_ST_STM32F1:STM32F103R6Tx +MCU_ST_STM32F1:STM32F103R8Hx +MCU_ST_STM32F1:STM32F103R8Tx +MCU_ST_STM32F1:STM32F103RBHx +MCU_ST_STM32F1:STM32F103RBTx +MCU_ST_STM32F1:STM32F103RCTx +MCU_ST_STM32F1:STM32F103RCYx +MCU_ST_STM32F1:STM32F103RDTx +MCU_ST_STM32F1:STM32F103RDYx +MCU_ST_STM32F1:STM32F103RETx +MCU_ST_STM32F1:STM32F103REYx +MCU_ST_STM32F1:STM32F103RFTx +MCU_ST_STM32F1:STM32F103RGTx +MCU_ST_STM32F1:STM32F103R_4-6_Hx +MCU_ST_STM32F1:STM32F103R_4-6_Tx +MCU_ST_STM32F1:STM32F103R_8-B_Hx +MCU_ST_STM32F1:STM32F103R_8-B_Tx +MCU_ST_STM32F1:STM32F103R_C-D-E_Tx +MCU_ST_STM32F1:STM32F103R_C-D-E_Yx +MCU_ST_STM32F1:STM32F103R_F-G_Tx +MCU_ST_STM32F1:STM32F103T4Ux +MCU_ST_STM32F1:STM32F103T6Ux +MCU_ST_STM32F1:STM32F103T8Ux +MCU_ST_STM32F1:STM32F103TBUx +MCU_ST_STM32F1:STM32F103T_4-6_Ux +MCU_ST_STM32F1:STM32F103T_8-B_Ux +MCU_ST_STM32F1:STM32F103V8Hx +MCU_ST_STM32F1:STM32F103V8Tx +MCU_ST_STM32F1:STM32F103VBHx +MCU_ST_STM32F1:STM32F103VBIx +MCU_ST_STM32F1:STM32F103VBTx +MCU_ST_STM32F1:STM32F103VCHx +MCU_ST_STM32F1:STM32F103VCTx +MCU_ST_STM32F1:STM32F103VDHx +MCU_ST_STM32F1:STM32F103VDTx +MCU_ST_STM32F1:STM32F103VEHx +MCU_ST_STM32F1:STM32F103VETx +MCU_ST_STM32F1:STM32F103VFTx +MCU_ST_STM32F1:STM32F103VGTx +MCU_ST_STM32F1:STM32F103V_8-B_Hx +MCU_ST_STM32F1:STM32F103V_8-B_Tx +MCU_ST_STM32F1:STM32F103V_C-D-E_Hx +MCU_ST_STM32F1:STM32F103V_C-D-E_Tx +MCU_ST_STM32F1:STM32F103V_F-G_Tx +MCU_ST_STM32F1:STM32F103ZCHx +MCU_ST_STM32F1:STM32F103ZCTx +MCU_ST_STM32F1:STM32F103ZDHx +MCU_ST_STM32F1:STM32F103ZDTx +MCU_ST_STM32F1:STM32F103ZEHx +MCU_ST_STM32F1:STM32F103ZETx +MCU_ST_STM32F1:STM32F103ZFHx +MCU_ST_STM32F1:STM32F103ZFTx +MCU_ST_STM32F1:STM32F103ZGHx +MCU_ST_STM32F1:STM32F103ZGTx +MCU_ST_STM32F1:STM32F103Z_C-D-E_Hx +MCU_ST_STM32F1:STM32F103Z_C-D-E_Tx +MCU_ST_STM32F1:STM32F103Z_F-G_Hx +MCU_ST_STM32F1:STM32F103Z_F-G_Tx +MCU_ST_STM32F1:STM32F105R8Tx +MCU_ST_STM32F1:STM32F105RBTx +MCU_ST_STM32F1:STM32F105RCTx +MCU_ST_STM32F1:STM32F105R_8-B-C_Tx +MCU_ST_STM32F1:STM32F105V8Hx +MCU_ST_STM32F1:STM32F105V8Tx +MCU_ST_STM32F1:STM32F105VBHx +MCU_ST_STM32F1:STM32F105VBTx +MCU_ST_STM32F1:STM32F105VCTx +MCU_ST_STM32F1:STM32F105V_8-B-C_Tx +MCU_ST_STM32F1:STM32F105V_8-B_Hx +MCU_ST_STM32F1:STM32F107RBTx +MCU_ST_STM32F1:STM32F107RCTx +MCU_ST_STM32F1:STM32F107R_B-C_Tx +MCU_ST_STM32F1:STM32F107VBTx +MCU_ST_STM32F1:STM32F107VCHx +MCU_ST_STM32F1:STM32F107VCTx +MCU_ST_STM32F1:STM32F107V_B-C_Tx +MCU_ST_STM32F2:STM32F205RBTx +MCU_ST_STM32F2:STM32F205RCTx +MCU_ST_STM32F2:STM32F205RETx +MCU_ST_STM32F2:STM32F205REYx +MCU_ST_STM32F2:STM32F205RFTx +MCU_ST_STM32F2:STM32F205RGEx +MCU_ST_STM32F2:STM32F205RGTx +MCU_ST_STM32F2:STM32F205RGYx +MCU_ST_STM32F2:STM32F205R_B-C-E-F-G_Tx +MCU_ST_STM32F2:STM32F205R_E-G_Yx +MCU_ST_STM32F2:STM32F205VBTx +MCU_ST_STM32F2:STM32F205VCTx +MCU_ST_STM32F2:STM32F205VETx +MCU_ST_STM32F2:STM32F205VFTx +MCU_ST_STM32F2:STM32F205VGTx +MCU_ST_STM32F2:STM32F205V_B-C-E-F-G_Tx +MCU_ST_STM32F2:STM32F205ZCTx +MCU_ST_STM32F2:STM32F205ZETx +MCU_ST_STM32F2:STM32F205ZFTx +MCU_ST_STM32F2:STM32F205ZGTx +MCU_ST_STM32F2:STM32F205Z_C-E-F-G_Tx +MCU_ST_STM32F2:STM32F207ICHx +MCU_ST_STM32F2:STM32F207ICTx +MCU_ST_STM32F2:STM32F207IEHx +MCU_ST_STM32F2:STM32F207IETx +MCU_ST_STM32F2:STM32F207IFHx +MCU_ST_STM32F2:STM32F207IFTx +MCU_ST_STM32F2:STM32F207IGHx +MCU_ST_STM32F2:STM32F207IGTx +MCU_ST_STM32F2:STM32F207I_C-E-F-G_Hx +MCU_ST_STM32F2:STM32F207I_C-E-F-G_Tx +MCU_ST_STM32F2:STM32F207VCTx +MCU_ST_STM32F2:STM32F207VETx +MCU_ST_STM32F2:STM32F207VFTx +MCU_ST_STM32F2:STM32F207VGTx +MCU_ST_STM32F2:STM32F207V_C-E-F-G_Tx +MCU_ST_STM32F2:STM32F207ZCTx +MCU_ST_STM32F2:STM32F207ZETx +MCU_ST_STM32F2:STM32F207ZFTx +MCU_ST_STM32F2:STM32F207ZGTx +MCU_ST_STM32F2:STM32F207Z_C-E-F-G_Tx +MCU_ST_STM32F2:STM32F215RETx +MCU_ST_STM32F2:STM32F215RGTx +MCU_ST_STM32F2:STM32F215R_E-G_Tx +MCU_ST_STM32F2:STM32F215VETx +MCU_ST_STM32F2:STM32F215VGTx +MCU_ST_STM32F2:STM32F215V_E-G_Tx +MCU_ST_STM32F2:STM32F215ZETx +MCU_ST_STM32F2:STM32F215ZGTx +MCU_ST_STM32F2:STM32F215Z_E-G_Tx +MCU_ST_STM32F2:STM32F217IEHx +MCU_ST_STM32F2:STM32F217IETx +MCU_ST_STM32F2:STM32F217IGHx +MCU_ST_STM32F2:STM32F217IGTx +MCU_ST_STM32F2:STM32F217I_E-G_Hx +MCU_ST_STM32F2:STM32F217I_E-G_Tx +MCU_ST_STM32F2:STM32F217VETx +MCU_ST_STM32F2:STM32F217VGTx +MCU_ST_STM32F2:STM32F217V_E-G_Tx +MCU_ST_STM32F2:STM32F217ZETx +MCU_ST_STM32F2:STM32F217ZGTx +MCU_ST_STM32F2:STM32F217Z_E-G_Tx +MCU_ST_STM32F3:STM32F301C6Tx +MCU_ST_STM32F3:STM32F301C8Tx +MCU_ST_STM32F3:STM32F301C8Yx +MCU_ST_STM32F3:STM32F301C_6-8_Tx +MCU_ST_STM32F3:STM32F301K6Tx +MCU_ST_STM32F3:STM32F301K6Ux +MCU_ST_STM32F3:STM32F301K8Tx +MCU_ST_STM32F3:STM32F301K8Ux +MCU_ST_STM32F3:STM32F301K_6-8_Tx +MCU_ST_STM32F3:STM32F301K_6-8_Ux +MCU_ST_STM32F3:STM32F301R6Tx +MCU_ST_STM32F3:STM32F301R8Tx +MCU_ST_STM32F3:STM32F301R_6-8_Tx +MCU_ST_STM32F3:STM32F302C6Tx +MCU_ST_STM32F3:STM32F302C8Tx +MCU_ST_STM32F3:STM32F302C8Yx +MCU_ST_STM32F3:STM32F302CBTx +MCU_ST_STM32F3:STM32F302CCTx +MCU_ST_STM32F3:STM32F302C_6-8_Tx +MCU_ST_STM32F3:STM32F302C_B-C_Tx +MCU_ST_STM32F3:STM32F302K6Ux +MCU_ST_STM32F3:STM32F302K8Ux +MCU_ST_STM32F3:STM32F302K_6-8_Ux +MCU_ST_STM32F3:STM32F302R6Tx +MCU_ST_STM32F3:STM32F302R8Tx +MCU_ST_STM32F3:STM32F302RBTx +MCU_ST_STM32F3:STM32F302RCTx +MCU_ST_STM32F3:STM32F302RDTx +MCU_ST_STM32F3:STM32F302RETx +MCU_ST_STM32F3:STM32F302R_6-8_Tx +MCU_ST_STM32F3:STM32F302R_B-C_Tx +MCU_ST_STM32F3:STM32F302R_D-E_Tx +MCU_ST_STM32F3:STM32F302VBTx +MCU_ST_STM32F3:STM32F302VCTx +MCU_ST_STM32F3:STM32F302VCYx +MCU_ST_STM32F3:STM32F302VDHx +MCU_ST_STM32F3:STM32F302VDTx +MCU_ST_STM32F3:STM32F302VEHx +MCU_ST_STM32F3:STM32F302VETx +MCU_ST_STM32F3:STM32F302V_B-C_Tx +MCU_ST_STM32F3:STM32F302V_D-E_Hx +MCU_ST_STM32F3:STM32F302V_D-E_Tx +MCU_ST_STM32F3:STM32F302ZDTx +MCU_ST_STM32F3:STM32F302ZETx +MCU_ST_STM32F3:STM32F302Z_D-E_Tx +MCU_ST_STM32F3:STM32F303C6Tx +MCU_ST_STM32F3:STM32F303C8Tx +MCU_ST_STM32F3:STM32F303C8Yx +MCU_ST_STM32F3:STM32F303CBTx +MCU_ST_STM32F3:STM32F303CCTx +MCU_ST_STM32F3:STM32F303C_6-8_Tx +MCU_ST_STM32F3:STM32F303C_B-C_Tx +MCU_ST_STM32F3:STM32F303K6Tx +MCU_ST_STM32F3:STM32F303K6Ux +MCU_ST_STM32F3:STM32F303K8Tx +MCU_ST_STM32F3:STM32F303K8Ux +MCU_ST_STM32F3:STM32F303K_6-8_Tx +MCU_ST_STM32F3:STM32F303K_6-8_Ux +MCU_ST_STM32F3:STM32F303R6Tx +MCU_ST_STM32F3:STM32F303R8Tx +MCU_ST_STM32F3:STM32F303RBTx +MCU_ST_STM32F3:STM32F303RCTx +MCU_ST_STM32F3:STM32F303RDTx +MCU_ST_STM32F3:STM32F303RETx +MCU_ST_STM32F3:STM32F303R_6-8_Tx +MCU_ST_STM32F3:STM32F303R_B-C_Tx +MCU_ST_STM32F3:STM32F303R_D-E_Tx +MCU_ST_STM32F3:STM32F303VBTx +MCU_ST_STM32F3:STM32F303VCTx +MCU_ST_STM32F3:STM32F303VCYx +MCU_ST_STM32F3:STM32F303VDHx +MCU_ST_STM32F3:STM32F303VDTx +MCU_ST_STM32F3:STM32F303VEHx +MCU_ST_STM32F3:STM32F303VETx +MCU_ST_STM32F3:STM32F303VEYx +MCU_ST_STM32F3:STM32F303V_B-C_Tx +MCU_ST_STM32F3:STM32F303V_D-E_Hx +MCU_ST_STM32F3:STM32F303V_D-E_Tx +MCU_ST_STM32F3:STM32F303ZDTx +MCU_ST_STM32F3:STM32F303ZETx +MCU_ST_STM32F3:STM32F303Z_D-E_Tx +MCU_ST_STM32F3:STM32F318C8Tx +MCU_ST_STM32F3:STM32F318C8Yx +MCU_ST_STM32F3:STM32F318K8Ux +MCU_ST_STM32F3:STM32F328C8Tx +MCU_ST_STM32F3:STM32F334C4Tx +MCU_ST_STM32F3:STM32F334C6Tx +MCU_ST_STM32F3:STM32F334C8Tx +MCU_ST_STM32F3:STM32F334C8Yx +MCU_ST_STM32F3:STM32F334C_4-6-8_Tx +MCU_ST_STM32F3:STM32F334K4Tx +MCU_ST_STM32F3:STM32F334K4Ux +MCU_ST_STM32F3:STM32F334K6Tx +MCU_ST_STM32F3:STM32F334K6Ux +MCU_ST_STM32F3:STM32F334K8Tx +MCU_ST_STM32F3:STM32F334K8Ux +MCU_ST_STM32F3:STM32F334K_4-6-8_Tx +MCU_ST_STM32F3:STM32F334K_4-6-8_Ux +MCU_ST_STM32F3:STM32F334R6Tx +MCU_ST_STM32F3:STM32F334R8Tx +MCU_ST_STM32F3:STM32F334R_6-8_Tx +MCU_ST_STM32F3:STM32F358CCTx +MCU_ST_STM32F3:STM32F358RCTx +MCU_ST_STM32F3:STM32F358VCTx +MCU_ST_STM32F3:STM32F373C8Tx +MCU_ST_STM32F3:STM32F373CBTx +MCU_ST_STM32F3:STM32F373CCTx +MCU_ST_STM32F3:STM32F373C_8-B-C_Tx +MCU_ST_STM32F3:STM32F373R8Tx +MCU_ST_STM32F3:STM32F373RBTx +MCU_ST_STM32F3:STM32F373RCTx +MCU_ST_STM32F3:STM32F373R_8-B-C_Tx +MCU_ST_STM32F3:STM32F373V8Hx +MCU_ST_STM32F3:STM32F373V8Tx +MCU_ST_STM32F3:STM32F373VBHx +MCU_ST_STM32F3:STM32F373VBTx +MCU_ST_STM32F3:STM32F373VCHx +MCU_ST_STM32F3:STM32F373VCTx +MCU_ST_STM32F3:STM32F373V_8-B-C_Hx +MCU_ST_STM32F3:STM32F373V_8-B-C_Tx +MCU_ST_STM32F3:STM32F378CCTx +MCU_ST_STM32F3:STM32F378RCTx +MCU_ST_STM32F3:STM32F378RCYx +MCU_ST_STM32F3:STM32F378VCHx +MCU_ST_STM32F3:STM32F378VCTx +MCU_ST_STM32F3:STM32F398VETx +MCU_ST_STM32F4:STM32F401CBUx +MCU_ST_STM32F4:STM32F401CBYx +MCU_ST_STM32F4:STM32F401CCFx +MCU_ST_STM32F4:STM32F401CCUx +MCU_ST_STM32F4:STM32F401CCYx +MCU_ST_STM32F4:STM32F401CDUx +MCU_ST_STM32F4:STM32F401CDYx +MCU_ST_STM32F4:STM32F401CEUx +MCU_ST_STM32F4:STM32F401CEYx +MCU_ST_STM32F4:STM32F401C_B-C_Ux +MCU_ST_STM32F4:STM32F401C_B-C_Yx +MCU_ST_STM32F4:STM32F401C_D-E_Ux +MCU_ST_STM32F4:STM32F401C_D-E_Yx +MCU_ST_STM32F4:STM32F401RBTx +MCU_ST_STM32F4:STM32F401RCTx +MCU_ST_STM32F4:STM32F401RDTx +MCU_ST_STM32F4:STM32F401RETx +MCU_ST_STM32F4:STM32F401R_B-C_Tx +MCU_ST_STM32F4:STM32F401R_D-E_Tx +MCU_ST_STM32F4:STM32F401VBHx +MCU_ST_STM32F4:STM32F401VBTx +MCU_ST_STM32F4:STM32F401VCHx +MCU_ST_STM32F4:STM32F401VCTx +MCU_ST_STM32F4:STM32F401VDHx +MCU_ST_STM32F4:STM32F401VDTx +MCU_ST_STM32F4:STM32F401VEHx +MCU_ST_STM32F4:STM32F401VETx +MCU_ST_STM32F4:STM32F401V_B-C_Hx +MCU_ST_STM32F4:STM32F401V_B-C_Tx +MCU_ST_STM32F4:STM32F401V_D-E_Hx +MCU_ST_STM32F4:STM32F401V_D-E_Tx +MCU_ST_STM32F4:STM32F405OEYx +MCU_ST_STM32F4:STM32F405OGYx +MCU_ST_STM32F4:STM32F405O_E-G_Yx +MCU_ST_STM32F4:STM32F405RGTx +MCU_ST_STM32F4:STM32F405VGTx +MCU_ST_STM32F4:STM32F405ZGTx +MCU_ST_STM32F4:STM32F407IEHx +MCU_ST_STM32F4:STM32F407IETx +MCU_ST_STM32F4:STM32F407IGHx +MCU_ST_STM32F4:STM32F407IGTx +MCU_ST_STM32F4:STM32F407I_E-G_Hx +MCU_ST_STM32F4:STM32F407I_E-G_Tx +MCU_ST_STM32F4:STM32F407VETx +MCU_ST_STM32F4:STM32F407VGTx +MCU_ST_STM32F4:STM32F407V_E-G_Tx +MCU_ST_STM32F4:STM32F407ZETx +MCU_ST_STM32F4:STM32F407ZGTx +MCU_ST_STM32F4:STM32F407Z_E-G_Tx +MCU_ST_STM32F4:STM32F410C8Tx +MCU_ST_STM32F4:STM32F410C8Ux +MCU_ST_STM32F4:STM32F410CBTx +MCU_ST_STM32F4:STM32F410CBUx +MCU_ST_STM32F4:STM32F410C_8-B_Tx +MCU_ST_STM32F4:STM32F410C_8-B_Ux +MCU_ST_STM32F4:STM32F410R8Ix +MCU_ST_STM32F4:STM32F410R8Tx +MCU_ST_STM32F4:STM32F410RBIx +MCU_ST_STM32F4:STM32F410RBTx +MCU_ST_STM32F4:STM32F410R_8-B_Ix +MCU_ST_STM32F4:STM32F410R_8-B_Tx +MCU_ST_STM32F4:STM32F410T8Yx +MCU_ST_STM32F4:STM32F410TBYx +MCU_ST_STM32F4:STM32F410T_8-B_Yx +MCU_ST_STM32F4:STM32F411CCUx +MCU_ST_STM32F4:STM32F411CCYx +MCU_ST_STM32F4:STM32F411CEUx +MCU_ST_STM32F4:STM32F411CEYx +MCU_ST_STM32F4:STM32F411C_C-E_Ux +MCU_ST_STM32F4:STM32F411C_C-E_Yx +MCU_ST_STM32F4:STM32F411RCTx +MCU_ST_STM32F4:STM32F411RETx +MCU_ST_STM32F4:STM32F411R_C-E_Tx +MCU_ST_STM32F4:STM32F411VCHx +MCU_ST_STM32F4:STM32F411VCTx +MCU_ST_STM32F4:STM32F411VEHx +MCU_ST_STM32F4:STM32F411VETx +MCU_ST_STM32F4:STM32F411V_C-E_Hx +MCU_ST_STM32F4:STM32F411V_C-E_Tx +MCU_ST_STM32F4:STM32F412CEUx +MCU_ST_STM32F4:STM32F412CGUx +MCU_ST_STM32F4:STM32F412C_E-G_Ux +MCU_ST_STM32F4:STM32F412RETx +MCU_ST_STM32F4:STM32F412REYx +MCU_ST_STM32F4:STM32F412REYxP +MCU_ST_STM32F4:STM32F412RGTx +MCU_ST_STM32F4:STM32F412RGYx +MCU_ST_STM32F4:STM32F412RGYxP +MCU_ST_STM32F4:STM32F412R_E-G_Tx +MCU_ST_STM32F4:STM32F412R_E-G_Yx +MCU_ST_STM32F4:STM32F412R_E-G_YxP +MCU_ST_STM32F4:STM32F412VEHx +MCU_ST_STM32F4:STM32F412VETx +MCU_ST_STM32F4:STM32F412VGHx +MCU_ST_STM32F4:STM32F412VGTx +MCU_ST_STM32F4:STM32F412V_E-G_Hx +MCU_ST_STM32F4:STM32F412V_E-G_Tx +MCU_ST_STM32F4:STM32F412ZEJx +MCU_ST_STM32F4:STM32F412ZETx +MCU_ST_STM32F4:STM32F412ZGJx +MCU_ST_STM32F4:STM32F412ZGTx +MCU_ST_STM32F4:STM32F412Z_E-G_Jx +MCU_ST_STM32F4:STM32F412Z_E-G_Tx +MCU_ST_STM32F4:STM32F413CGUx +MCU_ST_STM32F4:STM32F413CHUx +MCU_ST_STM32F4:STM32F413C_G-H_Ux +MCU_ST_STM32F4:STM32F413MGYx +MCU_ST_STM32F4:STM32F413MHYx +MCU_ST_STM32F4:STM32F413M_G-H_Yx +MCU_ST_STM32F4:STM32F413RGTx +MCU_ST_STM32F4:STM32F413RHTx +MCU_ST_STM32F4:STM32F413R_G-H_Tx +MCU_ST_STM32F4:STM32F413VGHx +MCU_ST_STM32F4:STM32F413VGTx +MCU_ST_STM32F4:STM32F413VHHx +MCU_ST_STM32F4:STM32F413VHTx +MCU_ST_STM32F4:STM32F413V_G-H_Hx +MCU_ST_STM32F4:STM32F413V_G-H_Tx +MCU_ST_STM32F4:STM32F413ZGJx +MCU_ST_STM32F4:STM32F413ZGTx +MCU_ST_STM32F4:STM32F413ZHJx +MCU_ST_STM32F4:STM32F413ZHTx +MCU_ST_STM32F4:STM32F413Z_G-H_Jx +MCU_ST_STM32F4:STM32F413Z_G-H_Tx +MCU_ST_STM32F4:STM32F415OGYx +MCU_ST_STM32F4:STM32F415RGTx +MCU_ST_STM32F4:STM32F415VGTx +MCU_ST_STM32F4:STM32F415ZGTx +MCU_ST_STM32F4:STM32F417IEHx +MCU_ST_STM32F4:STM32F417IETx +MCU_ST_STM32F4:STM32F417IGHx +MCU_ST_STM32F4:STM32F417IGTx +MCU_ST_STM32F4:STM32F417I_E-G_Hx +MCU_ST_STM32F4:STM32F417I_E-G_Tx +MCU_ST_STM32F4:STM32F417VETx +MCU_ST_STM32F4:STM32F417VGTx +MCU_ST_STM32F4:STM32F417V_E-G_Tx +MCU_ST_STM32F4:STM32F417ZETx +MCU_ST_STM32F4:STM32F417ZGTx +MCU_ST_STM32F4:STM32F417Z_E-G_Tx +MCU_ST_STM32F4:STM32F423CHUx +MCU_ST_STM32F4:STM32F423MHYx +MCU_ST_STM32F4:STM32F423RHTx +MCU_ST_STM32F4:STM32F423VHHx +MCU_ST_STM32F4:STM32F423VHTx +MCU_ST_STM32F4:STM32F423ZHJx +MCU_ST_STM32F4:STM32F423ZHTx +MCU_ST_STM32F4:STM32F427AGHx +MCU_ST_STM32F4:STM32F427AIHx +MCU_ST_STM32F4:STM32F427A_G-I_Hx +MCU_ST_STM32F4:STM32F427IGHx +MCU_ST_STM32F4:STM32F427IGTx +MCU_ST_STM32F4:STM32F427IIHx +MCU_ST_STM32F4:STM32F427IITx +MCU_ST_STM32F4:STM32F427I_G-I_Hx +MCU_ST_STM32F4:STM32F427I_G-I_Tx +MCU_ST_STM32F4:STM32F427VGTx +MCU_ST_STM32F4:STM32F427VITx +MCU_ST_STM32F4:STM32F427V_G-I_Tx +MCU_ST_STM32F4:STM32F427ZGTx +MCU_ST_STM32F4:STM32F427ZITx +MCU_ST_STM32F4:STM32F427Z_G-I_Tx +MCU_ST_STM32F4:STM32F429AGHx +MCU_ST_STM32F4:STM32F429AIHx +MCU_ST_STM32F4:STM32F429A_G-I_Hx +MCU_ST_STM32F4:STM32F429BETx +MCU_ST_STM32F4:STM32F429BGTx +MCU_ST_STM32F4:STM32F429BITx +MCU_ST_STM32F4:STM32F429B_E-G-I_Tx +MCU_ST_STM32F4:STM32F429IEHx +MCU_ST_STM32F4:STM32F429IETx +MCU_ST_STM32F4:STM32F429IGHx +MCU_ST_STM32F4:STM32F429IGTx +MCU_ST_STM32F4:STM32F429IIHx +MCU_ST_STM32F4:STM32F429IITx +MCU_ST_STM32F4:STM32F429I_E-G-I_Hx +MCU_ST_STM32F4:STM32F429I_E-G_Tx +MCU_ST_STM32F4:STM32F429NEHx +MCU_ST_STM32F4:STM32F429NGHx +MCU_ST_STM32F4:STM32F429NIHx +MCU_ST_STM32F4:STM32F429N_E-G_Hx +MCU_ST_STM32F4:STM32F429VETx +MCU_ST_STM32F4:STM32F429VGTx +MCU_ST_STM32F4:STM32F429VITx +MCU_ST_STM32F4:STM32F429V_E-G_Tx +MCU_ST_STM32F4:STM32F429ZETx +MCU_ST_STM32F4:STM32F429ZGTx +MCU_ST_STM32F4:STM32F429ZGYx +MCU_ST_STM32F4:STM32F429ZITx +MCU_ST_STM32F4:STM32F429ZIYx +MCU_ST_STM32F4:STM32F429Z_E-G_Tx +MCU_ST_STM32F4:STM32F437AIHx +MCU_ST_STM32F4:STM32F437IGHx +MCU_ST_STM32F4:STM32F437IGTx +MCU_ST_STM32F4:STM32F437IIHx +MCU_ST_STM32F4:STM32F437IITx +MCU_ST_STM32F4:STM32F437I_G-I_Hx +MCU_ST_STM32F4:STM32F437I_G-I_Tx +MCU_ST_STM32F4:STM32F437VGTx +MCU_ST_STM32F4:STM32F437VITx +MCU_ST_STM32F4:STM32F437V_G-I_Tx +MCU_ST_STM32F4:STM32F437ZGTx +MCU_ST_STM32F4:STM32F437ZITx +MCU_ST_STM32F4:STM32F437Z_G-I_Tx +MCU_ST_STM32F4:STM32F439AIHx +MCU_ST_STM32F4:STM32F439BGTx +MCU_ST_STM32F4:STM32F439BITx +MCU_ST_STM32F4:STM32F439B_G-I_Tx +MCU_ST_STM32F4:STM32F439IGHx +MCU_ST_STM32F4:STM32F439IGTx +MCU_ST_STM32F4:STM32F439IIHx +MCU_ST_STM32F4:STM32F439IITx +MCU_ST_STM32F4:STM32F439I_G-I_Hx +MCU_ST_STM32F4:STM32F439I_G-I_Tx +MCU_ST_STM32F4:STM32F439NGHx +MCU_ST_STM32F4:STM32F439NIHx +MCU_ST_STM32F4:STM32F439N_G-I_Hx +MCU_ST_STM32F4:STM32F439VGTx +MCU_ST_STM32F4:STM32F439VITx +MCU_ST_STM32F4:STM32F439V_G-I_Tx +MCU_ST_STM32F4:STM32F439ZGTx +MCU_ST_STM32F4:STM32F439ZGYx +MCU_ST_STM32F4:STM32F439ZITx +MCU_ST_STM32F4:STM32F439ZIYx +MCU_ST_STM32F4:STM32F439Z_G-I_Tx +MCU_ST_STM32F4:STM32F439Z_G-I_Yx +MCU_ST_STM32F4:STM32F446MCYx +MCU_ST_STM32F4:STM32F446MEYx +MCU_ST_STM32F4:STM32F446M_C-E_Yx +MCU_ST_STM32F4:STM32F446RCTx +MCU_ST_STM32F4:STM32F446RETx +MCU_ST_STM32F4:STM32F446R_C-E_Tx +MCU_ST_STM32F4:STM32F446VCTx +MCU_ST_STM32F4:STM32F446VETx +MCU_ST_STM32F4:STM32F446V_C-E_Tx +MCU_ST_STM32F4:STM32F446ZCHx +MCU_ST_STM32F4:STM32F446ZCJx +MCU_ST_STM32F4:STM32F446ZCTx +MCU_ST_STM32F4:STM32F446ZEHx +MCU_ST_STM32F4:STM32F446ZEJx +MCU_ST_STM32F4:STM32F446ZETx +MCU_ST_STM32F4:STM32F446Z_C-E_Hx +MCU_ST_STM32F4:STM32F446Z_C-E_Jx +MCU_ST_STM32F4:STM32F446Z_C-E_Tx +MCU_ST_STM32F4:STM32F469AEHx +MCU_ST_STM32F4:STM32F469AEYx +MCU_ST_STM32F4:STM32F469AGHx +MCU_ST_STM32F4:STM32F469AGYx +MCU_ST_STM32F4:STM32F469AIHx +MCU_ST_STM32F4:STM32F469AIYx +MCU_ST_STM32F4:STM32F469A_E-G-I_Hx +MCU_ST_STM32F4:STM32F469A_E-G-I_Yx +MCU_ST_STM32F4:STM32F469BETx +MCU_ST_STM32F4:STM32F469BGTx +MCU_ST_STM32F4:STM32F469BITx +MCU_ST_STM32F4:STM32F469B_E-G-I_Tx +MCU_ST_STM32F4:STM32F469IEHx +MCU_ST_STM32F4:STM32F469IETx +MCU_ST_STM32F4:STM32F469IGHx +MCU_ST_STM32F4:STM32F469IGTx +MCU_ST_STM32F4:STM32F469IIHx +MCU_ST_STM32F4:STM32F469IITx +MCU_ST_STM32F4:STM32F469I_E-G-I_Hx +MCU_ST_STM32F4:STM32F469I_E-G_Tx +MCU_ST_STM32F4:STM32F469NEHx +MCU_ST_STM32F4:STM32F469NGHx +MCU_ST_STM32F4:STM32F469NIHx +MCU_ST_STM32F4:STM32F469N_E-G_Hx +MCU_ST_STM32F4:STM32F469VETx +MCU_ST_STM32F4:STM32F469VGTx +MCU_ST_STM32F4:STM32F469VITx +MCU_ST_STM32F4:STM32F469V_E-G_Tx +MCU_ST_STM32F4:STM32F469ZETx +MCU_ST_STM32F4:STM32F469ZGTx +MCU_ST_STM32F4:STM32F469ZITx +MCU_ST_STM32F4:STM32F469Z_E-G_Tx +MCU_ST_STM32F4:STM32F479AGHx +MCU_ST_STM32F4:STM32F479AGYx +MCU_ST_STM32F4:STM32F479AIHx +MCU_ST_STM32F4:STM32F479AIYx +MCU_ST_STM32F4:STM32F479A_G-I_Hx +MCU_ST_STM32F4:STM32F479A_G-I_Yx +MCU_ST_STM32F4:STM32F479BGTx +MCU_ST_STM32F4:STM32F479BITx +MCU_ST_STM32F4:STM32F479B_G-I_Tx +MCU_ST_STM32F4:STM32F479IGHx +MCU_ST_STM32F4:STM32F479IGTx +MCU_ST_STM32F4:STM32F479IIHx +MCU_ST_STM32F4:STM32F479IITx +MCU_ST_STM32F4:STM32F479I_G-I_Hx +MCU_ST_STM32F4:STM32F479I_G-I_Tx +MCU_ST_STM32F4:STM32F479NGHx +MCU_ST_STM32F4:STM32F479NIHx +MCU_ST_STM32F4:STM32F479N_G-I_Hx +MCU_ST_STM32F4:STM32F479VGTx +MCU_ST_STM32F4:STM32F479VITx +MCU_ST_STM32F4:STM32F479V_G-I_Tx +MCU_ST_STM32F4:STM32F479ZGTx +MCU_ST_STM32F4:STM32F479ZITx +MCU_ST_STM32F4:STM32F479Z_G-I_Tx +MCU_ST_STM32F7:STM32F722ICKx +MCU_ST_STM32F7:STM32F722ICTx +MCU_ST_STM32F7:STM32F722IEKx +MCU_ST_STM32F7:STM32F722IETx +MCU_ST_STM32F7:STM32F722I_C-E_Kx +MCU_ST_STM32F7:STM32F722I_C-E_Tx +MCU_ST_STM32F7:STM32F722RCTx +MCU_ST_STM32F7:STM32F722RETx +MCU_ST_STM32F7:STM32F722R_C-E_Tx +MCU_ST_STM32F7:STM32F722VCTx +MCU_ST_STM32F7:STM32F722VETx +MCU_ST_STM32F7:STM32F722V_C-E_Tx +MCU_ST_STM32F7:STM32F722ZCTx +MCU_ST_STM32F7:STM32F722ZETx +MCU_ST_STM32F7:STM32F722Z_C-E_Tx +MCU_ST_STM32F7:STM32F723ICKx +MCU_ST_STM32F7:STM32F723ICTx +MCU_ST_STM32F7:STM32F723IEKx +MCU_ST_STM32F7:STM32F723IETx +MCU_ST_STM32F7:STM32F723I_C-E_Kx +MCU_ST_STM32F7:STM32F723I_C-E_Tx +MCU_ST_STM32F7:STM32F723VCTx +MCU_ST_STM32F7:STM32F723VCYx +MCU_ST_STM32F7:STM32F723VETx +MCU_ST_STM32F7:STM32F723VEYx +MCU_ST_STM32F7:STM32F723V_C-E_Tx +MCU_ST_STM32F7:STM32F723V_C-E_Yx +MCU_ST_STM32F7:STM32F723ZCIx +MCU_ST_STM32F7:STM32F723ZCTx +MCU_ST_STM32F7:STM32F723ZEIx +MCU_ST_STM32F7:STM32F723ZETx +MCU_ST_STM32F7:STM32F723Z_C-E_Ix +MCU_ST_STM32F7:STM32F723Z_C-E_Tx +MCU_ST_STM32F7:STM32F730I8Kx +MCU_ST_STM32F7:STM32F730R8Tx +MCU_ST_STM32F7:STM32F730V8Tx +MCU_ST_STM32F7:STM32F730Z8Tx +MCU_ST_STM32F7:STM32F732IEKx +MCU_ST_STM32F7:STM32F732IETx +MCU_ST_STM32F7:STM32F732RETx +MCU_ST_STM32F7:STM32F732VETx +MCU_ST_STM32F7:STM32F732ZETx +MCU_ST_STM32F7:STM32F733IEKx +MCU_ST_STM32F7:STM32F733IETx +MCU_ST_STM32F7:STM32F733VETx +MCU_ST_STM32F7:STM32F733VEYx +MCU_ST_STM32F7:STM32F733ZEIx +MCU_ST_STM32F7:STM32F733ZETx +MCU_ST_STM32F7:STM32F745IEKx +MCU_ST_STM32F7:STM32F745IETx +MCU_ST_STM32F7:STM32F745IGKx +MCU_ST_STM32F7:STM32F745IGTx +MCU_ST_STM32F7:STM32F745I_E-G_Kx +MCU_ST_STM32F7:STM32F745I_E-G_Tx +MCU_ST_STM32F7:STM32F745VEHx +MCU_ST_STM32F7:STM32F745VETx +MCU_ST_STM32F7:STM32F745VGHx +MCU_ST_STM32F7:STM32F745VGTx +MCU_ST_STM32F7:STM32F745V_E-G_Hx +MCU_ST_STM32F7:STM32F745V_E-G_Tx +MCU_ST_STM32F7:STM32F745ZETx +MCU_ST_STM32F7:STM32F745ZGTx +MCU_ST_STM32F7:STM32F745Z_E-G_Tx +MCU_ST_STM32F7:STM32F746BETx +MCU_ST_STM32F7:STM32F746BGTx +MCU_ST_STM32F7:STM32F746B_E-G_Tx +MCU_ST_STM32F7:STM32F746IEKx +MCU_ST_STM32F7:STM32F746IETx +MCU_ST_STM32F7:STM32F746IGKx +MCU_ST_STM32F7:STM32F746IGTx +MCU_ST_STM32F7:STM32F746I_E-G_Kx +MCU_ST_STM32F7:STM32F746NEHx +MCU_ST_STM32F7:STM32F746NGHx +MCU_ST_STM32F7:STM32F746VEHx +MCU_ST_STM32F7:STM32F746VETx +MCU_ST_STM32F7:STM32F746VGHx +MCU_ST_STM32F7:STM32F746VGTx +MCU_ST_STM32F7:STM32F746V_E-G_Hx +MCU_ST_STM32F7:STM32F746ZETx +MCU_ST_STM32F7:STM32F746ZEYx +MCU_ST_STM32F7:STM32F746ZGTx +MCU_ST_STM32F7:STM32F746ZGYx +MCU_ST_STM32F7:STM32F746Z_E-G_Yx +MCU_ST_STM32F7:STM32F750N8Hx +MCU_ST_STM32F7:STM32F750V8Tx +MCU_ST_STM32F7:STM32F750Z8Tx +MCU_ST_STM32F7:STM32F756BGTx +MCU_ST_STM32F7:STM32F756IGKx +MCU_ST_STM32F7:STM32F756IGTx +MCU_ST_STM32F7:STM32F756NGHx +MCU_ST_STM32F7:STM32F756VGHx +MCU_ST_STM32F7:STM32F756VGTx +MCU_ST_STM32F7:STM32F756ZGTx +MCU_ST_STM32F7:STM32F756ZGYx +MCU_ST_STM32F7:STM32F765BGTx +MCU_ST_STM32F7:STM32F765BITx +MCU_ST_STM32F7:STM32F765B_G-I_Tx +MCU_ST_STM32F7:STM32F765IGKx +MCU_ST_STM32F7:STM32F765IGTx +MCU_ST_STM32F7:STM32F765IIKx +MCU_ST_STM32F7:STM32F765IITx +MCU_ST_STM32F7:STM32F765I_G-I_Kx +MCU_ST_STM32F7:STM32F765I_G-I_Tx +MCU_ST_STM32F7:STM32F765NGHx +MCU_ST_STM32F7:STM32F765NIHx +MCU_ST_STM32F7:STM32F765N_G-I_Hx +MCU_ST_STM32F7:STM32F765VGHx +MCU_ST_STM32F7:STM32F765VGTx +MCU_ST_STM32F7:STM32F765VIHx +MCU_ST_STM32F7:STM32F765VITx +MCU_ST_STM32F7:STM32F765V_G-I_Hx +MCU_ST_STM32F7:STM32F765V_G-I_Tx +MCU_ST_STM32F7:STM32F765ZGTx +MCU_ST_STM32F7:STM32F765ZITx +MCU_ST_STM32F7:STM32F765Z_G-I_Tx +MCU_ST_STM32F7:STM32F767BGTx +MCU_ST_STM32F7:STM32F767BITx +MCU_ST_STM32F7:STM32F767B_G-I_Tx +MCU_ST_STM32F7:STM32F767IGKx +MCU_ST_STM32F7:STM32F767IGTx +MCU_ST_STM32F7:STM32F767IIKx +MCU_ST_STM32F7:STM32F767IITx +MCU_ST_STM32F7:STM32F767I_G-I_Kx +MCU_ST_STM32F7:STM32F767I_G-I_Tx +MCU_ST_STM32F7:STM32F767NGHx +MCU_ST_STM32F7:STM32F767NIHx +MCU_ST_STM32F7:STM32F767N_G-I_Hx +MCU_ST_STM32F7:STM32F767VGHx +MCU_ST_STM32F7:STM32F767VGTx +MCU_ST_STM32F7:STM32F767VIHx +MCU_ST_STM32F7:STM32F767VITx +MCU_ST_STM32F7:STM32F767ZGTx +MCU_ST_STM32F7:STM32F767ZITx +MCU_ST_STM32F7:STM32F768AIYx +MCU_ST_STM32F7:STM32F769AGYx +MCU_ST_STM32F7:STM32F769AIYx +MCU_ST_STM32F7:STM32F769A_G-I_Yx +MCU_ST_STM32F7:STM32F769BGTx +MCU_ST_STM32F7:STM32F769BITx +MCU_ST_STM32F7:STM32F769B_G-I_Tx +MCU_ST_STM32F7:STM32F769IGTx +MCU_ST_STM32F7:STM32F769IITx +MCU_ST_STM32F7:STM32F769NGHx +MCU_ST_STM32F7:STM32F769NIHx +MCU_ST_STM32F7:STM32F777BITx +MCU_ST_STM32F7:STM32F777IIKx +MCU_ST_STM32F7:STM32F777IITx +MCU_ST_STM32F7:STM32F777NIHx +MCU_ST_STM32F7:STM32F777VIHx +MCU_ST_STM32F7:STM32F777VITx +MCU_ST_STM32F7:STM32F777ZITx +MCU_ST_STM32F7:STM32F778AIYx +MCU_ST_STM32F7:STM32F779AIYx +MCU_ST_STM32F7:STM32F779BITx +MCU_ST_STM32F7:STM32F779IITx +MCU_ST_STM32F7:STM32F779NIHx +MCU_ST_STM32G0:STM32G030C6Tx +MCU_ST_STM32G0:STM32G030C8Tx +MCU_ST_STM32G0:STM32G030C_6-8_Tx +MCU_ST_STM32G0:STM32G030F6Px +MCU_ST_STM32G0:STM32G030J6Mx +MCU_ST_STM32G0:STM32G030K6Tx +MCU_ST_STM32G0:STM32G030K8Tx +MCU_ST_STM32G0:STM32G030K_6-8_Tx +MCU_ST_STM32G0:STM32G031C4Tx +MCU_ST_STM32G0:STM32G031C4Ux +MCU_ST_STM32G0:STM32G031C6Tx +MCU_ST_STM32G0:STM32G031C6Ux +MCU_ST_STM32G0:STM32G031C8Tx +MCU_ST_STM32G0:STM32G031C8Ux +MCU_ST_STM32G0:STM32G031C_4-6-8_Tx +MCU_ST_STM32G0:STM32G031C_4-6-8_Ux +MCU_ST_STM32G0:STM32G031F4Px +MCU_ST_STM32G0:STM32G031F6Px +MCU_ST_STM32G0:STM32G031F8Px +MCU_ST_STM32G0:STM32G031F_4-6-8_Px +MCU_ST_STM32G0:STM32G031G4Ux +MCU_ST_STM32G0:STM32G031G6Ux +MCU_ST_STM32G0:STM32G031G8Ux +MCU_ST_STM32G0:STM32G031G_4-6-8_Ux +MCU_ST_STM32G0:STM32G031J4Mx +MCU_ST_STM32G0:STM32G031J6Mx +MCU_ST_STM32G0:STM32G031J_4-6_Mx +MCU_ST_STM32G0:STM32G031K4Tx +MCU_ST_STM32G0:STM32G031K4Ux +MCU_ST_STM32G0:STM32G031K6Tx +MCU_ST_STM32G0:STM32G031K6Ux +MCU_ST_STM32G0:STM32G031K8Tx +MCU_ST_STM32G0:STM32G031K8Ux +MCU_ST_STM32G0:STM32G031K_4-6-8_Tx +MCU_ST_STM32G0:STM32G031K_4-6-8_Ux +MCU_ST_STM32G0:STM32G031Y8Yx +MCU_ST_STM32G0:STM32G041C6Tx +MCU_ST_STM32G0:STM32G041C6Ux +MCU_ST_STM32G0:STM32G041C8Tx +MCU_ST_STM32G0:STM32G041C8Ux +MCU_ST_STM32G0:STM32G041C_6-8_Tx +MCU_ST_STM32G0:STM32G041C_6-8_Ux +MCU_ST_STM32G0:STM32G041F6Px +MCU_ST_STM32G0:STM32G041F8Px +MCU_ST_STM32G0:STM32G041F_6-8_Px +MCU_ST_STM32G0:STM32G041G6Ux +MCU_ST_STM32G0:STM32G041G8Ux +MCU_ST_STM32G0:STM32G041G_6-8_Ux +MCU_ST_STM32G0:STM32G041J6Mx +MCU_ST_STM32G0:STM32G041K6Tx +MCU_ST_STM32G0:STM32G041K6Ux +MCU_ST_STM32G0:STM32G041K8Tx +MCU_ST_STM32G0:STM32G041K8Ux +MCU_ST_STM32G0:STM32G041K_6-8_Tx +MCU_ST_STM32G0:STM32G041K_6-8_Ux +MCU_ST_STM32G0:STM32G041Y8Yx +MCU_ST_STM32G0:STM32G050C6Tx +MCU_ST_STM32G0:STM32G050C8Tx +MCU_ST_STM32G0:STM32G050F6Px +MCU_ST_STM32G0:STM32G050K6Tx +MCU_ST_STM32G0:STM32G050K8Tx +MCU_ST_STM32G0:STM32G051C6Tx +MCU_ST_STM32G0:STM32G051C6Ux +MCU_ST_STM32G0:STM32G051C8Tx +MCU_ST_STM32G0:STM32G051C8Ux +MCU_ST_STM32G0:STM32G051C_6-8_Tx +MCU_ST_STM32G0:STM32G051C_6-8_Ux +MCU_ST_STM32G0:STM32G051F6Px +MCU_ST_STM32G0:STM32G051F8Px +MCU_ST_STM32G0:STM32G051F8Yx +MCU_ST_STM32G0:STM32G051F_6-8_Px +MCU_ST_STM32G0:STM32G051G6Ux +MCU_ST_STM32G0:STM32G051G8Ux +MCU_ST_STM32G0:STM32G051G_6-8_Ux +MCU_ST_STM32G0:STM32G051K6Tx +MCU_ST_STM32G0:STM32G051K6Ux +MCU_ST_STM32G0:STM32G051K8Tx +MCU_ST_STM32G0:STM32G051K8Ux +MCU_ST_STM32G0:STM32G051K_6-8_Tx +MCU_ST_STM32G0:STM32G051K_6-8_Ux +MCU_ST_STM32G0:STM32G061C6Tx +MCU_ST_STM32G0:STM32G061C6Ux +MCU_ST_STM32G0:STM32G061C8Tx +MCU_ST_STM32G0:STM32G061C8Ux +MCU_ST_STM32G0:STM32G061C_6-8_Tx +MCU_ST_STM32G0:STM32G061C_6-8_Ux +MCU_ST_STM32G0:STM32G061F6Px +MCU_ST_STM32G0:STM32G061F8Px +MCU_ST_STM32G0:STM32G061F8Yx +MCU_ST_STM32G0:STM32G061F_6-8_Px +MCU_ST_STM32G0:STM32G061G6Ux +MCU_ST_STM32G0:STM32G061G8Ux +MCU_ST_STM32G0:STM32G061G_6-8_Ux +MCU_ST_STM32G0:STM32G061K6Tx +MCU_ST_STM32G0:STM32G061K6Ux +MCU_ST_STM32G0:STM32G061K8Tx +MCU_ST_STM32G0:STM32G061K8Ux +MCU_ST_STM32G0:STM32G061K_6-8_Tx +MCU_ST_STM32G0:STM32G061K_6-8_Ux +MCU_ST_STM32G0:STM32G070CBTx +MCU_ST_STM32G0:STM32G070KBTx +MCU_ST_STM32G0:STM32G070RBTx +MCU_ST_STM32G0:STM32G071EBYx +MCU_ST_STM32G0:STM32G071G8UxN +MCU_ST_STM32G0:STM32G071GBUxN +MCU_ST_STM32G0:STM32G071G_8-B_UxN +MCU_ST_STM32G0:STM32G071K8TxN +MCU_ST_STM32G0:STM32G071K8UxN +MCU_ST_STM32G0:STM32G071KBTxN +MCU_ST_STM32G0:STM32G071KBUxN +MCU_ST_STM32G0:STM32G071K_8-B_TxN +MCU_ST_STM32G0:STM32G071K_8-B_UxN +MCU_ST_STM32G0:STM32G071RBIx +MCU_ST_STM32G0:STM32G081CBTx +MCU_ST_STM32G0:STM32G081CBUx +MCU_ST_STM32G0:STM32G081EBYx +MCU_ST_STM32G0:STM32G081GBUx +MCU_ST_STM32G0:STM32G081GBUxN +MCU_ST_STM32G0:STM32G081KBTx +MCU_ST_STM32G0:STM32G081KBTxN +MCU_ST_STM32G0:STM32G081KBUx +MCU_ST_STM32G0:STM32G081KBUxN +MCU_ST_STM32G0:STM32G081RBIx +MCU_ST_STM32G0:STM32G081RBTx +MCU_ST_STM32G0:STM32G0B0CETx +MCU_ST_STM32G0:STM32G0B0KETx +MCU_ST_STM32G0:STM32G0B0RETx +MCU_ST_STM32G0:STM32G0B0VETx +MCU_ST_STM32G0:STM32G0B1CBTx +MCU_ST_STM32G0:STM32G0B1CBTxN +MCU_ST_STM32G0:STM32G0B1CBUx +MCU_ST_STM32G0:STM32G0B1CBUxN +MCU_ST_STM32G0:STM32G0B1CCTx +MCU_ST_STM32G0:STM32G0B1CCTxN +MCU_ST_STM32G0:STM32G0B1CCUx +MCU_ST_STM32G0:STM32G0B1CCUxN +MCU_ST_STM32G0:STM32G0B1CETx +MCU_ST_STM32G0:STM32G0B1CETxN +MCU_ST_STM32G0:STM32G0B1CEUx +MCU_ST_STM32G0:STM32G0B1CEUxN +MCU_ST_STM32G0:STM32G0B1C_B-C-E_Tx +MCU_ST_STM32G0:STM32G0B1C_B-C-E_TxN +MCU_ST_STM32G0:STM32G0B1C_B-C-E_Ux +MCU_ST_STM32G0:STM32G0B1C_B-C-E_UxN +MCU_ST_STM32G0:STM32G0B1KBTx +MCU_ST_STM32G0:STM32G0B1KBTxN +MCU_ST_STM32G0:STM32G0B1KBUx +MCU_ST_STM32G0:STM32G0B1KBUxN +MCU_ST_STM32G0:STM32G0B1KCTx +MCU_ST_STM32G0:STM32G0B1KCTxN +MCU_ST_STM32G0:STM32G0B1KCUx +MCU_ST_STM32G0:STM32G0B1KCUxN +MCU_ST_STM32G0:STM32G0B1KETx +MCU_ST_STM32G0:STM32G0B1KETxN +MCU_ST_STM32G0:STM32G0B1KEUx +MCU_ST_STM32G0:STM32G0B1KEUxN +MCU_ST_STM32G0:STM32G0B1K_B-C-E_Tx +MCU_ST_STM32G0:STM32G0B1K_B-C-E_TxN +MCU_ST_STM32G0:STM32G0B1K_B-C-E_Ux +MCU_ST_STM32G0:STM32G0B1K_B-C-E_UxN +MCU_ST_STM32G0:STM32G0B1MBTx +MCU_ST_STM32G0:STM32G0B1MCTx +MCU_ST_STM32G0:STM32G0B1METx +MCU_ST_STM32G0:STM32G0B1M_B-C-E_Tx +MCU_ST_STM32G0:STM32G0B1NEYx +MCU_ST_STM32G0:STM32G0B1RBIxN +MCU_ST_STM32G0:STM32G0B1RBTx +MCU_ST_STM32G0:STM32G0B1RBTxN +MCU_ST_STM32G0:STM32G0B1RCIxN +MCU_ST_STM32G0:STM32G0B1RCTx +MCU_ST_STM32G0:STM32G0B1RCTxN +MCU_ST_STM32G0:STM32G0B1REIxN +MCU_ST_STM32G0:STM32G0B1RETx +MCU_ST_STM32G0:STM32G0B1RETxN +MCU_ST_STM32G0:STM32G0B1R_B-C-E_IxN +MCU_ST_STM32G0:STM32G0B1R_B-C-E_Tx +MCU_ST_STM32G0:STM32G0B1R_B-C-E_TxN +MCU_ST_STM32G0:STM32G0B1VBIx +MCU_ST_STM32G0:STM32G0B1VBTx +MCU_ST_STM32G0:STM32G0B1VCIx +MCU_ST_STM32G0:STM32G0B1VCTx +MCU_ST_STM32G0:STM32G0B1VEIx +MCU_ST_STM32G0:STM32G0B1VETx +MCU_ST_STM32G0:STM32G0B1V_B-C-E_Ix +MCU_ST_STM32G0:STM32G0B1V_B-C-E_Tx +MCU_ST_STM32G0:STM32G0C1CCTx +MCU_ST_STM32G0:STM32G0C1CCTxN +MCU_ST_STM32G0:STM32G0C1CCUx +MCU_ST_STM32G0:STM32G0C1CCUxN +MCU_ST_STM32G0:STM32G0C1CETx +MCU_ST_STM32G0:STM32G0C1CETxN +MCU_ST_STM32G0:STM32G0C1CEUx +MCU_ST_STM32G0:STM32G0C1CEUxN +MCU_ST_STM32G0:STM32G0C1C_C-E_Tx +MCU_ST_STM32G0:STM32G0C1C_C-E_TxN +MCU_ST_STM32G0:STM32G0C1C_C-E_Ux +MCU_ST_STM32G0:STM32G0C1C_C-E_UxN +MCU_ST_STM32G0:STM32G0C1KCTx +MCU_ST_STM32G0:STM32G0C1KCTxN +MCU_ST_STM32G0:STM32G0C1KCUx +MCU_ST_STM32G0:STM32G0C1KCUxN +MCU_ST_STM32G0:STM32G0C1KETx +MCU_ST_STM32G0:STM32G0C1KETxN +MCU_ST_STM32G0:STM32G0C1KEUx +MCU_ST_STM32G0:STM32G0C1KEUxN +MCU_ST_STM32G0:STM32G0C1K_C-E_Tx +MCU_ST_STM32G0:STM32G0C1K_C-E_TxN +MCU_ST_STM32G0:STM32G0C1K_C-E_Ux +MCU_ST_STM32G0:STM32G0C1K_C-E_UxN +MCU_ST_STM32G0:STM32G0C1MCTx +MCU_ST_STM32G0:STM32G0C1METx +MCU_ST_STM32G0:STM32G0C1M_C-E_Tx +MCU_ST_STM32G0:STM32G0C1NEYx +MCU_ST_STM32G0:STM32G0C1RCIxN +MCU_ST_STM32G0:STM32G0C1RCTx +MCU_ST_STM32G0:STM32G0C1RCTxN +MCU_ST_STM32G0:STM32G0C1REIxN +MCU_ST_STM32G0:STM32G0C1RETx +MCU_ST_STM32G0:STM32G0C1RETxN +MCU_ST_STM32G0:STM32G0C1R_C-E_IxN +MCU_ST_STM32G0:STM32G0C1R_C-E_Tx +MCU_ST_STM32G0:STM32G0C1R_C-E_TxN +MCU_ST_STM32G0:STM32G0C1VCIx +MCU_ST_STM32G0:STM32G0C1VCTx +MCU_ST_STM32G0:STM32G0C1VEIx +MCU_ST_STM32G0:STM32G0C1VETx +MCU_ST_STM32G0:STM32G0C1V_C-E_Ix +MCU_ST_STM32G0:STM32G0C1V_C-E_Tx +MCU_ST_STM32G4:STM32G431C6Tx +MCU_ST_STM32G4:STM32G431C6Ux +MCU_ST_STM32G4:STM32G431C8Tx +MCU_ST_STM32G4:STM32G431C8Ux +MCU_ST_STM32G4:STM32G431CBTx +MCU_ST_STM32G4:STM32G431CBTxZ +MCU_ST_STM32G4:STM32G431CBUx +MCU_ST_STM32G4:STM32G431CBYx +MCU_ST_STM32G4:STM32G431C_6-8-B_Tx +MCU_ST_STM32G4:STM32G431C_6-8-B_Ux +MCU_ST_STM32G4:STM32G431K6Tx +MCU_ST_STM32G4:STM32G431K6Ux +MCU_ST_STM32G4:STM32G431K8Tx +MCU_ST_STM32G4:STM32G431K8Ux +MCU_ST_STM32G4:STM32G431KBTx +MCU_ST_STM32G4:STM32G431KBUx +MCU_ST_STM32G4:STM32G431K_6-8-B_Tx +MCU_ST_STM32G4:STM32G431K_6-8-B_Ux +MCU_ST_STM32G4:STM32G431M6Tx +MCU_ST_STM32G4:STM32G431M8Tx +MCU_ST_STM32G4:STM32G431MBTx +MCU_ST_STM32G4:STM32G431M_6-8-B_Tx +MCU_ST_STM32G4:STM32G431R6Ix +MCU_ST_STM32G4:STM32G431R6Tx +MCU_ST_STM32G4:STM32G431R8Ix +MCU_ST_STM32G4:STM32G431R8Tx +MCU_ST_STM32G4:STM32G431RBIx +MCU_ST_STM32G4:STM32G431RBTx +MCU_ST_STM32G4:STM32G431RBTxZ +MCU_ST_STM32G4:STM32G431R_6-8-B_Ix +MCU_ST_STM32G4:STM32G431R_6-8-B_Tx +MCU_ST_STM32G4:STM32G431V6Tx +MCU_ST_STM32G4:STM32G431V8Tx +MCU_ST_STM32G4:STM32G431VBTx +MCU_ST_STM32G4:STM32G431V_6-8-B_Tx +MCU_ST_STM32G4:STM32G441CBTx +MCU_ST_STM32G4:STM32G441CBUx +MCU_ST_STM32G4:STM32G441CBYx +MCU_ST_STM32G4:STM32G441KBTx +MCU_ST_STM32G4:STM32G441KBUx +MCU_ST_STM32G4:STM32G441MBTx +MCU_ST_STM32G4:STM32G441RBIx +MCU_ST_STM32G4:STM32G441RBTx +MCU_ST_STM32G4:STM32G441VBTx +MCU_ST_STM32G4:STM32G473CBTx +MCU_ST_STM32G4:STM32G473CBUx +MCU_ST_STM32G4:STM32G473CCTx +MCU_ST_STM32G4:STM32G473CCUx +MCU_ST_STM32G4:STM32G473CETx +MCU_ST_STM32G4:STM32G473CEUx +MCU_ST_STM32G4:STM32G473C_B-C-E_Tx +MCU_ST_STM32G4:STM32G473C_B-C-E_Ux +MCU_ST_STM32G4:STM32G473MBTx +MCU_ST_STM32G4:STM32G473MCTx +MCU_ST_STM32G4:STM32G473METx +MCU_ST_STM32G4:STM32G473MEYx +MCU_ST_STM32G4:STM32G473M_B-C-E_Tx +MCU_ST_STM32G4:STM32G473PBIx +MCU_ST_STM32G4:STM32G473PCIx +MCU_ST_STM32G4:STM32G473PEIx +MCU_ST_STM32G4:STM32G473P_B-C-E_Ix +MCU_ST_STM32G4:STM32G473QBTx +MCU_ST_STM32G4:STM32G473QCTx +MCU_ST_STM32G4:STM32G473QETx +MCU_ST_STM32G4:STM32G473QETxZ +MCU_ST_STM32G4:STM32G473Q_B-C-E_Tx +MCU_ST_STM32G4:STM32G473RBTx +MCU_ST_STM32G4:STM32G473RCTx +MCU_ST_STM32G4:STM32G473RETx +MCU_ST_STM32G4:STM32G473RETxZ +MCU_ST_STM32G4:STM32G473R_B-C-E_Tx +MCU_ST_STM32G4:STM32G473VBHx +MCU_ST_STM32G4:STM32G473VBTx +MCU_ST_STM32G4:STM32G473VCHx +MCU_ST_STM32G4:STM32G473VCTx +MCU_ST_STM32G4:STM32G473VEHx +MCU_ST_STM32G4:STM32G473VETx +MCU_ST_STM32G4:STM32G473V_B-C-E_Hx +MCU_ST_STM32G4:STM32G473V_B-C-E_Tx +MCU_ST_STM32G4:STM32G474CBTx +MCU_ST_STM32G4:STM32G474CBUx +MCU_ST_STM32G4:STM32G474CCTx +MCU_ST_STM32G4:STM32G474CCUx +MCU_ST_STM32G4:STM32G474CETx +MCU_ST_STM32G4:STM32G474CEUx +MCU_ST_STM32G4:STM32G474C_B-C-E_Tx +MCU_ST_STM32G4:STM32G474C_B-C-E_Ux +MCU_ST_STM32G4:STM32G474MBTx +MCU_ST_STM32G4:STM32G474MCTx +MCU_ST_STM32G4:STM32G474METx +MCU_ST_STM32G4:STM32G474MEYx +MCU_ST_STM32G4:STM32G474M_B-C-E_Tx +MCU_ST_STM32G4:STM32G474PBIx +MCU_ST_STM32G4:STM32G474PCIx +MCU_ST_STM32G4:STM32G474PEIx +MCU_ST_STM32G4:STM32G474P_B-C-E_Ix +MCU_ST_STM32G4:STM32G474QBTx +MCU_ST_STM32G4:STM32G474QCTx +MCU_ST_STM32G4:STM32G474QETx +MCU_ST_STM32G4:STM32G474Q_B-C-E_Tx +MCU_ST_STM32G4:STM32G474RBTx +MCU_ST_STM32G4:STM32G474RCTx +MCU_ST_STM32G4:STM32G474RETx +MCU_ST_STM32G4:STM32G474R_B-C-E_Tx +MCU_ST_STM32G4:STM32G474VBHx +MCU_ST_STM32G4:STM32G474VBTx +MCU_ST_STM32G4:STM32G474VCHx +MCU_ST_STM32G4:STM32G474VCTx +MCU_ST_STM32G4:STM32G474VEHx +MCU_ST_STM32G4:STM32G474VETx +MCU_ST_STM32G4:STM32G474V_B-C-E_Hx +MCU_ST_STM32G4:STM32G474V_B-C-E_Tx +MCU_ST_STM32G4:STM32G483CETx +MCU_ST_STM32G4:STM32G483CEUx +MCU_ST_STM32G4:STM32G483METx +MCU_ST_STM32G4:STM32G483MEYx +MCU_ST_STM32G4:STM32G483PEIx +MCU_ST_STM32G4:STM32G483QETx +MCU_ST_STM32G4:STM32G483RETx +MCU_ST_STM32G4:STM32G483VEHx +MCU_ST_STM32G4:STM32G483VETx +MCU_ST_STM32G4:STM32G484CETx +MCU_ST_STM32G4:STM32G484CEUx +MCU_ST_STM32G4:STM32G484METx +MCU_ST_STM32G4:STM32G484MEYx +MCU_ST_STM32G4:STM32G484PEIx +MCU_ST_STM32G4:STM32G484QETx +MCU_ST_STM32G4:STM32G484RETx +MCU_ST_STM32G4:STM32G484VEHx +MCU_ST_STM32G4:STM32G484VETx +MCU_ST_STM32G4:STM32G491CCTx +MCU_ST_STM32G4:STM32G491CCUx +MCU_ST_STM32G4:STM32G491CETx +MCU_ST_STM32G4:STM32G491CEUx +MCU_ST_STM32G4:STM32G491C_C-E_Tx +MCU_ST_STM32G4:STM32G491C_C-E_Ux +MCU_ST_STM32G4:STM32G491KCUx +MCU_ST_STM32G4:STM32G491KEUx +MCU_ST_STM32G4:STM32G491K_C-E_Ux +MCU_ST_STM32G4:STM32G491MCSx +MCU_ST_STM32G4:STM32G491MCTx +MCU_ST_STM32G4:STM32G491MESx +MCU_ST_STM32G4:STM32G491METx +MCU_ST_STM32G4:STM32G491M_C-E_Sx +MCU_ST_STM32G4:STM32G491M_C-E_Tx +MCU_ST_STM32G4:STM32G491RCIx +MCU_ST_STM32G4:STM32G491RCTx +MCU_ST_STM32G4:STM32G491REIx +MCU_ST_STM32G4:STM32G491RETx +MCU_ST_STM32G4:STM32G491RETxZ +MCU_ST_STM32G4:STM32G491REYx +MCU_ST_STM32G4:STM32G491R_C-E_Ix +MCU_ST_STM32G4:STM32G491R_C-E_Tx +MCU_ST_STM32G4:STM32G491VCTx +MCU_ST_STM32G4:STM32G491VETx +MCU_ST_STM32G4:STM32G491V_C-E_Tx +MCU_ST_STM32G4:STM32G4A1CETx +MCU_ST_STM32G4:STM32G4A1CEUx +MCU_ST_STM32G4:STM32G4A1KEUx +MCU_ST_STM32G4:STM32G4A1MESx +MCU_ST_STM32G4:STM32G4A1METx +MCU_ST_STM32G4:STM32G4A1REIx +MCU_ST_STM32G4:STM32G4A1RETx +MCU_ST_STM32G4:STM32G4A1REYx +MCU_ST_STM32G4:STM32G4A1VETx +MCU_ST_STM32H5:STM32H503CBTx +MCU_ST_STM32H5:STM32H503CBUx +MCU_ST_STM32H5:STM32H503EBYx +MCU_ST_STM32H5:STM32H503KBUx +MCU_ST_STM32H5:STM32H503RBTx +MCU_ST_STM32H5:STM32H523CCTx +MCU_ST_STM32H5:STM32H523CCUx +MCU_ST_STM32H5:STM32H523CETx +MCU_ST_STM32H5:STM32H523CEUx +MCU_ST_STM32H5:STM32H523RCTx +MCU_ST_STM32H5:STM32H523RETx +MCU_ST_STM32H5:STM32H523VCIx +MCU_ST_STM32H5:STM32H523VCTx +MCU_ST_STM32H5:STM32H523VEIx +MCU_ST_STM32H5:STM32H523VETx +MCU_ST_STM32H5:STM32H523ZCJx +MCU_ST_STM32H5:STM32H523ZCTx +MCU_ST_STM32H5:STM32H523ZEJx +MCU_ST_STM32H5:STM32H523ZETx +MCU_ST_STM32H5:STM32H533CETx +MCU_ST_STM32H5:STM32H533CEUx +MCU_ST_STM32H5:STM32H533RETx +MCU_ST_STM32H5:STM32H533VEIx +MCU_ST_STM32H5:STM32H533VETx +MCU_ST_STM32H5:STM32H533ZEJx +MCU_ST_STM32H5:STM32H533ZETx +MCU_ST_STM32H5:STM32H562AGIx +MCU_ST_STM32H5:STM32H562AIIx +MCU_ST_STM32H5:STM32H562IGKx +MCU_ST_STM32H5:STM32H562IGTx +MCU_ST_STM32H5:STM32H562IIKx +MCU_ST_STM32H5:STM32H562IITx +MCU_ST_STM32H5:STM32H562RGTx +MCU_ST_STM32H5:STM32H562RGVx +MCU_ST_STM32H5:STM32H562RITx +MCU_ST_STM32H5:STM32H562RIVx +MCU_ST_STM32H5:STM32H562VGTx +MCU_ST_STM32H5:STM32H562VITx +MCU_ST_STM32H5:STM32H562ZGTx +MCU_ST_STM32H5:STM32H562ZITx +MCU_ST_STM32H5:STM32H563AGIx +MCU_ST_STM32H5:STM32H563AIIx +MCU_ST_STM32H5:STM32H563AIIxQ +MCU_ST_STM32H5:STM32H563IGKx +MCU_ST_STM32H5:STM32H563IGTx +MCU_ST_STM32H5:STM32H563IIKx +MCU_ST_STM32H5:STM32H563IIKxQ +MCU_ST_STM32H5:STM32H563IITx +MCU_ST_STM32H5:STM32H563IITxQ +MCU_ST_STM32H5:STM32H563MIYxQ +MCU_ST_STM32H5:STM32H563RGTx +MCU_ST_STM32H5:STM32H563RGVx +MCU_ST_STM32H5:STM32H563RITx +MCU_ST_STM32H5:STM32H563RIVx +MCU_ST_STM32H5:STM32H563VGTx +MCU_ST_STM32H5:STM32H563VITx +MCU_ST_STM32H5:STM32H563VITxQ +MCU_ST_STM32H5:STM32H563ZGTx +MCU_ST_STM32H5:STM32H563ZITx +MCU_ST_STM32H5:STM32H563ZITxQ +MCU_ST_STM32H5:STM32H573AIIx +MCU_ST_STM32H5:STM32H573AIIxQ +MCU_ST_STM32H5:STM32H573IIKx +MCU_ST_STM32H5:STM32H573IIKxQ +MCU_ST_STM32H5:STM32H573IITx +MCU_ST_STM32H5:STM32H573IITxQ +MCU_ST_STM32H5:STM32H573MIYxQ +MCU_ST_STM32H5:STM32H573RITx +MCU_ST_STM32H5:STM32H573RIVx +MCU_ST_STM32H5:STM32H573VITx +MCU_ST_STM32H5:STM32H573VITxQ +MCU_ST_STM32H5:STM32H573ZITx +MCU_ST_STM32H5:STM32H573ZITxQ +MCU_ST_STM32H7:STM32H723VEHx +MCU_ST_STM32H7:STM32H723VETx +MCU_ST_STM32H7:STM32H723VGHx +MCU_ST_STM32H7:STM32H723VGTx +MCU_ST_STM32H7:STM32H723ZEIx +MCU_ST_STM32H7:STM32H723ZETx +MCU_ST_STM32H7:STM32H723ZGIx +MCU_ST_STM32H7:STM32H723ZGTx +MCU_ST_STM32H7:STM32H725AEIx +MCU_ST_STM32H7:STM32H725AGIx +MCU_ST_STM32H7:STM32H725IEKx +MCU_ST_STM32H7:STM32H725IETx +MCU_ST_STM32H7:STM32H725IGKx +MCU_ST_STM32H7:STM32H725IGTx +MCU_ST_STM32H7:STM32H725REVx +MCU_ST_STM32H7:STM32H725RGVx +MCU_ST_STM32H7:STM32H725VEHx +MCU_ST_STM32H7:STM32H725VETx +MCU_ST_STM32H7:STM32H725VGHx +MCU_ST_STM32H7:STM32H725VGTx +MCU_ST_STM32H7:STM32H725VGYx +MCU_ST_STM32H7:STM32H725ZETx +MCU_ST_STM32H7:STM32H725ZGTx +MCU_ST_STM32H7:STM32H730ABIxQ +MCU_ST_STM32H7:STM32H730IBKxQ +MCU_ST_STM32H7:STM32H730IBTxQ +MCU_ST_STM32H7:STM32H730VBHx +MCU_ST_STM32H7:STM32H730VBTx +MCU_ST_STM32H7:STM32H730ZBIx +MCU_ST_STM32H7:STM32H730ZBTx +MCU_ST_STM32H7:STM32H733VGHx +MCU_ST_STM32H7:STM32H733VGTx +MCU_ST_STM32H7:STM32H733ZGIx +MCU_ST_STM32H7:STM32H733ZGTx +MCU_ST_STM32H7:STM32H735AGIx +MCU_ST_STM32H7:STM32H735IGKx +MCU_ST_STM32H7:STM32H735IGTx +MCU_ST_STM32H7:STM32H735RGVx +MCU_ST_STM32H7:STM32H735VGHx +MCU_ST_STM32H7:STM32H735VGTx +MCU_ST_STM32H7:STM32H735VGYx +MCU_ST_STM32H7:STM32H735ZGTx +MCU_ST_STM32H7:STM32H742AGIx +MCU_ST_STM32H7:STM32H742AIIx +MCU_ST_STM32H7:STM32H742A_G-I_Ix +MCU_ST_STM32H7:STM32H742BGTx +MCU_ST_STM32H7:STM32H742BITx +MCU_ST_STM32H7:STM32H742B_G-I_Tx +MCU_ST_STM32H7:STM32H742IGKx +MCU_ST_STM32H7:STM32H742IGTx +MCU_ST_STM32H7:STM32H742IIKx +MCU_ST_STM32H7:STM32H742IITx +MCU_ST_STM32H7:STM32H742I_G-I_Kx +MCU_ST_STM32H7:STM32H742I_G-I_Tx +MCU_ST_STM32H7:STM32H742VGHx +MCU_ST_STM32H7:STM32H742VGTx +MCU_ST_STM32H7:STM32H742VIHx +MCU_ST_STM32H7:STM32H742VITx +MCU_ST_STM32H7:STM32H742V_G-I_Hx +MCU_ST_STM32H7:STM32H742V_G-I_Tx +MCU_ST_STM32H7:STM32H742XGHx +MCU_ST_STM32H7:STM32H742XIHx +MCU_ST_STM32H7:STM32H742X_G-I_Hx +MCU_ST_STM32H7:STM32H742ZGTx +MCU_ST_STM32H7:STM32H742ZITx +MCU_ST_STM32H7:STM32H742Z_G-I_Tx +MCU_ST_STM32H7:STM32H743AGIx +MCU_ST_STM32H7:STM32H743AIIx +MCU_ST_STM32H7:STM32H743A_G-I_Ix +MCU_ST_STM32H7:STM32H743BGTx +MCU_ST_STM32H7:STM32H743BITx +MCU_ST_STM32H7:STM32H743IGKx +MCU_ST_STM32H7:STM32H743IGTx +MCU_ST_STM32H7:STM32H743IIKx +MCU_ST_STM32H7:STM32H743IITx +MCU_ST_STM32H7:STM32H743VGHx +MCU_ST_STM32H7:STM32H743VGTx +MCU_ST_STM32H7:STM32H743VIHx +MCU_ST_STM32H7:STM32H743VITx +MCU_ST_STM32H7:STM32H743V_G-I_Hx +MCU_ST_STM32H7:STM32H743XGHx +MCU_ST_STM32H7:STM32H743XIHx +MCU_ST_STM32H7:STM32H743ZGTx +MCU_ST_STM32H7:STM32H743ZITx +MCU_ST_STM32H7:STM32H745BGTx +MCU_ST_STM32H7:STM32H745BITx +MCU_ST_STM32H7:STM32H745IGKx +MCU_ST_STM32H7:STM32H745IGTx +MCU_ST_STM32H7:STM32H745IIKx +MCU_ST_STM32H7:STM32H745IITx +MCU_ST_STM32H7:STM32H745XGHx +MCU_ST_STM32H7:STM32H745XIHx +MCU_ST_STM32H7:STM32H745ZGTx +MCU_ST_STM32H7:STM32H745ZITx +MCU_ST_STM32H7:STM32H747AGIx +MCU_ST_STM32H7:STM32H747AIIx +MCU_ST_STM32H7:STM32H747A_G-I_Ix +MCU_ST_STM32H7:STM32H747BGTx +MCU_ST_STM32H7:STM32H747BITx +MCU_ST_STM32H7:STM32H747IGTx +MCU_ST_STM32H7:STM32H747IITx +MCU_ST_STM32H7:STM32H747XGHx +MCU_ST_STM32H7:STM32H747XIHx +MCU_ST_STM32H7:STM32H747ZIYx +MCU_ST_STM32H7:STM32H750IBKx +MCU_ST_STM32H7:STM32H750IBTx +MCU_ST_STM32H7:STM32H750VBTx +MCU_ST_STM32H7:STM32H750XBHx +MCU_ST_STM32H7:STM32H750ZBTx +MCU_ST_STM32H7:STM32H753AIIx +MCU_ST_STM32H7:STM32H753BITx +MCU_ST_STM32H7:STM32H753IIKx +MCU_ST_STM32H7:STM32H753IITx +MCU_ST_STM32H7:STM32H753VIHx +MCU_ST_STM32H7:STM32H753VITx +MCU_ST_STM32H7:STM32H753XIHx +MCU_ST_STM32H7:STM32H753ZITx +MCU_ST_STM32H7:STM32H755BITx +MCU_ST_STM32H7:STM32H755IIKx +MCU_ST_STM32H7:STM32H755IITx +MCU_ST_STM32H7:STM32H755XIHx +MCU_ST_STM32H7:STM32H755ZITx +MCU_ST_STM32H7:STM32H757AIIx +MCU_ST_STM32H7:STM32H757BITx +MCU_ST_STM32H7:STM32H757IITx +MCU_ST_STM32H7:STM32H757XIHx +MCU_ST_STM32H7:STM32H757ZIYx +MCU_ST_STM32H7:STM32H7A3AGIxQ +MCU_ST_STM32H7:STM32H7A3AIIxQ +MCU_ST_STM32H7:STM32H7A3A_G-I_IxQ +MCU_ST_STM32H7:STM32H7A3IGKx +MCU_ST_STM32H7:STM32H7A3IGKxQ +MCU_ST_STM32H7:STM32H7A3IGTx +MCU_ST_STM32H7:STM32H7A3IGTxQ +MCU_ST_STM32H7:STM32H7A3IIKx +MCU_ST_STM32H7:STM32H7A3IIKxQ +MCU_ST_STM32H7:STM32H7A3IITx +MCU_ST_STM32H7:STM32H7A3IITxQ +MCU_ST_STM32H7:STM32H7A3I_G-I_Kx +MCU_ST_STM32H7:STM32H7A3I_G-I_KxQ +MCU_ST_STM32H7:STM32H7A3I_G-I_Tx +MCU_ST_STM32H7:STM32H7A3I_G-I_TxQ +MCU_ST_STM32H7:STM32H7A3LGHxQ +MCU_ST_STM32H7:STM32H7A3LIHxQ +MCU_ST_STM32H7:STM32H7A3L_G-I_HxQ +MCU_ST_STM32H7:STM32H7A3NGHx +MCU_ST_STM32H7:STM32H7A3NIHx +MCU_ST_STM32H7:STM32H7A3N_G-I_Hx +MCU_ST_STM32H7:STM32H7A3QIYxQ +MCU_ST_STM32H7:STM32H7A3RGTx +MCU_ST_STM32H7:STM32H7A3RITx +MCU_ST_STM32H7:STM32H7A3R_G-I_Tx +MCU_ST_STM32H7:STM32H7A3VGHx +MCU_ST_STM32H7:STM32H7A3VGHxQ +MCU_ST_STM32H7:STM32H7A3VGTx +MCU_ST_STM32H7:STM32H7A3VGTxQ +MCU_ST_STM32H7:STM32H7A3VIHx +MCU_ST_STM32H7:STM32H7A3VIHxQ +MCU_ST_STM32H7:STM32H7A3VITx +MCU_ST_STM32H7:STM32H7A3VITxQ +MCU_ST_STM32H7:STM32H7A3V_G-I_Hx +MCU_ST_STM32H7:STM32H7A3V_G-I_HxQ +MCU_ST_STM32H7:STM32H7A3V_G-I_Tx +MCU_ST_STM32H7:STM32H7A3V_G-I_TxQ +MCU_ST_STM32H7:STM32H7A3ZGTx +MCU_ST_STM32H7:STM32H7A3ZGTxQ +MCU_ST_STM32H7:STM32H7A3ZITx +MCU_ST_STM32H7:STM32H7A3ZITxQ +MCU_ST_STM32H7:STM32H7A3Z_G-I_Tx +MCU_ST_STM32H7:STM32H7A3Z_G-I_TxQ +MCU_ST_STM32H7:STM32H7B0ABIxQ +MCU_ST_STM32H7:STM32H7B0IBKxQ +MCU_ST_STM32H7:STM32H7B0IBTx +MCU_ST_STM32H7:STM32H7B0RBTx +MCU_ST_STM32H7:STM32H7B0VBTx +MCU_ST_STM32H7:STM32H7B0ZBTx +MCU_ST_STM32H7:STM32H7B3AIIxQ +MCU_ST_STM32H7:STM32H7B3IIKx +MCU_ST_STM32H7:STM32H7B3IIKxQ +MCU_ST_STM32H7:STM32H7B3IITx +MCU_ST_STM32H7:STM32H7B3IITxQ +MCU_ST_STM32H7:STM32H7B3LIHxQ +MCU_ST_STM32H7:STM32H7B3NIHx +MCU_ST_STM32H7:STM32H7B3QIYxQ +MCU_ST_STM32H7:STM32H7B3RITx +MCU_ST_STM32H7:STM32H7B3VIHx +MCU_ST_STM32H7:STM32H7B3VIHxQ +MCU_ST_STM32H7:STM32H7B3VITx +MCU_ST_STM32H7:STM32H7B3VITxQ +MCU_ST_STM32H7:STM32H7B3ZITx +MCU_ST_STM32H7:STM32H7B3ZITxQ +MCU_ST_STM32H7:STM32H7R3A8Ix +MCU_ST_STM32H7:STM32H7R3I8Kx +MCU_ST_STM32H7:STM32H7R3I8Tx +MCU_ST_STM32H7:STM32H7R3L8Hx +MCU_ST_STM32H7:STM32H7R3L8HxH +MCU_ST_STM32H7:STM32H7R3R8Vx +MCU_ST_STM32H7:STM32H7R3V8Hx +MCU_ST_STM32H7:STM32H7R3V8Tx +MCU_ST_STM32H7:STM32H7R3V8Yx +MCU_ST_STM32H7:STM32H7R3Z8Jx +MCU_ST_STM32H7:STM32H7R3Z8Tx +MCU_ST_STM32H7:STM32H7R7A8Ix +MCU_ST_STM32H7:STM32H7R7I8Kx +MCU_ST_STM32H7:STM32H7R7I8Tx +MCU_ST_STM32H7:STM32H7R7L8Hx +MCU_ST_STM32H7:STM32H7R7L8HxH +MCU_ST_STM32H7:STM32H7R7Z8Jx +MCU_ST_STM32H7:STM32H7S3A8Ix +MCU_ST_STM32H7:STM32H7S3I8Kx +MCU_ST_STM32H7:STM32H7S3I8Tx +MCU_ST_STM32H7:STM32H7S3L8Hx +MCU_ST_STM32H7:STM32H7S3L8HxH +MCU_ST_STM32H7:STM32H7S3R8Vx +MCU_ST_STM32H7:STM32H7S3V8Hx +MCU_ST_STM32H7:STM32H7S3V8Tx +MCU_ST_STM32H7:STM32H7S3V8Yx +MCU_ST_STM32H7:STM32H7S3Z8Jx +MCU_ST_STM32H7:STM32H7S3Z8Tx +MCU_ST_STM32H7:STM32H7S7A8Ix +MCU_ST_STM32H7:STM32H7S7I8Kx +MCU_ST_STM32H7:STM32H7S7I8Tx +MCU_ST_STM32H7:STM32H7S7L8Hx +MCU_ST_STM32H7:STM32H7S7L8HxH +MCU_ST_STM32H7:STM32H7S7Z8Jx +MCU_ST_STM32L0:STM32L010C6Tx +MCU_ST_STM32L0:STM32L010F4Px +MCU_ST_STM32L0:STM32L010K4Tx +MCU_ST_STM32L0:STM32L010K8Tx +MCU_ST_STM32L0:STM32L010R8Tx +MCU_ST_STM32L0:STM32L010RBTx +MCU_ST_STM32L0:STM32L011D3Px +MCU_ST_STM32L0:STM32L011D4Px +MCU_ST_STM32L0:STM32L011D_3-4_Px +MCU_ST_STM32L0:STM32L011E3Yx +MCU_ST_STM32L0:STM32L011E4Yx +MCU_ST_STM32L0:STM32L011E_3-4_Yx +MCU_ST_STM32L0:STM32L011F3Px +MCU_ST_STM32L0:STM32L011F3Ux +MCU_ST_STM32L0:STM32L011F4Px +MCU_ST_STM32L0:STM32L011F4Ux +MCU_ST_STM32L0:STM32L011F_3-4_Px +MCU_ST_STM32L0:STM32L011F_3-4_Ux +MCU_ST_STM32L0:STM32L011G3Ux +MCU_ST_STM32L0:STM32L011G4Ux +MCU_ST_STM32L0:STM32L011G_3-4_Ux +MCU_ST_STM32L0:STM32L011K3Tx +MCU_ST_STM32L0:STM32L011K3Ux +MCU_ST_STM32L0:STM32L011K4Tx +MCU_ST_STM32L0:STM32L011K4Ux +MCU_ST_STM32L0:STM32L011K_3-4_Tx +MCU_ST_STM32L0:STM32L011K_3-4_Ux +MCU_ST_STM32L0:STM32L021D4Px +MCU_ST_STM32L0:STM32L021F4Px +MCU_ST_STM32L0:STM32L021F4Ux +MCU_ST_STM32L0:STM32L021G4Ux +MCU_ST_STM32L0:STM32L021K4Tx +MCU_ST_STM32L0:STM32L021K4Ux +MCU_ST_STM32L0:STM32L031C4Tx +MCU_ST_STM32L0:STM32L031C4Ux +MCU_ST_STM32L0:STM32L031C6Tx +MCU_ST_STM32L0:STM32L031C6Ux +MCU_ST_STM32L0:STM32L031C_4-6_Tx +MCU_ST_STM32L0:STM32L031C_4-6_Ux +MCU_ST_STM32L0:STM32L031E4Yx +MCU_ST_STM32L0:STM32L031E6Yx +MCU_ST_STM32L0:STM32L031E_4-6_Yx +MCU_ST_STM32L0:STM32L031F4Px +MCU_ST_STM32L0:STM32L031F6Px +MCU_ST_STM32L0:STM32L031F_4-6_Px +MCU_ST_STM32L0:STM32L031G4Ux +MCU_ST_STM32L0:STM32L031G6Ux +MCU_ST_STM32L0:STM32L031G6UxS +MCU_ST_STM32L0:STM32L031G_4-6_Ux +MCU_ST_STM32L0:STM32L031K4Tx +MCU_ST_STM32L0:STM32L031K4Ux +MCU_ST_STM32L0:STM32L031K6Tx +MCU_ST_STM32L0:STM32L031K6Ux +MCU_ST_STM32L0:STM32L031K_4-6_Tx +MCU_ST_STM32L0:STM32L031K_4-6_Ux +MCU_ST_STM32L0:STM32L041C4Tx +MCU_ST_STM32L0:STM32L041C6Tx +MCU_ST_STM32L0:STM32L041C6Ux +MCU_ST_STM32L0:STM32L041C_4-6_Tx +MCU_ST_STM32L0:STM32L041E6Yx +MCU_ST_STM32L0:STM32L041F6Px +MCU_ST_STM32L0:STM32L041G6Ux +MCU_ST_STM32L0:STM32L041G6UxS +MCU_ST_STM32L0:STM32L041K6Tx +MCU_ST_STM32L0:STM32L041K6Ux +MCU_ST_STM32L0:STM32L051C6Tx +MCU_ST_STM32L0:STM32L051C6Ux +MCU_ST_STM32L0:STM32L051C8Tx +MCU_ST_STM32L0:STM32L051C8Ux +MCU_ST_STM32L0:STM32L051C_6-8_Tx +MCU_ST_STM32L0:STM32L051C_6-8_Ux +MCU_ST_STM32L0:STM32L051K6Tx +MCU_ST_STM32L0:STM32L051K6Ux +MCU_ST_STM32L0:STM32L051K8Tx +MCU_ST_STM32L0:STM32L051K8Ux +MCU_ST_STM32L0:STM32L051K_6-8_Tx +MCU_ST_STM32L0:STM32L051K_6-8_Ux +MCU_ST_STM32L0:STM32L051R6Hx +MCU_ST_STM32L0:STM32L051R6Tx +MCU_ST_STM32L0:STM32L051R8Hx +MCU_ST_STM32L0:STM32L051R8Tx +MCU_ST_STM32L0:STM32L051R_6-8_Hx +MCU_ST_STM32L0:STM32L051R_6-8_Tx +MCU_ST_STM32L0:STM32L051T6Yx +MCU_ST_STM32L0:STM32L051T8Yx +MCU_ST_STM32L0:STM32L051T_6-8_Yx +MCU_ST_STM32L0:STM32L052C6Tx +MCU_ST_STM32L0:STM32L052C6Ux +MCU_ST_STM32L0:STM32L052C8Tx +MCU_ST_STM32L0:STM32L052C8Ux +MCU_ST_STM32L0:STM32L052C_6-8_Tx +MCU_ST_STM32L0:STM32L052C_6-8_Ux +MCU_ST_STM32L0:STM32L052K6Tx +MCU_ST_STM32L0:STM32L052K6Ux +MCU_ST_STM32L0:STM32L052K8Tx +MCU_ST_STM32L0:STM32L052K8Ux +MCU_ST_STM32L0:STM32L052K_6-8_Tx +MCU_ST_STM32L0:STM32L052K_6-8_Ux +MCU_ST_STM32L0:STM32L052R6Hx +MCU_ST_STM32L0:STM32L052R6Tx +MCU_ST_STM32L0:STM32L052R8Hx +MCU_ST_STM32L0:STM32L052R8Tx +MCU_ST_STM32L0:STM32L052R_6-8_Hx +MCU_ST_STM32L0:STM32L052R_6-8_Tx +MCU_ST_STM32L0:STM32L052T6Yx +MCU_ST_STM32L0:STM32L052T8Fx +MCU_ST_STM32L0:STM32L052T8Yx +MCU_ST_STM32L0:STM32L052T_6-8_Yx +MCU_ST_STM32L0:STM32L053C6Tx +MCU_ST_STM32L0:STM32L053C6Ux +MCU_ST_STM32L0:STM32L053C8Tx +MCU_ST_STM32L0:STM32L053C8Ux +MCU_ST_STM32L0:STM32L053C_6-8_Tx +MCU_ST_STM32L0:STM32L053C_6-8_Ux +MCU_ST_STM32L0:STM32L053R6Hx +MCU_ST_STM32L0:STM32L053R6Tx +MCU_ST_STM32L0:STM32L053R8Hx +MCU_ST_STM32L0:STM32L053R8Tx +MCU_ST_STM32L0:STM32L053R_6-8_Hx +MCU_ST_STM32L0:STM32L053R_6-8_Tx +MCU_ST_STM32L0:STM32L062C8Ux +MCU_ST_STM32L0:STM32L062K8Tx +MCU_ST_STM32L0:STM32L062K8Ux +MCU_ST_STM32L0:STM32L063C8Tx +MCU_ST_STM32L0:STM32L063C8Ux +MCU_ST_STM32L0:STM32L063R8Tx +MCU_ST_STM32L0:STM32L071C8Tx +MCU_ST_STM32L0:STM32L071C8Ux +MCU_ST_STM32L0:STM32L071CBTx +MCU_ST_STM32L0:STM32L071CBUx +MCU_ST_STM32L0:STM32L071CBYx +MCU_ST_STM32L0:STM32L071CZTx +MCU_ST_STM32L0:STM32L071CZUx +MCU_ST_STM32L0:STM32L071CZYx +MCU_ST_STM32L0:STM32L071C_B-Z_Tx +MCU_ST_STM32L0:STM32L071C_B-Z_Ux +MCU_ST_STM32L0:STM32L071C_B-Z_Yx +MCU_ST_STM32L0:STM32L071K8Ux +MCU_ST_STM32L0:STM32L071KBTx +MCU_ST_STM32L0:STM32L071KBUx +MCU_ST_STM32L0:STM32L071KZTx +MCU_ST_STM32L0:STM32L071KZUx +MCU_ST_STM32L0:STM32L071K_B-Z_Tx +MCU_ST_STM32L0:STM32L071K_B-Z_Ux +MCU_ST_STM32L0:STM32L071RBHx +MCU_ST_STM32L0:STM32L071RBTx +MCU_ST_STM32L0:STM32L071RZHx +MCU_ST_STM32L0:STM32L071RZTx +MCU_ST_STM32L0:STM32L071R_B-Z_Hx +MCU_ST_STM32L0:STM32L071R_B-Z_Tx +MCU_ST_STM32L0:STM32L071V8Ix +MCU_ST_STM32L0:STM32L071V8Tx +MCU_ST_STM32L0:STM32L071VBIx +MCU_ST_STM32L0:STM32L071VBTx +MCU_ST_STM32L0:STM32L071VZIx +MCU_ST_STM32L0:STM32L071VZTx +MCU_ST_STM32L0:STM32L071V_B-Z_Ix +MCU_ST_STM32L0:STM32L071V_B-Z_Tx +MCU_ST_STM32L0:STM32L072CBTx +MCU_ST_STM32L0:STM32L072CBUx +MCU_ST_STM32L0:STM32L072CBYx +MCU_ST_STM32L0:STM32L072CZEx +MCU_ST_STM32L0:STM32L072CZTx +MCU_ST_STM32L0:STM32L072CZUx +MCU_ST_STM32L0:STM32L072CZYx +MCU_ST_STM32L0:STM32L072C_B-Z_Tx +MCU_ST_STM32L0:STM32L072C_B-Z_Ux +MCU_ST_STM32L0:STM32L072C_B-Z_Yx +MCU_ST_STM32L0:STM32L072KBTx +MCU_ST_STM32L0:STM32L072KBUx +MCU_ST_STM32L0:STM32L072KZTx +MCU_ST_STM32L0:STM32L072KZUx +MCU_ST_STM32L0:STM32L072K_B-Z_Tx +MCU_ST_STM32L0:STM32L072K_B-Z_Ux +MCU_ST_STM32L0:STM32L072RBHx +MCU_ST_STM32L0:STM32L072RBIx +MCU_ST_STM32L0:STM32L072RBTx +MCU_ST_STM32L0:STM32L072RZHx +MCU_ST_STM32L0:STM32L072RZIx +MCU_ST_STM32L0:STM32L072RZTx +MCU_ST_STM32L0:STM32L072R_B-Z_Hx +MCU_ST_STM32L0:STM32L072R_B-Z_Ix +MCU_ST_STM32L0:STM32L072R_B-Z_Tx +MCU_ST_STM32L0:STM32L072V8Ix +MCU_ST_STM32L0:STM32L072V8Tx +MCU_ST_STM32L0:STM32L072VBIx +MCU_ST_STM32L0:STM32L072VBTx +MCU_ST_STM32L0:STM32L072VZIx +MCU_ST_STM32L0:STM32L072VZTx +MCU_ST_STM32L0:STM32L072V_B-Z_Ix +MCU_ST_STM32L0:STM32L072V_B-Z_Tx +MCU_ST_STM32L0:STM32L073CBTx +MCU_ST_STM32L0:STM32L073CBUx +MCU_ST_STM32L0:STM32L073CZTx +MCU_ST_STM32L0:STM32L073CZUx +MCU_ST_STM32L0:STM32L073CZYx +MCU_ST_STM32L0:STM32L073C_B-Z_Tx +MCU_ST_STM32L0:STM32L073C_B-Z_Ux +MCU_ST_STM32L0:STM32L073RBHx +MCU_ST_STM32L0:STM32L073RBTx +MCU_ST_STM32L0:STM32L073RZHx +MCU_ST_STM32L0:STM32L073RZIx +MCU_ST_STM32L0:STM32L073RZTx +MCU_ST_STM32L0:STM32L073R_B-Z_Hx +MCU_ST_STM32L0:STM32L073R_B-Z_Tx +MCU_ST_STM32L0:STM32L073V8Ix +MCU_ST_STM32L0:STM32L073V8Tx +MCU_ST_STM32L0:STM32L073VBIx +MCU_ST_STM32L0:STM32L073VBTx +MCU_ST_STM32L0:STM32L073VZIx +MCU_ST_STM32L0:STM32L073VZTx +MCU_ST_STM32L0:STM32L073V_B-Z_Ix +MCU_ST_STM32L0:STM32L073V_B-Z_Tx +MCU_ST_STM32L0:STM32L081CBTx +MCU_ST_STM32L0:STM32L081CZTx +MCU_ST_STM32L0:STM32L081CZUx +MCU_ST_STM32L0:STM32L081C_B-Z_Tx +MCU_ST_STM32L0:STM32L081KZTx +MCU_ST_STM32L0:STM32L081KZUx +MCU_ST_STM32L0:STM32L082CZUx +MCU_ST_STM32L0:STM32L082CZYx +MCU_ST_STM32L0:STM32L082KBTx +MCU_ST_STM32L0:STM32L082KBUx +MCU_ST_STM32L0:STM32L082KZTx +MCU_ST_STM32L0:STM32L082KZUx +MCU_ST_STM32L0:STM32L082K_B-Z_Tx +MCU_ST_STM32L0:STM32L082K_B-Z_Ux +MCU_ST_STM32L0:STM32L083CBTx +MCU_ST_STM32L0:STM32L083CZTx +MCU_ST_STM32L0:STM32L083CZUx +MCU_ST_STM32L0:STM32L083C_B-Z_Tx +MCU_ST_STM32L0:STM32L083RBHx +MCU_ST_STM32L0:STM32L083RBTx +MCU_ST_STM32L0:STM32L083RZHx +MCU_ST_STM32L0:STM32L083RZTx +MCU_ST_STM32L0:STM32L083R_B-Z_Hx +MCU_ST_STM32L0:STM32L083R_B-Z_Tx +MCU_ST_STM32L0:STM32L083V8Ix +MCU_ST_STM32L0:STM32L083V8Tx +MCU_ST_STM32L0:STM32L083VBIx +MCU_ST_STM32L0:STM32L083VBTx +MCU_ST_STM32L0:STM32L083VZIx +MCU_ST_STM32L0:STM32L083VZTx +MCU_ST_STM32L0:STM32L083V_B-Z_Ix +MCU_ST_STM32L0:STM32L083V_B-Z_Tx +MCU_ST_STM32L1:STM32L100C6Ux +MCU_ST_STM32L1:STM32L100C6UxA +MCU_ST_STM32L1:STM32L100R8Tx +MCU_ST_STM32L1:STM32L100R8TxA +MCU_ST_STM32L1:STM32L100RBTx +MCU_ST_STM32L1:STM32L100RBTxA +MCU_ST_STM32L1:STM32L100RCTx +MCU_ST_STM32L1:STM32L100R_8-B_Tx +MCU_ST_STM32L1:STM32L100R_8-B_TxA +MCU_ST_STM32L1:STM32L151C6Tx +MCU_ST_STM32L1:STM32L151C6TxA +MCU_ST_STM32L1:STM32L151C6Ux +MCU_ST_STM32L1:STM32L151C6UxA +MCU_ST_STM32L1:STM32L151C8Tx +MCU_ST_STM32L1:STM32L151C8TxA +MCU_ST_STM32L1:STM32L151C8Ux +MCU_ST_STM32L1:STM32L151C8UxA +MCU_ST_STM32L1:STM32L151CBTx +MCU_ST_STM32L1:STM32L151CBTxA +MCU_ST_STM32L1:STM32L151CBUx +MCU_ST_STM32L1:STM32L151CBUxA +MCU_ST_STM32L1:STM32L151CCTx +MCU_ST_STM32L1:STM32L151CCUx +MCU_ST_STM32L1:STM32L151C_6-8-B_Tx +MCU_ST_STM32L1:STM32L151C_6-8-B_TxA +MCU_ST_STM32L1:STM32L151C_6-8-B_Ux +MCU_ST_STM32L1:STM32L151C_6-8-B_UxA +MCU_ST_STM32L1:STM32L151QCHx +MCU_ST_STM32L1:STM32L151QDHx +MCU_ST_STM32L1:STM32L151QEHx +MCU_ST_STM32L1:STM32L151R6Hx +MCU_ST_STM32L1:STM32L151R6HxA +MCU_ST_STM32L1:STM32L151R6Tx +MCU_ST_STM32L1:STM32L151R6TxA +MCU_ST_STM32L1:STM32L151R8Hx +MCU_ST_STM32L1:STM32L151R8HxA +MCU_ST_STM32L1:STM32L151R8Tx +MCU_ST_STM32L1:STM32L151R8TxA +MCU_ST_STM32L1:STM32L151RBHx +MCU_ST_STM32L1:STM32L151RBHxA +MCU_ST_STM32L1:STM32L151RBTx +MCU_ST_STM32L1:STM32L151RBTxA +MCU_ST_STM32L1:STM32L151RCTx +MCU_ST_STM32L1:STM32L151RCTxA +MCU_ST_STM32L1:STM32L151RCYx +MCU_ST_STM32L1:STM32L151RDTx +MCU_ST_STM32L1:STM32L151RDYx +MCU_ST_STM32L1:STM32L151RETx +MCU_ST_STM32L1:STM32L151R_6-8-B_Hx +MCU_ST_STM32L1:STM32L151R_6-8-B_HxA +MCU_ST_STM32L1:STM32L151R_6-8-B_Tx +MCU_ST_STM32L1:STM32L151R_6-8-B_TxA +MCU_ST_STM32L1:STM32L151UCYx +MCU_ST_STM32L1:STM32L151V8Hx +MCU_ST_STM32L1:STM32L151V8HxA +MCU_ST_STM32L1:STM32L151V8Tx +MCU_ST_STM32L1:STM32L151V8TxA +MCU_ST_STM32L1:STM32L151VBHx +MCU_ST_STM32L1:STM32L151VBHxA +MCU_ST_STM32L1:STM32L151VBTx +MCU_ST_STM32L1:STM32L151VBTxA +MCU_ST_STM32L1:STM32L151VCHx +MCU_ST_STM32L1:STM32L151VCTx +MCU_ST_STM32L1:STM32L151VCTxA +MCU_ST_STM32L1:STM32L151VDTx +MCU_ST_STM32L1:STM32L151VDTxX +MCU_ST_STM32L1:STM32L151VDYxX +MCU_ST_STM32L1:STM32L151VETx +MCU_ST_STM32L1:STM32L151VEYx +MCU_ST_STM32L1:STM32L151V_8-B_Hx +MCU_ST_STM32L1:STM32L151V_8-B_HxA +MCU_ST_STM32L1:STM32L151V_8-B_Tx +MCU_ST_STM32L1:STM32L151V_8-B_TxA +MCU_ST_STM32L1:STM32L151ZCTx +MCU_ST_STM32L1:STM32L151ZDTx +MCU_ST_STM32L1:STM32L151ZETx +MCU_ST_STM32L1:STM32L152C6Tx +MCU_ST_STM32L1:STM32L152C6TxA +MCU_ST_STM32L1:STM32L152C6Ux +MCU_ST_STM32L1:STM32L152C6UxA +MCU_ST_STM32L1:STM32L152C8Tx +MCU_ST_STM32L1:STM32L152C8TxA +MCU_ST_STM32L1:STM32L152C8Ux +MCU_ST_STM32L1:STM32L152C8UxA +MCU_ST_STM32L1:STM32L152CBTx +MCU_ST_STM32L1:STM32L152CBTxA +MCU_ST_STM32L1:STM32L152CBUx +MCU_ST_STM32L1:STM32L152CBUxA +MCU_ST_STM32L1:STM32L152CCTx +MCU_ST_STM32L1:STM32L152CCUx +MCU_ST_STM32L1:STM32L152C_6-8-B_Tx +MCU_ST_STM32L1:STM32L152C_6-8-B_TxA +MCU_ST_STM32L1:STM32L152C_6-8-B_Ux +MCU_ST_STM32L1:STM32L152C_6-8-B_UxA +MCU_ST_STM32L1:STM32L152QCHx +MCU_ST_STM32L1:STM32L152QDHx +MCU_ST_STM32L1:STM32L152QEHx +MCU_ST_STM32L1:STM32L152R6Hx +MCU_ST_STM32L1:STM32L152R6HxA +MCU_ST_STM32L1:STM32L152R6Tx +MCU_ST_STM32L1:STM32L152R6TxA +MCU_ST_STM32L1:STM32L152R8Hx +MCU_ST_STM32L1:STM32L152R8HxA +MCU_ST_STM32L1:STM32L152R8Tx +MCU_ST_STM32L1:STM32L152R8TxA +MCU_ST_STM32L1:STM32L152RBHx +MCU_ST_STM32L1:STM32L152RBHxA +MCU_ST_STM32L1:STM32L152RBTx +MCU_ST_STM32L1:STM32L152RBTxA +MCU_ST_STM32L1:STM32L152RCTx +MCU_ST_STM32L1:STM32L152RCTxA +MCU_ST_STM32L1:STM32L152RDTx +MCU_ST_STM32L1:STM32L152RDYx +MCU_ST_STM32L1:STM32L152RETx +MCU_ST_STM32L1:STM32L152R_6-8-B_Hx +MCU_ST_STM32L1:STM32L152R_6-8-B_HxA +MCU_ST_STM32L1:STM32L152R_6-8-B_Tx +MCU_ST_STM32L1:STM32L152R_6-8-B_TxA +MCU_ST_STM32L1:STM32L152UCYx +MCU_ST_STM32L1:STM32L152V8Hx +MCU_ST_STM32L1:STM32L152V8HxA +MCU_ST_STM32L1:STM32L152V8Tx +MCU_ST_STM32L1:STM32L152V8TxA +MCU_ST_STM32L1:STM32L152VBHx +MCU_ST_STM32L1:STM32L152VBHxA +MCU_ST_STM32L1:STM32L152VBTx +MCU_ST_STM32L1:STM32L152VBTxA +MCU_ST_STM32L1:STM32L152VCHx +MCU_ST_STM32L1:STM32L152VCTx +MCU_ST_STM32L1:STM32L152VCTxA +MCU_ST_STM32L1:STM32L152VDTx +MCU_ST_STM32L1:STM32L152VDTxX +MCU_ST_STM32L1:STM32L152VETx +MCU_ST_STM32L1:STM32L152VEYx +MCU_ST_STM32L1:STM32L152V_8-B_Hx +MCU_ST_STM32L1:STM32L152V_8-B_HxA +MCU_ST_STM32L1:STM32L152V_8-B_Tx +MCU_ST_STM32L1:STM32L152V_8-B_TxA +MCU_ST_STM32L1:STM32L152ZCTx +MCU_ST_STM32L1:STM32L152ZDTx +MCU_ST_STM32L1:STM32L152ZETx +MCU_ST_STM32L1:STM32L162QCHx +MCU_ST_STM32L1:STM32L162QDHx +MCU_ST_STM32L1:STM32L162RCTx +MCU_ST_STM32L1:STM32L162RCTxA +MCU_ST_STM32L1:STM32L162RDTx +MCU_ST_STM32L1:STM32L162RDYx +MCU_ST_STM32L1:STM32L162RETx +MCU_ST_STM32L1:STM32L162VCHx +MCU_ST_STM32L1:STM32L162VCTx +MCU_ST_STM32L1:STM32L162VCTxA +MCU_ST_STM32L1:STM32L162VDTx +MCU_ST_STM32L1:STM32L162VDYxX +MCU_ST_STM32L1:STM32L162VETx +MCU_ST_STM32L1:STM32L162VEYx +MCU_ST_STM32L1:STM32L162ZCTx +MCU_ST_STM32L1:STM32L162ZDTx +MCU_ST_STM32L1:STM32L162ZETx +MCU_ST_STM32L4:STM32L412C8Tx +MCU_ST_STM32L4:STM32L412C8Ux +MCU_ST_STM32L4:STM32L412CBTx +MCU_ST_STM32L4:STM32L412CBTxP +MCU_ST_STM32L4:STM32L412CBUx +MCU_ST_STM32L4:STM32L412CBUxP +MCU_ST_STM32L4:STM32L412K8Tx +MCU_ST_STM32L4:STM32L412K8Ux +MCU_ST_STM32L4:STM32L412KBTx +MCU_ST_STM32L4:STM32L412KBUx +MCU_ST_STM32L4:STM32L412R8Ix +MCU_ST_STM32L4:STM32L412R8Tx +MCU_ST_STM32L4:STM32L412RBIx +MCU_ST_STM32L4:STM32L412RBIxP +MCU_ST_STM32L4:STM32L412RBTx +MCU_ST_STM32L4:STM32L412RBTxP +MCU_ST_STM32L4:STM32L412T8Yx +MCU_ST_STM32L4:STM32L412TBYx +MCU_ST_STM32L4:STM32L412TBYxP +MCU_ST_STM32L4:STM32L422CBTx +MCU_ST_STM32L4:STM32L422CBUx +MCU_ST_STM32L4:STM32L422KBTx +MCU_ST_STM32L4:STM32L422KBUx +MCU_ST_STM32L4:STM32L422RBIx +MCU_ST_STM32L4:STM32L422RBTx +MCU_ST_STM32L4:STM32L422TBYx +MCU_ST_STM32L4:STM32L431CBTx +MCU_ST_STM32L4:STM32L431CBUx +MCU_ST_STM32L4:STM32L431CBYx +MCU_ST_STM32L4:STM32L431CCTx +MCU_ST_STM32L4:STM32L431CCUx +MCU_ST_STM32L4:STM32L431CCYx +MCU_ST_STM32L4:STM32L431C_B-C_Tx +MCU_ST_STM32L4:STM32L431C_B-C_Ux +MCU_ST_STM32L4:STM32L431C_B-C_Yx +MCU_ST_STM32L4:STM32L431KBUx +MCU_ST_STM32L4:STM32L431KCUx +MCU_ST_STM32L4:STM32L431K_B-C_Ux +MCU_ST_STM32L4:STM32L431RBIx +MCU_ST_STM32L4:STM32L431RBTx +MCU_ST_STM32L4:STM32L431RBYx +MCU_ST_STM32L4:STM32L431RCIx +MCU_ST_STM32L4:STM32L431RCTx +MCU_ST_STM32L4:STM32L431RCYx +MCU_ST_STM32L4:STM32L431R_B-C_Ix +MCU_ST_STM32L4:STM32L431R_B-C_Tx +MCU_ST_STM32L4:STM32L431R_B-C_Yx +MCU_ST_STM32L4:STM32L431VCIx +MCU_ST_STM32L4:STM32L431VCTx +MCU_ST_STM32L4:STM32L432KBUx +MCU_ST_STM32L4:STM32L432KCUx +MCU_ST_STM32L4:STM32L432K_B-C_Ux +MCU_ST_STM32L4:STM32L433CBTx +MCU_ST_STM32L4:STM32L433CBUx +MCU_ST_STM32L4:STM32L433CBYx +MCU_ST_STM32L4:STM32L433CCTx +MCU_ST_STM32L4:STM32L433CCUx +MCU_ST_STM32L4:STM32L433CCYx +MCU_ST_STM32L4:STM32L433C_B-C_Tx +MCU_ST_STM32L4:STM32L433C_B-C_Ux +MCU_ST_STM32L4:STM32L433C_B-C_Yx +MCU_ST_STM32L4:STM32L433RBIx +MCU_ST_STM32L4:STM32L433RBTx +MCU_ST_STM32L4:STM32L433RBYx +MCU_ST_STM32L4:STM32L433RCIx +MCU_ST_STM32L4:STM32L433RCTx +MCU_ST_STM32L4:STM32L433RCTxP +MCU_ST_STM32L4:STM32L433RCYx +MCU_ST_STM32L4:STM32L433R_B-C_Ix +MCU_ST_STM32L4:STM32L433R_B-C_Tx +MCU_ST_STM32L4:STM32L433R_B-C_Yx +MCU_ST_STM32L4:STM32L433VCIx +MCU_ST_STM32L4:STM32L433VCTx +MCU_ST_STM32L4:STM32L442KCUx +MCU_ST_STM32L4:STM32L443CCFx +MCU_ST_STM32L4:STM32L443CCTx +MCU_ST_STM32L4:STM32L443CCUx +MCU_ST_STM32L4:STM32L443CCYx +MCU_ST_STM32L4:STM32L443RCIx +MCU_ST_STM32L4:STM32L443RCTx +MCU_ST_STM32L4:STM32L443RCYx +MCU_ST_STM32L4:STM32L443VCIx +MCU_ST_STM32L4:STM32L443VCTx +MCU_ST_STM32L4:STM32L451CCUx +MCU_ST_STM32L4:STM32L451CETx +MCU_ST_STM32L4:STM32L451CEUx +MCU_ST_STM32L4:STM32L451C_C-E_Ux +MCU_ST_STM32L4:STM32L451RCIx +MCU_ST_STM32L4:STM32L451RCTx +MCU_ST_STM32L4:STM32L451RCYx +MCU_ST_STM32L4:STM32L451REIx +MCU_ST_STM32L4:STM32L451RETx +MCU_ST_STM32L4:STM32L451REYx +MCU_ST_STM32L4:STM32L451R_C-E_Ix +MCU_ST_STM32L4:STM32L451R_C-E_Tx +MCU_ST_STM32L4:STM32L451R_C-E_Yx +MCU_ST_STM32L4:STM32L451VCIx +MCU_ST_STM32L4:STM32L451VCTx +MCU_ST_STM32L4:STM32L451VEIx +MCU_ST_STM32L4:STM32L451VETx +MCU_ST_STM32L4:STM32L451V_C-E_Ix +MCU_ST_STM32L4:STM32L451V_C-E_Tx +MCU_ST_STM32L4:STM32L452CCUx +MCU_ST_STM32L4:STM32L452CETx +MCU_ST_STM32L4:STM32L452CETxP +MCU_ST_STM32L4:STM32L452CEUx +MCU_ST_STM32L4:STM32L452C_C-E_Ux +MCU_ST_STM32L4:STM32L452RCIx +MCU_ST_STM32L4:STM32L452RCTx +MCU_ST_STM32L4:STM32L452RCYx +MCU_ST_STM32L4:STM32L452REIx +MCU_ST_STM32L4:STM32L452RETx +MCU_ST_STM32L4:STM32L452RETxP +MCU_ST_STM32L4:STM32L452REYx +MCU_ST_STM32L4:STM32L452REYxP +MCU_ST_STM32L4:STM32L452R_C-E_Ix +MCU_ST_STM32L4:STM32L452R_C-E_Tx +MCU_ST_STM32L4:STM32L452R_C-E_Yx +MCU_ST_STM32L4:STM32L452VCIx +MCU_ST_STM32L4:STM32L452VCTx +MCU_ST_STM32L4:STM32L452VEIx +MCU_ST_STM32L4:STM32L452VETx +MCU_ST_STM32L4:STM32L452V_C-E_Ix +MCU_ST_STM32L4:STM32L452V_C-E_Tx +MCU_ST_STM32L4:STM32L462CETx +MCU_ST_STM32L4:STM32L462CEUx +MCU_ST_STM32L4:STM32L462REIx +MCU_ST_STM32L4:STM32L462RETx +MCU_ST_STM32L4:STM32L462REYx +MCU_ST_STM32L4:STM32L462VEIx +MCU_ST_STM32L4:STM32L462VETx +MCU_ST_STM32L4:STM32L471QEIx +MCU_ST_STM32L4:STM32L471QGIx +MCU_ST_STM32L4:STM32L471Q_E-G_Ix +MCU_ST_STM32L4:STM32L471RETx +MCU_ST_STM32L4:STM32L471RGTx +MCU_ST_STM32L4:STM32L471R_E-G_Tx +MCU_ST_STM32L4:STM32L471VETx +MCU_ST_STM32L4:STM32L471VGTx +MCU_ST_STM32L4:STM32L471V_E-G_Tx +MCU_ST_STM32L4:STM32L471ZEJx +MCU_ST_STM32L4:STM32L471ZETx +MCU_ST_STM32L4:STM32L471ZGJx +MCU_ST_STM32L4:STM32L471ZGTx +MCU_ST_STM32L4:STM32L471Z_E-G_Jx +MCU_ST_STM32L4:STM32L471Z_E-G_Tx +MCU_ST_STM32L4:STM32L475RCTx +MCU_ST_STM32L4:STM32L475RETx +MCU_ST_STM32L4:STM32L475RGTx +MCU_ST_STM32L4:STM32L475R_C-E-G_Tx +MCU_ST_STM32L4:STM32L475VCTx +MCU_ST_STM32L4:STM32L475VETx +MCU_ST_STM32L4:STM32L475VGTx +MCU_ST_STM32L4:STM32L475V_C-E-G_Tx +MCU_ST_STM32L4:STM32L476JEYx +MCU_ST_STM32L4:STM32L476JGYx +MCU_ST_STM32L4:STM32L476JGYxP +MCU_ST_STM32L4:STM32L476J_E-G_Yx +MCU_ST_STM32L4:STM32L476MEYx +MCU_ST_STM32L4:STM32L476MGYx +MCU_ST_STM32L4:STM32L476M_E-G_Yx +MCU_ST_STM32L4:STM32L476QEIx +MCU_ST_STM32L4:STM32L476QGIx +MCU_ST_STM32L4:STM32L476QGIxP +MCU_ST_STM32L4:STM32L476Q_E-G_Ix +MCU_ST_STM32L4:STM32L476RCTx +MCU_ST_STM32L4:STM32L476RETx +MCU_ST_STM32L4:STM32L476RGTx +MCU_ST_STM32L4:STM32L476R_C-E-G_Tx +MCU_ST_STM32L4:STM32L476VCTx +MCU_ST_STM32L4:STM32L476VETx +MCU_ST_STM32L4:STM32L476VGTx +MCU_ST_STM32L4:STM32L476VGYxP +MCU_ST_STM32L4:STM32L476V_C-E-G_Tx +MCU_ST_STM32L4:STM32L476ZETx +MCU_ST_STM32L4:STM32L476ZGJx +MCU_ST_STM32L4:STM32L476ZGTx +MCU_ST_STM32L4:STM32L476ZGTxP +MCU_ST_STM32L4:STM32L476Z_E-G_Tx +MCU_ST_STM32L4:STM32L486JGYx +MCU_ST_STM32L4:STM32L486QGIx +MCU_ST_STM32L4:STM32L486RGTx +MCU_ST_STM32L4:STM32L486VGTx +MCU_ST_STM32L4:STM32L486ZGTx +MCU_ST_STM32L4:STM32L496AEIx +MCU_ST_STM32L4:STM32L496AGIx +MCU_ST_STM32L4:STM32L496AGIxP +MCU_ST_STM32L4:STM32L496A_E-G_Ix +MCU_ST_STM32L4:STM32L496QEIx +MCU_ST_STM32L4:STM32L496QGIx +MCU_ST_STM32L4:STM32L496QGIxP +MCU_ST_STM32L4:STM32L496QGIxS +MCU_ST_STM32L4:STM32L496Q_E-G_Ix +MCU_ST_STM32L4:STM32L496RETx +MCU_ST_STM32L4:STM32L496RGTx +MCU_ST_STM32L4:STM32L496RGTxP +MCU_ST_STM32L4:STM32L496R_E-G_Tx +MCU_ST_STM32L4:STM32L496VETx +MCU_ST_STM32L4:STM32L496VGTx +MCU_ST_STM32L4:STM32L496VGTxP +MCU_ST_STM32L4:STM32L496VGYx +MCU_ST_STM32L4:STM32L496VGYxP +MCU_ST_STM32L4:STM32L496V_E-G_Tx +MCU_ST_STM32L4:STM32L496WGYxP +MCU_ST_STM32L4:STM32L496ZETx +MCU_ST_STM32L4:STM32L496ZGTx +MCU_ST_STM32L4:STM32L496ZGTxP +MCU_ST_STM32L4:STM32L496Z_E-G_Tx +MCU_ST_STM32L4:STM32L4A6AGIx +MCU_ST_STM32L4:STM32L4A6AGIxP +MCU_ST_STM32L4:STM32L4A6QGIx +MCU_ST_STM32L4:STM32L4A6QGIxP +MCU_ST_STM32L4:STM32L4A6RGTx +MCU_ST_STM32L4:STM32L4A6RGTxP +MCU_ST_STM32L4:STM32L4A6VGTx +MCU_ST_STM32L4:STM32L4A6VGTxP +MCU_ST_STM32L4:STM32L4A6VGYx +MCU_ST_STM32L4:STM32L4A6VGYxP +MCU_ST_STM32L4:STM32L4A6ZGTx +MCU_ST_STM32L4:STM32L4A6ZGTxP +MCU_ST_STM32L4:STM32L4P5AEIx +MCU_ST_STM32L4:STM32L4P5AGIx +MCU_ST_STM32L4:STM32L4P5AGIxP +MCU_ST_STM32L4:STM32L4P5A_G-E_Ix +MCU_ST_STM32L4:STM32L4P5CETx +MCU_ST_STM32L4:STM32L4P5CEUx +MCU_ST_STM32L4:STM32L4P5CGTx +MCU_ST_STM32L4:STM32L4P5CGTxP +MCU_ST_STM32L4:STM32L4P5CGUx +MCU_ST_STM32L4:STM32L4P5CGUxP +MCU_ST_STM32L4:STM32L4P5C_G-E_Tx +MCU_ST_STM32L4:STM32L4P5C_G-E_Ux +MCU_ST_STM32L4:STM32L4P5QEIx +MCU_ST_STM32L4:STM32L4P5QGIx +MCU_ST_STM32L4:STM32L4P5QGIxP +MCU_ST_STM32L4:STM32L4P5QGIxS +MCU_ST_STM32L4:STM32L4P5Q_G-E_Ix +MCU_ST_STM32L4:STM32L4P5RETx +MCU_ST_STM32L4:STM32L4P5RGTx +MCU_ST_STM32L4:STM32L4P5RGTxP +MCU_ST_STM32L4:STM32L4P5R_G-E_Tx +MCU_ST_STM32L4:STM32L4P5VETx +MCU_ST_STM32L4:STM32L4P5VEYx +MCU_ST_STM32L4:STM32L4P5VGTx +MCU_ST_STM32L4:STM32L4P5VGTxP +MCU_ST_STM32L4:STM32L4P5VGYx +MCU_ST_STM32L4:STM32L4P5VGYxP +MCU_ST_STM32L4:STM32L4P5V_G-E_Tx +MCU_ST_STM32L4:STM32L4P5V_G-E_Yx +MCU_ST_STM32L4:STM32L4P5ZETx +MCU_ST_STM32L4:STM32L4P5ZGTx +MCU_ST_STM32L4:STM32L4P5ZGTxP +MCU_ST_STM32L4:STM32L4P5Z_G-E_Tx +MCU_ST_STM32L4:STM32L4Q5AGIx +MCU_ST_STM32L4:STM32L4Q5AGIxP +MCU_ST_STM32L4:STM32L4Q5CGTx +MCU_ST_STM32L4:STM32L4Q5CGTxP +MCU_ST_STM32L4:STM32L4Q5CGUx +MCU_ST_STM32L4:STM32L4Q5CGUxP +MCU_ST_STM32L4:STM32L4Q5QGIx +MCU_ST_STM32L4:STM32L4Q5QGIxP +MCU_ST_STM32L4:STM32L4Q5RGTx +MCU_ST_STM32L4:STM32L4Q5RGTxP +MCU_ST_STM32L4:STM32L4Q5VGTx +MCU_ST_STM32L4:STM32L4Q5VGTxP +MCU_ST_STM32L4:STM32L4Q5VGYx +MCU_ST_STM32L4:STM32L4Q5VGYxP +MCU_ST_STM32L4:STM32L4Q5ZGTx +MCU_ST_STM32L4:STM32L4Q5ZGTxP +MCU_ST_STM32L4:STM32L4R5AGIx +MCU_ST_STM32L4:STM32L4R5AIIx +MCU_ST_STM32L4:STM32L4R5AIIxP +MCU_ST_STM32L4:STM32L4R5A_G-I_Ix +MCU_ST_STM32L4:STM32L4R5QGIx +MCU_ST_STM32L4:STM32L4R5QGIxS +MCU_ST_STM32L4:STM32L4R5QIIx +MCU_ST_STM32L4:STM32L4R5QIIxP +MCU_ST_STM32L4:STM32L4R5Q_G-I_Ix +MCU_ST_STM32L4:STM32L4R5VGTx +MCU_ST_STM32L4:STM32L4R5VITx +MCU_ST_STM32L4:STM32L4R5V_G-I_Tx +MCU_ST_STM32L4:STM32L4R5ZGTx +MCU_ST_STM32L4:STM32L4R5ZGYx +MCU_ST_STM32L4:STM32L4R5ZITx +MCU_ST_STM32L4:STM32L4R5ZITxP +MCU_ST_STM32L4:STM32L4R5ZIYx +MCU_ST_STM32L4:STM32L4R5Z_G-I_Tx +MCU_ST_STM32L4:STM32L4R5Z_G-I_Yx +MCU_ST_STM32L4:STM32L4R7AIIx +MCU_ST_STM32L4:STM32L4R7VITx +MCU_ST_STM32L4:STM32L4R7ZITx +MCU_ST_STM32L4:STM32L4R9AGIx +MCU_ST_STM32L4:STM32L4R9AIIx +MCU_ST_STM32L4:STM32L4R9A_G-I_Ix +MCU_ST_STM32L4:STM32L4R9VGTx +MCU_ST_STM32L4:STM32L4R9VITx +MCU_ST_STM32L4:STM32L4R9V_G-I_Tx +MCU_ST_STM32L4:STM32L4R9ZGJx +MCU_ST_STM32L4:STM32L4R9ZGTx +MCU_ST_STM32L4:STM32L4R9ZGYx +MCU_ST_STM32L4:STM32L4R9ZIJx +MCU_ST_STM32L4:STM32L4R9ZITx +MCU_ST_STM32L4:STM32L4R9ZIYx +MCU_ST_STM32L4:STM32L4R9ZIYxP +MCU_ST_STM32L4:STM32L4R9Z_G-I_Jx +MCU_ST_STM32L4:STM32L4R9Z_G-I_Tx +MCU_ST_STM32L4:STM32L4R9Z_G-I_Yx +MCU_ST_STM32L4:STM32L4S5AIIx +MCU_ST_STM32L4:STM32L4S5QIIx +MCU_ST_STM32L4:STM32L4S5VITx +MCU_ST_STM32L4:STM32L4S5ZITx +MCU_ST_STM32L4:STM32L4S5ZIYx +MCU_ST_STM32L4:STM32L4S7AIIx +MCU_ST_STM32L4:STM32L4S7VITx +MCU_ST_STM32L4:STM32L4S7ZITx +MCU_ST_STM32L4:STM32L4S9AIIx +MCU_ST_STM32L4:STM32L4S9VITx +MCU_ST_STM32L4:STM32L4S9ZIJx +MCU_ST_STM32L4:STM32L4S9ZITx +MCU_ST_STM32L4:STM32L4S9ZIYx +MCU_ST_STM32L5:STM32L552CCTx +MCU_ST_STM32L5:STM32L552CCUx +MCU_ST_STM32L5:STM32L552CETx +MCU_ST_STM32L5:STM32L552CETxP +MCU_ST_STM32L5:STM32L552CEUx +MCU_ST_STM32L5:STM32L552CEUxP +MCU_ST_STM32L5:STM32L552C_C-E_Tx +MCU_ST_STM32L5:STM32L552C_C-E_Ux +MCU_ST_STM32L5:STM32L552MEYxP +MCU_ST_STM32L5:STM32L552MEYxQ +MCU_ST_STM32L5:STM32L552QCIxQ +MCU_ST_STM32L5:STM32L552QEIx +MCU_ST_STM32L5:STM32L552QEIxP +MCU_ST_STM32L5:STM32L552QEIxQ +MCU_ST_STM32L5:STM32L552Q_C-E_IxQ +MCU_ST_STM32L5:STM32L552RCTx +MCU_ST_STM32L5:STM32L552RETx +MCU_ST_STM32L5:STM32L552RETxP +MCU_ST_STM32L5:STM32L552RETxQ +MCU_ST_STM32L5:STM32L552R_C-E_Tx +MCU_ST_STM32L5:STM32L552VCTxQ +MCU_ST_STM32L5:STM32L552VETx +MCU_ST_STM32L5:STM32L552VETxQ +MCU_ST_STM32L5:STM32L552V_C-E_TxQ +MCU_ST_STM32L5:STM32L552ZCTxQ +MCU_ST_STM32L5:STM32L552ZETx +MCU_ST_STM32L5:STM32L552ZETxQ +MCU_ST_STM32L5:STM32L552Z_C-E_TxQ +MCU_ST_STM32L5:STM32L562CETx +MCU_ST_STM32L5:STM32L562CETxP +MCU_ST_STM32L5:STM32L562CEUx +MCU_ST_STM32L5:STM32L562CEUxP +MCU_ST_STM32L5:STM32L562MEYxP +MCU_ST_STM32L5:STM32L562MEYxQ +MCU_ST_STM32L5:STM32L562QEIx +MCU_ST_STM32L5:STM32L562QEIxP +MCU_ST_STM32L5:STM32L562QEIxQ +MCU_ST_STM32L5:STM32L562RETx +MCU_ST_STM32L5:STM32L562RETxP +MCU_ST_STM32L5:STM32L562RETxQ +MCU_ST_STM32L5:STM32L562VETx +MCU_ST_STM32L5:STM32L562VETxQ +MCU_ST_STM32L5:STM32L562ZETx +MCU_ST_STM32L5:STM32L562ZETxQ +MCU_ST_STM32MP1:STM32MP131AAEx +MCU_ST_STM32MP1:STM32MP131AAFx +MCU_ST_STM32MP1:STM32MP131AAGx +MCU_ST_STM32MP1:STM32MP131CAEx +MCU_ST_STM32MP1:STM32MP131CAFx +MCU_ST_STM32MP1:STM32MP131CAGx +MCU_ST_STM32MP1:STM32MP131DAEx +MCU_ST_STM32MP1:STM32MP131DAFx +MCU_ST_STM32MP1:STM32MP131DAGx +MCU_ST_STM32MP1:STM32MP131FAEx +MCU_ST_STM32MP1:STM32MP131FAFx +MCU_ST_STM32MP1:STM32MP131FAGx +MCU_ST_STM32MP1:STM32MP133AAEx +MCU_ST_STM32MP1:STM32MP133AAFx +MCU_ST_STM32MP1:STM32MP133AAGx +MCU_ST_STM32MP1:STM32MP133CAEx +MCU_ST_STM32MP1:STM32MP133CAFx +MCU_ST_STM32MP1:STM32MP133CAGx +MCU_ST_STM32MP1:STM32MP133DAEx +MCU_ST_STM32MP1:STM32MP133DAFx +MCU_ST_STM32MP1:STM32MP133DAGx +MCU_ST_STM32MP1:STM32MP133FAEx +MCU_ST_STM32MP1:STM32MP133FAFx +MCU_ST_STM32MP1:STM32MP133FAGx +MCU_ST_STM32MP1:STM32MP135AAEx +MCU_ST_STM32MP1:STM32MP135AAFx +MCU_ST_STM32MP1:STM32MP135AAGx +MCU_ST_STM32MP1:STM32MP135CAEx +MCU_ST_STM32MP1:STM32MP135CAFx +MCU_ST_STM32MP1:STM32MP135CAGx +MCU_ST_STM32MP1:STM32MP135DAEx +MCU_ST_STM32MP1:STM32MP135DAFx +MCU_ST_STM32MP1:STM32MP135DAGx +MCU_ST_STM32MP1:STM32MP135FAEx +MCU_ST_STM32MP1:STM32MP135FAFx +MCU_ST_STM32MP1:STM32MP135FAGx +MCU_ST_STM32MP1:STM32MP151AAAx +MCU_ST_STM32MP1:STM32MP151AABx +MCU_ST_STM32MP1:STM32MP151AACx +MCU_ST_STM32MP1:STM32MP151AADx +MCU_ST_STM32MP1:STM32MP151CAAx +MCU_ST_STM32MP1:STM32MP151CABx +MCU_ST_STM32MP1:STM32MP151CACx +MCU_ST_STM32MP1:STM32MP151CADx +MCU_ST_STM32MP1:STM32MP151DAAx +MCU_ST_STM32MP1:STM32MP151DABx +MCU_ST_STM32MP1:STM32MP151DACx +MCU_ST_STM32MP1:STM32MP151DADx +MCU_ST_STM32MP1:STM32MP151FAAx +MCU_ST_STM32MP1:STM32MP151FABx +MCU_ST_STM32MP1:STM32MP151FACx +MCU_ST_STM32MP1:STM32MP151FADx +MCU_ST_STM32MP1:STM32MP153AAAx +MCU_ST_STM32MP1:STM32MP153AABx +MCU_ST_STM32MP1:STM32MP153AACx +MCU_ST_STM32MP1:STM32MP153AADx +MCU_ST_STM32MP1:STM32MP153CAAx +MCU_ST_STM32MP1:STM32MP153CABx +MCU_ST_STM32MP1:STM32MP153CACx +MCU_ST_STM32MP1:STM32MP153CADx +MCU_ST_STM32MP1:STM32MP153DAAx +MCU_ST_STM32MP1:STM32MP153DABx +MCU_ST_STM32MP1:STM32MP153DACx +MCU_ST_STM32MP1:STM32MP153DADx +MCU_ST_STM32MP1:STM32MP153FAAx +MCU_ST_STM32MP1:STM32MP153FABx +MCU_ST_STM32MP1:STM32MP153FACx +MCU_ST_STM32MP1:STM32MP153FADx +MCU_ST_STM32MP1:STM32MP157AAAx +MCU_ST_STM32MP1:STM32MP157AABx +MCU_ST_STM32MP1:STM32MP157AACx +MCU_ST_STM32MP1:STM32MP157AADx +MCU_ST_STM32MP1:STM32MP157CAAx +MCU_ST_STM32MP1:STM32MP157CABx +MCU_ST_STM32MP1:STM32MP157CACx +MCU_ST_STM32MP1:STM32MP157CADx +MCU_ST_STM32MP1:STM32MP157DAAx +MCU_ST_STM32MP1:STM32MP157DABx +MCU_ST_STM32MP1:STM32MP157DACx +MCU_ST_STM32MP1:STM32MP157DADx +MCU_ST_STM32MP1:STM32MP157FAAx +MCU_ST_STM32MP1:STM32MP157FABx +MCU_ST_STM32MP1:STM32MP157FACx +MCU_ST_STM32MP1:STM32MP157FADx +MCU_ST_STM32U0:STM32U031C6Tx +MCU_ST_STM32U0:STM32U031C6Ux +MCU_ST_STM32U0:STM32U031C8Tx +MCU_ST_STM32U0:STM32U031C8Ux +MCU_ST_STM32U0:STM32U031F4Px +MCU_ST_STM32U0:STM32U031F6Px +MCU_ST_STM32U0:STM32U031F8Px +MCU_ST_STM32U0:STM32U031G6Yx +MCU_ST_STM32U0:STM32U031G8Yx +MCU_ST_STM32U0:STM32U031K4Ux +MCU_ST_STM32U0:STM32U031K6Ux +MCU_ST_STM32U0:STM32U031K8Ux +MCU_ST_STM32U0:STM32U031R6Ix +MCU_ST_STM32U0:STM32U031R6Tx +MCU_ST_STM32U0:STM32U031R8Ix +MCU_ST_STM32U0:STM32U031R8Tx +MCU_ST_STM32U0:STM32U073C8Tx +MCU_ST_STM32U0:STM32U073C8Ux +MCU_ST_STM32U0:STM32U073CBTx +MCU_ST_STM32U0:STM32U073CBUx +MCU_ST_STM32U0:STM32U073CCTx +MCU_ST_STM32U0:STM32U073CCUx +MCU_ST_STM32U0:STM32U073H8Yx +MCU_ST_STM32U0:STM32U073HBYx +MCU_ST_STM32U0:STM32U073HCYx +MCU_ST_STM32U0:STM32U073K8Ux +MCU_ST_STM32U0:STM32U073KBUx +MCU_ST_STM32U0:STM32U073KCUx +MCU_ST_STM32U0:STM32U073M8Ix +MCU_ST_STM32U0:STM32U073M8Tx +MCU_ST_STM32U0:STM32U073MBIx +MCU_ST_STM32U0:STM32U073MBTx +MCU_ST_STM32U0:STM32U073MCIx +MCU_ST_STM32U0:STM32U073MCTx +MCU_ST_STM32U0:STM32U073R8Ix +MCU_ST_STM32U0:STM32U073R8Tx +MCU_ST_STM32U0:STM32U073RBIx +MCU_ST_STM32U0:STM32U073RBTx +MCU_ST_STM32U0:STM32U073RCIx +MCU_ST_STM32U0:STM32U073RCTx +MCU_ST_STM32U0:STM32U083CCTx +MCU_ST_STM32U0:STM32U083CCUx +MCU_ST_STM32U0:STM32U083HCYx +MCU_ST_STM32U0:STM32U083KCUx +MCU_ST_STM32U0:STM32U083MCIx +MCU_ST_STM32U0:STM32U083MCTx +MCU_ST_STM32U0:STM32U083RCIx +MCU_ST_STM32U0:STM32U083RCTx +MCU_ST_STM32U5:STM32U535CBTx +MCU_ST_STM32U5:STM32U535CBTxQ +MCU_ST_STM32U5:STM32U535CBUx +MCU_ST_STM32U5:STM32U535CBUxQ +MCU_ST_STM32U5:STM32U535CCTx +MCU_ST_STM32U5:STM32U535CCTxQ +MCU_ST_STM32U5:STM32U535CCUx +MCU_ST_STM32U5:STM32U535CCUxQ +MCU_ST_STM32U5:STM32U535CETx +MCU_ST_STM32U5:STM32U535CETxQ +MCU_ST_STM32U5:STM32U535CEUx +MCU_ST_STM32U5:STM32U535CEUxQ +MCU_ST_STM32U5:STM32U535JEYxQ +MCU_ST_STM32U5:STM32U535NCYxQ +MCU_ST_STM32U5:STM32U535NEYxQ +MCU_ST_STM32U5:STM32U535RBIx +MCU_ST_STM32U5:STM32U535RBIxQ +MCU_ST_STM32U5:STM32U535RBTx +MCU_ST_STM32U5:STM32U535RBTxQ +MCU_ST_STM32U5:STM32U535RCIx +MCU_ST_STM32U5:STM32U535RCIxQ +MCU_ST_STM32U5:STM32U535RCTx +MCU_ST_STM32U5:STM32U535RCTxQ +MCU_ST_STM32U5:STM32U535REIx +MCU_ST_STM32U5:STM32U535REIxQ +MCU_ST_STM32U5:STM32U535RETx +MCU_ST_STM32U5:STM32U535RETxQ +MCU_ST_STM32U5:STM32U535VCIx +MCU_ST_STM32U5:STM32U535VCIxQ +MCU_ST_STM32U5:STM32U535VCTx +MCU_ST_STM32U5:STM32U535VCTxQ +MCU_ST_STM32U5:STM32U535VEIx +MCU_ST_STM32U5:STM32U535VEIxQ +MCU_ST_STM32U5:STM32U535VETx +MCU_ST_STM32U5:STM32U535VETxQ +MCU_ST_STM32U5:STM32U545CETx +MCU_ST_STM32U5:STM32U545CETxQ +MCU_ST_STM32U5:STM32U545CEUx +MCU_ST_STM32U5:STM32U545CEUxQ +MCU_ST_STM32U5:STM32U545JEYxQ +MCU_ST_STM32U5:STM32U545NEYxQ +MCU_ST_STM32U5:STM32U545REIx +MCU_ST_STM32U5:STM32U545REIxQ +MCU_ST_STM32U5:STM32U545RETx +MCU_ST_STM32U5:STM32U545RETxQ +MCU_ST_STM32U5:STM32U545VEIx +MCU_ST_STM32U5:STM32U545VEIxQ +MCU_ST_STM32U5:STM32U545VETx +MCU_ST_STM32U5:STM32U545VETxQ +MCU_ST_STM32U5:STM32U575AGIx +MCU_ST_STM32U5:STM32U575AGIxQ +MCU_ST_STM32U5:STM32U575AIIx +MCU_ST_STM32U5:STM32U575AIIxQ +MCU_ST_STM32U5:STM32U575CGTx +MCU_ST_STM32U5:STM32U575CGTxQ +MCU_ST_STM32U5:STM32U575CGUx +MCU_ST_STM32U5:STM32U575CGUxQ +MCU_ST_STM32U5:STM32U575CITx +MCU_ST_STM32U5:STM32U575CITxQ +MCU_ST_STM32U5:STM32U575CIUx +MCU_ST_STM32U5:STM32U575CIUxQ +MCU_ST_STM32U5:STM32U575OGYxQ +MCU_ST_STM32U5:STM32U575OIYxQ +MCU_ST_STM32U5:STM32U575QGIx +MCU_ST_STM32U5:STM32U575QGIxQ +MCU_ST_STM32U5:STM32U575QIIx +MCU_ST_STM32U5:STM32U575QIIxQ +MCU_ST_STM32U5:STM32U575RGTx +MCU_ST_STM32U5:STM32U575RGTxQ +MCU_ST_STM32U5:STM32U575RITx +MCU_ST_STM32U5:STM32U575RITxQ +MCU_ST_STM32U5:STM32U575VGTx +MCU_ST_STM32U5:STM32U575VGTxQ +MCU_ST_STM32U5:STM32U575VITx +MCU_ST_STM32U5:STM32U575VITxQ +MCU_ST_STM32U5:STM32U575ZGTx +MCU_ST_STM32U5:STM32U575ZGTxQ +MCU_ST_STM32U5:STM32U575ZITx +MCU_ST_STM32U5:STM32U575ZITxQ +MCU_ST_STM32U5:STM32U585AIIx +MCU_ST_STM32U5:STM32U585AIIxQ +MCU_ST_STM32U5:STM32U585CITx +MCU_ST_STM32U5:STM32U585CITxQ +MCU_ST_STM32U5:STM32U585CIUx +MCU_ST_STM32U5:STM32U585CIUxQ +MCU_ST_STM32U5:STM32U585OIYxQ +MCU_ST_STM32U5:STM32U585QIIx +MCU_ST_STM32U5:STM32U585QIIxQ +MCU_ST_STM32U5:STM32U585RITx +MCU_ST_STM32U5:STM32U585RITxQ +MCU_ST_STM32U5:STM32U585VITx +MCU_ST_STM32U5:STM32U585VITxQ +MCU_ST_STM32U5:STM32U585ZITx +MCU_ST_STM32U5:STM32U585ZITxQ +MCU_ST_STM32U5:STM32U595AIHx +MCU_ST_STM32U5:STM32U595AIHxQ +MCU_ST_STM32U5:STM32U595AJHx +MCU_ST_STM32U5:STM32U595AJHxQ +MCU_ST_STM32U5:STM32U595QIIx +MCU_ST_STM32U5:STM32U595QIIxQ +MCU_ST_STM32U5:STM32U595QJIx +MCU_ST_STM32U5:STM32U595QJIxQ +MCU_ST_STM32U5:STM32U595RITx +MCU_ST_STM32U5:STM32U595RITxQ +MCU_ST_STM32U5:STM32U595RJTx +MCU_ST_STM32U5:STM32U595RJTxQ +MCU_ST_STM32U5:STM32U595VITx +MCU_ST_STM32U5:STM32U595VITxQ +MCU_ST_STM32U5:STM32U595VJTx +MCU_ST_STM32U5:STM32U595VJTxQ +MCU_ST_STM32U5:STM32U595ZITx +MCU_ST_STM32U5:STM32U595ZITxQ +MCU_ST_STM32U5:STM32U595ZIYxQ +MCU_ST_STM32U5:STM32U595ZJTx +MCU_ST_STM32U5:STM32U595ZJTxQ +MCU_ST_STM32U5:STM32U595ZJYxQ +MCU_ST_STM32U5:STM32U599BJYxQ +MCU_ST_STM32U5:STM32U599NIHxQ +MCU_ST_STM32U5:STM32U599NJHxQ +MCU_ST_STM32U5:STM32U599VITxQ +MCU_ST_STM32U5:STM32U599VJTx +MCU_ST_STM32U5:STM32U599VJTxQ +MCU_ST_STM32U5:STM32U599ZITxQ +MCU_ST_STM32U5:STM32U599ZIYxQ +MCU_ST_STM32U5:STM32U599ZJTxQ +MCU_ST_STM32U5:STM32U599ZJYxQ +MCU_ST_STM32U5:STM32U5A5AJHx +MCU_ST_STM32U5:STM32U5A5AJHxQ +MCU_ST_STM32U5:STM32U5A5QIIxQ +MCU_ST_STM32U5:STM32U5A5QJIx +MCU_ST_STM32U5:STM32U5A5QJIxQ +MCU_ST_STM32U5:STM32U5A5RJTx +MCU_ST_STM32U5:STM32U5A5RJTxQ +MCU_ST_STM32U5:STM32U5A5VJTx +MCU_ST_STM32U5:STM32U5A5VJTxQ +MCU_ST_STM32U5:STM32U5A5ZJTx +MCU_ST_STM32U5:STM32U5A5ZJTxQ +MCU_ST_STM32U5:STM32U5A5ZJYxQ +MCU_ST_STM32U5:STM32U5A9BJYxQ +MCU_ST_STM32U5:STM32U5A9NJHxQ +MCU_ST_STM32U5:STM32U5A9VJTxQ +MCU_ST_STM32U5:STM32U5A9ZJTxQ +MCU_ST_STM32U5:STM32U5A9ZJYxQ +MCU_ST_STM32U5:STM32U5F7VITx +MCU_ST_STM32U5:STM32U5F7VITxQ +MCU_ST_STM32U5:STM32U5F7VJTx +MCU_ST_STM32U5:STM32U5F7VJTxQ +MCU_ST_STM32U5:STM32U5F9BJYxQ +MCU_ST_STM32U5:STM32U5F9NJHxQ +MCU_ST_STM32U5:STM32U5F9VITxQ +MCU_ST_STM32U5:STM32U5F9VJTxQ +MCU_ST_STM32U5:STM32U5F9ZIJxQ +MCU_ST_STM32U5:STM32U5F9ZITxQ +MCU_ST_STM32U5:STM32U5F9ZJJxQ +MCU_ST_STM32U5:STM32U5F9ZJTxQ +MCU_ST_STM32U5:STM32U5G7VJTx +MCU_ST_STM32U5:STM32U5G7VJTxQ +MCU_ST_STM32U5:STM32U5G9BJYxQ +MCU_ST_STM32U5:STM32U5G9NJHxQ +MCU_ST_STM32U5:STM32U5G9VJTxQ +MCU_ST_STM32U5:STM32U5G9ZJJxQ +MCU_ST_STM32U5:STM32U5G9ZJTxQ +MCU_ST_STM32WB:STM32WB05KZVx +MCU_ST_STM32WB:STM32WB06KCVx +MCU_ST_STM32WB:STM32WB07KCVx +MCU_ST_STM32WB:STM32WB09KEVx +MCU_ST_STM32WB:STM32WB10CCUx +MCU_ST_STM32WB:STM32WB15CCUx +MCU_ST_STM32WB:STM32WB15CCUxE +MCU_ST_STM32WB:STM32WB15CCYx +MCU_ST_STM32WB:STM32WB30CEUxA +MCU_ST_STM32WB:STM32WB35CCUxA +MCU_ST_STM32WB:STM32WB35CEUxA +MCU_ST_STM32WB:STM32WB35C_C-E_UxA +MCU_ST_STM32WB:STM32WB50CGUx +MCU_ST_STM32WB:STM32WB55CCUx +MCU_ST_STM32WB:STM32WB55CEUx +MCU_ST_STM32WB:STM32WB55CGUx +MCU_ST_STM32WB:STM32WB55RCVx +MCU_ST_STM32WB:STM32WB55REVx +MCU_ST_STM32WB:STM32WB55RGVx +MCU_ST_STM32WB:STM32WB55VCQx +MCU_ST_STM32WB:STM32WB55VCYx +MCU_ST_STM32WB:STM32WB55VEQx +MCU_ST_STM32WB:STM32WB55VEYx +MCU_ST_STM32WB:STM32WB55VGQx +MCU_ST_STM32WB:STM32WB55VGYx +MCU_ST_STM32WB:STM32WB55VYYx +MCU_ST_STM32WB:STM32WBA52CEUx +MCU_ST_STM32WB:STM32WBA52CGUx +MCU_ST_STM32WB:STM32WBA52KEUx +MCU_ST_STM32WB:STM32WBA52KGUx +MCU_ST_STM32WB:STM32WBA54CEUx +MCU_ST_STM32WB:STM32WBA54CGUx +MCU_ST_STM32WB:STM32WBA54KEUx +MCU_ST_STM32WB:STM32WBA54KGUx +MCU_ST_STM32WB:STM32WBA55CEUx +MCU_ST_STM32WB:STM32WBA55CGUx +MCU_ST_STM32WB:STM32WBA55HEFx +MCU_ST_STM32WB:STM32WBA55HGFx +MCU_ST_STM32WB:STM32WBA55UEIx +MCU_ST_STM32WB:STM32WBA55UGIx +MCU_ST_STM32WL:STM32WL54CCUx +MCU_ST_STM32WL:STM32WL54JCIx +MCU_ST_STM32WL:STM32WL55CCUx +MCU_ST_STM32WL:STM32WL55JCIx +MCU_ST_STM32WL:STM32WLE4C8Ux +MCU_ST_STM32WL:STM32WLE4CBUx +MCU_ST_STM32WL:STM32WLE4CCUx +MCU_ST_STM32WL:STM32WLE4J8Ix +MCU_ST_STM32WL:STM32WLE4JBIx +MCU_ST_STM32WL:STM32WLE4JCIx +MCU_ST_STM32WL:STM32WLE5C8Ux +MCU_ST_STM32WL:STM32WLE5CBUx +MCU_ST_STM32WL:STM32WLE5CCUx +MCU_ST_STM32WL:STM32WLE5J8Ix +MCU_ST_STM32WL:STM32WLE5JBIx +MCU_ST_STM32WL:STM32WLE5JCIx +MCU_ST_STM8:STM8AF6223 +MCU_ST_STM8:STM8AF6223A +MCU_ST_STM8:STM8AL3188T +MCU_ST_STM8:STM8AL3189T +MCU_ST_STM8:STM8AL318AT +MCU_ST_STM8:STM8AL3L88T +MCU_ST_STM8:STM8AL3L89T +MCU_ST_STM8:STM8AL3L8AT +MCU_ST_STM8:STM8L051F3P +MCU_ST_STM8:STM8L101F1U +MCU_ST_STM8:STM8L101F2P +MCU_ST_STM8:STM8L101F2U +MCU_ST_STM8:STM8L101F3P +MCU_ST_STM8:STM8L101F3U +MCU_ST_STM8:STM8L151C2T +MCU_ST_STM8:STM8L151C3T +MCU_ST_STM8:STM8L152R6T +MCU_ST_STM8:STM8L152R8T +MCU_ST_STM8:STM8S001J3M +MCU_ST_STM8:STM8S003F3P +MCU_ST_STM8:STM8S003F3U +MCU_ST_STM8:STM8S003K3T +MCU_ST_STM8:STM8S207C6 +MCU_ST_STM8:STM8S207C8 +MCU_ST_STM8:STM8S207CB +MCU_ST_STM8:STM8S207MB +MCU_ST_STM8:STM8S207R6 +MCU_ST_STM8:STM8S207R8 +MCU_ST_STM8:STM8S207RB +MCU_ST_STM8:STM8S208C6 +MCU_ST_STM8:STM8S208C8 +MCU_ST_STM8:STM8S208CB +MCU_ST_STM8:STM8S208MB +MCU_ST_STM8:STM8S208R6 +MCU_ST_STM8:STM8S208R8 +MCU_ST_STM8:STM8S208RB +MCU_Texas:LM3S6911-EQC50 +MCU_Texas:LM3S6911-IQC50 +MCU_Texas:LM4F110B2QR +MCU_Texas:LM4F110C4QR +MCU_Texas:LM4F110E5QR +MCU_Texas:LM4F110H5QR +MCU_Texas:LM4F111B2QR +MCU_Texas:LM4F111C4QR +MCU_Texas:LM4F111E5QR +MCU_Texas:LM4F111H5QR +MCU_Texas:MSP432E401Y +MCU_Texas:TM4C1230C3PM +MCU_Texas:TM4C1230D5PM +MCU_Texas:TM4C1230E6PM +MCU_Texas:TM4C1230H6PM +MCU_Texas:TM4C1231C3PM +MCU_Texas:TM4C1231D5PM +MCU_Texas:TM4C1231E6PM +MCU_Texas:TM4C1231H6PM +MCU_Texas:TMS320LF2406 +MCU_Texas:TMS470R1B768 +MCU_Texas_MSP430:CC430F5133xRGZ +MCU_Texas_MSP430:CC430F5135xRGZ +MCU_Texas_MSP430:CC430F5137xRGZ +MCU_Texas_MSP430:MSP430AFE221IPW +MCU_Texas_MSP430:MSP430AFE222IPW +MCU_Texas_MSP430:MSP430AFE223IPW +MCU_Texas_MSP430:MSP430AFE231IPW +MCU_Texas_MSP430:MSP430AFE232IPW +MCU_Texas_MSP430:MSP430AFE233IPW +MCU_Texas_MSP430:MSP430AFE251IPW +MCU_Texas_MSP430:MSP430AFE252IPW +MCU_Texas_MSP430:MSP430AFE253IPW +MCU_Texas_MSP430:MSP430F1101AIDGV +MCU_Texas_MSP430:MSP430F1101AIDW +MCU_Texas_MSP430:MSP430F1101AIPW +MCU_Texas_MSP430:MSP430F1101AIRGE +MCU_Texas_MSP430:MSP430F1111AIDGV +MCU_Texas_MSP430:MSP430F1111AIDW +MCU_Texas_MSP430:MSP430F1111AIPW +MCU_Texas_MSP430:MSP430F1111AIRGE +MCU_Texas_MSP430:MSP430F1121AIDGV +MCU_Texas_MSP430:MSP430F1121AIDW +MCU_Texas_MSP430:MSP430F1121AIPW +MCU_Texas_MSP430:MSP430F1121AIRGE +MCU_Texas_MSP430:MSP430F1122IDW +MCU_Texas_MSP430:MSP430F1122IPW +MCU_Texas_MSP430:MSP430F1122IRHB +MCU_Texas_MSP430:MSP430F1132IDW +MCU_Texas_MSP430:MSP430F1132IPW +MCU_Texas_MSP430:MSP430F1132IRHB +MCU_Texas_MSP430:MSP430F1222IDW +MCU_Texas_MSP430:MSP430F1222IPW +MCU_Texas_MSP430:MSP430F1222IRHB +MCU_Texas_MSP430:MSP430F122IDW +MCU_Texas_MSP430:MSP430F122IPW +MCU_Texas_MSP430:MSP430F122IRHB +MCU_Texas_MSP430:MSP430F1232IDW +MCU_Texas_MSP430:MSP430F1232IPW +MCU_Texas_MSP430:MSP430F1232IRHB +MCU_Texas_MSP430:MSP430F123IDW +MCU_Texas_MSP430:MSP430F123IPW +MCU_Texas_MSP430:MSP430F123IRHB +MCU_Texas_MSP430:MSP430F2001IN +MCU_Texas_MSP430:MSP430F2001IPW +MCU_Texas_MSP430:MSP430F2001IRSA +MCU_Texas_MSP430:MSP430F2002IN +MCU_Texas_MSP430:MSP430F2002IPW +MCU_Texas_MSP430:MSP430F2002IRSA +MCU_Texas_MSP430:MSP430F2003IN +MCU_Texas_MSP430:MSP430F2003IPW +MCU_Texas_MSP430:MSP430F2003IRSA +MCU_Texas_MSP430:MSP430F2011IN +MCU_Texas_MSP430:MSP430F2011IPW +MCU_Texas_MSP430:MSP430F2011IRSA +MCU_Texas_MSP430:MSP430F2012IN +MCU_Texas_MSP430:MSP430F2012IPW +MCU_Texas_MSP430:MSP430F2012IRSA +MCU_Texas_MSP430:MSP430F2013IN +MCU_Texas_MSP430:MSP430F2013IPW +MCU_Texas_MSP430:MSP430F2013IRSA +MCU_Texas_MSP430:MSP430F2101IDGV +MCU_Texas_MSP430:MSP430F2101IDW +MCU_Texas_MSP430:MSP430F2101IPW +MCU_Texas_MSP430:MSP430F2101IRGE +MCU_Texas_MSP430:MSP430F2111IDGV +MCU_Texas_MSP430:MSP430F2111IDW +MCU_Texas_MSP430:MSP430F2111IPW +MCU_Texas_MSP430:MSP430F2111IRGE +MCU_Texas_MSP430:MSP430F2112IPW +MCU_Texas_MSP430:MSP430F2112IRHB +MCU_Texas_MSP430:MSP430F2112IRTV +MCU_Texas_MSP430:MSP430F2121IDGV +MCU_Texas_MSP430:MSP430F2121IDW +MCU_Texas_MSP430:MSP430F2121IPW +MCU_Texas_MSP430:MSP430F2121IRGE +MCU_Texas_MSP430:MSP430F2122IPW +MCU_Texas_MSP430:MSP430F2122IRHB +MCU_Texas_MSP430:MSP430F2122IRTV +MCU_Texas_MSP430:MSP430F2131IDGV +MCU_Texas_MSP430:MSP430F2131IDW +MCU_Texas_MSP430:MSP430F2131IPW +MCU_Texas_MSP430:MSP430F2131IRGE +MCU_Texas_MSP430:MSP430F2132IPW +MCU_Texas_MSP430:MSP430F2132IRHB +MCU_Texas_MSP430:MSP430F2132IRTV +MCU_Texas_MSP430:MSP430F2232IDA +MCU_Texas_MSP430:MSP430F2232IRHA +MCU_Texas_MSP430:MSP430F2232IYFF +MCU_Texas_MSP430:MSP430F2234IDA +MCU_Texas_MSP430:MSP430F2234IRHA +MCU_Texas_MSP430:MSP430F2234IYFF +MCU_Texas_MSP430:MSP430F2252IDA +MCU_Texas_MSP430:MSP430F2252IRHA +MCU_Texas_MSP430:MSP430F2252IYFF +MCU_Texas_MSP430:MSP430F2254IDA +MCU_Texas_MSP430:MSP430F2254IRHA +MCU_Texas_MSP430:MSP430F2254IYFF +MCU_Texas_MSP430:MSP430F2272IDA +MCU_Texas_MSP430:MSP430F2272IRHA +MCU_Texas_MSP430:MSP430F2272IYFF +MCU_Texas_MSP430:MSP430F2274IDA +MCU_Texas_MSP430:MSP430F2274IRHA +MCU_Texas_MSP430:MSP430F2274IYFF +MCU_Texas_MSP430:MSP430F2330IRHA +MCU_Texas_MSP430:MSP430F2330IYFF +MCU_Texas_MSP430:MSP430F2350IRHA +MCU_Texas_MSP430:MSP430F2350IYFF +MCU_Texas_MSP430:MSP430F2370IRHA +MCU_Texas_MSP430:MSP430F2370IYFF +MCU_Texas_MSP430:MSP430F2618-EP +MCU_Texas_MSP430:MSP430F5217IRGC +MCU_Texas_MSP430:MSP430F5217IYFF +MCU_Texas_MSP430:MSP430F5219IRGC +MCU_Texas_MSP430:MSP430F5219IYFF +MCU_Texas_MSP430:MSP430F5227IRGC +MCU_Texas_MSP430:MSP430F5227IYFF +MCU_Texas_MSP430:MSP430F5229IRGC +MCU_Texas_MSP430:MSP430F5229IYFF +MCU_Texas_MSP430:MSP430F5232IRGZ +MCU_Texas_MSP430:MSP430F5234IRGZ +MCU_Texas_MSP430:MSP430F5237IRGC +MCU_Texas_MSP430:MSP430F5239IRGC +MCU_Texas_MSP430:MSP430F5242IRGZ +MCU_Texas_MSP430:MSP430F5244IRGZ +MCU_Texas_MSP430:MSP430F5247IRGC +MCU_Texas_MSP430:MSP430F5249IRGC +MCU_Texas_MSP430:MSP430F5304IPT +MCU_Texas_MSP430:MSP430F5304IRGZ +MCU_Texas_MSP430:MSP430F5308IPT +MCU_Texas_MSP430:MSP430F5308IRGC +MCU_Texas_MSP430:MSP430F5308IRGZ +MCU_Texas_MSP430:MSP430F5308IZQE +MCU_Texas_MSP430:MSP430F5309IPT +MCU_Texas_MSP430:MSP430F5309IRGC +MCU_Texas_MSP430:MSP430F5309IRGZ +MCU_Texas_MSP430:MSP430F5309IZQE +MCU_Texas_MSP430:MSP430F5310IPT +MCU_Texas_MSP430:MSP430F5310IRGC +MCU_Texas_MSP430:MSP430F5310IRGZ +MCU_Texas_MSP430:MSP430F5310IZQE +MCU_Texas_MSP430:MSP430F5333IPZ +MCU_Texas_MSP430:MSP430F5333IZQW +MCU_Texas_MSP430:MSP430F5335IPZ +MCU_Texas_MSP430:MSP430F5335IZQW +MCU_Texas_MSP430:MSP430F5336IPZ +MCU_Texas_MSP430:MSP430F5336IZQW +MCU_Texas_MSP430:MSP430F5338IPZ +MCU_Texas_MSP430:MSP430F5338IZQW +MCU_Texas_MSP430:MSP430F5340IRGZ +MCU_Texas_MSP430:MSP430F5341IRGZ +MCU_Texas_MSP430:MSP430F5342IRGZ +MCU_Texas_MSP430:MSP430F5358IZQW +MCU_Texas_MSP430:MSP430F5359IZQW +MCU_Texas_MSP430:MSP430F5500IRGZ +MCU_Texas_MSP430:MSP430F5501IRGZ +MCU_Texas_MSP430:MSP430F5502IRGZ +MCU_Texas_MSP430:MSP430F5503IRGZ +MCU_Texas_MSP430:MSP430F5504IRGZ +MCU_Texas_MSP430:MSP430F5505IRGZ +MCU_Texas_MSP430:MSP430F5506IRGZ +MCU_Texas_MSP430:MSP430F5507IRGZ +MCU_Texas_MSP430:MSP430F5508IRGZ +MCU_Texas_MSP430:MSP430F5509IRGZ +MCU_Texas_MSP430:MSP430F5510IRGZ +MCU_Texas_MSP430:MSP430F5524IYFF +MCU_Texas_MSP430:MSP430F5526IYFF +MCU_Texas_MSP430:MSP430F5528IYFF +MCU_Texas_MSP430:MSP430F5630IZQW +MCU_Texas_MSP430:MSP430F5631IZQW +MCU_Texas_MSP430:MSP430F5632IZQW +MCU_Texas_MSP430:MSP430F5633IZQW +MCU_Texas_MSP430:MSP430F5634IZQW +MCU_Texas_MSP430:MSP430F5635IZQW +MCU_Texas_MSP430:MSP430F5636IZQW +MCU_Texas_MSP430:MSP430F5637IZQW +MCU_Texas_MSP430:MSP430F5638IZQW +MCU_Texas_MSP430:MSP430F5658IZQW +MCU_Texas_MSP430:MSP430F5659IZQW +MCU_Texas_MSP430:MSP430FR5720IRGE +MCU_Texas_MSP430:MSP430FR5722IRGE +MCU_Texas_MSP430:MSP430FR5724IRGE +MCU_Texas_MSP430:MSP430FR5726IRGE +MCU_Texas_MSP430:MSP430FR5728IRGE +MCU_Texas_MSP430:MSP430FR5730IRGE +MCU_Texas_MSP430:MSP430FR5732IRGE +MCU_Texas_MSP430:MSP430FR5734IRGE +MCU_Texas_MSP430:MSP430FR5736IRGE +MCU_Texas_MSP430:MSP430FR5738IRGE +MCU_Texas_MSP430:MSP430G2001IN14 +MCU_Texas_MSP430:MSP430G2001IPW14 +MCU_Texas_MSP430:MSP430G2001IRSA16 +MCU_Texas_MSP430:MSP430G2101IN14 +MCU_Texas_MSP430:MSP430G2101IPW14 +MCU_Texas_MSP430:MSP430G2101IRSA16 +MCU_Texas_MSP430:MSP430G2102IN20 +MCU_Texas_MSP430:MSP430G2102IPW14 +MCU_Texas_MSP430:MSP430G2102IPW20 +MCU_Texas_MSP430:MSP430G2102IRSA16 +MCU_Texas_MSP430:MSP430G2111IN14 +MCU_Texas_MSP430:MSP430G2111IPW14 +MCU_Texas_MSP430:MSP430G2111IRSA16 +MCU_Texas_MSP430:MSP430G2112IN20 +MCU_Texas_MSP430:MSP430G2112IPW14 +MCU_Texas_MSP430:MSP430G2112IPW20 +MCU_Texas_MSP430:MSP430G2112IRSA16 +MCU_Texas_MSP430:MSP430G2113IPW20 +MCU_Texas_MSP430:MSP430G2121IN14 +MCU_Texas_MSP430:MSP430G2121IPW14 +MCU_Texas_MSP430:MSP430G2121IRSA16 +MCU_Texas_MSP430:MSP430G2131IN14 +MCU_Texas_MSP430:MSP430G2131IPW14 +MCU_Texas_MSP430:MSP430G2131IRSA16 +MCU_Texas_MSP430:MSP430G2132IN20 +MCU_Texas_MSP430:MSP430G2132IPW14 +MCU_Texas_MSP430:MSP430G2132IPW20 +MCU_Texas_MSP430:MSP430G2132IRSA16 +MCU_Texas_MSP430:MSP430G2152IN20 +MCU_Texas_MSP430:MSP430G2152IPW14 +MCU_Texas_MSP430:MSP430G2152IPW20 +MCU_Texas_MSP430:MSP430G2152IRSA16 +MCU_Texas_MSP430:MSP430G2153IN20 +MCU_Texas_MSP430:MSP430G2153IPW20 +MCU_Texas_MSP430:MSP430G2153IPW28 +MCU_Texas_MSP430:MSP430G2153IRHB32 +MCU_Texas_MSP430:MSP430G2201IN14 +MCU_Texas_MSP430:MSP430G2201IPW14 +MCU_Texas_MSP430:MSP430G2201IRSA16 +MCU_Texas_MSP430:MSP430G2202IN20 +MCU_Texas_MSP430:MSP430G2202IPW14 +MCU_Texas_MSP430:MSP430G2202IPW20 +MCU_Texas_MSP430:MSP430G2202IRSA16 +MCU_Texas_MSP430:MSP430G2203IN20 +MCU_Texas_MSP430:MSP430G2203IPW20 +MCU_Texas_MSP430:MSP430G2203IPW28 +MCU_Texas_MSP430:MSP430G2203IRHB32 +MCU_Texas_MSP430:MSP430G2210ID +MCU_Texas_MSP430:MSP430G2211IN14 +MCU_Texas_MSP430:MSP430G2211IPW14 +MCU_Texas_MSP430:MSP430G2211IRSA16 +MCU_Texas_MSP430:MSP430G2212IN20 +MCU_Texas_MSP430:MSP430G2212IPW14 +MCU_Texas_MSP430:MSP430G2212IPW20 +MCU_Texas_MSP430:MSP430G2212IRSA16 +MCU_Texas_MSP430:MSP430G2213IN20 +MCU_Texas_MSP430:MSP430G2213IPW20 +MCU_Texas_MSP430:MSP430G2213IPW28 +MCU_Texas_MSP430:MSP430G2213IRHB32 +MCU_Texas_MSP430:MSP430G2221IN14 +MCU_Texas_MSP430:MSP430G2221IPW14 +MCU_Texas_MSP430:MSP430G2221IRSA16 +MCU_Texas_MSP430:MSP430G2230ID +MCU_Texas_MSP430:MSP430G2231IN14 +MCU_Texas_MSP430:MSP430G2231IPW14 +MCU_Texas_MSP430:MSP430G2231IRSA16 +MCU_Texas_MSP430:MSP430G2232IN20 +MCU_Texas_MSP430:MSP430G2232IPW14 +MCU_Texas_MSP430:MSP430G2232IPW20 +MCU_Texas_MSP430:MSP430G2232IRSA16 +MCU_Texas_MSP430:MSP430G2233IN20 +MCU_Texas_MSP430:MSP430G2233IPW20 +MCU_Texas_MSP430:MSP430G2233IPW28 +MCU_Texas_MSP430:MSP430G2233IRHB32 +MCU_Texas_MSP430:MSP430G2252IN20 +MCU_Texas_MSP430:MSP430G2252IPW14 +MCU_Texas_MSP430:MSP430G2252IPW20 +MCU_Texas_MSP430:MSP430G2252IRSA16 +MCU_Texas_MSP430:MSP430G2253IN20 +MCU_Texas_MSP430:MSP430G2253IPW20 +MCU_Texas_MSP430:MSP430G2253IPW28 +MCU_Texas_MSP430:MSP430G2253IRHB32 +MCU_Texas_MSP430:MSP430G2302IN20 +MCU_Texas_MSP430:MSP430G2302IPW14 +MCU_Texas_MSP430:MSP430G2302IPW20 +MCU_Texas_MSP430:MSP430G2302IRSA16 +MCU_Texas_MSP430:MSP430G2303IN20 +MCU_Texas_MSP430:MSP430G2303IPW20 +MCU_Texas_MSP430:MSP430G2303IPW28 +MCU_Texas_MSP430:MSP430G2303IRHB32 +MCU_Texas_MSP430:MSP430G2312IN20 +MCU_Texas_MSP430:MSP430G2312IPW14 +MCU_Texas_MSP430:MSP430G2312IPW20 +MCU_Texas_MSP430:MSP430G2312IRSA16 +MCU_Texas_MSP430:MSP430G2313IN20 +MCU_Texas_MSP430:MSP430G2313IPW20 +MCU_Texas_MSP430:MSP430G2313IPW28 +MCU_Texas_MSP430:MSP430G2313IRHB32 +MCU_Texas_MSP430:MSP430G2332IN20 +MCU_Texas_MSP430:MSP430G2332IPW14 +MCU_Texas_MSP430:MSP430G2332IPW20 +MCU_Texas_MSP430:MSP430G2332IRSA16 +MCU_Texas_MSP430:MSP430G2333IN20 +MCU_Texas_MSP430:MSP430G2333IPW20 +MCU_Texas_MSP430:MSP430G2333IPW28 +MCU_Texas_MSP430:MSP430G2333IRHB32 +MCU_Texas_MSP430:MSP430G2352IN20 +MCU_Texas_MSP430:MSP430G2352IPW14 +MCU_Texas_MSP430:MSP430G2352IPW20 +MCU_Texas_MSP430:MSP430G2352IRSA16 +MCU_Texas_MSP430:MSP430G2353IN20 +MCU_Texas_MSP430:MSP430G2353IPW20 +MCU_Texas_MSP430:MSP430G2353IPW28 +MCU_Texas_MSP430:MSP430G2353IRHB32 +MCU_Texas_MSP430:MSP430G2402IN20 +MCU_Texas_MSP430:MSP430G2402IPW14 +MCU_Texas_MSP430:MSP430G2402IPW20 +MCU_Texas_MSP430:MSP430G2402IRSA16 +MCU_Texas_MSP430:MSP430G2403IN20 +MCU_Texas_MSP430:MSP430G2403IPW20 +MCU_Texas_MSP430:MSP430G2403IPW28 +MCU_Texas_MSP430:MSP430G2403IRHB32 +MCU_Texas_MSP430:MSP430G2412IN20 +MCU_Texas_MSP430:MSP430G2412IPW14 +MCU_Texas_MSP430:MSP430G2412IPW20 +MCU_Texas_MSP430:MSP430G2412IRSA16 +MCU_Texas_MSP430:MSP430G2413IN20 +MCU_Texas_MSP430:MSP430G2413IPW20 +MCU_Texas_MSP430:MSP430G2413IPW28 +MCU_Texas_MSP430:MSP430G2413IRHB32 +MCU_Texas_MSP430:MSP430G2432IN20 +MCU_Texas_MSP430:MSP430G2432IPW14 +MCU_Texas_MSP430:MSP430G2432IPW20 +MCU_Texas_MSP430:MSP430G2432IRSA16 +MCU_Texas_MSP430:MSP430G2433IN20 +MCU_Texas_MSP430:MSP430G2433IPW20 +MCU_Texas_MSP430:MSP430G2433IPW28 +MCU_Texas_MSP430:MSP430G2433IRHB32 +MCU_Texas_MSP430:MSP430G2444IDA38 +MCU_Texas_MSP430:MSP430G2444IRHA40 +MCU_Texas_MSP430:MSP430G2444IYFF +MCU_Texas_MSP430:MSP430G2452IN20 +MCU_Texas_MSP430:MSP430G2452IPW14 +MCU_Texas_MSP430:MSP430G2452IPW20 +MCU_Texas_MSP430:MSP430G2452IRSA16 +MCU_Texas_MSP430:MSP430G2453IN20 +MCU_Texas_MSP430:MSP430G2453IPW20 +MCU_Texas_MSP430:MSP430G2453IPW28 +MCU_Texas_MSP430:MSP430G2453IRHB32 +MCU_Texas_MSP430:MSP430G2513IN20 +MCU_Texas_MSP430:MSP430G2513IPW20 +MCU_Texas_MSP430:MSP430G2513IPW28 +MCU_Texas_MSP430:MSP430G2513IRHB32 +MCU_Texas_MSP430:MSP430G2533IN20 +MCU_Texas_MSP430:MSP430G2533IPW20 +MCU_Texas_MSP430:MSP430G2533IPW28 +MCU_Texas_MSP430:MSP430G2533IRHB32 +MCU_Texas_MSP430:MSP430G2544IDA38 +MCU_Texas_MSP430:MSP430G2544IRHA40 +MCU_Texas_MSP430:MSP430G2544IYFF +MCU_Texas_MSP430:MSP430G2553IN20 +MCU_Texas_MSP430:MSP430G2553IPW20 +MCU_Texas_MSP430:MSP430G2553IPW28 +MCU_Texas_MSP430:MSP430G2553IRHB32 +MCU_Texas_MSP430:MSP430G2744IDA38 +MCU_Texas_MSP430:MSP430G2744IRHA40 +MCU_Texas_MSP430:MSP430G2744IYFF +MCU_Texas_MSP430:MSP430G2755IDA38 +MCU_Texas_MSP430:MSP430G2755IRHA40 +MCU_Texas_MSP430:MSP430G2855IDA38 +MCU_Texas_MSP430:MSP430G2855IRHA40 +MCU_Texas_MSP430:MSP430G2955IDA38 +MCU_Texas_MSP430:MSP430G2955IRHA40 +MCU_Texas_SimpleLink:CC1312R1F3RGZ +MCU_WCH_CH32V0:CH32V003AxMx +MCU_WCH_CH32V0:CH32V003FxPx +MCU_WCH_CH32V0:CH32V003FxUx +MCU_WCH_CH32V0:CH32V003JxMx +MCU_WCH_CH32V2:CH32V203CxTx +MCU_WCH_CH32V2:CH32V203F6P6 +MCU_WCH_CH32V2:CH32V203GxUx +MCU_WCH_CH32V3:CH32V30xCxTx +MCU_WCH_CH32V3:CH32V30xFxPx +MCU_WCH_CH32V3:CH32V30xRxTx +MCU_WCH_CH32V3:CH32V30xVxTx +MCU_WCH_CH32V3:CH32V30xWxUx +MCU_WCH_CH32X0:CH32X035G8U6 +Mechanical:DIN_Rail_Adapter +Mechanical:Fiducial +Mechanical:Heatsink +Mechanical:Heatsink_Pad +Mechanical:Heatsink_Pad_2Pin +Mechanical:Heatsink_Pad_3Pin +Mechanical:Housing +Mechanical:Housing_Pad +Mechanical:MountingHole +Mechanical:MountingHole_Pad +Mechanical:MountingHole_Pad_MP +Memory_EEPROM:24AA02-OT +Memory_EEPROM:24AA025E-OT +Memory_EEPROM:24AA025E-SN +Memory_EEPROM:24AA02E-OT +Memory_EEPROM:24AA02E-SN +Memory_EEPROM:24LC00 +Memory_EEPROM:24LC01 +Memory_EEPROM:24LC02 +Memory_EEPROM:24LC04 +Memory_EEPROM:24LC08 +Memory_EEPROM:24LC1025 +Memory_EEPROM:24LC128 +Memory_EEPROM:24LC16 +Memory_EEPROM:24LC256 +Memory_EEPROM:24LC32 +Memory_EEPROM:24LC512 +Memory_EEPROM:24LC64 +Memory_EEPROM:25CSM04xxMF +Memory_EEPROM:25CSM04xxSN +Memory_EEPROM:25LCxxx +Memory_EEPROM:25LCxxx-MC +Memory_EEPROM:25LCxxx-MF +Memory_EEPROM:28C256 +Memory_EEPROM:93AAxxA +Memory_EEPROM:93AAxxAT-xOT +Memory_EEPROM:93AAxxB +Memory_EEPROM:93AAxxBT-xOT +Memory_EEPROM:93AAxxC +Memory_EEPROM:93CxxA +Memory_EEPROM:93CxxB +Memory_EEPROM:93CxxC +Memory_EEPROM:93LCxxA +Memory_EEPROM:93LCxxAxxOT +Memory_EEPROM:93LCxxB +Memory_EEPROM:93LCxxBxxOT +Memory_EEPROM:93LCxxC +Memory_EEPROM:AT24CS01-MAHM +Memory_EEPROM:AT24CS01-SSHM +Memory_EEPROM:AT24CS01-STUM +Memory_EEPROM:AT24CS01-XHM +Memory_EEPROM:AT24CS02-MAHM +Memory_EEPROM:AT24CS02-SSHM +Memory_EEPROM:AT24CS02-STUM +Memory_EEPROM:AT24CS02-XHM +Memory_EEPROM:AT24CS04-MAHM +Memory_EEPROM:AT24CS04-SSHM +Memory_EEPROM:AT24CS04-STUM +Memory_EEPROM:AT24CS04-XHM +Memory_EEPROM:AT24CS08-MAHM +Memory_EEPROM:AT24CS08-SSHM +Memory_EEPROM:AT24CS08-STUM +Memory_EEPROM:AT24CS08-XHM +Memory_EEPROM:AT24CS16-MAHM +Memory_EEPROM:AT24CS16-SSHM +Memory_EEPROM:AT24CS16-STUM +Memory_EEPROM:AT24CS16-XHM +Memory_EEPROM:AT24CS32-MAHM +Memory_EEPROM:AT24CS32-SSHM +Memory_EEPROM:AT24CS32-STUM +Memory_EEPROM:AT24CS32-XHM +Memory_EEPROM:AT24CS64-MAHM +Memory_EEPROM:AT24CS64-SSHM +Memory_EEPROM:AT24CS64-XHM +Memory_EEPROM:AT25xxx +Memory_EEPROM:AT25xxx-MA +Memory_EEPROM:BR25Sxxx +Memory_EEPROM:BR25xxx-NUX +Memory_EEPROM:CAT24C128 +Memory_EEPROM:CAT24C256 +Memory_EEPROM:CAT24M01L +Memory_EEPROM:CAT24M01W +Memory_EEPROM:CAT24M01X +Memory_EEPROM:CAT24M01Y +Memory_EEPROM:CAT250xxx +Memory_EEPROM:CAT250xxx-HU4 +Memory_EEPROM:DS2431 +Memory_EEPROM:DS2431P +Memory_EEPROM:DS2431Q +Memory_EEPROM:DS28E07 +Memory_EEPROM:DS28E07P +Memory_EEPROM:DS28E07Q +Memory_EEPROM:KM28C64A +Memory_EEPROM:KM28C65A +Memory_EEPROM:M24C01-FDW +Memory_EEPROM:M24C01-FMN +Memory_EEPROM:M24C01-RDW +Memory_EEPROM:M24C01-RMN +Memory_EEPROM:M24C01-WDW +Memory_EEPROM:M24C01-WMN +Memory_EEPROM:M24C02-FDW +Memory_EEPROM:M24C02-FMN +Memory_EEPROM:M24C02-RDW +Memory_EEPROM:M24C02-RMN +Memory_EEPROM:M24C02-WDW +Memory_EEPROM:M24C02-WMN +Memory_EEPROM:M95256-WMN6P +Memory_EEPROM:M95512-Axxx-MF +Memory_EEPROM:TMS4C1050N +Memory_EPROM:27128 +Memory_EPROM:27256 +Memory_EPROM:27512 +Memory_EPROM:2764 +Memory_EPROM:27C010 +Memory_EPROM:27C020 +Memory_EPROM:27C040 +Memory_EPROM:27C080 +Memory_EPROM:27C128 +Memory_EPROM:27C256 +Memory_EPROM:27C512 +Memory_EPROM:27C512PLCC +Memory_EPROM:27C64 +Memory_Flash:28F400 +Memory_Flash:29F010-TSOP-SP +Memory_Flash:29W040 +Memory_Flash:AM29F400BB-90SC +Memory_Flash:AM29F400Bx-xxEx +Memory_Flash:AM29F400Bx-xxSx +Memory_Flash:AM29PDL128G +Memory_Flash:AT25DF041x-UxN-x +Memory_Flash:AT25SF081-SSHD-X +Memory_Flash:AT25SF081-SSHF-X +Memory_Flash:AT25SF081-XMHD-X +Memory_Flash:AT25SF081-XMHF-X +Memory_Flash:AT25SL321-U +Memory_Flash:AT45DB161-JC +Memory_Flash:AT45DB161-RC +Memory_Flash:AT45DB161-TC +Memory_Flash:AT45DB161B-RC +Memory_Flash:AT45DB161B-RC-2.5 +Memory_Flash:AT45DB161B-TC +Memory_Flash:AT45DB161B-TC-2.5 +Memory_Flash:AT45DB161D-SU +Memory_Flash:GD25D05CT +Memory_Flash:GD25D10CT +Memory_Flash:GD25QxxxEY +Memory_Flash:IS25WP256D-xM +Memory_Flash:M25PX32-VMP +Memory_Flash:M25PX32-VMW +Memory_Flash:M29W004 +Memory_Flash:M29W008 +Memory_Flash:MT25QUxxxxxx1xW7 +Memory_Flash:MX25L3233FM +Memory_Flash:MX25L3233FM1 +Memory_Flash:MX25L3233FM2 +Memory_Flash:MX25L3233FZN +Memory_Flash:MX25R3235FM1xx0 +Memory_Flash:MX25R3235FM1xx1 +Memory_Flash:MX25R3235FM2xx0 +Memory_Flash:MX25R3235FM2xx1 +Memory_Flash:MX25R3235FZNxx0 +Memory_Flash:MX25R3235FZNxx1 +Memory_Flash:SST25VF080B-50-4x-S2Ax +Memory_Flash:SST39SF010 +Memory_Flash:SST39SF020 +Memory_Flash:SST39SF040 +Memory_Flash:W25Q128JVE +Memory_Flash:W25Q128JVP +Memory_Flash:W25Q128JVS +Memory_Flash:W25Q16JVSS +Memory_Flash:W25Q32JVSS +Memory_Flash:W25Q32JVZP +Memory_Flash:W25X20CLSN +Memory_Flash:W25X20CLZP +Memory_Flash:W25X40CLSN +Memory_Flash:W25X40CLSS +Memory_Flash:W25X40CLSV +Memory_Flash:XTSD01G +Memory_Flash:XTSD02G +Memory_Flash:XTSD04G +Memory_Flash:XTSD08G +Memory_NVRAM:47C04 +Memory_NVRAM:47C16 +Memory_NVRAM:47L04 +Memory_NVRAM:47L16 +Memory_NVRAM:CY14B256LA-SP +Memory_NVRAM:CY14B256LA-SZ +Memory_NVRAM:CY14B256LA-ZS +Memory_NVRAM:CY14E256LA-SZ +Memory_NVRAM:CY14E256LA-ZS +Memory_NVRAM:CY14U256LA-BA +Memory_NVRAM:CY14V256LA-BA +Memory_NVRAM:FM1608B-SG +Memory_NVRAM:FM16W08-SG +Memory_NVRAM:FM1808B-SG +Memory_NVRAM:FM18W08-SG +Memory_NVRAM:FM24C64B +Memory_NVRAM:FM24C64C +Memory_NVRAM:FM24CL16B +Memory_NVRAM:MB85RS128B +Memory_NVRAM:MB85RS16 +Memory_NVRAM:MB85RS1MT +Memory_NVRAM:MB85RS256B +Memory_NVRAM:MB85RS2MT +Memory_NVRAM:MB85RS512T +Memory_NVRAM:MB85RS64 +Memory_NVRAM:MR20H40 +Memory_NVRAM:MR25H40 +Memory_NVRAM:STK14C88 +Memory_NVRAM:STK14C88-3 +Memory_NVRAM:STK14C88C +Memory_NVRAM:STK14C88C-3 +Memory_RAM:AS4C256M16D3 +Memory_RAM:AS4C4M16SA +Memory_RAM:AS6C1008-xxB +Memory_RAM:AS6C1008-xxP +Memory_RAM:AS6C1008-xxS +Memory_RAM:AS6C1008-xxST +Memory_RAM:AS6C1008-xxT +Memory_RAM:AS6C1616 +Memory_RAM:AS6C4008-55PCN +Memory_RAM:AS7C1024B-xxJ +Memory_RAM:AS7C1024B-xxT +Memory_RAM:AS7C1024B-xxTJ +Memory_RAM:AS7C31024B-xxJ +Memory_RAM:AS7C31024B-xxST +Memory_RAM:AS7C31024B-xxT +Memory_RAM:AS7C31024B-xxTJ +Memory_RAM:CY62128EV30xx-xxS +Memory_RAM:CY62128EV30xx-xxZ +Memory_RAM:CY62128Exx-xxS +Memory_RAM:CY62128Exx-xxZ +Memory_RAM:CY62256-70PC +Memory_RAM:CY7C199 +Memory_RAM:ESP-PSRAM32 +Memory_RAM:H5AN8G8NAFR-UHC +Memory_RAM:HM62256BLP +Memory_RAM:HM628128D_DIP32_SOP32 +Memory_RAM:HM628128D_TSOP32 +Memory_RAM:HM628128_DIP32_SOP32 +Memory_RAM:HM628128_TSOP32 +Memory_RAM:HY6264AxJ +Memory_RAM:HY6264AxP +Memory_RAM:IDT7006PF +Memory_RAM:IDT7027_TQ100 +Memory_RAM:IDT7132 +Memory_RAM:IDT71V65903S +Memory_RAM:IDT7201 +Memory_RAM:IDT7202 +Memory_RAM:IDT7203 +Memory_RAM:IDT7204 +Memory_RAM:IDT7205 +Memory_RAM:IDT7206 +Memory_RAM:IDT7207 +Memory_RAM:IDT7208 +Memory_RAM:IS42S16400J-xC +Memory_RAM:IS42S16400J-xT +Memory_RAM:IS43LQ32256A-062BLI +Memory_RAM:IS43LQ32256AL-062BLI +Memory_RAM:IS61C5128AL-10KLI +Memory_RAM:IS61C5128AL-10TLI +Memory_RAM:IS61C5128AS-25HLI +Memory_RAM:IS61C5128AS-25QLI +Memory_RAM:IS61C5128AS-25TLI +Memory_RAM:IS62C256AL +Memory_RAM:IS64C5128AL-12CTLA3 +Memory_RAM:IS64C5128AL-12KLA3 +Memory_RAM:IS65C256AL +Memory_RAM:IS6xC1024AL-xxH +Memory_RAM:IS6xC1024AL-xxJ +Memory_RAM:IS6xC1024AL-xxK +Memory_RAM:IS6xC1024AL-xxT +Memory_RAM:KM62256CLP +Memory_RAM:M48Tx2 +Memory_RAM:M48Zx2 +Memory_RAM:MK4116N +Memory_RAM:MK4164N +Memory_RAM:MT48LC16M16A2P +Memory_RAM:MT48LC16M16A2TG +Memory_RAM:MT48LC32M8A2P +Memory_RAM:MT48LC32M8A2TG +Memory_RAM:MT48LC64M4A2P +Memory_RAM:MT48LC64M4A2TG +Memory_RAM:R1LP0108ESF +Memory_RAM:R1LP0108ESN +Memory_RAM:W9812G6KH-5 +Memory_RAM:W9812G6KH-6 +Memory_RAM:W9812G6KH-6I +Memory_RAM:W9812G6KH-75 +Memory_ROM:XC18V01SO20 +Memory_ROM:XCF08P +Memory_UniqueID:DS2401P +Memory_UniqueID:DS2401Z +Motor:Fan +Motor:Fan_3pin +Motor:Fan_4pin +Motor:Fan_ALT +Motor:Fan_CPU_4pin +Motor:Fan_IEC-60617 +Motor:Fan_ISO-14617 +Motor:Fan_PC_Chassis +Motor:Fan_Tacho +Motor:Fan_Tacho_PWM +Motor:Motor_AC +Motor:Motor_DC +Motor:Motor_DC_ALT +Motor:Motor_Servo +Motor:Motor_Servo_AirTronics +Motor:Motor_Servo_Futaba_J +Motor:Motor_Servo_Grapner_JR +Motor:Motor_Servo_Hitec +Motor:Motor_Servo_JR +Motor:Motor_Servo_Robbe +Motor:Stepper_Motor_bipolar +Motor:Stepper_Motor_unipolar_5pin +Motor:Stepper_Motor_unipolar_6pin +Oscillator:5P49V6965 +Oscillator:ABLNO +Oscillator:ACO-xxxMHz +Oscillator:ACO-xxxMHz-A +Oscillator:ASCO +Oscillator:ASDMB-xxxMHz +Oscillator:ASE-xxxMHz +Oscillator:ASV-xxxMHz +Oscillator:CFPS-72 +Oscillator:CVCO55xx +Oscillator:CXO_DIP14 +Oscillator:CXO_DIP8 +Oscillator:DFA-S11 +Oscillator:DFA-S15 +Oscillator:DFA-S2 +Oscillator:DFA-S3 +Oscillator:DGOF5S3 +Oscillator:ECS-2520MV-xxx-xx +Oscillator:FT5HN +Oscillator:FT5HV +Oscillator:GTXO-14T +Oscillator:GTXO-14V +Oscillator:GTXO-S14T +Oscillator:GTXO-S14V +Oscillator:IQXO-70 +Oscillator:JTOS-25 +Oscillator:JTOS-50 +Oscillator:KC2520Z +Oscillator:KT2520K-T +Oscillator:LTC6905xS5-100 +Oscillator:LTC6905xS5-133 +Oscillator:LTC6905xS5-80 +Oscillator:LTC6905xS5-96 +Oscillator:MAX7375AXR105 +Oscillator:MAX7375AXR185 +Oscillator:MAX7375AXR365 +Oscillator:MAX7375AXR375 +Oscillator:MAX7375AXR405 +Oscillator:MAX7375AXR425 +Oscillator:MAX7375AXR805 +Oscillator:MV267 +Oscillator:MV317 +Oscillator:NB3N502 +Oscillator:NB3N511 +Oscillator:OCXO-14 +Oscillator:OH300 +Oscillator:SG-210SCD +Oscillator:SG-210SDD +Oscillator:SG-210SED +Oscillator:SG-210STF +Oscillator:SG-211 +Oscillator:SG-3030CM +Oscillator:SG-5032CAN +Oscillator:SG-5032CBN +Oscillator:SG-5032CCN +Oscillator:SG-51 +Oscillator:SG-531 +Oscillator:SG-615 +Oscillator:SG-7050CAN +Oscillator:SG-7050CBN +Oscillator:SG-7050CCN +Oscillator:SG-8002CA +Oscillator:SG-8002CE +Oscillator:SG-8002DB +Oscillator:SG-8002DC +Oscillator:SG-8002JA +Oscillator:SG-8002JC +Oscillator:SG-8002LB +Oscillator:Si512A_2.5x3.2mm +Oscillator:Si513A_2.5x3.2mm +Oscillator:Si5351A-B-GM +Oscillator:Si5351A-B-GT +Oscillator:Si5351B-B-GM +Oscillator:Si5351C-B-GM +Oscillator:Si570 +Oscillator:Si571 +Oscillator:SiT8008xx-1x-xxE +Oscillator:SiT8008xx-1x-xxN +Oscillator:SiT8008xx-1x-xxS +Oscillator:SiT8008xx-2x-xxE +Oscillator:SiT8008xx-2x-xxN +Oscillator:SiT8008xx-2x-xxS +Oscillator:SiT8008xx-3x-xxE +Oscillator:SiT8008xx-3x-xxN +Oscillator:SiT8008xx-3x-xxS +Oscillator:SiT8008xx-7x-xxE +Oscillator:SiT8008xx-7x-xxN +Oscillator:SiT8008xx-7x-xxS +Oscillator:SiT8008xx-8x-xxE +Oscillator:SiT8008xx-8x-xxN +Oscillator:SiT8008xx-8x-xxS +Oscillator:SiT9365xx-xBx-xxE +Oscillator:SiT9365xx-xBx-xxN +Oscillator:SiT9366xx-xBx-xxE +Oscillator:SiT9366xx-xBx-xxN +Oscillator:SiT9367xx-xBx-xxE +Oscillator:SiT9367xx-xBx-xxN +Oscillator:TCXO-14 +Oscillator:TCXO3 +Oscillator:TFT660 +Oscillator:TFT680 +Oscillator:TG2520SMN-xx.xxxxxxMhz-xxxxNM +Oscillator:TXC-7C +Oscillator:VC-81 +Oscillator:VC-83 +Oscillator:VTCXO-14 +Oscillator:XO32 +Oscillator:XO53 +Oscillator:XO91 +Oscillator:XUX51 +Oscillator:XUX52 +Oscillator:XUX53 +Oscillator:XUX71 +Oscillator:XUX72 +Oscillator:XUX73 +Oscillator:XUY51 +Oscillator:XUY52 +Oscillator:XUY53 +Oscillator:XUY71 +Oscillator:XUY72 +Oscillator:XUY73 +Potentiometer_Digital:AD5253 +Potentiometer_Digital:AD5254 +Potentiometer_Digital:AD5272BCP +Potentiometer_Digital:AD5272BRM +Potentiometer_Digital:AD5274BCP +Potentiometer_Digital:AD5274BRM +Potentiometer_Digital:AD5280 +Potentiometer_Digital:AD5282 +Potentiometer_Digital:AD5290 +Potentiometer_Digital:AD5293 +Potentiometer_Digital:DS1267_DIP +Potentiometer_Digital:DS1267_SOIC +Potentiometer_Digital:DS1267_TSSOP +Potentiometer_Digital:DS1882E +Potentiometer_Digital:MAX5436 +Potentiometer_Digital:MAX5438 +Potentiometer_Digital:MCP4011-xxxxMS +Potentiometer_Digital:MCP4011-xxxxSN +Potentiometer_Digital:MCP4012-xxxxCH +Potentiometer_Digital:MCP4013-xxxxCH +Potentiometer_Digital:MCP4014-xxxxOT +Potentiometer_Digital:MCP4017-xxxxLT +Potentiometer_Digital:MCP4018-xxxxLT +Potentiometer_Digital:MCP4019-xxxxLT +Potentiometer_Digital:MCP4021-xxxxMS +Potentiometer_Digital:MCP4021-xxxxSN +Potentiometer_Digital:MCP4022-xxxxCH +Potentiometer_Digital:MCP4023-xxxxCH +Potentiometer_Digital:MCP4024-xxxxOT +Potentiometer_Digital:MCP41010 +Potentiometer_Digital:MCP41050 +Potentiometer_Digital:MCP41100 +Potentiometer_Digital:MCP4131-xxxx-P +Potentiometer_Digital:MCP4132-xxxx-P +Potentiometer_Digital:MCP4141-xxxx-P +Potentiometer_Digital:MCP4142-xxxx-P +Potentiometer_Digital:MCP4151-xxxx-P +Potentiometer_Digital:MCP4152-xxxx-P +Potentiometer_Digital:MCP4161-xxxx-P +Potentiometer_Digital:MCP4162-xxxx-P +Potentiometer_Digital:MCP42010 +Potentiometer_Digital:MCP42050 +Potentiometer_Digital:MCP42100 +Potentiometer_Digital:MCP4251-xxxx-ML +Potentiometer_Digital:MCP4251-xxxx-P +Potentiometer_Digital:MCP4251-xxxx-SL +Potentiometer_Digital:MCP4251-xxxx-ST +Potentiometer_Digital:MCP4431-xxxx-ST +Potentiometer_Digital:MCP4441-xxxx-ST +Potentiometer_Digital:MCP4451-xxxx-ST +Potentiometer_Digital:MCP4461-xxxx-ST +Potentiometer_Digital:MCP45HV31-MQ +Potentiometer_Digital:MCP45HV31-ST +Potentiometer_Digital:MCP45HV51-MQ +Potentiometer_Digital:MCP45HV51-ST +Potentiometer_Digital:TPL0401A-10-Q1 +Potentiometer_Digital:TPL0401B-10-Q1 +Potentiometer_Digital:X9118 +Potentiometer_Digital:X9250 +Potentiometer_Digital:X9258 +power:+10V +power:+12C +power:+12L +power:+12LF +power:+12P +power:+12V +power:+12VA +power:+15V +power:+1V0 +power:+1V1 +power:+1V2 +power:+1V35 +power:+1V5 +power:+1V8 +power:+24V +power:+28V +power:+2V5 +power:+2V8 +power:+3.3V +power:+3.3VA +power:+3.3VADC +power:+3.3VDAC +power:+3.3VP +power:+36V +power:+3V0 +power:+3V3 +power:+3V8 +power:+48V +power:+4V +power:+5C +power:+5F +power:+5P +power:+5V +power:+5VA +power:+5VD +power:+5VL +power:+5VP +power:+6V +power:+7.5V +power:+8V +power:+9V +power:+9VA +power:+BATT +power:+VDC +power:+VSW +power:-10V +power:-12V +power:-12VA +power:-15V +power:-24V +power:-2V5 +power:-36V +power:-3V3 +power:-48V +power:-5V +power:-5VA +power:-6V +power:-8V +power:-9V +power:-9VA +power:-BATT +power:-VDC +power:-VSW +power:AC +power:Earth +power:Earth_Clean +power:Earth_Protective +power:GND +power:GND1 +power:GND2 +power:GND3 +power:GNDA +power:GNDD +power:GNDPWR +power:GNDREF +power:GNDS +power:HT +power:LINE +power:NEUT +power:PRI_HI +power:PRI_LO +power:PRI_MID +power:PWR_FLAG +power:VAA +power:VAC +power:VBUS +power:VCC +power:VCCQ +power:VCOM +power:VD +power:VDC +power:VDD +power:VDDA +power:VDDF +power:VEE +power:VMEM +power:VPP +power:VS +power:VSS +power:VSSA +power:Vdrive +Power_Management:AAT4610BIGV-1-T1 +Power_Management:AAT4610BIGV-T1 +Power_Management:AAT4616IGV-1-T1 +Power_Management:AAT4616IGV-T1 +Power_Management:ADM1270ACPZ +Power_Management:ADM1270ARQZ +Power_Management:AP2161W +Power_Management:AP2171W +Power_Management:AP22804AW5 +Power_Management:AP22804BW5 +Power_Management:AP22814AW5 +Power_Management:AP22814BW5 +Power_Management:AP22816AKEWT +Power_Management:AP22816BKEWT +Power_Management:AP22817AKEWT +Power_Management:AP22817BKEWT +Power_Management:AP22818AKEWT +Power_Management:AP22818BKEWT +Power_Management:AP22913CN4 +Power_Management:AUIPS1041R +Power_Management:AUIPS1042G +Power_Management:AUIPS1051L +Power_Management:AUIPS1052G +Power_Management:AUIPS2031R +Power_Management:AUIPS2041L +Power_Management:AUIPS2051L +Power_Management:AUIPS2052G +Power_Management:AUIPS6011R +Power_Management:AUIPS6031R +Power_Management:AUIPS6041G +Power_Management:AUIPS6044G +Power_Management:AUIPS7081R +Power_Management:AUIPS7081S +Power_Management:AUIPS7091G +Power_Management:AUIPS7111S +Power_Management:AUIPS7121R +Power_Management:AUIPS7125R +Power_Management:AUIPS7141R +Power_Management:AUIPS7142G +Power_Management:AUIPS71451G +Power_Management:AUIPS7145R +Power_Management:AUIPS72211R +Power_Management:AUIPS7221R +Power_Management:AUIR3313S +Power_Management:AUIR3314S +Power_Management:AUIR3315S +Power_Management:AUIR3316S +Power_Management:AUIR3320S +Power_Management:AUIR33402S +Power_Management:BD2222G +Power_Management:BD2242G +Power_Management:BD2243G +Power_Management:BD48ExxG +Power_Management:BD48KxxG +Power_Management:BD48LxxG +Power_Management:BD48xxFVE +Power_Management:BD49ExxG +Power_Management:BD49KxxG +Power_Management:BD49LxxG +Power_Management:BD49xxFVE +Power_Management:BQ24230RGT +Power_Management:BTN8982TA +Power_Management:BTS40K2-1EJC +Power_Management:BTS443P +Power_Management:BTS462TATMA1 +Power_Management:BTS50010-1TAD +Power_Management:BTS50055-1TMA +Power_Management:BTS50055-1TMC +Power_Management:BTS50080-1TEA +Power_Management:BTS50080-1TEB +Power_Management:BTS50080-1TMA +Power_Management:BTS50080-1TMC +Power_Management:BTS50085-1TMA +Power_Management:BTS5012SDA +Power_Management:BTS5014SDA +Power_Management:BTS5016SDA +Power_Management:BTS5030-1EJA +Power_Management:BTS5045-1EJA +Power_Management:BTS5090-1EJA +Power_Management:BTS5200-1EJA +Power_Management:BTS6133D +Power_Management:BTS6142D +Power_Management:BTS6143D +Power_Management:BTS6163D +Power_Management:BTS6200-1EJA +Power_Management:BTS7004-1EPP +Power_Management:BTS711L1 +Power_Management:BTS712N1 +Power_Management:BTS716G +Power_Management:BTS716GB +Power_Management:BTS721L1 +Power_Management:BTS724G +Power_Management:CAP002DG +Power_Management:CAP003DG +Power_Management:CAP004DG +Power_Management:CAP005DG +Power_Management:CAP006DG +Power_Management:CAP007DG +Power_Management:CAP008DG +Power_Management:CAP009DG +Power_Management:CAP012DG +Power_Management:CAP013DG +Power_Management:CAP014DG +Power_Management:CAP015DG +Power_Management:CAP016DG +Power_Management:CAP017DG +Power_Management:CAP018DG +Power_Management:CAP019DG +Power_Management:CAP200DG +Power_Management:CAP300DG +Power_Management:DS1210 +Power_Management:EPC23102 +Power_Management:EPC23103 +Power_Management:EPC23104 +Power_Management:FPF2000 +Power_Management:FPF2001 +Power_Management:FPF2002 +Power_Management:FPF2003 +Power_Management:FPF2004 +Power_Management:FPF2005 +Power_Management:FPF2006 +Power_Management:FPF2007 +Power_Management:HF81 +Power_Management:INA3221 +Power_Management:IPS6011PBF +Power_Management:IPS6011RPBF +Power_Management:IPS6011SPBF +Power_Management:IPS6021PBF +Power_Management:IPS6021RPBF +Power_Management:IPS6021SPBF +Power_Management:IPS6031PBF +Power_Management:IPS6031RPBF +Power_Management:IPS6031SPBF +Power_Management:IPS6041GPBF +Power_Management:IPS6041PBF +Power_Management:IPS6041RPBF +Power_Management:IPS6041SPBF +Power_Management:IPS7091GPBF +Power_Management:IPS7091PBF +Power_Management:IPS7091SPBF +Power_Management:IRS25751L +Power_Management:ITS5215 +Power_Management:LM5050-1 +Power_Management:LM5050-2 +Power_Management:LM5051 +Power_Management:LM5060 +Power_Management:LM50672NPAR +Power_Management:LM5067MM-1 +Power_Management:LM5067MM-2 +Power_Management:LM5067MMX-2 +Power_Management:LM5067MWX-1 +Power_Management:LM66100DCK +Power_Management:LM74700 +Power_Management:LMG3410 +Power_Management:LMG5200 +Power_Management:LT1641-1 +Power_Management:LT1641-2 +Power_Management:LT4230xDD +Power_Management:LT4231xUF +Power_Management:LT4320xDD-1 +Power_Management:LTC4242xUHF +Power_Management:LTC4357DCB +Power_Management:LTC4357MS8 +Power_Management:LTC4359-DCB +Power_Management:LTC4359-MS8 +Power_Management:LTC4364CDE +Power_Management:LTC4364CMS +Power_Management:LTC4364CS +Power_Management:LTC4364HDE +Power_Management:LTC4364HMS +Power_Management:LTC4364HS +Power_Management:LTC4364IDE +Power_Management:LTC4364IMS +Power_Management:LTC4364IS +Power_Management:LTC4365DDB +Power_Management:LTC4365DDB-1 +Power_Management:LTC4365TS8 +Power_Management:LTC4365TS8-1 +Power_Management:LTC4365xTS8 +Power_Management:LTC4370xDE +Power_Management:LTC4370xMS +Power_Management:LTC4412xS6 +Power_Management:LTC4417CGN +Power_Management:LTC4417CUF +Power_Management:LTC4417HGN +Power_Management:LTC4417HUF +Power_Management:LTC4417IGN +Power_Management:LTC4417IUF +Power_Management:MAX14919xUP +Power_Management:MAX8586 +Power_Management:MAX9611 +Power_Management:MAX9612 +Power_Management:MIC2007YM6 +Power_Management:MIC2008YM6 +Power_Management:MIC2017YM6 +Power_Management:MIC2018YM6 +Power_Management:MIC2025-1YM +Power_Management:MIC2025-1YMM +Power_Management:MIC2025-2YM +Power_Management:MIC2025-2YMM +Power_Management:MIC2026-1BN +Power_Management:MIC2026-1xM +Power_Management:MIC2026-2BN +Power_Management:MIC2026-2xM +Power_Management:MIC2090-1YM5 +Power_Management:MIC2090-2YM5 +Power_Management:MIC2091-1YM5 +Power_Management:MIC2091-2YM5 +Power_Management:MIC2544-2YM +Power_Management:MIC2587-1 +Power_Management:MIC2587R-1 +Power_Management:NIS5420MTxTXG +Power_Management:NPC45560-H +Power_Management:NPC45560-L +Power_Management:PD70224 +Power_Management:RT9701 +Power_Management:RT9742AGJ5F +Power_Management:RT9742ANGJ5F +Power_Management:RT9742BGJ5F +Power_Management:RT9742BNGJ5F +Power_Management:SN6505ADBV +Power_Management:SN6505BDBV +Power_Management:SN6507DGQ +Power_Management:STM6600 +Power_Management:STM6601 +Power_Management:SiP32431DR3 +Power_Management:SiP32432DR3 +Power_Management:TEA1708T +Power_Management:TLE8102SG +Power_Management:TLE8104E +Power_Management:TPS2041B +Power_Management:TPS2042D +Power_Management:TPS2044D +Power_Management:TPS2051CDBV +Power_Management:TPS2054D +Power_Management:TPS2065CDBV +Power_Management:TPS2065CDBVx-2 +Power_Management:TPS2069CDBV +Power_Management:TPS2116DRL +Power_Management:TPS22810DRV +Power_Management:TPS22917DBV +Power_Management:TPS22929D +Power_Management:TPS22993 +Power_Management:TPS2412D +Power_Management:TPS2412PW +Power_Management:TPS2419D +Power_Management:TPS2419PW +Power_Management:TPS2592xx +Power_Management:TPS26630RGE +Power_Management:TPS26631PWP +Power_Management:TPS26631RGE +Power_Management:TPS26632RGE +Power_Management:TPS26633PWP +Power_Management:TPS26633RGE +Power_Management:TPS26635RGE +Power_Management:TPS26636PWP +Power_Management:TSM102 +Power_Management:TSM102A +Power_Management:TSM103W +Power_Management:TSM103WA +Power_Management:UCC39002D +Power_Protection:CDNBS08-SLVU2.8-4 +Power_Protection:CDSOT236-0504C +Power_Protection:CM1213A-01SO +Power_Protection:CM1624 +Power_Protection:D3V3X8U9LP3810 +Power_Protection:D3V3XA4B10LP +Power_Protection:DT1240A-08LP3810 +Power_Protection:ECMF02-2AMX6 +Power_Protection:ECMF04-4HSWM10 +Power_Protection:EMI2121MTTAG +Power_Protection:EMI8132 +Power_Protection:ESD224DQA +Power_Protection:ESDA14V2SC5 +Power_Protection:ESDA5V3L +Power_Protection:ESDA5V3SC5 +Power_Protection:ESDA6V1-5SC6 +Power_Protection:ESDA6V1BC6 +Power_Protection:ESDA6V1SC5 +Power_Protection:ESDLC5V0PB8 +Power_Protection:IP3319CX6 +Power_Protection:IP4234CZ6 +Power_Protection:IP4251CZ8-4-TTL +Power_Protection:IP4252CZ12 +Power_Protection:IP4252CZ16 +Power_Protection:IP4252CZ8 +Power_Protection:IP4252CZ8-4-TTL +Power_Protection:IP4253CZ8-4-TTL +Power_Protection:IP4254CZ8-4-TTL +Power_Protection:NCP349MN +Power_Protection:NCP349MNAE +Power_Protection:NCP349MNAM +Power_Protection:NCP349MNBG +Power_Protection:NCP349MNBK +Power_Protection:NCP361MU +Power_Protection:NCP361SN +Power_Protection:NUF4401MN +Power_Protection:NUP2105L +Power_Protection:NUP2202 +Power_Protection:NUP4202 +Power_Protection:PCMF3USB3S +Power_Protection:PESD3V3L4UF +Power_Protection:PESD3V3L4UG +Power_Protection:PESD3V3L4UW +Power_Protection:PESD3V3L5UF +Power_Protection:PESD3V3L5UV +Power_Protection:PESD3V3L5UY +Power_Protection:PESD5V0L4UF +Power_Protection:PESD5V0L4UG +Power_Protection:PESD5V0L4UW +Power_Protection:PESD5V0L5UF +Power_Protection:PESD5V0L5UV +Power_Protection:PESD5V0L5UY +Power_Protection:PRTR5V0U2X +Power_Protection:RCLAMP0502B +Power_Protection:RCLAMP0502BA +Power_Protection:RCLAMP0582B +Power_Protection:RCLAMP3328P +Power_Protection:SN65220 +Power_Protection:SN65240 +Power_Protection:SN75240 +Power_Protection:SP0502BAHT +Power_Protection:SP0502BAJT +Power_Protection:SP0503BAHT +Power_Protection:SP0504BAHT +Power_Protection:SP0504BAJT +Power_Protection:SP0505BAHT +Power_Protection:SP0505BAJT +Power_Protection:SP7538P +Power_Protection:SRV05-4 +Power_Protection:SZNUP2105L +Power_Protection:TBU-CA-025-050-WH +Power_Protection:TBU-CA-025-100-WH +Power_Protection:TBU-CA-025-200-WH +Power_Protection:TBU-CA-025-300-WH +Power_Protection:TBU-CA-025-500-WH +Power_Protection:TBU-CA-040-050-WH +Power_Protection:TBU-CA-040-100-WH +Power_Protection:TBU-CA-040-200-WH +Power_Protection:TBU-CA-040-300-WH +Power_Protection:TBU-CA-040-500-WH +Power_Protection:TBU-CA-050-050-WH +Power_Protection:TBU-CA-050-100-WH +Power_Protection:TBU-CA-050-200-WH +Power_Protection:TBU-CA-050-300-WH +Power_Protection:TBU-CA-050-500-WH +Power_Protection:TBU-CA-065-050-WH +Power_Protection:TBU-CA-065-100-WH +Power_Protection:TBU-CA-065-200-WH +Power_Protection:TBU-CA-065-300-WH +Power_Protection:TBU-CA-065-500-WH +Power_Protection:TBU-CA-085-050-WH +Power_Protection:TBU-CA-085-100-WH +Power_Protection:TBU-CA-085-200-WH +Power_Protection:TBU-CA-085-300-WH +Power_Protection:TBU-CA-085-500-WH +Power_Protection:TPD1E05U06DPY +Power_Protection:TPD1E05U06DYA +Power_Protection:TPD2E2U06DCK +Power_Protection:TPD2E2U06DRL +Power_Protection:TPD2EUSB30 +Power_Protection:TPD2EUSB30A +Power_Protection:TPD2S017 +Power_Protection:TPD3E001DRLR +Power_Protection:TPD3F303DPV +Power_Protection:TPD3S014 +Power_Protection:TPD3S044 +Power_Protection:TPD4E02B04DQA +Power_Protection:TPD4E05U06DQA +Power_Protection:TPD4EUSB30 +Power_Protection:TPD4S014 +Power_Protection:TPD4S1394 +Power_Protection:TPD6E05U06RVZ +Power_Protection:TPD6F003 +Power_Protection:TPD6S300A +Power_Protection:TPD8F003 +Power_Protection:TVS0500DRV +Power_Protection:TVS1400DRV +Power_Protection:TVS1800DRV +Power_Protection:TVS2200DRV +Power_Protection:TVS2700DRV +Power_Protection:TVS3300DRV +Power_Protection:USB6B1 +Power_Protection:USBLC6-2P6 +Power_Protection:USBLC6-2SC6 +Power_Protection:USBLC6-4SC6 +Power_Protection:WE-TVS-82400102 +Power_Protection:WE-TVS-824014881 +Power_Protection:WE-TVS-824015043 +Power_Protection:ZEN056V075A48LS +Power_Protection:ZEN056V115A24LS +Power_Protection:ZEN056V130A24LS +Power_Protection:ZEN056V230A16LS +Power_Protection:ZEN059V130A24LS +Power_Protection:ZEN065V130A24LS +Power_Protection:ZEN065V230A16LS +Power_Protection:ZEN098V130A24LS +Power_Protection:ZEN098V230A16LS +Power_Protection:ZEN132V075A48LS +Power_Protection:ZEN132V130A24LS +Power_Protection:ZEN132V230A16LS +Power_Protection:ZEN164V130A24LS +Power_Supervisor:CAT811JTBI-GT3 +Power_Supervisor:CAT811LTBI-GT3 +Power_Supervisor:CAT811MTBI-GT3 +Power_Supervisor:CAT811RTBI-GT3 +Power_Supervisor:CAT811STBI-GT3 +Power_Supervisor:CAT811TTBI-GT3 +Power_Supervisor:CAT811ZTBI-GT3 +Power_Supervisor:DIO705 +Power_Supervisor:DIO706 +Power_Supervisor:DIO706J +Power_Supervisor:DIO706R +Power_Supervisor:DIO706S +Power_Supervisor:DIO706T +Power_Supervisor:LM3880 +Power_Supervisor:LM809 +Power_Supervisor:LM810 +Power_Supervisor:MAX16050xTI +Power_Supervisor:MAX16051xTI +Power_Supervisor:MAX6355 +Power_Supervisor:MAX6369 +Power_Supervisor:MAX6370 +Power_Supervisor:MAX6371 +Power_Supervisor:MAX6372 +Power_Supervisor:MAX6373 +Power_Supervisor:MAX6374 +Power_Supervisor:MAX690ACSA +Power_Supervisor:MAX690xPA +Power_Supervisor:MAX691xPE +Power_Supervisor:MAX691xWE +Power_Supervisor:MAX692ACSA +Power_Supervisor:MAX692xPA +Power_Supervisor:MAX694xPA +Power_Supervisor:MAX802LCSA +Power_Supervisor:MAX805LCSA +Power_Supervisor:MAX811LEUS-T +Power_Supervisor:MAX811MEUS-T +Power_Supervisor:MAX811REUS-T +Power_Supervisor:MAX811SEUS-T +Power_Supervisor:MAX811TEUS-T +Power_Supervisor:MC34064D +Power_Supervisor:MC34064DM +Power_Supervisor:MC34064P +Power_Supervisor:MC34064SN +Power_Supervisor:MCP100-270D +Power_Supervisor:MCP100-300D +Power_Supervisor:MCP100-315D +Power_Supervisor:MCP100-450D +Power_Supervisor:MCP100-460D +Power_Supervisor:MCP100-475D +Power_Supervisor:MCP100-485D +Power_Supervisor:MCP101-270D +Power_Supervisor:MCP101-300D +Power_Supervisor:MCP101-315D +Power_Supervisor:MCP101-450D +Power_Supervisor:MCP101-460D +Power_Supervisor:MCP101-475D +Power_Supervisor:MCP101-485D +Power_Supervisor:MCP120-xxxDxTO +Power_Supervisor:MCP120-xxxGxTO +Power_Supervisor:MCP120-xxxHxTO +Power_Supervisor:MCP120-xxxxSN +Power_Supervisor:MCP120-xxxxTT +Power_Supervisor:MCP130-xxxDxTO +Power_Supervisor:MCP130-xxxFxTO +Power_Supervisor:MCP130-xxxHxTO +Power_Supervisor:MCP130-xxxxSN +Power_Supervisor:MCP130-xxxxTT +Power_Supervisor:MIC811JUY +Power_Supervisor:MIC811LUY +Power_Supervisor:MIC811MUY +Power_Supervisor:MIC811RUY +Power_Supervisor:MIC811SUY +Power_Supervisor:MIC811TUY +Power_Supervisor:TCM809 +Power_Supervisor:TCM810 +Power_Supervisor:TL7702A +Power_Supervisor:TL7702B +Power_Supervisor:TL7705A +Power_Supervisor:TL7705AxPS +Power_Supervisor:TL7705B +Power_Supervisor:TL7709A +Power_Supervisor:TL7712A +Power_Supervisor:TL7715A +Power_Supervisor:TL7733B +Power_Supervisor:TLV810EA29DBZ +Power_Supervisor:TPS3430WDRC +Power_Supervisor:TPS3702 +Power_Supervisor:TPS3808DBV +Power_Supervisor:TPS3831 +Power_Supervisor:TPS3839DBZ +Power_Supervisor:TPS3839DQN +Reference_Current:LM134H +Reference_Current:LM234Z-3 +Reference_Current:LM234Z-6 +Reference_Current:LM334M +Reference_Current:LM334SM +Reference_Current:LM334Z +Reference_Current:LM334Z-LFT1 +Reference_Current:LT3092xDD +Reference_Current:LT3092xST +Reference_Current:LT3092xTS8 +Reference_Current:PSSI2021SAY +Reference_Current:REF200AU +Reference_Voltage:AD586 +Reference_Voltage:ADR1399KEZ +Reference_Voltage:ADR1399KHZ +Reference_Voltage:ADR420ARMZ +Reference_Voltage:ADR421ARMZ +Reference_Voltage:ADR423ARMZ +Reference_Voltage:ADR425ARMZ +Reference_Voltage:ADR440ARMZ +Reference_Voltage:ADR440xRZ +Reference_Voltage:ADR441ARMZ +Reference_Voltage:ADR441xRZ +Reference_Voltage:ADR443ARMZ +Reference_Voltage:ADR443xRZ +Reference_Voltage:ADR444ARMZ +Reference_Voltage:ADR444xRZ +Reference_Voltage:ADR445ARMZ +Reference_Voltage:ADR445xRZ +Reference_Voltage:ADR4520 +Reference_Voltage:ADR4525 +Reference_Voltage:ADR4530 +Reference_Voltage:ADR4533 +Reference_Voltage:ADR4540 +Reference_Voltage:ADR4550 +Reference_Voltage:CJ432 +Reference_Voltage:ISL21070CIH320Z-TK +Reference_Voltage:ISL21070CIH325Z-TK +Reference_Voltage:ISL21070DIH306Z-TK +Reference_Voltage:LM285D-1.2 +Reference_Voltage:LM285D-2.5 +Reference_Voltage:LM285M-ADJ +Reference_Voltage:LM285S-1.2 +Reference_Voltage:LM285S-2.5 +Reference_Voltage:LM285Z-1.2 +Reference_Voltage:LM285Z-2.5 +Reference_Voltage:LM285Z-ADJ +Reference_Voltage:LM329xZ +Reference_Voltage:LM385BZ-1.2 +Reference_Voltage:LM385BZ-2.5 +Reference_Voltage:LM385D-1.2 +Reference_Voltage:LM385D-2.5 +Reference_Voltage:LM385M-ADJ +Reference_Voltage:LM385S-1.2 +Reference_Voltage:LM385S-2.5 +Reference_Voltage:LM385Z-1.2 +Reference_Voltage:LM385Z-2.5 +Reference_Voltage:LM385Z-ADJ +Reference_Voltage:LM399 +Reference_Voltage:LM4030-2.5 +Reference_Voltage:LM4030-4.096 +Reference_Voltage:LM4040DBZ-10 +Reference_Voltage:LM4040DBZ-2.0 +Reference_Voltage:LM4040DBZ-2.5 +Reference_Voltage:LM4040DBZ-3 +Reference_Voltage:LM4040DBZ-4.1 +Reference_Voltage:LM4040DBZ-5 +Reference_Voltage:LM4040DBZ-8.2 +Reference_Voltage:LM4040DCK-10 +Reference_Voltage:LM4040DCK-2.0 +Reference_Voltage:LM4040DCK-2.5 +Reference_Voltage:LM4040DCK-3 +Reference_Voltage:LM4040DCK-4.1 +Reference_Voltage:LM4040DCK-5 +Reference_Voltage:LM4040DCK-8.2 +Reference_Voltage:LM4040LP-10 +Reference_Voltage:LM4040LP-2.0 +Reference_Voltage:LM4040LP-2.5 +Reference_Voltage:LM4040LP-3 +Reference_Voltage:LM4040LP-4.1 +Reference_Voltage:LM4040LP-5 +Reference_Voltage:LM4040LP-8.2 +Reference_Voltage:LM4041LP-ADJ +Reference_Voltage:LM4125AIM5-2.5 +Reference_Voltage:LM4125IM5-2.0 +Reference_Voltage:LM4125IM5-2.5 +Reference_Voltage:LM4128 +Reference_Voltage:LM4132xMF-1.8 +Reference_Voltage:LM4132xMF-2.0 +Reference_Voltage:LM4132xMF-2.5 +Reference_Voltage:LM4132xMF-3.0 +Reference_Voltage:LM4132xMF-3.3 +Reference_Voltage:LM4132xMF-4.1 +Reference_Voltage:LT1009CMS8 +Reference_Voltage:LT1009xS8 +Reference_Voltage:LT1009xZ +Reference_Voltage:LT1019xN8 +Reference_Voltage:LT1461AxS8-2.5 +Reference_Voltage:LT1461AxS8-3 +Reference_Voltage:LT1461AxS8-3.3 +Reference_Voltage:LT1461AxS8-4 +Reference_Voltage:LT1461AxS8-5 +Reference_Voltage:LT1461BxS8-2.5 +Reference_Voltage:LT1461BxS8-3 +Reference_Voltage:LT1461BxS8-3.3 +Reference_Voltage:LT1461BxS8-4 +Reference_Voltage:LT1461BxS8-5 +Reference_Voltage:LT1461CxS8-2.5 +Reference_Voltage:LT1461CxS8-3 +Reference_Voltage:LT1461CxS8-3.3 +Reference_Voltage:LT1461CxS8-4 +Reference_Voltage:LT1461CxS8-5 +Reference_Voltage:LT1461DxS8-2.5 +Reference_Voltage:LT1461DxS8-3 +Reference_Voltage:LT1461DxS8-3.3 +Reference_Voltage:LT1461DxS8-4 +Reference_Voltage:LT1461DxS8-5 +Reference_Voltage:LT1634BCMS8-1.25 +Reference_Voltage:LT1634BCMS8-2.5 +Reference_Voltage:LT1634CCZ-1.25 +Reference_Voltage:LT1634CCZ-2.5 +Reference_Voltage:LT1634CCZ-4.096 +Reference_Voltage:LT1634CCZ-5 +Reference_Voltage:LT1634xxS8-1.25 +Reference_Voltage:LT1634xxS8-2.5 +Reference_Voltage:LT1634xxS8-4.096 +Reference_Voltage:LT1634xxS8-5 +Reference_Voltage:LT1790-1.25 +Reference_Voltage:LT1790-2.048 +Reference_Voltage:LT1790-2.5 +Reference_Voltage:LT1790-3 +Reference_Voltage:LT1790-3.3 +Reference_Voltage:LT1790-4.096 +Reference_Voltage:LT1790-5 +Reference_Voltage:LT6657AHMS8-2.5 +Reference_Voltage:LT6657AHMS8-3 +Reference_Voltage:LT6657AHMS8-4.096 +Reference_Voltage:LT6657AHMS8-5 +Reference_Voltage:LT6657BHMS8-2.5 +Reference_Voltage:LT6657BHMS8-3 +Reference_Voltage:LT6657BHMS8-4.096 +Reference_Voltage:LT6657BHMS8-5 +Reference_Voltage:MAX6001 +Reference_Voltage:MAX6002 +Reference_Voltage:MAX6003 +Reference_Voltage:MAX6004 +Reference_Voltage:MAX6005 +Reference_Voltage:MAX6035xSA25 +Reference_Voltage:MAX6035xxUR25 +Reference_Voltage:MAX6035xxUR30 +Reference_Voltage:MAX6035xxUR50 +Reference_Voltage:MAX6070AAUT12+T +Reference_Voltage:MAX6070AAUT18+T +Reference_Voltage:MAX6070AAUT18V+T +Reference_Voltage:MAX6070AAUT21+T +Reference_Voltage:MAX6070AAUT25+T +Reference_Voltage:MAX6070AAUT30+T +Reference_Voltage:MAX6070AAUT33+T +Reference_Voltage:MAX6070AAUT33V+T +Reference_Voltage:MAX6070AAUT41+T +Reference_Voltage:MAX6070AAUT50+T +Reference_Voltage:MAX6070AAUT50V+T +Reference_Voltage:MAX6070BAUT12+T +Reference_Voltage:MAX6070BAUT12V+T +Reference_Voltage:MAX6070BAUT18+T +Reference_Voltage:MAX6070BAUT21+T +Reference_Voltage:MAX6070BAUT21V+T +Reference_Voltage:MAX6070BAUT25+T +Reference_Voltage:MAX6070BAUT25V+T +Reference_Voltage:MAX6070BAUT30+T +Reference_Voltage:MAX6070BAUT33+T +Reference_Voltage:MAX6070BAUT33V+T +Reference_Voltage:MAX6070BAUT41+T +Reference_Voltage:MAX6070BAUT41V+T +Reference_Voltage:MAX6070BAUT50+T +Reference_Voltage:MAX6070BAUT50V+T +Reference_Voltage:MAX6070DAUT12V+T +Reference_Voltage:MAX6070DAUT25V+T +Reference_Voltage:MAX6070DAUT30V+T +Reference_Voltage:MAX6070DAUT41V+T +Reference_Voltage:MAX6071AAUT12+T +Reference_Voltage:MAX6071AAUT18+T +Reference_Voltage:MAX6071AAUT21+T +Reference_Voltage:MAX6071AAUT25+T +Reference_Voltage:MAX6071AAUT30+T +Reference_Voltage:MAX6071AAUT30V+T +Reference_Voltage:MAX6071AAUT33+T +Reference_Voltage:MAX6071AAUT41+T +Reference_Voltage:MAX6071AAUT50+T +Reference_Voltage:MAX6071BAUT12+T +Reference_Voltage:MAX6071BAUT18+T +Reference_Voltage:MAX6071BAUT21+T +Reference_Voltage:MAX6071BAUT25+T +Reference_Voltage:MAX6071BAUT25V+T +Reference_Voltage:MAX6071BAUT30+T +Reference_Voltage:MAX6071BAUT33+T +Reference_Voltage:MAX6071BAUT41+T +Reference_Voltage:MAX6071BAUT50+T +Reference_Voltage:MAX6100 +Reference_Voltage:MAX6101 +Reference_Voltage:MAX6102 +Reference_Voltage:MAX6103 +Reference_Voltage:MAX6104 +Reference_Voltage:MAX6105 +Reference_Voltage:MAX6106 +Reference_Voltage:MAX6107 +Reference_Voltage:MAX6225 +Reference_Voltage:MAX6241 +Reference_Voltage:MAX6250 +Reference_Voltage:MAX6325 +Reference_Voltage:MAX6341 +Reference_Voltage:MAX6350 +Reference_Voltage:MAX872 +Reference_Voltage:MAX874 +Reference_Voltage:MCP1501-10xCH +Reference_Voltage:MCP1501-10xRW +Reference_Voltage:MCP1501-10xSN +Reference_Voltage:MCP1501-12xCH +Reference_Voltage:MCP1501-12xRW +Reference_Voltage:MCP1501-12xSN +Reference_Voltage:MCP1501-18xCH +Reference_Voltage:MCP1501-18xRW +Reference_Voltage:MCP1501-18xSN +Reference_Voltage:MCP1501-20xCH +Reference_Voltage:MCP1501-20xRW +Reference_Voltage:MCP1501-20xSN +Reference_Voltage:MCP1501-25xCH +Reference_Voltage:MCP1501-25xRW +Reference_Voltage:MCP1501-25xSN +Reference_Voltage:MCP1501-30xCH +Reference_Voltage:MCP1501-30xRW +Reference_Voltage:MCP1501-30xSN +Reference_Voltage:MCP1501-33xCH +Reference_Voltage:MCP1501-33xRW +Reference_Voltage:MCP1501-33xSN +Reference_Voltage:MCP1501-40xCH +Reference_Voltage:MCP1501-40xRW +Reference_Voltage:MCP1501-40xSN +Reference_Voltage:MCP1525-TO +Reference_Voltage:MCP1525-TT +Reference_Voltage:MCP1541-TO +Reference_Voltage:MCP1541-TT +Reference_Voltage:REF01CP +Reference_Voltage:REF01CS +Reference_Voltage:REF01EZ +Reference_Voltage:REF01HP +Reference_Voltage:REF01HZ +Reference_Voltage:REF02AP +Reference_Voltage:REF02AU +Reference_Voltage:REF02AZ +Reference_Voltage:REF02BP +Reference_Voltage:REF02BU +Reference_Voltage:REF02CP +Reference_Voltage:REF02CS +Reference_Voltage:REF02EZ +Reference_Voltage:REF02HP +Reference_Voltage:REF02HS +Reference_Voltage:REF02HZ +Reference_Voltage:REF02Z +Reference_Voltage:REF03GP +Reference_Voltage:REF03GS +Reference_Voltage:REF102AP +Reference_Voltage:REF102AU +Reference_Voltage:REF102BP +Reference_Voltage:REF102BU +Reference_Voltage:REF102CP +Reference_Voltage:REF102CU +Reference_Voltage:REF191 +Reference_Voltage:REF192 +Reference_Voltage:REF193 +Reference_Voltage:REF194 +Reference_Voltage:REF195 +Reference_Voltage:REF196 +Reference_Voltage:REF198 +Reference_Voltage:REF2025 +Reference_Voltage:REF2030 +Reference_Voltage:REF2033 +Reference_Voltage:REF2041 +Reference_Voltage:REF3012 +Reference_Voltage:REF3020 +Reference_Voltage:REF3025 +Reference_Voltage:REF3030 +Reference_Voltage:REF3033 +Reference_Voltage:REF3040 +Reference_Voltage:REF3212AMDBVREP +Reference_Voltage:REF3220AMDBVREP +Reference_Voltage:REF3225AMDBVREP +Reference_Voltage:REF3230AMDBVREP +Reference_Voltage:REF3233AMDBVREP +Reference_Voltage:REF3240AMDBVREP +Reference_Voltage:REF35102QDBVR +Reference_Voltage:REF35120QDBVR +Reference_Voltage:REF35125QDBVR +Reference_Voltage:REF35160QDBVR +Reference_Voltage:REF35170QDBVR +Reference_Voltage:REF35180QDBVR +Reference_Voltage:REF35205QDBVR +Reference_Voltage:REF35250QDBVR +Reference_Voltage:REF35300QDBVR +Reference_Voltage:REF35330QDBVR +Reference_Voltage:REF35360QDBVR +Reference_Voltage:REF35409QDBVR +Reference_Voltage:REF35500QDBVR +Reference_Voltage:REF5010AD +Reference_Voltage:REF5010ADGK +Reference_Voltage:REF5010ID +Reference_Voltage:REF5010IDGK +Reference_Voltage:REF5020AD +Reference_Voltage:REF5020ADGK +Reference_Voltage:REF5020ID +Reference_Voltage:REF5020IDGK +Reference_Voltage:REF5025AD +Reference_Voltage:REF5025ADGK +Reference_Voltage:REF5025ID +Reference_Voltage:REF5025IDGK +Reference_Voltage:REF5030AD +Reference_Voltage:REF5030ADGK +Reference_Voltage:REF5030ID +Reference_Voltage:REF5030IDGK +Reference_Voltage:REF5040AD +Reference_Voltage:REF5040ADGK +Reference_Voltage:REF5040ID +Reference_Voltage:REF5040IDGK +Reference_Voltage:REF5045AD +Reference_Voltage:REF5045ADGK +Reference_Voltage:REF5045ID +Reference_Voltage:REF5045IDGK +Reference_Voltage:REF5050AD +Reference_Voltage:REF5050ADGK +Reference_Voltage:REF5050ID +Reference_Voltage:REF5050IDGK +Reference_Voltage:REF6025xDGK +Reference_Voltage:REF6030xDGK +Reference_Voltage:REF6033xDGK +Reference_Voltage:REF6041xDGK +Reference_Voltage:REF6045xDGK +Reference_Voltage:REF6050xDGK +Reference_Voltage:TL431D +Reference_Voltage:TL431DBV +Reference_Voltage:TL431DBZ +Reference_Voltage:TL431DCK +Reference_Voltage:TL431KTP +Reference_Voltage:TL431LP +Reference_Voltage:TL431P +Reference_Voltage:TL431PK +Reference_Voltage:TL431PS +Reference_Voltage:TL431PW +Reference_Voltage:TL432DBV +Reference_Voltage:TL432DBZ +Reference_Voltage:TL432PK +Reference_Voltage:TLE2425xD +Reference_Voltage:TLE2425xLP +Reference_Voltage:TLE2426xD +Reference_Voltage:TLE2426xLP +Reference_Voltage:TLE2426xP +Regulator_Controller:ICE1PCS01 +Regulator_Controller:ICE1PCS02 +Regulator_Controller:ICE2PCS01 +Regulator_Controller:ICE2PCS02 +Regulator_Controller:ICE2PCS03 +Regulator_Controller:ICE2PCS04 +Regulator_Controller:ICE2PCS05 +Regulator_Controller:ICE2PCS06 +Regulator_Controller:ICE3PCS01 +Regulator_Controller:ICE3PCS02 +Regulator_Controller:ICE3PCS03 +Regulator_Controller:IR1153S +Regulator_Controller:IR1155S +Regulator_Controller:IR1161L +Regulator_Controller:IR11662S +Regulator_Controller:IR11672AS +Regulator_Controller:IR1167S +Regulator_Controller:IR11682S +Regulator_Controller:IR11688S +Regulator_Controller:IR1168S +Regulator_Controller:IR1169S +Regulator_Controller:IRS2505L +Regulator_Controller:ISL6551 +Regulator_Controller:L4981A +Regulator_Controller:L4981B +Regulator_Controller:L4984D +Regulator_Controller:L6561 +Regulator_Controller:L6562 +Regulator_Controller:L6562A +Regulator_Controller:L6562AT +Regulator_Controller:L6563 +Regulator_Controller:L6563A +Regulator_Controller:L6563H +Regulator_Controller:L6563S +Regulator_Controller:L6564 +Regulator_Controller:L6564H +Regulator_Controller:L6564T +Regulator_Controller:L6598 +Regulator_Controller:L6599 +Regulator_Controller:L6727 +Regulator_Controller:LM25119 +Regulator_Controller:LM3478MA +Regulator_Controller:LM3478MM +Regulator_Controller:LM3478QMM +Regulator_Controller:LM5023 +Regulator_Controller:LT1248 +Regulator_Controller:LT1249 +Regulator_Controller:LT1509 +Regulator_Controller:LT4295xUFD +Regulator_Controller:LTC1624CS8 +Regulator_Controller:LTC3805xMSE +Regulator_Controller:LTC3890 +Regulator_Controller:LTC3890-1 +Regulator_Controller:LTC3892 +Regulator_Controller:LTC3892-1 +Regulator_Controller:LTC3892-2 +Regulator_Controller:LTC7810 +Regulator_Controller:NCP1200D100 +Regulator_Controller:NCP1200D40 +Regulator_Controller:NCP1200D60 +Regulator_Controller:NCP1200P100 +Regulator_Controller:NCP1200P40 +Regulator_Controller:NCP1200P60 +Regulator_Controller:NCP1203D100 +Regulator_Controller:NCP1203D40 +Regulator_Controller:NCP1203D60 +Regulator_Controller:NCP1203P100 +Regulator_Controller:NCP1203P40 +Regulator_Controller:NCP1203P60 +Regulator_Controller:NCP1207A +Regulator_Controller:NCP1207B +Regulator_Controller:NCP1217AD100 +Regulator_Controller:NCP1217AD133 +Regulator_Controller:NCP1217AD65 +Regulator_Controller:NCP1217AP100 +Regulator_Controller:NCP1217AP133 +Regulator_Controller:NCP1217AP65 +Regulator_Controller:NCP1217D100 +Regulator_Controller:NCP1217D133 +Regulator_Controller:NCP1217D65 +Regulator_Controller:NCP1217P100 +Regulator_Controller:NCP1217P133 +Regulator_Controller:NCP1217P65 +Regulator_Controller:NCP1280 +Regulator_Controller:NCP1380A +Regulator_Controller:NCP1380B +Regulator_Controller:NCP1380C +Regulator_Controller:NCP1380D +Regulator_Controller:NCP1653 +Regulator_Controller:NCP1653A +Regulator_Controller:NCP4308AD +Regulator_Controller:NCP4308AMT +Regulator_Controller:NCP4308DD +Regulator_Controller:NCP4308DMN +Regulator_Controller:NCP4308DMT +Regulator_Controller:NCP4308QD +Regulator_Controller:SG3525 +Regulator_Controller:SG3527 +Regulator_Controller:TDA4862 +Regulator_Controller:TDA4863 +Regulator_Controller:TDA4863-2 +Regulator_Controller:TL494 +Regulator_Controller:TPS2375-1 +Regulator_Controller:UC3525 +Regulator_Controller:UC3527 +Regulator_Controller:UC3842_DIP8 +Regulator_Controller:UC3842_SOIC14 +Regulator_Controller:UC3842_SOIC16 +Regulator_Controller:UC3842_SOIC8 +Regulator_Controller:UC3843_DIP8 +Regulator_Controller:UC3843_SOIC14 +Regulator_Controller:UC3843_SOIC8 +Regulator_Controller:UC3844_DIP8 +Regulator_Controller:UC3844_SOIC14 +Regulator_Controller:UC3844_SOIC8 +Regulator_Controller:UC3845_DIP8 +Regulator_Controller:UC3845_SOIC14 +Regulator_Controller:UC3845_SOIC8 +Regulator_Controller:UC3854 +Regulator_Controller:UCC1895J +Regulator_Controller:UCC24610D +Regulator_Controller:UCC24610DRB +Regulator_Controller:UCC24612-1DBV +Regulator_Controller:UCC24612-2DBV +Regulator_Controller:UCC24630DBV +Regulator_Controller:UCC24636DBV +Regulator_Controller:UCC25600 +Regulator_Controller:UCC256301 +Regulator_Controller:UCC25800 +Regulator_Controller:UCC2891 +Regulator_Controller:UCC2892 +Regulator_Controller:UCC2893 +Regulator_Controller:UCC2894 +Regulator_Controller:UCC2895DW +Regulator_Controller:UCC2895N +Regulator_Controller:UCC2895PW +Regulator_Controller:UCC2897 +Regulator_Controller:UCC3800 +Regulator_Controller:UCC3801 +Regulator_Controller:UCC3802 +Regulator_Controller:UCC3803 +Regulator_Controller:UCC3804 +Regulator_Controller:UCC3805 +Regulator_Controller:UCC3808D +Regulator_Controller:UCC3808N +Regulator_Controller:UCC3813-0 +Regulator_Controller:UCC3813-1 +Regulator_Controller:UCC3813-2 +Regulator_Controller:UCC3813-3 +Regulator_Controller:UCC3813-4 +Regulator_Controller:UCC3813-5 +Regulator_Controller:UCC3895DW +Regulator_Controller:UCC3895N +Regulator_Controller:UCC3895PW +Regulator_Current:HV100K5-G +Regulator_Current:HV101K5-G +Regulator_Linear:ADP3336ARMZ +Regulator_Linear:ADP7118ACPZN +Regulator_Linear:ADP7142ARDZ +Regulator_Linear:ADP7142ARDZ-1.8 +Regulator_Linear:ADP7142ARDZ-2.5 +Regulator_Linear:ADP7142ARDZ-3.3 +Regulator_Linear:ADP7142ARDZ-5.0 +Regulator_Linear:ADP7142AUJZ +Regulator_Linear:ADP7142AUJZ-1.8 +Regulator_Linear:ADP7142AUJZ-2.5 +Regulator_Linear:ADP7142AUJZ-3.3 +Regulator_Linear:ADP7142AUJZ-5.0 +Regulator_Linear:ADP7182AUJZ +Regulator_Linear:ADP7182AUJZ-1.8 +Regulator_Linear:ADP7182AUJZ-2.5 +Regulator_Linear:ADP7182AUJZ-3.3 +Regulator_Linear:ADP7182AUJZ-5.0 +Regulator_Linear:AMS1117 +Regulator_Linear:AMS1117-1.5 +Regulator_Linear:AMS1117-1.8 +Regulator_Linear:AMS1117-2.5 +Regulator_Linear:AMS1117-2.85 +Regulator_Linear:AMS1117-3.3 +Regulator_Linear:AMS1117-5.0 +Regulator_Linear:AMS1117CD +Regulator_Linear:AMS1117CD-1.5 +Regulator_Linear:AMS1117CD-1.8 +Regulator_Linear:AMS1117CD-2.5 +Regulator_Linear:AMS1117CD-2.85 +Regulator_Linear:AMS1117CD-3.3 +Regulator_Linear:AMS1117CD-5.0 +Regulator_Linear:AMS1117CS +Regulator_Linear:AMS1117CS-1.5 +Regulator_Linear:AMS1117CS-1.8 +Regulator_Linear:AMS1117CS-2.5 +Regulator_Linear:AMS1117CS-2.85 +Regulator_Linear:AMS1117CS-3.3 +Regulator_Linear:AMS1117CS-5.0 +Regulator_Linear:AP1117-15 +Regulator_Linear:AP1117-18 +Regulator_Linear:AP1117-25 +Regulator_Linear:AP1117-33 +Regulator_Linear:AP1117-50 +Regulator_Linear:AP1117-ADJ +Regulator_Linear:AP130-15Y +Regulator_Linear:AP130-18Y +Regulator_Linear:AP130-20Y +Regulator_Linear:AP130-25Y +Regulator_Linear:AP130-28Y +Regulator_Linear:AP130-30Y +Regulator_Linear:AP130-33Y +Regulator_Linear:AP130-35Y +Regulator_Linear:AP131-15 +Regulator_Linear:AP131-18 +Regulator_Linear:AP131-20 +Regulator_Linear:AP131-25 +Regulator_Linear:AP131-28 +Regulator_Linear:AP131-29 +Regulator_Linear:AP131-30 +Regulator_Linear:AP131-33 +Regulator_Linear:AP131-35 +Regulator_Linear:AP2112K-1.2 +Regulator_Linear:AP2112K-1.8 +Regulator_Linear:AP2112K-2.5 +Regulator_Linear:AP2112K-2.6 +Regulator_Linear:AP2112K-3.3 +Regulator_Linear:AP2127K-1.0 +Regulator_Linear:AP2127K-1.2 +Regulator_Linear:AP2127K-1.5 +Regulator_Linear:AP2127K-1.8 +Regulator_Linear:AP2127K-2.5 +Regulator_Linear:AP2127K-2.8 +Regulator_Linear:AP2127K-3.0 +Regulator_Linear:AP2127K-3.3 +Regulator_Linear:AP2127K-4.2 +Regulator_Linear:AP2127K-4.75 +Regulator_Linear:AP2127K-ADJ +Regulator_Linear:AP2127N-1.0 +Regulator_Linear:AP2127N-1.2 +Regulator_Linear:AP2127N-1.5 +Regulator_Linear:AP2127N-1.8 +Regulator_Linear:AP2127N-2.5 +Regulator_Linear:AP2127N-2.8 +Regulator_Linear:AP2127N-3.0 +Regulator_Linear:AP2127N-3.3 +Regulator_Linear:AP2127N3-1.2 +Regulator_Linear:AP2127N3-1.5 +Regulator_Linear:AP2127R-3.3 +Regulator_Linear:AP2204K-1.5 +Regulator_Linear:AP2204K-1.8 +Regulator_Linear:AP2204K-2.5 +Regulator_Linear:AP2204K-2.8 +Regulator_Linear:AP2204K-3.0 +Regulator_Linear:AP2204K-3.3 +Regulator_Linear:AP2204K-5.0 +Regulator_Linear:AP2204K-ADJ +Regulator_Linear:AP2204MP-ADJ +Regulator_Linear:AP2204R-1.5 +Regulator_Linear:AP2204R-1.8 +Regulator_Linear:AP2204R-2.5 +Regulator_Linear:AP2204R-2.8 +Regulator_Linear:AP2204R-3.0 +Regulator_Linear:AP2204R-3.3 +Regulator_Linear:AP2204R-5.0 +Regulator_Linear:AP2204RA-3.3 +Regulator_Linear:AP2204RA-5.0 +Regulator_Linear:AP2204RB-3.3 +Regulator_Linear:AP2204RB-5.0 +Regulator_Linear:AP22615AWU +Regulator_Linear:AP22615BWU +Regulator_Linear:AP7361C-10E +Regulator_Linear:AP7361C-12E +Regulator_Linear:AP7361C-15E +Regulator_Linear:AP7361C-18E +Regulator_Linear:AP7361C-25E +Regulator_Linear:AP7361C-28E +Regulator_Linear:AP7361C-33E +Regulator_Linear:AP7370-12FDC +Regulator_Linear:AP7370-15FDC +Regulator_Linear:AP7370-18FDC +Regulator_Linear:AP7370-28FDC +Regulator_Linear:AP7370-30FDC +Regulator_Linear:AP7370-33FDC +Regulator_Linear:AP7370-36FDC +Regulator_Linear:AP7370-50FDC +Regulator_Linear:AP7381-28SA-7 +Regulator_Linear:AP7381-33SA-7 +Regulator_Linear:AP7381-50SA-7 +Regulator_Linear:AP7381-70SA-7 +Regulator_Linear:AP7384-28SA +Regulator_Linear:AP7384-28V +Regulator_Linear:AP7384-28Y +Regulator_Linear:AP7384-33SA +Regulator_Linear:AP7384-33V +Regulator_Linear:AP7384-33Y +Regulator_Linear:AP7384-50SA +Regulator_Linear:AP7384-50V +Regulator_Linear:AP7384-50Y +Regulator_Linear:AP7384-70SA +Regulator_Linear:AP7384-70V +Regulator_Linear:AP7384-70Y +Regulator_Linear:APE8865N-12-HF-3 +Regulator_Linear:APE8865N-15-HF-3 +Regulator_Linear:APE8865N-16-HF-3 +Regulator_Linear:APE8865N-17-HF-3 +Regulator_Linear:APE8865N-18-HF-3 +Regulator_Linear:APE8865N-19-HF-3 +Regulator_Linear:APE8865N-20-HF-3 +Regulator_Linear:APE8865N-21-HF-3 +Regulator_Linear:APE8865N-22-HF-3 +Regulator_Linear:APE8865N-23-HF-3 +Regulator_Linear:APE8865N-24-HF-3 +Regulator_Linear:APE8865N-25-HF-3 +Regulator_Linear:APE8865N-26-HF-3 +Regulator_Linear:APE8865N-27-HF-3 +Regulator_Linear:APE8865N-28-HF-3 +Regulator_Linear:APE8865N-29-HF-3 +Regulator_Linear:APE8865N-30-HF-3 +Regulator_Linear:APE8865N-31-HF-3 +Regulator_Linear:APE8865N-32-HF-3 +Regulator_Linear:APE8865N-33-HF-3 +Regulator_Linear:APE8865NL-12-HF-3 +Regulator_Linear:APE8865NL-15-HF-3 +Regulator_Linear:APE8865NL-16-HF-3 +Regulator_Linear:APE8865NL-17-HF-3 +Regulator_Linear:APE8865NL-18-HF-3 +Regulator_Linear:APE8865NL-19-HF-3 +Regulator_Linear:APE8865NL-20-HF-3 +Regulator_Linear:APE8865NL-21-HF-3 +Regulator_Linear:APE8865NL-22-HF-3 +Regulator_Linear:APE8865NL-23-HF-3 +Regulator_Linear:APE8865NL-24-HF-3 +Regulator_Linear:APE8865NL-25-HF-3 +Regulator_Linear:APE8865NL-26-HF-3 +Regulator_Linear:APE8865NL-27-HF-3 +Regulator_Linear:APE8865NL-28-HF-3 +Regulator_Linear:APE8865NL-29-HF-3 +Regulator_Linear:APE8865NL-30-HF-3 +Regulator_Linear:APE8865NL-31-HF-3 +Regulator_Linear:APE8865NL-32-HF-3 +Regulator_Linear:APE8865NL-33-HF-3 +Regulator_Linear:APE8865NR-12-HF-3 +Regulator_Linear:APE8865NR-15-HF-3 +Regulator_Linear:APE8865NR-16-HF-3 +Regulator_Linear:APE8865NR-17-HF-3 +Regulator_Linear:APE8865NR-18-HF-3 +Regulator_Linear:APE8865NR-19-HF-3 +Regulator_Linear:APE8865NR-20-HF-3 +Regulator_Linear:APE8865NR-21-HF-3 +Regulator_Linear:APE8865NR-22-HF-3 +Regulator_Linear:APE8865NR-23-HF-3 +Regulator_Linear:APE8865NR-24-HF-3 +Regulator_Linear:APE8865NR-25-HF-3 +Regulator_Linear:APE8865NR-26-HF-3 +Regulator_Linear:APE8865NR-27-HF-3 +Regulator_Linear:APE8865NR-28-HF-3 +Regulator_Linear:APE8865NR-29-HF-3 +Regulator_Linear:APE8865NR-30-HF-3 +Regulator_Linear:APE8865NR-31-HF-3 +Regulator_Linear:APE8865NR-32-HF-3 +Regulator_Linear:APE8865NR-33-HF-3 +Regulator_Linear:APE8865U4-12-HF-3 +Regulator_Linear:APE8865U4-15-HF-3 +Regulator_Linear:APE8865U4-16-HF-3 +Regulator_Linear:APE8865U4-17-HF-3 +Regulator_Linear:APE8865U4-18-HF-3 +Regulator_Linear:APE8865U4-19-HF-3 +Regulator_Linear:APE8865U4-20-HF-3 +Regulator_Linear:APE8865U4-21-HF-3 +Regulator_Linear:APE8865U4-22-HF-3 +Regulator_Linear:APE8865U4-23-HF-3 +Regulator_Linear:APE8865U4-24-HF-3 +Regulator_Linear:APE8865U4-25-HF-3 +Regulator_Linear:APE8865U4-26-HF-3 +Regulator_Linear:APE8865U4-27-HF-3 +Regulator_Linear:APE8865U4-28-HF-3 +Regulator_Linear:APE8865U4-29-HF-3 +Regulator_Linear:APE8865U4-30-HF-3 +Regulator_Linear:APE8865U4-31-HF-3 +Regulator_Linear:APE8865U4-32-HF-3 +Regulator_Linear:APE8865U4-33-HF-3 +Regulator_Linear:APE8865U5-12-HF-3 +Regulator_Linear:APE8865U5-15-HF-3 +Regulator_Linear:APE8865U5-16-HF-3 +Regulator_Linear:APE8865U5-17-HF-3 +Regulator_Linear:APE8865U5-18-HF-3 +Regulator_Linear:APE8865U5-19-HF-3 +Regulator_Linear:APE8865U5-20-HF-3 +Regulator_Linear:APE8865U5-21-HF-3 +Regulator_Linear:APE8865U5-22-HF-3 +Regulator_Linear:APE8865U5-23-HF-3 +Regulator_Linear:APE8865U5-24-HF-3 +Regulator_Linear:APE8865U5-25-HF-3 +Regulator_Linear:APE8865U5-26-HF-3 +Regulator_Linear:APE8865U5-27-HF-3 +Regulator_Linear:APE8865U5-28-HF-3 +Regulator_Linear:APE8865U5-29-HF-3 +Regulator_Linear:APE8865U5-30-HF-3 +Regulator_Linear:APE8865U5-31-HF-3 +Regulator_Linear:APE8865U5-32-HF-3 +Regulator_Linear:APE8865U5-33-HF-3 +Regulator_Linear:APE8865Y5-12-HF-3 +Regulator_Linear:APE8865Y5-15-HF-3 +Regulator_Linear:APE8865Y5-16-HF-3 +Regulator_Linear:APE8865Y5-17-HF-3 +Regulator_Linear:APE8865Y5-18-HF-3 +Regulator_Linear:APE8865Y5-19-HF-3 +Regulator_Linear:APE8865Y5-20-HF-3 +Regulator_Linear:APE8865Y5-21-HF-3 +Regulator_Linear:APE8865Y5-22-HF-3 +Regulator_Linear:APE8865Y5-23-HF-3 +Regulator_Linear:APE8865Y5-24-HF-3 +Regulator_Linear:APE8865Y5-25-HF-3 +Regulator_Linear:APE8865Y5-26-HF-3 +Regulator_Linear:APE8865Y5-27-HF-3 +Regulator_Linear:APE8865Y5-28-HF-3 +Regulator_Linear:APE8865Y5-29-HF-3 +Regulator_Linear:APE8865Y5-30-HF-3 +Regulator_Linear:APE8865Y5-31-HF-3 +Regulator_Linear:APE8865Y5-32-HF-3 +Regulator_Linear:APE8865Y5-33-HF-3 +Regulator_Linear:AZ1084-1.2 +Regulator_Linear:AZ1084-1.5 +Regulator_Linear:AZ1084-1.8 +Regulator_Linear:AZ1084-2.5 +Regulator_Linear:AZ1084-2.85 +Regulator_Linear:AZ1084-3.3 +Regulator_Linear:AZ1084-5.0 +Regulator_Linear:AZ1117-1.2 +Regulator_Linear:AZ1117-1.5 +Regulator_Linear:AZ1117-1.8 +Regulator_Linear:AZ1117-2.5 +Regulator_Linear:AZ1117-2.85 +Regulator_Linear:AZ1117-3.3 +Regulator_Linear:AZ1117-5.0 +Regulator_Linear:AZ1117-ADJ +Regulator_Linear:AZ1117D-ADJ +Regulator_Linear:AZ1117H-ADJ +Regulator_Linear:AZ1117R-ADJ +Regulator_Linear:AZ1117S-ADJ +Regulator_Linear:AZ1117T-ADJ +Regulator_Linear:BD00FC0WEFJ +Regulator_Linear:BD00FC0WFP +Regulator_Linear:BD15GA3WEFJ +Regulator_Linear:BD15GA5WEFJ +Regulator_Linear:BD18GA3WEFJ +Regulator_Linear:BD18GA5WEFJ +Regulator_Linear:BD25GA3WEFJ +Regulator_Linear:BD25GA5WEFJ +Regulator_Linear:BD30FC0WEFJ +Regulator_Linear:BD30FC0WFP +Regulator_Linear:BD30GA3WEFJ +Regulator_Linear:BD30GA5WEFJ +Regulator_Linear:BD33FC0FP +Regulator_Linear:BD33FC0WEFJ +Regulator_Linear:BD33FC0WFP +Regulator_Linear:BD33GA3WEFJ +Regulator_Linear:BD33GA5WEFJ +Regulator_Linear:BD50FC0FP +Regulator_Linear:BD50FC0WEFJ +Regulator_Linear:BD50FC0WFP +Regulator_Linear:BD50GA3WEFJ +Regulator_Linear:BD50GA5WEFJ +Regulator_Linear:BD60FC0WEFJ +Regulator_Linear:BD60FC0WFP +Regulator_Linear:BD60GA3WEFJ +Regulator_Linear:BD60GA5WEFJ +Regulator_Linear:BD70FC0WEFJ +Regulator_Linear:BD70FC0WFP +Regulator_Linear:BD70GA3WEFJ +Regulator_Linear:BD70GA5WEFJ +Regulator_Linear:BD80FC0WEFJ +Regulator_Linear:BD80FC0WFP +Regulator_Linear:BD80GA3WEFJ +Regulator_Linear:BD80GA5WEFJ +Regulator_Linear:BD90FC0WEFJ +Regulator_Linear:BD90FC0WFP +Regulator_Linear:BD90GA3WEFJ +Regulator_Linear:BD90GA5WEFJ +Regulator_Linear:BDJ0FC0WEFJ +Regulator_Linear:BDJ0FC0WFP +Regulator_Linear:BDJ0GA3WEFJ +Regulator_Linear:BDJ0GA5WEFJ +Regulator_Linear:BDJ2FC0WEFJ +Regulator_Linear:BDJ2FC0WFP +Regulator_Linear:BDJ2GA3WEFJ +Regulator_Linear:BDJ2GA5WEFJ +Regulator_Linear:BDJ5FC0WEFJ +Regulator_Linear:BDJ5FC0WFP +Regulator_Linear:HT75xx-1-SOT89 +Regulator_Linear:ICL7663 +Regulator_Linear:ICL7664 +Regulator_Linear:IFX25401TBV +Regulator_Linear:IFX25401TBV50 +Regulator_Linear:IFX25401TEV +Regulator_Linear:IFX25401TEV50 +Regulator_Linear:IFX27001TFV +Regulator_Linear:IFX27001TFV15 +Regulator_Linear:IFX27001TFV18 +Regulator_Linear:IFX27001TFV26 +Regulator_Linear:IFX27001TFV33 +Regulator_Linear:IFX27001TFV50 +Regulator_Linear:KA378R05 +Regulator_Linear:KA378R12C +Regulator_Linear:KA378R33 +Regulator_Linear:KA78M05_TO252 +Regulator_Linear:KF25BDT +Regulator_Linear:KF33BDT +Regulator_Linear:KF50BDT +Regulator_Linear:KF80BDT +Regulator_Linear:L200 +Regulator_Linear:L7805 +Regulator_Linear:L7806 +Regulator_Linear:L7808 +Regulator_Linear:L7809 +Regulator_Linear:L7812 +Regulator_Linear:L7815 +Regulator_Linear:L7818 +Regulator_Linear:L7824 +Regulator_Linear:L7885 +Regulator_Linear:L78L05_SO8 +Regulator_Linear:L78L05_SOT89 +Regulator_Linear:L78L05_TO92 +Regulator_Linear:L78L06_SO8 +Regulator_Linear:L78L06_SOT89 +Regulator_Linear:L78L06_TO92 +Regulator_Linear:L78L08_SO8 +Regulator_Linear:L78L08_SOT89 +Regulator_Linear:L78L08_TO92 +Regulator_Linear:L78L09_SO8 +Regulator_Linear:L78L09_SOT89 +Regulator_Linear:L78L09_TO92 +Regulator_Linear:L78L10_SO8 +Regulator_Linear:L78L10_SOT89 +Regulator_Linear:L78L10_TO92 +Regulator_Linear:L78L12_SO8 +Regulator_Linear:L78L12_SOT89 +Regulator_Linear:L78L12_TO92 +Regulator_Linear:L78L15_SO8 +Regulator_Linear:L78L15_SOT89 +Regulator_Linear:L78L15_TO92 +Regulator_Linear:L78L18_SO8 +Regulator_Linear:L78L18_SOT89 +Regulator_Linear:L78L18_TO92 +Regulator_Linear:L78L24_SO8 +Regulator_Linear:L78L24_SOT89 +Regulator_Linear:L78L24_TO92 +Regulator_Linear:L78L33_SO8 +Regulator_Linear:L78L33_SOT89 +Regulator_Linear:L78L33_TO92 +Regulator_Linear:L7905 +Regulator_Linear:L7908 +Regulator_Linear:L7912 +Regulator_Linear:L7915 +Regulator_Linear:L79L05_SO8 +Regulator_Linear:L79L05_SOT89 +Regulator_Linear:L79L05_TO92 +Regulator_Linear:L79L08_SO8 +Regulator_Linear:L79L08_SOT89 +Regulator_Linear:L79L08_TO92 +Regulator_Linear:L79L12_SO8 +Regulator_Linear:L79L12_SOT89 +Regulator_Linear:L79L12_TO92 +Regulator_Linear:L79L15_SO8 +Regulator_Linear:L79L15_SOT89 +Regulator_Linear:L79L15_TO92 +Regulator_Linear:LD0186D2T50TR +Regulator_Linear:LD1086D2M33TR +Regulator_Linear:LD1086D2MTR +Regulator_Linear:LD1086D2T12TR +Regulator_Linear:LD1086D2T18TR +Regulator_Linear:LD1086D2T33TR +Regulator_Linear:LD1086D2TTR +Regulator_Linear:LD1086DT18TR +Regulator_Linear:LD1086DT25TR +Regulator_Linear:LD1086DT33TR +Regulator_Linear:LD1086DT50TR +Regulator_Linear:LD1086DTTR +Regulator_Linear:LD1086PUR +Regulator_Linear:LD1086V-DG +Regulator_Linear:LD1086V18-DG +Regulator_Linear:LD1086V33-DG +Regulator_Linear:LD1117S12TR_SOT223 +Regulator_Linear:LD1117S18TR_SOT223 +Regulator_Linear:LD1117S25TR_SOT223 +Regulator_Linear:LD1117S33TR_SOT223 +Regulator_Linear:LD1117S50TR_SOT223 +Regulator_Linear:LD39015M08R +Regulator_Linear:LD39015M10R +Regulator_Linear:LD39015M125R +Regulator_Linear:LD39015M12R +Regulator_Linear:LD39015M15R +Regulator_Linear:LD39015M18R +Regulator_Linear:LD39015M25R +Regulator_Linear:LD39015M33R +Regulator_Linear:LD39150DT18 +Regulator_Linear:LD39150DT25 +Regulator_Linear:LD39150DT33 +Regulator_Linear:LD3985G122R_TSOT23 +Regulator_Linear:LD3985G18R_TSOT23 +Regulator_Linear:LD3985G25R_TSOT23 +Regulator_Linear:LD3985G26R_TSOT23 +Regulator_Linear:LD3985G27R_TSOT23 +Regulator_Linear:LD3985G28R_TSOT23 +Regulator_Linear:LD3985G30R_TSOT23 +Regulator_Linear:LD3985G33R_TSOT23 +Regulator_Linear:LD3985G47R_TSOT23 +Regulator_Linear:LD3985M122R_SOT23 +Regulator_Linear:LD3985M18R_SOT23 +Regulator_Linear:LD3985M25R_SOT23 +Regulator_Linear:LD3985M26R_SOT23 +Regulator_Linear:LD3985M27R_SOT23 +Regulator_Linear:LD3985M28R_SOT23 +Regulator_Linear:LD3985M29R_SOT23 +Regulator_Linear:LD3985M30R_SOT23 +Regulator_Linear:LD3985M33R_SOT23 +Regulator_Linear:LD3985M47R_SOT23 +Regulator_Linear:LDK130-08_SOT23_SOT353 +Regulator_Linear:LDK130-10_SOT23_SOT353 +Regulator_Linear:LDK130-12_SOT23_SOT353 +Regulator_Linear:LDK130-15_SOT23_SOT353 +Regulator_Linear:LDK130-18_SOT23_SOT353 +Regulator_Linear:LDK130-25_SOT23_SOT353 +Regulator_Linear:LDK130-29_SOT23_SOT353 +Regulator_Linear:LDK130-30_SOT23_SOT353 +Regulator_Linear:LDK130-32_SOT23_SOT353 +Regulator_Linear:LDK130-33_SOT23_SOT353 +Regulator_Linear:LDK130-ADJ_DFN6 +Regulator_Linear:LDK130-ADJ_SOT23_SOT353 +Regulator_Linear:LDK130PU08R_DFN6 +Regulator_Linear:LDK130PU10R_DFN6 +Regulator_Linear:LDK130PU12R_DFN6 +Regulator_Linear:LDK130PU15R_DFN6 +Regulator_Linear:LDK130PU18R_DFN6 +Regulator_Linear:LDK130PU25R_DFN6 +Regulator_Linear:LDK130PU29R_DFN6 +Regulator_Linear:LDK130PU30R_DFN6 +Regulator_Linear:LDK130PU32R_DFN6 +Regulator_Linear:LDK130PU33R_DFN6 +Regulator_Linear:LF120_TO220 +Regulator_Linear:LF120_TO252 +Regulator_Linear:LF15_TO220 +Regulator_Linear:LF15_TO252 +Regulator_Linear:LF18_TO220 +Regulator_Linear:LF18_TO252 +Regulator_Linear:LF25_TO220 +Regulator_Linear:LF25_TO252 +Regulator_Linear:LF33_TO220 +Regulator_Linear:LF33_TO252 +Regulator_Linear:LF47_TO220 +Regulator_Linear:LF47_TO252 +Regulator_Linear:LF50_TO220 +Regulator_Linear:LF50_TO252 +Regulator_Linear:LF60_TO220 +Regulator_Linear:LF60_TO252 +Regulator_Linear:LF80_TO220 +Regulator_Linear:LF80_TO252 +Regulator_Linear:LF85_TO220 +Regulator_Linear:LF85_TO252 +Regulator_Linear:LF90_TO220 +Regulator_Linear:LF90_TO252 +Regulator_Linear:LK112M15TR +Regulator_Linear:LK112M18TR +Regulator_Linear:LK112M25TR +Regulator_Linear:LK112M33TR +Regulator_Linear:LK112M50TR +Regulator_Linear:LK112M55TR +Regulator_Linear:LK112M60TR +Regulator_Linear:LK112M80TR +Regulator_Linear:LM1084-3.3 +Regulator_Linear:LM1084-5.0 +Regulator_Linear:LM1084-ADJ +Regulator_Linear:LM1085-12 +Regulator_Linear:LM1085-3.3 +Regulator_Linear:LM1085-5.0 +Regulator_Linear:LM1085-ADJ +Regulator_Linear:LM1117DT-1.8 +Regulator_Linear:LM1117DT-2.5 +Regulator_Linear:LM1117DT-3.3 +Regulator_Linear:LM1117DT-5.0 +Regulator_Linear:LM1117DT-ADJ +Regulator_Linear:LM1117LD-1.8 +Regulator_Linear:LM1117LD-2.5 +Regulator_Linear:LM1117LD-3.3 +Regulator_Linear:LM1117LD-5.0 +Regulator_Linear:LM1117LD-ADJ +Regulator_Linear:LM1117MP-1.8 +Regulator_Linear:LM1117MP-2.5 +Regulator_Linear:LM1117MP-3.3 +Regulator_Linear:LM1117MP-5.0 +Regulator_Linear:LM1117MP-ADJ +Regulator_Linear:LM1117S-3.3 +Regulator_Linear:LM1117S-5.0 +Regulator_Linear:LM1117S-ADJ +Regulator_Linear:LM1117T-2.5 +Regulator_Linear:LM1117T-3.3 +Regulator_Linear:LM1117T-5.0 +Regulator_Linear:LM1117T-ADJ +Regulator_Linear:LM117_TO3 +Regulator_Linear:LM117_TO39 +Regulator_Linear:LM137_TO3 +Regulator_Linear:LM150_TO3 +Regulator_Linear:LM2931-3.3_TO220 +Regulator_Linear:LM2931-5.0_SO8 +Regulator_Linear:LM2931-5.0_TO220 +Regulator_Linear:LM2931-ADJ_TO92 +Regulator_Linear:LM2931AZ-5.0_TO92 +Regulator_Linear:LM2936-3.0 +Regulator_Linear:LM2936-3.0_TO92 +Regulator_Linear:LM2936-3.3 +Regulator_Linear:LM2936-3.3_TO92 +Regulator_Linear:LM2936-5.0 +Regulator_Linear:LM2936-5.0_TO92 +Regulator_Linear:LM2937xMP +Regulator_Linear:LM2937xS +Regulator_Linear:LM2937xT +Regulator_Linear:LM2990SX-12 +Regulator_Linear:LM2990SX-15 +Regulator_Linear:LM2990SX-5.0 +Regulator_Linear:LM317L_SO8 +Regulator_Linear:LM317L_SOT-89 +Regulator_Linear:LM317L_TO92 +Regulator_Linear:LM317_SOT-223 +Regulator_Linear:LM317_TO-220 +Regulator_Linear:LM317_TO-252 +Regulator_Linear:LM317_TO-263 +Regulator_Linear:LM317_TO3 +Regulator_Linear:LM317_TO39 +Regulator_Linear:LM337L_SO8 +Regulator_Linear:LM337L_TO92 +Regulator_Linear:LM337_SOT223 +Regulator_Linear:LM337_TO220 +Regulator_Linear:LM337_TO252 +Regulator_Linear:LM337_TO263 +Regulator_Linear:LM337_TO3 +Regulator_Linear:LM337_TO39 +Regulator_Linear:LM341T-05_TO220 +Regulator_Linear:LM341T-12_TO220 +Regulator_Linear:LM341T-15_TO220 +Regulator_Linear:LM3480-12 +Regulator_Linear:LM3480-15 +Regulator_Linear:LM3480-3.3 +Regulator_Linear:LM3480-5.0 +Regulator_Linear:LM350_TO220 +Regulator_Linear:LM350_TO3 +Regulator_Linear:LM723_DIP14 +Regulator_Linear:LM723_TO100 +Regulator_Linear:LM7805_TO220 +Regulator_Linear:LM7806_TO220 +Regulator_Linear:LM7808_TO220 +Regulator_Linear:LM7809_TO220 +Regulator_Linear:LM7810_TO220 +Regulator_Linear:LM7812_TO220 +Regulator_Linear:LM7815_TO220 +Regulator_Linear:LM7818_TO220 +Regulator_Linear:LM7824_TO220 +Regulator_Linear:LM78L05_SO8 +Regulator_Linear:LM78L05_TO92 +Regulator_Linear:LM78L12_SO8 +Regulator_Linear:LM78L12_TO92 +Regulator_Linear:LM78M05_TO220 +Regulator_Linear:LM78M05_TO252 +Regulator_Linear:LM7905_TO220 +Regulator_Linear:LM7906_TO220 +Regulator_Linear:LM7908_TO220 +Regulator_Linear:LM7909_TO220 +Regulator_Linear:LM7910_TO220 +Regulator_Linear:LM7912_TO220 +Regulator_Linear:LM7915_TO220 +Regulator_Linear:LM7918_TO220 +Regulator_Linear:LM7924_TO220 +Regulator_Linear:LM79L05_TO92 +Regulator_Linear:LM79L12_TO92 +Regulator_Linear:LM79L15_TO92 +Regulator_Linear:LM79M05_TO220 +Regulator_Linear:LM79M12_TO220 +Regulator_Linear:LM79M15_TO220 +Regulator_Linear:LP2950-3.0_TO252 +Regulator_Linear:LP2950-3.0_TO92 +Regulator_Linear:LP2950-3.3_TO252 +Regulator_Linear:LP2950-3.3_TO92 +Regulator_Linear:LP2950-5.0_TO252 +Regulator_Linear:LP2950-5.0_TO92 +Regulator_Linear:LP2951-3.0_DIP +Regulator_Linear:LP2951-3.0_SOIC +Regulator_Linear:LP2951-3.0_VSSOP +Regulator_Linear:LP2951-3.0_WSON +Regulator_Linear:LP2951-3.3_DIP +Regulator_Linear:LP2951-3.3_SOIC +Regulator_Linear:LP2951-3.3_VSSOP +Regulator_Linear:LP2951-3.3_WSON +Regulator_Linear:LP2951-5.0_DIP +Regulator_Linear:LP2951-5.0_SOIC +Regulator_Linear:LP2951-5.0_VSSOP +Regulator_Linear:LP2951-5.0_WSON +Regulator_Linear:LP2980-ADJ +Regulator_Linear:LP2985-1.8 +Regulator_Linear:LP2985-10.0 +Regulator_Linear:LP2985-2.5 +Regulator_Linear:LP2985-2.8 +Regulator_Linear:LP2985-2.9 +Regulator_Linear:LP2985-3.0 +Regulator_Linear:LP2985-3.1 +Regulator_Linear:LP2985-3.3 +Regulator_Linear:LP2985-5.0 +Regulator_Linear:LP2987-3.0_SOIC8_VSSOP8 +Regulator_Linear:LP2987-3.0_WSON8 +Regulator_Linear:LP2987-3.3_SOIC8_VSSOP8 +Regulator_Linear:LP2987-3.3_WSON8 +Regulator_Linear:LP2987-5.0_SOIC8_VSSOP8 +Regulator_Linear:LP2987-5.0_WSON8 +Regulator_Linear:LP2988-2.8_SOIC8_VSSOP8 +Regulator_Linear:LP2988-2.8_WSON8 +Regulator_Linear:LP2988-3.0_SOIC8_VSSOP8 +Regulator_Linear:LP2988-3.0_WSON8 +Regulator_Linear:LP2988-3.3_SOIC8_VSSOP8 +Regulator_Linear:LP2988-3.3_WSON8 +Regulator_Linear:LP2988-3.8_SOIC8_VSSOP8 +Regulator_Linear:LP2988-3.8_WSON8 +Regulator_Linear:LP2988-5.0_SOIC8_VSSOP8 +Regulator_Linear:LP2988-5.0_WSON8 +Regulator_Linear:LP38512MR-ADJ +Regulator_Linear:LP38512TJ-1.8 +Regulator_Linear:LP38512TJ-ADJ +Regulator_Linear:LP38512TS-1.8 +Regulator_Linear:LP38691SD-1.8 +Regulator_Linear:LP38691SD-2.5 +Regulator_Linear:LP38691SD-3.3 +Regulator_Linear:LP38691SD-5.0 +Regulator_Linear:LP38693DT-1.8 +Regulator_Linear:LP38693DT-2.5 +Regulator_Linear:LP38693DT-3.3 +Regulator_Linear:LP38693DT-5.0 +Regulator_Linear:LP38693MP-1.8 +Regulator_Linear:LP38693MP-2.5 +Regulator_Linear:LP38693MP-3.3 +Regulator_Linear:LP38693MP-5.0 +Regulator_Linear:LP38693SD-1.8 +Regulator_Linear:LP38693SD-2.5 +Regulator_Linear:LP38693SD-3.3 +Regulator_Linear:LP38693SD-5.0 +Regulator_Linear:LP3963-1.8 +Regulator_Linear:LP3963-2.5 +Regulator_Linear:LP3963-3.3 +Regulator_Linear:LP3966-1.8 +Regulator_Linear:LP3966-2.5 +Regulator_Linear:LP3966-3.3 +Regulator_Linear:LP3966-ADJ +Regulator_Linear:LP3982ILD-1.8 +Regulator_Linear:LP3982ILD-2.5 +Regulator_Linear:LP3982ILD-3.0 +Regulator_Linear:LP3982ILD-3.3 +Regulator_Linear:LP3982ILD-ADJ +Regulator_Linear:LP3982IMM-1.8 +Regulator_Linear:LP3982IMM-2.5 +Regulator_Linear:LP3982IMM-3.0 +Regulator_Linear:LP3982IMM-3.3 +Regulator_Linear:LP3982IMM-ADJ +Regulator_Linear:LP3982IMMX-2.82 +Regulator_Linear:LP3987H-33B5 +Regulator_Linear:LP3992-18B5 +Regulator_Linear:LP5907MFX-1.2 +Regulator_Linear:LP5907MFX-1.5 +Regulator_Linear:LP5907MFX-1.8 +Regulator_Linear:LP5907MFX-2.5 +Regulator_Linear:LP5907MFX-2.8 +Regulator_Linear:LP5907MFX-2.85 +Regulator_Linear:LP5907MFX-2.9 +Regulator_Linear:LP5907MFX-3.0 +Regulator_Linear:LP5907MFX-3.1 +Regulator_Linear:LP5907MFX-3.2 +Regulator_Linear:LP5907MFX-3.3 +Regulator_Linear:LP5907MFX-4.5 +Regulator_Linear:LP5912-0.9DRV +Regulator_Linear:LP5912-1.0DRV +Regulator_Linear:LP5912-1.1DRV +Regulator_Linear:LP5912-1.2DRV +Regulator_Linear:LP5912-1.5DRV +Regulator_Linear:LP5912-1.8DRV +Regulator_Linear:LP5912-2.5DRV +Regulator_Linear:LP5912-2.8DRV +Regulator_Linear:LP5912-3.0DRV +Regulator_Linear:LP5912-3.3DRV +Regulator_Linear:LP5912-5.0DRV +Regulator_Linear:LR8K4-G +Regulator_Linear:LR8N3-G +Regulator_Linear:LR8N8-G +Regulator_Linear:LT1033C +Regulator_Linear:LT1083-12 +Regulator_Linear:LT1083-3.3 +Regulator_Linear:LT1083-3.6 +Regulator_Linear:LT1083-5.0 +Regulator_Linear:LT1083-ADJ +Regulator_Linear:LT1084-12 +Regulator_Linear:LT1084-3.3 +Regulator_Linear:LT1084-3.6 +Regulator_Linear:LT1084-5.0 +Regulator_Linear:LT1084-ADJ +Regulator_Linear:LT1085-12 +Regulator_Linear:LT1085-3.3 +Regulator_Linear:LT1085-3.6 +Regulator_Linear:LT1085-5.0 +Regulator_Linear:LT1085-ADJ +Regulator_Linear:LT1086-12 +Regulator_Linear:LT1086-2.85 +Regulator_Linear:LT1086-3.3 +Regulator_Linear:LT1086-3.6 +Regulator_Linear:LT1086-5.0 +Regulator_Linear:LT1086-ADJ +Regulator_Linear:LT1117-2.85 +Regulator_Linear:LT1117-3.3 +Regulator_Linear:LT1117-5.0 +Regulator_Linear:LT1117-ADJ +Regulator_Linear:LT1129-3.3_SO8 +Regulator_Linear:LT1129-3.3_SOT223 +Regulator_Linear:LT1129-3.3_TO220_TO263 +Regulator_Linear:LT1129-5.0_SO8 +Regulator_Linear:LT1129-5.0_SOT223 +Regulator_Linear:LT1129-5.0_TO220_TO263 +Regulator_Linear:LT1129-ADJ_SO8 +Regulator_Linear:LT1129-ADJ_TO220_TO263 +Regulator_Linear:LT1175-5_SO8_DIP8 +Regulator_Linear:LT1175-5_SOT223 +Regulator_Linear:LT1175-5_TO263_TO220 +Regulator_Linear:LT1175-ADJ_SO8_DIP8 +Regulator_Linear:LT1175-ADJ_SOT223 +Regulator_Linear:LT1175-ADJ_TO263_TO220 +Regulator_Linear:LT1584-3.3 +Regulator_Linear:LT1584-3.38 +Regulator_Linear:LT1584-3.45 +Regulator_Linear:LT1584-3.6 +Regulator_Linear:LT1584-ADJ +Regulator_Linear:LT1585-3.3 +Regulator_Linear:LT1585-3.38 +Regulator_Linear:LT1585-3.45 +Regulator_Linear:LT1585-3.6 +Regulator_Linear:LT1585-ADJ +Regulator_Linear:LT1587-3.3 +Regulator_Linear:LT1587-3.45 +Regulator_Linear:LT1587-3.6 +Regulator_Linear:LT1587-ADJ +Regulator_Linear:LT1761-1.2 +Regulator_Linear:LT1761-1.5 +Regulator_Linear:LT1761-1.8 +Regulator_Linear:LT1761-2 +Regulator_Linear:LT1761-2.5 +Regulator_Linear:LT1761-2.8 +Regulator_Linear:LT1761-3 +Regulator_Linear:LT1761-3.3 +Regulator_Linear:LT1761-5 +Regulator_Linear:LT1761-BYP +Regulator_Linear:LT1761-SD +Regulator_Linear:LT1762 +Regulator_Linear:LT1762-2.5 +Regulator_Linear:LT1762-3 +Regulator_Linear:LT1762-3.3 +Regulator_Linear:LT1762-5 +Regulator_Linear:LT1962 +Regulator_Linear:LT1962-1.5 +Regulator_Linear:LT1962-1.8 +Regulator_Linear:LT1962-2.5 +Regulator_Linear:LT1962-3 +Regulator_Linear:LT1962-3.3 +Regulator_Linear:LT1962-5 +Regulator_Linear:LT1963AEQ +Regulator_Linear:LT1963AxQ-1.5 +Regulator_Linear:LT1963AxQ-1.8 +Regulator_Linear:LT1963AxQ-2.5 +Regulator_Linear:LT1963AxQ-3.3 +Regulator_Linear:LT1963AxST-1.5 +Regulator_Linear:LT1963AxST-1.8 +Regulator_Linear:LT1963AxST-2.5 +Regulator_Linear:LT1963AxST-3.3 +Regulator_Linear:LT1963EQ +Regulator_Linear:LT1964-5 +Regulator_Linear:LT1964-BYP +Regulator_Linear:LT1964-SD +Regulator_Linear:LT3010 +Regulator_Linear:LT3010-5 +Regulator_Linear:LT3011xDD +Regulator_Linear:LT3011xMSE +Regulator_Linear:LT3014xDD +Regulator_Linear:LT3014xS5 +Regulator_Linear:LT3015Q +Regulator_Linear:LT3015xQ-12 +Regulator_Linear:LT3015xQ-15 +Regulator_Linear:LT3015xQ-2.5 +Regulator_Linear:LT3015xQ-3 +Regulator_Linear:LT3015xQ-3.3 +Regulator_Linear:LT3015xQ-5 +Regulator_Linear:LT3032 +Regulator_Linear:LT3032-12 +Regulator_Linear:LT3032-15 +Regulator_Linear:LT3032-3.3 +Regulator_Linear:LT3032-5 +Regulator_Linear:LT3033xUDC +Regulator_Linear:LT3042xMSE +Regulator_Linear:LT3045xDD +Regulator_Linear:LT3045xMSE +Regulator_Linear:LT3080xDD +Regulator_Linear:LT3080xMS8E +Regulator_Linear:LT3080xQ +Regulator_Linear:LT3080xST +Regulator_Linear:LT3080xT +Regulator_Linear:LT3091xT7 +Regulator_Linear:LT3093xMSE +Regulator_Linear:LT3094xDD +Regulator_Linear:LT3094xMSE +Regulator_Linear:LTC3026-1 +Regulator_Linear:MAX1615xUK +Regulator_Linear:MAX1616xUK +Regulator_Linear:MAX1658ESA +Regulator_Linear:MAX1659ESA +Regulator_Linear:MAX16910 +Regulator_Linear:MAX38908xTD +Regulator_Linear:MAX38909xTD +Regulator_Linear:MAX38911ATA+ +Regulator_Linear:MAX38912ATA+ +Regulator_Linear:MAX5092AATE +Regulator_Linear:MAX5092BATE +Regulator_Linear:MAX5093AATE +Regulator_Linear:MAX5093BATE +Regulator_Linear:MAX603 +Regulator_Linear:MAX604 +Regulator_Linear:MAX663 +Regulator_Linear:MAX664 +Regulator_Linear:MAX666 +Regulator_Linear:MAX882 +Regulator_Linear:MAX883 +Regulator_Linear:MAX884 +Regulator_Linear:MC78L05_SO8 +Regulator_Linear:MC78L05_SOT89 +Regulator_Linear:MC78L05_TO92 +Regulator_Linear:MC78L06_SO8 +Regulator_Linear:MC78L06_TO92 +Regulator_Linear:MC78L08_SO8 +Regulator_Linear:MC78L08_SOT89 +Regulator_Linear:MC78L08_TO92 +Regulator_Linear:MC78L12_SO8 +Regulator_Linear:MC78L12_SOT89 +Regulator_Linear:MC78L12_TO92 +Regulator_Linear:MC78L15_SO8 +Regulator_Linear:MC78L15_TO92 +Regulator_Linear:MC78L18_SO8 +Regulator_Linear:MC78L18_TO92 +Regulator_Linear:MC78L24_SO8 +Regulator_Linear:MC78L24_TO92 +Regulator_Linear:MC78LC18NTR +Regulator_Linear:MC78LC25NTR +Regulator_Linear:MC78LC30NTR +Regulator_Linear:MC78LC33NTR +Regulator_Linear:MC78LC50NTR +Regulator_Linear:MC78M05_TO252 +Regulator_Linear:MC7905 +Regulator_Linear:MC7905.2 +Regulator_Linear:MC7906 +Regulator_Linear:MC7908 +Regulator_Linear:MC7912 +Regulator_Linear:MC7915 +Regulator_Linear:MC7918 +Regulator_Linear:MC7924 +Regulator_Linear:MC79L05_SO8 +Regulator_Linear:MC79L12_SO8 +Regulator_Linear:MC79L15_SO8 +Regulator_Linear:MC79M05_TO220 +Regulator_Linear:MC79M05_TO252 +Regulator_Linear:MC79M08_TO220 +Regulator_Linear:MC79M08_TO252 +Regulator_Linear:MC79M12_TO220 +Regulator_Linear:MC79M12_TO252 +Regulator_Linear:MC79M15_TO220 +Regulator_Linear:MC79M15_TO252 +Regulator_Linear:MCP1700x-120xxMB +Regulator_Linear:MCP1700x-120xxTO +Regulator_Linear:MCP1700x-120xxTT +Regulator_Linear:MCP1700x-180xxMB +Regulator_Linear:MCP1700x-180xxTO +Regulator_Linear:MCP1700x-180xxTT +Regulator_Linear:MCP1700x-250xxMB +Regulator_Linear:MCP1700x-250xxTO +Regulator_Linear:MCP1700x-250xxTT +Regulator_Linear:MCP1700x-280xxMB +Regulator_Linear:MCP1700x-280xxTO +Regulator_Linear:MCP1700x-280xxTT +Regulator_Linear:MCP1700x-300xxMB +Regulator_Linear:MCP1700x-300xxTO +Regulator_Linear:MCP1700x-300xxTT +Regulator_Linear:MCP1700x-330xxMB +Regulator_Linear:MCP1700x-330xxTO +Regulator_Linear:MCP1700x-330xxTT +Regulator_Linear:MCP1700x-500xxMB +Regulator_Linear:MCP1700x-500xxTO +Regulator_Linear:MCP1700x-500xxTT +Regulator_Linear:MCP1703Ax-120xxDB +Regulator_Linear:MCP1703Ax-120xxMB +Regulator_Linear:MCP1703Ax-120xxTT +Regulator_Linear:MCP1703Ax-150xxDB +Regulator_Linear:MCP1703Ax-150xxMB +Regulator_Linear:MCP1703Ax-150xxTT +Regulator_Linear:MCP1703Ax-180xxDB +Regulator_Linear:MCP1703Ax-180xxMB +Regulator_Linear:MCP1703Ax-180xxTT +Regulator_Linear:MCP1703Ax-250xxDB +Regulator_Linear:MCP1703Ax-250xxMB +Regulator_Linear:MCP1703Ax-250xxTT +Regulator_Linear:MCP1703Ax-280xxDB +Regulator_Linear:MCP1703Ax-280xxMB +Regulator_Linear:MCP1703Ax-280xxTT +Regulator_Linear:MCP1703Ax-300xxDB +Regulator_Linear:MCP1703Ax-300xxMB +Regulator_Linear:MCP1703Ax-300xxTT +Regulator_Linear:MCP1703Ax-330xxDB +Regulator_Linear:MCP1703Ax-330xxMB +Regulator_Linear:MCP1703Ax-330xxTT +Regulator_Linear:MCP1703Ax-400xxDB +Regulator_Linear:MCP1703Ax-400xxMB +Regulator_Linear:MCP1703Ax-400xxTT +Regulator_Linear:MCP1703Ax-500xxDB +Regulator_Linear:MCP1703Ax-500xxMB +Regulator_Linear:MCP1703Ax-500xxTT +Regulator_Linear:MCP1727-0802xMF +Regulator_Linear:MCP1727-0802xSN +Regulator_Linear:MCP1727-1202xMF +Regulator_Linear:MCP1727-1202xSN +Regulator_Linear:MCP1727-1802xMF +Regulator_Linear:MCP1727-1802xSN +Regulator_Linear:MCP1727-2502xMF +Regulator_Linear:MCP1727-2502xSN +Regulator_Linear:MCP1727-3002xMF +Regulator_Linear:MCP1727-3002xSN +Regulator_Linear:MCP1727-3302xMF +Regulator_Linear:MCP1727-3302xSN +Regulator_Linear:MCP1727-5002xMF +Regulator_Linear:MCP1727-5002xSN +Regulator_Linear:MCP1727-ADJxMF +Regulator_Linear:MCP1727-ADJxSN +Regulator_Linear:MCP1754S-1802xCB +Regulator_Linear:MCP1754S-1802xMB +Regulator_Linear:MCP1754S-3302xCB +Regulator_Linear:MCP1754S-3302xMB +Regulator_Linear:MCP1754S-5002xCB +Regulator_Linear:MCP1754S-5002xMB +Regulator_Linear:MCP1792x-3302xCB +Regulator_Linear:MCP1792x-3302xDB +Regulator_Linear:MCP1792x-4102xCB +Regulator_Linear:MCP1792x-4102xDB +Regulator_Linear:MCP1792x-5002xCB +Regulator_Linear:MCP1792x-5002xDB +Regulator_Linear:MCP1799x-330xxTT +Regulator_Linear:MCP1799x-500xxTT +Regulator_Linear:MCP1802x-xx02xOT +Regulator_Linear:MCP1804x-1802xDB +Regulator_Linear:MCP1804x-1802xMB +Regulator_Linear:MCP1804x-1802xMT +Regulator_Linear:MCP1804x-1802xOT +Regulator_Linear:MCP1804x-2502xDB +Regulator_Linear:MCP1804x-2502xMB +Regulator_Linear:MCP1804x-2502xMT +Regulator_Linear:MCP1804x-2502xOT +Regulator_Linear:MCP1804x-3002xDB +Regulator_Linear:MCP1804x-3002xMB +Regulator_Linear:MCP1804x-3002xMT +Regulator_Linear:MCP1804x-3002xOT +Regulator_Linear:MCP1804x-3302xDB +Regulator_Linear:MCP1804x-3302xMB +Regulator_Linear:MCP1804x-3302xMT +Regulator_Linear:MCP1804x-3302xOT +Regulator_Linear:MCP1804x-5002xDB +Regulator_Linear:MCP1804x-5002xMB +Regulator_Linear:MCP1804x-5002xMT +Regulator_Linear:MCP1804x-5002xOT +Regulator_Linear:MCP1804x-A002xDB +Regulator_Linear:MCP1804x-A002xMB +Regulator_Linear:MCP1804x-A002xMT +Regulator_Linear:MCP1804x-A002xOT +Regulator_Linear:MCP1804x-C002xDB +Regulator_Linear:MCP1804x-C002xMB +Regulator_Linear:MCP1804x-C002xMT +Regulator_Linear:MCP1804x-C002xOT +Regulator_Linear:MCP1825S +Regulator_Linear:MCP1826S +Regulator_Linear:ME6211C10M5 +Regulator_Linear:ME6211C12M5 +Regulator_Linear:ME6211C15M5 +Regulator_Linear:ME6211C18M5 +Regulator_Linear:ME6211C21M5 +Regulator_Linear:ME6211C25M5 +Regulator_Linear:ME6211C27M5 +Regulator_Linear:ME6211C28M5 +Regulator_Linear:ME6211C29M5 +Regulator_Linear:ME6211C30M5 +Regulator_Linear:ME6211C33M5 +Regulator_Linear:ME6211C50M5 +Regulator_Linear:MIC29152WT +Regulator_Linear:MIC29152WU +Regulator_Linear:MIC29153WT +Regulator_Linear:MIC29153WU +Regulator_Linear:MIC29302AWD +Regulator_Linear:MIC29302AWU +Regulator_Linear:MIC29302WT +Regulator_Linear:MIC29302WU +Regulator_Linear:MIC29303WT +Regulator_Linear:MIC29303WU +Regulator_Linear:MIC29502WT +Regulator_Linear:MIC29502WU +Regulator_Linear:MIC29503WT +Regulator_Linear:MIC29503WU +Regulator_Linear:MIC29752WT +Regulator_Linear:MIC5205-2.5YM5 +Regulator_Linear:MIC5205-2.7YM5 +Regulator_Linear:MIC5205-2.85YM5 +Regulator_Linear:MIC5205-2.8YM5 +Regulator_Linear:MIC5205-2.9YM5 +Regulator_Linear:MIC5205-3.0YM5 +Regulator_Linear:MIC5205-3.1YM5 +Regulator_Linear:MIC5205-3.2YM5 +Regulator_Linear:MIC5205-3.3YM5 +Regulator_Linear:MIC5205-3.6YM5 +Regulator_Linear:MIC5205-3.8YM5 +Regulator_Linear:MIC5205-4.0YM5 +Regulator_Linear:MIC5205-5.0YM5 +Regulator_Linear:MIC5205YM5 +Regulator_Linear:MIC5219-2.5YM5 +Regulator_Linear:MIC5219-2.5YMM +Regulator_Linear:MIC5219-2.6YM5 +Regulator_Linear:MIC5219-2.7YM5 +Regulator_Linear:MIC5219-2.85YM5 +Regulator_Linear:MIC5219-2.85YMM +Regulator_Linear:MIC5219-2.8YM5 +Regulator_Linear:MIC5219-2.9YM5 +Regulator_Linear:MIC5219-3.0YM5 +Regulator_Linear:MIC5219-3.0YMM +Regulator_Linear:MIC5219-3.1YM5 +Regulator_Linear:MIC5219-3.3YM5 +Regulator_Linear:MIC5219-3.3YMM +Regulator_Linear:MIC5219-3.6YM5 +Regulator_Linear:MIC5219-3.6YMM +Regulator_Linear:MIC5219-5.0YM5 +Regulator_Linear:MIC5219-5.0YMM +Regulator_Linear:MIC5219YM5 +Regulator_Linear:MIC5219YMM +Regulator_Linear:MIC5317-1.0xM5 +Regulator_Linear:MIC5317-1.2xM5 +Regulator_Linear:MIC5317-1.5xM5 +Regulator_Linear:MIC5317-1.8xM5 +Regulator_Linear:MIC5317-2.5xM5 +Regulator_Linear:MIC5317-2.8xM5 +Regulator_Linear:MIC5317-3.0xM5 +Regulator_Linear:MIC5317-3.3xM5 +Regulator_Linear:MIC5350 +Regulator_Linear:MIC5353-1.8YMT +Regulator_Linear:MIC5353-2.5YMT +Regulator_Linear:MIC5353-2.6YMT +Regulator_Linear:MIC5353-2.8YMT +Regulator_Linear:MIC5353-3.0YMT +Regulator_Linear:MIC5353-3.3YMT +Regulator_Linear:MIC5353YMT +Regulator_Linear:MIC5355-G4YMME +Regulator_Linear:MIC5355-JGYMME +Regulator_Linear:MIC5355-S4YMME +Regulator_Linear:MIC5355-SCYMME +Regulator_Linear:MIC5355-SGYMME +Regulator_Linear:MIC5356-G4YMME +Regulator_Linear:MIC5356-JGYMME +Regulator_Linear:MIC5356-MGYML +Regulator_Linear:MIC5356-MMYML +Regulator_Linear:MIC5356-S4YMME +Regulator_Linear:MIC5356-SCYMME +Regulator_Linear:MIC5356-SGYMME +Regulator_Linear:MIC5365-3.3YC5 +Regulator_Linear:MIC5365-3.3YD5 +Regulator_Linear:MIC5366-3.3YC5 +Regulator_Linear:MIC5501-3.0YM5 +Regulator_Linear:MIC5504-1.2YM5 +Regulator_Linear:MIC5504-1.8YM5 +Regulator_Linear:MIC5504-2.5YM5 +Regulator_Linear:MIC5504-2.8YM5 +Regulator_Linear:MIC5504-3.3YM5 +Regulator_Linear:NCP1117-1.5_SOT223 +Regulator_Linear:NCP1117-1.5_TO252 +Regulator_Linear:NCP1117-1.8_SOT223 +Regulator_Linear:NCP1117-1.8_TO252 +Regulator_Linear:NCP1117-1.9_TO252 +Regulator_Linear:NCP1117-12_SOT223 +Regulator_Linear:NCP1117-12_TO252 +Regulator_Linear:NCP1117-2.0_SOT223 +Regulator_Linear:NCP1117-2.0_TO252 +Regulator_Linear:NCP1117-2.5_SOT223 +Regulator_Linear:NCP1117-2.5_TO252 +Regulator_Linear:NCP1117-2.85_SOT223 +Regulator_Linear:NCP1117-2.85_TO252 +Regulator_Linear:NCP1117-3.3_SOT223 +Regulator_Linear:NCP1117-3.3_TO252 +Regulator_Linear:NCP1117-5.0_SOT223 +Regulator_Linear:NCP1117-5.0_TO252 +Regulator_Linear:NCP1117-ADJ_SOT223 +Regulator_Linear:NCP1117-ADJ_TO252 +Regulator_Linear:NCP115AMX120TCG +Regulator_Linear:NCP115AMX250TCG +Regulator_Linear:NCP133AMX100TCG +Regulator_Linear:NCP163AFCS120T2G +Regulator_Linear:NCP163AFCS180T2G +Regulator_Linear:NCP163AFCS250T2G +Regulator_Linear:NCP163AFCS260T2G +Regulator_Linear:NCP163AFCS270T2G +Regulator_Linear:NCP163AFCS280T2G +Regulator_Linear:NCP163AFCS285T2G +Regulator_Linear:NCP163AFCS290T2G +Regulator_Linear:NCP163AFCS2925T2G +Regulator_Linear:NCP163AFCS514T2G +Regulator_Linear:NCP163AFCT120T2G +Regulator_Linear:NCP163AFCT180T2G +Regulator_Linear:NCP163AFCT250T2G +Regulator_Linear:NCP163AFCT260T2G +Regulator_Linear:NCP163AFCT270T2G +Regulator_Linear:NCP163AFCT280T2G +Regulator_Linear:NCP163AFCT285T2G +Regulator_Linear:NCP163AFCT290T2G +Regulator_Linear:NCP163AFCT2925T2G +Regulator_Linear:NCP163AFCT300T2G +Regulator_Linear:NCP163AFCT330T2G +Regulator_Linear:NCP163AFCT514T2G +Regulator_Linear:NCP163ASN150T1G +Regulator_Linear:NCP163ASN180T1G +Regulator_Linear:NCP163ASN250T1G +Regulator_Linear:NCP163ASN270T1G +Regulator_Linear:NCP163ASN280T1G +Regulator_Linear:NCP163ASN300T1G +Regulator_Linear:NCP163ASN330T1G +Regulator_Linear:NCP163ASN350T1G +Regulator_Linear:NCP163ASN500T1G +Regulator_Linear:NCP163BFCS180T2G +Regulator_Linear:NCP163BFCS2925T2G +Regulator_Linear:NCP163BFCT180T2G +Regulator_Linear:NCP163CFCS285T2G +Regulator_Linear:NCP662SQ15 +Regulator_Linear:NCP662SQ18 +Regulator_Linear:NCP662SQ33 +Regulator_Linear:NCP662SQ50 +Regulator_Linear:NCP718xSN120 +Regulator_Linear:NCP718xSN150 +Regulator_Linear:NCP718xSN180 +Regulator_Linear:NCP718xSN250 +Regulator_Linear:NCP718xSN300 +Regulator_Linear:NCP718xSN330 +Regulator_Linear:NCP718xSN500 +Regulator_Linear:NCV8114ASN120T1G +Regulator_Linear:NCV8114ASN150T1G +Regulator_Linear:NCV8114ASN165T1G +Regulator_Linear:NCV8114ASN180T1G +Regulator_Linear:NCV8114ASN250T1G +Regulator_Linear:NCV8114ASN280T1G +Regulator_Linear:NCV8114ASN300T1G +Regulator_Linear:NCV8114ASN330T1G +Regulator_Linear:NCV8114BSN120T1G +Regulator_Linear:NCV8114BSN150T1G +Regulator_Linear:NCV8114BSN180T1G +Regulator_Linear:NCV8114BSN280T1G +Regulator_Linear:NCV8114BSN300T1G +Regulator_Linear:NCV8114BSN330T1G +Regulator_Linear:NDP6802SF-33 +Regulator_Linear:NDP6802SF-50 +Regulator_Linear:NDP6802SF-A2 +Regulator_Linear:NVC8674DS120 +Regulator_Linear:NVC8674DS50 +Regulator_Linear:OM1323_TO220 +Regulator_Linear:SPX2920M3-3.3_SOT223 +Regulator_Linear:SPX2920M3-5.0_SOT223 +Regulator_Linear:SPX2920S-3.3_SO8 +Regulator_Linear:SPX2920S-5.0_SO8 +Regulator_Linear:SPX2920T-3.3_TO263 +Regulator_Linear:SPX2920T-5.0_TO263 +Regulator_Linear:SPX2920U-3.3_TO220 +Regulator_Linear:SPX2920U-5.0_TO220 +Regulator_Linear:SPX3819M5-L +Regulator_Linear:SPX3819M5-L-1-2 +Regulator_Linear:SPX3819M5-L-1-5 +Regulator_Linear:SPX3819M5-L-1-8 +Regulator_Linear:SPX3819M5-L-2-5 +Regulator_Linear:SPX3819M5-L-3-0 +Regulator_Linear:SPX3819M5-L-3-3 +Regulator_Linear:SPX3819M5-L-5-0 +Regulator_Linear:TC1014-xCT +Regulator_Linear:TC1015-xCT +Regulator_Linear:TC1017-xCT +Regulator_Linear:TC1017-xLT +Regulator_Linear:TC1017R-xLT +Regulator_Linear:TC1054 +Regulator_Linear:TC1055 +Regulator_Linear:TC1185-xCT +Regulator_Linear:TC1186 +Regulator_Linear:TC1262-25 +Regulator_Linear:TC1262-28 +Regulator_Linear:TC1262-30 +Regulator_Linear:TC1262-33 +Regulator_Linear:TC1262-50 +Regulator_Linear:TC2014-1.8VxCTTR +Regulator_Linear:TC2014-2.5VxCTTR +Regulator_Linear:TC2014-2.6VxCTTR +Regulator_Linear:TC2014-2.7VxCTTR +Regulator_Linear:TC2014-2.85VxCTTR +Regulator_Linear:TC2014-2.8VxCTTR +Regulator_Linear:TC2014-3.0VxCTTR +Regulator_Linear:TC2014-3.3VxCTTR +Regulator_Linear:TC2014-5.0VxCTTR +Regulator_Linear:TC2015-1.8VxCTTR +Regulator_Linear:TC2015-2.5VxCTTR +Regulator_Linear:TC2015-2.6VxCTTR +Regulator_Linear:TC2015-2.7VxCTTR +Regulator_Linear:TC2015-2.85VxCTTR +Regulator_Linear:TC2015-2.8VxCTTR +Regulator_Linear:TC2015-3.0VxCTTR +Regulator_Linear:TC2015-3.3VxCTTR +Regulator_Linear:TC2015-5.0VxCTTR +Regulator_Linear:TC2185-1.8VxCTTR +Regulator_Linear:TC2185-2.5VxCTTR +Regulator_Linear:TC2185-2.6VxCTTR +Regulator_Linear:TC2185-2.7VxCTTR +Regulator_Linear:TC2185-2.85VxCTTR +Regulator_Linear:TC2185-2.8VxCTTR +Regulator_Linear:TC2185-3.0VxCTTR +Regulator_Linear:TC2185-3.3VxCTTR +Regulator_Linear:TC2185-5.0VxCTTR +Regulator_Linear:TCR2EE10 +Regulator_Linear:TCR2EE105 +Regulator_Linear:TCR2EE11 +Regulator_Linear:TCR2EE115 +Regulator_Linear:TCR2EE12 +Regulator_Linear:TCR2EE125 +Regulator_Linear:TCR2EE13 +Regulator_Linear:TCR2EE135 +Regulator_Linear:TCR2EE14 +Regulator_Linear:TCR2EE145 +Regulator_Linear:TCR2EE15 +Regulator_Linear:TCR2EE17 +Regulator_Linear:TCR2EE18 +Regulator_Linear:TCR2EE185 +Regulator_Linear:TCR2EE19 +Regulator_Linear:TCR2EE20 +Regulator_Linear:TCR2EE24 +Regulator_Linear:TCR2EE25 +Regulator_Linear:TCR2EE27 +Regulator_Linear:TCR2EE275 +Regulator_Linear:TLV1117-15 +Regulator_Linear:TLV1117-18 +Regulator_Linear:TLV1117-25 +Regulator_Linear:TLV1117-33 +Regulator_Linear:TLV1117-50 +Regulator_Linear:TLV1117-ADJ +Regulator_Linear:TLV70012_SOT23-5 +Regulator_Linear:TLV70012_SOT353 +Regulator_Linear:TLV70012_WSON6 +Regulator_Linear:TLV70013_SOT23-5 +Regulator_Linear:TLV70015_SOT23-5 +Regulator_Linear:TLV70015_SOT353 +Regulator_Linear:TLV70015_WSON6 +Regulator_Linear:TLV70018_SOT23-5 +Regulator_Linear:TLV70018_SOT353 +Regulator_Linear:TLV70018_WSON6 +Regulator_Linear:TLV70019_SOT23-5 +Regulator_Linear:TLV70025_SOT23-5 +Regulator_Linear:TLV70025_SOT353 +Regulator_Linear:TLV70025_WSON6 +Regulator_Linear:TLV70028_SOT353 +Regulator_Linear:TLV70028_WSON6 +Regulator_Linear:TLV70029_WSON6 +Regulator_Linear:TLV70030_SOT23-5 +Regulator_Linear:TLV70030_SOT353 +Regulator_Linear:TLV70030_WSON6 +Regulator_Linear:TLV70031_WSON6 +Regulator_Linear:TLV70032_SOT23-5 +Regulator_Linear:TLV70033_SOT23-5 +Regulator_Linear:TLV70033_SOT353 +Regulator_Linear:TLV70033_WSON6 +Regulator_Linear:TLV70036_SOT23-5 +Regulator_Linear:TLV70036_WSON6 +Regulator_Linear:TLV70212_SOT23-5 +Regulator_Linear:TLV70215_SOT23-5 +Regulator_Linear:TLV70218_SOT23-5 +Regulator_Linear:TLV70225_SOT23-5 +Regulator_Linear:TLV70225_WSON6 +Regulator_Linear:TLV70228_SOT23-5 +Regulator_Linear:TLV70228_WSON6 +Regulator_Linear:TLV70229_WSON6 +Regulator_Linear:TLV70230_SOT23-5 +Regulator_Linear:TLV70231_SOT23-5 +Regulator_Linear:TLV70233_SOT23-5 +Regulator_Linear:TLV70233_WSON6 +Regulator_Linear:TLV70235_SOT23-5 +Regulator_Linear:TLV70236_WSON6 +Regulator_Linear:TLV70237_SOT23-5 +Regulator_Linear:TLV70237_WSON6 +Regulator_Linear:TLV70242PDSE +Regulator_Linear:TLV70245_SOT23-5 +Regulator_Linear:TLV702475_SOT23-5 +Regulator_Linear:TLV7113318DDSE +Regulator_Linear:TLV7113333DDSE +Regulator_Linear:TLV71209_SOT23-5 +Regulator_Linear:TLV71210_SOT23-5 +Regulator_Linear:TLV71211_SOT23-5 +Regulator_Linear:TLV71310PDBV +Regulator_Linear:TLV71311PDBV +Regulator_Linear:TLV71312PDBV +Regulator_Linear:TLV71315PDBV +Regulator_Linear:TLV713185PDBV +Regulator_Linear:TLV71318PDBV +Regulator_Linear:TLV71325PDBV +Regulator_Linear:TLV713285PDBV +Regulator_Linear:TLV71328PDBV +Regulator_Linear:TLV71330PDBV +Regulator_Linear:TLV71333PDBV +Regulator_Linear:TLV71x_WSON-6 +Regulator_Linear:TLV73310PDBV +Regulator_Linear:TLV73311PDBV +Regulator_Linear:TLV73312PDBV +Regulator_Linear:TLV73315PDBV +Regulator_Linear:TLV73318PDBV +Regulator_Linear:TLV73325PDBV +Regulator_Linear:TLV733285PDBV +Regulator_Linear:TLV73328PDBV +Regulator_Linear:TLV73330PDBV +Regulator_Linear:TLV73333PDBV +Regulator_Linear:TLV75509PDBV +Regulator_Linear:TLV75509PDRV +Regulator_Linear:TLV75510PDBV +Regulator_Linear:TLV75510PDRV +Regulator_Linear:TLV75512PDBV +Regulator_Linear:TLV75512PDRV +Regulator_Linear:TLV75515PDBV +Regulator_Linear:TLV75515PDRV +Regulator_Linear:TLV75518PDBV +Regulator_Linear:TLV75518PDRV +Regulator_Linear:TLV75519PDBV +Regulator_Linear:TLV75519PDRV +Regulator_Linear:TLV75525PDBV +Regulator_Linear:TLV75525PDRV +Regulator_Linear:TLV75528PDBV +Regulator_Linear:TLV75528PDRV +Regulator_Linear:TLV75529PDBV +Regulator_Linear:TLV75529PDRV +Regulator_Linear:TLV75530PDBV +Regulator_Linear:TLV75530PDRV +Regulator_Linear:TLV75533PDBV +Regulator_Linear:TLV75533PDRV +Regulator_Linear:TLV75709PDBV +Regulator_Linear:TLV75709PDRV +Regulator_Linear:TLV75710PDBV +Regulator_Linear:TLV75710PDRV +Regulator_Linear:TLV75712PDBV +Regulator_Linear:TLV75712PDRV +Regulator_Linear:TLV75715PDBV +Regulator_Linear:TLV75715PDRV +Regulator_Linear:TLV75718PDBV +Regulator_Linear:TLV75718PDRV +Regulator_Linear:TLV75719PDBV +Regulator_Linear:TLV75719PDRV +Regulator_Linear:TLV75725PDBV +Regulator_Linear:TLV75725PDRV +Regulator_Linear:TLV75728PDBV +Regulator_Linear:TLV75728PDRV +Regulator_Linear:TLV75729PDBV +Regulator_Linear:TLV75730PDBV +Regulator_Linear:TLV75730PDRV +Regulator_Linear:TLV75733PDBV +Regulator_Linear:TLV75733PDRV +Regulator_Linear:TLV75740PDRV +Regulator_Linear:TLV75801PDBV +Regulator_Linear:TLV75801PDRV +Regulator_Linear:TLV76133DCY +Regulator_Linear:TLV76150DCY +Regulator_Linear:TLV76701DRVx +Regulator_Linear:TLV76701QWDRBxQ1 +Regulator_Linear:TLV76708DRVx +Regulator_Linear:TLV76718DRVx +Regulator_Linear:TLV76728DRVx +Regulator_Linear:TLV76733DRVx +Regulator_Linear:TLV76733QWDRBxQ1 +Regulator_Linear:TLV76750DRVx +Regulator_Linear:TLV76750QWDRBxQ1 +Regulator_Linear:TLV76760QWDRBxQ1 +Regulator_Linear:TLV76780QWDRBxQ1 +Regulator_Linear:TLV76790QWDRBxQ1 +Regulator_Linear:TPS51200DRC +Regulator_Linear:TPS70202_HTSSOP20 +Regulator_Linear:TPS70245_HTSSOP20 +Regulator_Linear:TPS70248_HTSSOP20 +Regulator_Linear:TPS70251_HTSSOP20 +Regulator_Linear:TPS70258_HTSSOP20 +Regulator_Linear:TPS70302 +Regulator_Linear:TPS70345 +Regulator_Linear:TPS70348 +Regulator_Linear:TPS70351 +Regulator_Linear:TPS70358 +Regulator_Linear:TPS70402 +Regulator_Linear:TPS70445 +Regulator_Linear:TPS70448 +Regulator_Linear:TPS70451 +Regulator_Linear:TPS70458 +Regulator_Linear:TPS7101 +Regulator_Linear:TPS7133 +Regulator_Linear:TPS7148 +Regulator_Linear:TPS7150 +Regulator_Linear:TPS71518__SC70 +Regulator_Linear:TPS71519__SC70 +Regulator_Linear:TPS71523__SC70 +Regulator_Linear:TPS71525__SC70 +Regulator_Linear:TPS71530__SC70 +Regulator_Linear:TPS71533__SC70 +Regulator_Linear:TPS715345__SC70 +Regulator_Linear:TPS71550__SC70 +Regulator_Linear:TPS72201 +Regulator_Linear:TPS72215 +Regulator_Linear:TPS72216 +Regulator_Linear:TPS72218 +Regulator_Linear:TPS72301DBV +Regulator_Linear:TPS72301DDC +Regulator_Linear:TPS72325DBV +Regulator_Linear:TPS73018DBV +Regulator_Linear:TPS730285DBV +Regulator_Linear:TPS73101DBV +Regulator_Linear:TPS731125DBV +Regulator_Linear:TPS73115DBV +Regulator_Linear:TPS73118DBV +Regulator_Linear:TPS73125DBV +Regulator_Linear:TPS73130DBV +Regulator_Linear:TPS73131DBV +Regulator_Linear:TPS73132DBV +Regulator_Linear:TPS73133DBV +Regulator_Linear:TPS73150DBV +Regulator_Linear:TPS73601DBV +Regulator_Linear:TPS736125DBV +Regulator_Linear:TPS73615DBV +Regulator_Linear:TPS73616DBV +Regulator_Linear:TPS73618DBV +Regulator_Linear:TPS73619DBV +Regulator_Linear:TPS73625DBV +Regulator_Linear:TPS73630DBV +Regulator_Linear:TPS73632DBV +Regulator_Linear:TPS73633DBV +Regulator_Linear:TPS73643DBV +Regulator_Linear:TPS74401_VQFN +Regulator_Linear:TPS74801AWDRC +Regulator_Linear:TPS74801DRC +Regulator_Linear:TPS75005RGW +Regulator_Linear:TPS76301 +Regulator_Linear:TPS76316 +Regulator_Linear:TPS76318 +Regulator_Linear:TPS76325 +Regulator_Linear:TPS76327 +Regulator_Linear:TPS76329 +Regulator_Linear:TPS76330 +Regulator_Linear:TPS76333 +Regulator_Linear:TPS76338 +Regulator_Linear:TPS76350 +Regulator_Linear:TPS76901 +Regulator_Linear:TPS76912 +Regulator_Linear:TPS76915 +Regulator_Linear:TPS76918 +Regulator_Linear:TPS76925 +Regulator_Linear:TPS76927 +Regulator_Linear:TPS76928 +Regulator_Linear:TPS76930 +Regulator_Linear:TPS76933 +Regulator_Linear:TPS76950 +Regulator_Linear:TPS77701_HTSSOP20 +Regulator_Linear:TPS77701_SO8 +Regulator_Linear:TPS77715_HTSSOP20 +Regulator_Linear:TPS77715_SO8 +Regulator_Linear:TPS77718_HTSSOP20 +Regulator_Linear:TPS77718_SO8 +Regulator_Linear:TPS77725_HTSSOP20 +Regulator_Linear:TPS77725_SO8 +Regulator_Linear:TPS77733_HTSSOP20 +Regulator_Linear:TPS77733_SO8 +Regulator_Linear:TPS77801_HTSSOP20 +Regulator_Linear:TPS77801_SO8 +Regulator_Linear:TPS77815_HTSSOP20 +Regulator_Linear:TPS77815_SO8 +Regulator_Linear:TPS77818_HTSSOP20 +Regulator_Linear:TPS77818_SO8 +Regulator_Linear:TPS77825_HTSSOP20 +Regulator_Linear:TPS77825_SO8 +Regulator_Linear:TPS77833_HTSSOP20 +Regulator_Linear:TPS77833_SO8 +Regulator_Linear:TPS78218DDC +Regulator_Linear:TPS78223DDC +Regulator_Linear:TPS78225DDC +Regulator_Linear:TPS78227DDC +Regulator_Linear:TPS78228DDC +Regulator_Linear:TPS78230DDC +Regulator_Linear:TPS78233DDC +Regulator_Linear:TPS78236DDC +Regulator_Linear:TPS79301-EP +Regulator_Linear:TPS79318-EP +Regulator_Linear:TPS79325-EP +Regulator_Linear:TPS79328-EP +Regulator_Linear:TPS793285-EP +Regulator_Linear:TPS79330-EP +Regulator_Linear:TPS79333-EP +Regulator_Linear:TPS793475-EP +Regulator_Linear:TPS7A0508PDBV +Regulator_Linear:TPS7A0508PDBZ +Regulator_Linear:TPS7A0510PDBV +Regulator_Linear:TPS7A0512PDBV +Regulator_Linear:TPS7A0512PDBZ +Regulator_Linear:TPS7A0515PDBV +Regulator_Linear:TPS7A0518PDBV +Regulator_Linear:TPS7A0518PDBZ +Regulator_Linear:TPS7A0520PDBZ +Regulator_Linear:TPS7A0522PDBV +Regulator_Linear:TPS7A0522PDBZ +Regulator_Linear:TPS7A0525PDBV +Regulator_Linear:TPS7A0527PDBZ +Regulator_Linear:TPS7A05285PDBV +Regulator_Linear:TPS7A0528PDBZ +Regulator_Linear:TPS7A0530PDBV +Regulator_Linear:TPS7A0530PDBZ +Regulator_Linear:TPS7A0531PDBV +Regulator_Linear:TPS7A0533PDBV +Regulator_Linear:TPS7A0533PDBZ +Regulator_Linear:TPS7A20xxxDQN +Regulator_Linear:TPS7A3301RGW +Regulator_Linear:TPS7A39 +Regulator_Linear:TPS7A4101DGN +Regulator_Linear:TPS7A4701xRGW +Regulator_Linear:TPS7A7001DDA +Regulator_Linear:TPS7A7200RGW +Regulator_Linear:TPS7A90 +Regulator_Linear:TPS7A91 +Regulator_Linear:UA78M05QDCYRQ1 +Regulator_Linear:UA78M08QDCYRQ1 +Regulator_Linear:UA78M10QDCYRQ1 +Regulator_Linear:UA78M33QDCYRQ1 +Regulator_Linear:XC6206PxxxMR +Regulator_Linear:XC6210B332MR +Regulator_Linear:XC6220B331MR +Regulator_Linear:uA7805 +Regulator_Linear:uA7808 +Regulator_Linear:uA7810 +Regulator_Linear:uA7812 +Regulator_Linear:uA7815 +Regulator_Linear:uA7824 +Regulator_SwitchedCapacitor:CAT3200 +Regulator_SwitchedCapacitor:CAT3200-5 +Regulator_SwitchedCapacitor:ICL7660 +Regulator_SwitchedCapacitor:LM2665M6 +Regulator_SwitchedCapacitor:LM2775DSG +Regulator_SwitchedCapacitor:LM2776 +Regulator_SwitchedCapacitor:LM27761 +Regulator_SwitchedCapacitor:LM27762 +Regulator_SwitchedCapacitor:LM7705 +Regulator_SwitchedCapacitor:LMC7660 +Regulator_SwitchedCapacitor:LT1054 +Regulator_SwitchedCapacitor:LT1054L +Regulator_SwitchedCapacitor:LT1054xSW +Regulator_SwitchedCapacitor:LTC1044 +Regulator_SwitchedCapacitor:LTC1502xMS8-3.3 +Regulator_SwitchedCapacitor:LTC1502xS8-3.3 +Regulator_SwitchedCapacitor:LTC1503CMS8-1.8 +Regulator_SwitchedCapacitor:LTC1503CMS8-2 +Regulator_SwitchedCapacitor:LTC1503xS8-1.8 +Regulator_SwitchedCapacitor:LTC1503xS8-2 +Regulator_SwitchedCapacitor:LTC1751 +Regulator_SwitchedCapacitor:LTC1754 +Regulator_SwitchedCapacitor:LTC3260xDE +Regulator_SwitchedCapacitor:LTC3260xMSE +Regulator_SwitchedCapacitor:LTC660 +Regulator_SwitchedCapacitor:MAX1044 +Regulator_SwitchedCapacitor:RT9361AxE +Regulator_SwitchedCapacitor:RT9361BxE +Regulator_SwitchedCapacitor:TPS60151DRV +Regulator_SwitchedCapacitor:TPS60400DBV +Regulator_SwitchedCapacitor:TPS60401DBV +Regulator_SwitchedCapacitor:TPS60402DBV +Regulator_SwitchedCapacitor:TPS60403DBV +Regulator_SwitchedCapacitor:TPS60500DGS +Regulator_SwitchedCapacitor:TPS60501DGS +Regulator_SwitchedCapacitor:TPS60502DGS +Regulator_SwitchedCapacitor:TPS60503DGS +Regulator_Switching:171050601 +Regulator_Switching:A4403GEU +Regulator_Switching:AAT1217ICA-1.2 +Regulator_Switching:AAT1217ICA-3.3 +Regulator_Switching:AAT1217ICA-5.0 +Regulator_Switching:AAT1217IGU-3.3 +Regulator_Switching:ADP1108AN +Regulator_Switching:ADP1108AN-12 +Regulator_Switching:ADP1108AN-3.3 +Regulator_Switching:ADP1108AN-5 +Regulator_Switching:ADP1108AR +Regulator_Switching:ADP1108AR-12 +Regulator_Switching:ADP1108AR-3.3 +Regulator_Switching:ADP1108AR-5 +Regulator_Switching:ADP2108AUJ-1.0 +Regulator_Switching:ADP2108AUJ-1.1 +Regulator_Switching:ADP2108AUJ-1.2 +Regulator_Switching:ADP2108AUJ-1.3 +Regulator_Switching:ADP2108AUJ-1.5 +Regulator_Switching:ADP2108AUJ-1.8 +Regulator_Switching:ADP2108AUJ-1.82 +Regulator_Switching:ADP2108AUJ-2.3 +Regulator_Switching:ADP2108AUJ-2.5 +Regulator_Switching:ADP2108AUJ-3.0 +Regulator_Switching:ADP2108AUJ-3.3 +Regulator_Switching:ADP2302ARDZ +Regulator_Switching:ADP2302ARDZ-2.5 +Regulator_Switching:ADP2302ARDZ-3.3 +Regulator_Switching:ADP2302ARDZ-5.0 +Regulator_Switching:ADP2303ARDZ +Regulator_Switching:ADP2303ARDZ-2.5 +Regulator_Switching:ADP2303ARDZ-3.3 +Regulator_Switching:ADP2303ARDZ-5.0 +Regulator_Switching:ADP2360xCP +Regulator_Switching:ADP2360xCP-3.3 +Regulator_Switching:ADP2360xCP-5.0 +Regulator_Switching:ADP5054 +Regulator_Switching:ADP5070AREZ +Regulator_Switching:ADP5071AREZ +Regulator_Switching:ADuM6000 +Regulator_Switching:AOZ1280CI +Regulator_Switching:AOZ1282CI +Regulator_Switching:AOZ1282CI-1 +Regulator_Switching:AOZ6663DI +Regulator_Switching:AOZ6663DI-01 +Regulator_Switching:AP3012 +Regulator_Switching:AP3211K +Regulator_Switching:AP3402 +Regulator_Switching:AP3441SHE +Regulator_Switching:AP62150WU +Regulator_Switching:AP62150Z6 +Regulator_Switching:AP62250WU +Regulator_Switching:AP62250Z6 +Regulator_Switching:AP62300TWU +Regulator_Switching:AP62300WU +Regulator_Switching:AP62300Z6 +Regulator_Switching:AP62301WU +Regulator_Switching:AP63200WU +Regulator_Switching:AP63201WU +Regulator_Switching:AP63203WU +Regulator_Switching:AP63205WU +Regulator_Switching:AP6502 +Regulator_Switching:AP6503 +Regulator_Switching:AP65111AWU +Regulator_Switching:APE1707H-12-HF +Regulator_Switching:APE1707H-33-HF +Regulator_Switching:APE1707H-50-HF +Regulator_Switching:APE1707H-HF +Regulator_Switching:APE1707M-12-HF +Regulator_Switching:APE1707M-33-HF +Regulator_Switching:APE1707M-50-HF +Regulator_Switching:APE1707M-HF +Regulator_Switching:APE1707S-12-HF +Regulator_Switching:APE1707S-33-HF +Regulator_Switching:APE1707S-50-HF +Regulator_Switching:APE1707S-HF +Regulator_Switching:BD9001F +Regulator_Switching:BD9778F +Regulator_Switching:BD9778HFP +Regulator_Switching:BD9781HFP +Regulator_Switching:BD9G341EFJ +Regulator_Switching:CRE1S0305S3C +Regulator_Switching:CRE1S0505DC +Regulator_Switching:CRE1S0505S3C +Regulator_Switching:CRE1S0505SC +Regulator_Switching:CRE1S0515SC +Regulator_Switching:CRE1S1205SC +Regulator_Switching:CRE1S1212SC +Regulator_Switching:CRE1S2405SC +Regulator_Switching:CRE1S2412SC +Regulator_Switching:DIO6970 +Regulator_Switching:FSBH0170 +Regulator_Switching:FSBH0170A +Regulator_Switching:FSBH0170W +Regulator_Switching:FSBH0270 +Regulator_Switching:FSBH0270A +Regulator_Switching:FSBH0270W +Regulator_Switching:FSBH0370 +Regulator_Switching:FSBH0F70A +Regulator_Switching:FSBH0F70WA +Regulator_Switching:FSDH321 +Regulator_Switching:FSDH321L +Regulator_Switching:FSDL321 +Regulator_Switching:FSDL321L +Regulator_Switching:FSL136MRT +Regulator_Switching:FSQ0565RQLDTU +Regulator_Switching:FSQ0565RQWDTU +Regulator_Switching:FSQ0565RSLDTU +Regulator_Switching:FSQ0565RSWDTU +Regulator_Switching:GL2576-12SF8DR +Regulator_Switching:GL2576-12TA5R +Regulator_Switching:GL2576-12TB5T +Regulator_Switching:GL2576-15SF8DR +Regulator_Switching:GL2576-15TA5R +Regulator_Switching:GL2576-15TB5T +Regulator_Switching:GL2576-3.3SF8DR +Regulator_Switching:GL2576-3.3TA5R +Regulator_Switching:GL2576-3.3TB5T +Regulator_Switching:GL2576-5.0SF8DR +Regulator_Switching:GL2576-5.0TA5R +Regulator_Switching:GL2576-5.0TB5T +Regulator_Switching:GL2576-ASF8DR +Regulator_Switching:GL2576-ATA5R +Regulator_Switching:GL2576-ATB5T +Regulator_Switching:HT7463A +Regulator_Switching:HT7463B +Regulator_Switching:ISL8117FRZ +Regulator_Switching:ISL8117FVEZ +Regulator_Switching:KA5H02659RN +Regulator_Switching:KA5H0265RCTU +Regulator_Switching:KA5H0265RCYDTU +Regulator_Switching:KA5H0280RTU +Regulator_Switching:KA5H0280RYDTU +Regulator_Switching:KA5L0265RTU +Regulator_Switching:KA5L0265RYDTU +Regulator_Switching:KA5M02659RN +Regulator_Switching:KA5M0265RTU +Regulator_Switching:KA5M0265RYDTU +Regulator_Switching:KA5M0280RTU +Regulator_Switching:KA5M0280RYDTU +Regulator_Switching:L4962-A +Regulator_Switching:L4962E-A +Regulator_Switching:L4962EH-A +Regulator_Switching:L5973D +Regulator_Switching:L7980A +Regulator_Switching:LD7575 +Regulator_Switching:LGS5116B +Regulator_Switching:LGS5145 +Regulator_Switching:LGS6302B5 +Regulator_Switching:LM22676MR-5 +Regulator_Switching:LM22676MR-ADJ +Regulator_Switching:LM22678TJ-5 +Regulator_Switching:LM22678TJ-ADJ +Regulator_Switching:LM25085MM +Regulator_Switching:LM25085MY +Regulator_Switching:LM25085SD +Regulator_Switching:LM2574HVM-12 +Regulator_Switching:LM2574HVM-15 +Regulator_Switching:LM2574HVM-3.3 +Regulator_Switching:LM2574HVM-5 +Regulator_Switching:LM2574HVM-ADJ +Regulator_Switching:LM2574HVN-12 +Regulator_Switching:LM2574HVN-15 +Regulator_Switching:LM2574HVN-3.3 +Regulator_Switching:LM2574HVN-5 +Regulator_Switching:LM2574HVN-ADJ +Regulator_Switching:LM2574M-12 +Regulator_Switching:LM2574M-15 +Regulator_Switching:LM2574M-3.3 +Regulator_Switching:LM2574M-5 +Regulator_Switching:LM2574M-ADJ +Regulator_Switching:LM2574N-12 +Regulator_Switching:LM2574N-15 +Regulator_Switching:LM2574N-3.3 +Regulator_Switching:LM2574N-5 +Regulator_Switching:LM2574N-ADJ +Regulator_Switching:LM2575-12BT +Regulator_Switching:LM2575-12BU +Regulator_Switching:LM2575-3.3BT +Regulator_Switching:LM2575-3.3BU +Regulator_Switching:LM2575-5.0BT +Regulator_Switching:LM2575-5.0BU +Regulator_Switching:LM2575BT-ADJ +Regulator_Switching:LM2575BU-ADJ +Regulator_Switching:LM2576HVS-12 +Regulator_Switching:LM2576HVS-15 +Regulator_Switching:LM2576HVS-3.3 +Regulator_Switching:LM2576HVS-5 +Regulator_Switching:LM2576HVS-ADJ +Regulator_Switching:LM2576HVT-12 +Regulator_Switching:LM2576HVT-15 +Regulator_Switching:LM2576HVT-3.3 +Regulator_Switching:LM2576HVT-5 +Regulator_Switching:LM2576HVT-ADJ +Regulator_Switching:LM2576S-12 +Regulator_Switching:LM2576S-15 +Regulator_Switching:LM2576S-3.3 +Regulator_Switching:LM2576S-5 +Regulator_Switching:LM2576S-ADJ +Regulator_Switching:LM2576T-12 +Regulator_Switching:LM2576T-15 +Regulator_Switching:LM2576T-3.3 +Regulator_Switching:LM2576T-5 +Regulator_Switching:LM2576T-ADJ +Regulator_Switching:LM2578 +Regulator_Switching:LM2594HVM-12 +Regulator_Switching:LM2594HVM-3.3 +Regulator_Switching:LM2594HVM-5.0 +Regulator_Switching:LM2594HVM-ADJ +Regulator_Switching:LM2594HVN-12 +Regulator_Switching:LM2594HVN-3.3 +Regulator_Switching:LM2594HVN-5.0 +Regulator_Switching:LM2594HVN-ADJ +Regulator_Switching:LM2594M-12 +Regulator_Switching:LM2594M-3.3 +Regulator_Switching:LM2594M-5.0 +Regulator_Switching:LM2594M-ADJ +Regulator_Switching:LM2594N-12 +Regulator_Switching:LM2594N-3.3 +Regulator_Switching:LM2594N-5.0 +Regulator_Switching:LM2594N-ADJ +Regulator_Switching:LM2595S-12 +Regulator_Switching:LM2595S-3.3 +Regulator_Switching:LM2595S-5 +Regulator_Switching:LM2595S-ADJ +Regulator_Switching:LM2595T-12 +Regulator_Switching:LM2595T-3.3 +Regulator_Switching:LM2595T-5 +Regulator_Switching:LM2595T-ADJ +Regulator_Switching:LM2596S-12 +Regulator_Switching:LM2596S-3.3 +Regulator_Switching:LM2596S-5 +Regulator_Switching:LM2596S-ADJ +Regulator_Switching:LM2596T-12 +Regulator_Switching:LM2596T-3.3 +Regulator_Switching:LM2596T-5 +Regulator_Switching:LM2596T-ADJ +Regulator_Switching:LM2611xMF +Regulator_Switching:LM26480SQ +Regulator_Switching:LM2672M-12 +Regulator_Switching:LM2672M-3.3 +Regulator_Switching:LM2672M-5.0 +Regulator_Switching:LM2672M-ADJ +Regulator_Switching:LM2672N-12 +Regulator_Switching:LM2672N-3.3 +Regulator_Switching:LM2672N-5.0 +Regulator_Switching:LM2672N-ADJ +Regulator_Switching:LM2674M-12 +Regulator_Switching:LM2674M-3.3 +Regulator_Switching:LM2674M-5.0 +Regulator_Switching:LM2674M-ADJ +Regulator_Switching:LM2674N-12 +Regulator_Switching:LM2674N-3.3 +Regulator_Switching:LM2674N-5.0 +Regulator_Switching:LM2674N-ADJ +Regulator_Switching:LM2675M-12 +Regulator_Switching:LM2675M-3.3 +Regulator_Switching:LM2675M-5 +Regulator_Switching:LM2675M-ADJ +Regulator_Switching:LM2675N-12 +Regulator_Switching:LM2675N-3.3 +Regulator_Switching:LM2675N-5 +Regulator_Switching:LM2675N-ADJ +Regulator_Switching:LM27313XMF +Regulator_Switching:LM2731XMF +Regulator_Switching:LM2731YMF +Regulator_Switching:LM2733XMF +Regulator_Switching:LM2733YMF +Regulator_Switching:LM2734X +Regulator_Switching:LM2734Y +Regulator_Switching:LM2735XMF +Regulator_Switching:LM2840X +Regulator_Switching:LM2840Y +Regulator_Switching:LM2841X +Regulator_Switching:LM2841Y +Regulator_Switching:LM2842X +Regulator_Switching:LM2842Y +Regulator_Switching:LM3150MH +Regulator_Switching:LM3407MY +Regulator_Switching:LM3578 +Regulator_Switching:LM3670MF +Regulator_Switching:LM5001MA +Regulator_Switching:LM5006MM +Regulator_Switching:LM5007MM +Regulator_Switching:LM5007SD +Regulator_Switching:LM5008MM +Regulator_Switching:LM5008SD +Regulator_Switching:LM5009MM +Regulator_Switching:LM5009SD +Regulator_Switching:LM5017MR +Regulator_Switching:LM5017SD +Regulator_Switching:LM5022MM +Regulator_Switching:LM5088-1 +Regulator_Switching:LM5088-2 +Regulator_Switching:LM5118MH +Regulator_Switching:LM5161PWP +Regulator_Switching:LM5164DDA +Regulator_Switching:LM5165 +Regulator_Switching:LM5165X +Regulator_Switching:LM5165Y +Regulator_Switching:LM5166 +Regulator_Switching:LM5166X +Regulator_Switching:LM5166Y +Regulator_Switching:LM5175PWP +Regulator_Switching:LM5175RHF +Regulator_Switching:LM5176PWP +Regulator_Switching:LM5176RHF +Regulator_Switching:LMR10510XMF +Regulator_Switching:LMR10510YMF +Regulator_Switching:LMR10510YSD +Regulator_Switching:LMR14206 +Regulator_Switching:LMR16006YQ +Regulator_Switching:LMR16006YQ3 +Regulator_Switching:LMR16006YQ5 +Regulator_Switching:LMR33610ADDAR +Regulator_Switching:LMR33610BDDAR +Regulator_Switching:LMR33620ADDA +Regulator_Switching:LMR33620BDDA +Regulator_Switching:LMR33620CDDA +Regulator_Switching:LMR33630ADDA +Regulator_Switching:LMR33630BDDA +Regulator_Switching:LMR33630CDDA +Regulator_Switching:LMR33640ADDA +Regulator_Switching:LMR33640DDDA +Regulator_Switching:LMR36510ADDA +Regulator_Switching:LMR50410 +Regulator_Switching:LMR51430 +Regulator_Switching:LMR62014XMF +Regulator_Switching:LMR62421XMF +Regulator_Switching:LMR62421XSD +Regulator_Switching:LMR64010XMF +Regulator_Switching:LMZ13608 +Regulator_Switching:LMZ22003TZ +Regulator_Switching:LMZ22005TZ +Regulator_Switching:LMZ23603TZ +Regulator_Switching:LMZ23605TZ +Regulator_Switching:LMZM23600 +Regulator_Switching:LMZM23600V3 +Regulator_Switching:LMZM23600V5 +Regulator_Switching:LMZM23601 +Regulator_Switching:LMZM23601V3 +Regulator_Switching:LMZM23601V5 +Regulator_Switching:LNK302D +Regulator_Switching:LNK302G +Regulator_Switching:LNK302P +Regulator_Switching:LNK304D +Regulator_Switching:LNK304G +Regulator_Switching:LNK304P +Regulator_Switching:LNK305D +Regulator_Switching:LNK305G +Regulator_Switching:LNK305P +Regulator_Switching:LNK306D +Regulator_Switching:LNK306G +Regulator_Switching:LNK306P +Regulator_Switching:LNK3202D +Regulator_Switching:LNK3202G +Regulator_Switching:LNK3202P +Regulator_Switching:LNK3204D +Regulator_Switching:LNK3204G +Regulator_Switching:LNK3204P +Regulator_Switching:LNK3205D +Regulator_Switching:LNK3205G +Regulator_Switching:LNK3205P +Regulator_Switching:LNK3206D +Regulator_Switching:LNK3206G +Regulator_Switching:LNK3206P +Regulator_Switching:LNK362D +Regulator_Switching:LNK362G +Regulator_Switching:LNK362P +Regulator_Switching:LNK363D +Regulator_Switching:LNK363G +Regulator_Switching:LNK363P +Regulator_Switching:LNK364D +Regulator_Switching:LNK364G +Regulator_Switching:LNK364P +Regulator_Switching:LNK403EG +Regulator_Switching:LNK403LG +Regulator_Switching:LNK404EG +Regulator_Switching:LNK404LG +Regulator_Switching:LNK405EG +Regulator_Switching:LNK405LG +Regulator_Switching:LNK406EG +Regulator_Switching:LNK406LG +Regulator_Switching:LNK407EG +Regulator_Switching:LNK407LG +Regulator_Switching:LNK408EG +Regulator_Switching:LNK408LG +Regulator_Switching:LNK409EG +Regulator_Switching:LNK409LG +Regulator_Switching:LNK410EG +Regulator_Switching:LNK410LG +Regulator_Switching:LNK413EG +Regulator_Switching:LNK413LG +Regulator_Switching:LNK414EG +Regulator_Switching:LNK414LG +Regulator_Switching:LNK415EG +Regulator_Switching:LNK415LG +Regulator_Switching:LNK416EG +Regulator_Switching:LNK416LG +Regulator_Switching:LNK417EG +Regulator_Switching:LNK417LG +Regulator_Switching:LNK418EG +Regulator_Switching:LNK418LG +Regulator_Switching:LNK419EG +Regulator_Switching:LNK419LG +Regulator_Switching:LNK420EG +Regulator_Switching:LNK420LG +Regulator_Switching:LNK454D +Regulator_Switching:LNK456D +Regulator_Switching:LNK457D +Regulator_Switching:LNK457K +Regulator_Switching:LNK457V +Regulator_Switching:LNK458K +Regulator_Switching:LNK458V +Regulator_Switching:LNK460K +Regulator_Switching:LNK460V +Regulator_Switching:LNK562D +Regulator_Switching:LNK562G +Regulator_Switching:LNK562P +Regulator_Switching:LNK563D +Regulator_Switching:LNK563G +Regulator_Switching:LNK563P +Regulator_Switching:LNK564D +Regulator_Switching:LNK564G +Regulator_Switching:LNK564P +Regulator_Switching:LNK603DG +Regulator_Switching:LNK603PG +Regulator_Switching:LNK604DG +Regulator_Switching:LNK604PG +Regulator_Switching:LNK605DG +Regulator_Switching:LNK605PG +Regulator_Switching:LNK606DG +Regulator_Switching:LNK606GG +Regulator_Switching:LNK606PG +Regulator_Switching:LNK613DG +Regulator_Switching:LNK613PG +Regulator_Switching:LNK614DG +Regulator_Switching:LNK614PG +Regulator_Switching:LNK615DG +Regulator_Switching:LNK615PG +Regulator_Switching:LNK616DG +Regulator_Switching:LNK616GG +Regulator_Switching:LNK616PG +Regulator_Switching:LNK623DG +Regulator_Switching:LNK623PG +Regulator_Switching:LNK624DG +Regulator_Switching:LNK624PG +Regulator_Switching:LNK625DG +Regulator_Switching:LNK625PG +Regulator_Switching:LNK626DG +Regulator_Switching:LNK626PG +Regulator_Switching:LNK632DG +Regulator_Switching:LT1073CN +Regulator_Switching:LT1073CN-12 +Regulator_Switching:LT1073CN-5 +Regulator_Switching:LT1073CS +Regulator_Switching:LT1073CS-12 +Regulator_Switching:LT1073CS-5 +Regulator_Switching:LT1108CN +Regulator_Switching:LT1108CN-12 +Regulator_Switching:LT1108CN-5 +Regulator_Switching:LT1108CS +Regulator_Switching:LT1108CS-12 +Regulator_Switching:LT1108CS-5 +Regulator_Switching:LT1301 +Regulator_Switching:LT1307BCMS8 +Regulator_Switching:LT1307BCS8 +Regulator_Switching:LT1307CMS8 +Regulator_Switching:LT1307CN8 +Regulator_Switching:LT1307CS8 +Regulator_Switching:LT1372CN8 +Regulator_Switching:LT1372CS8 +Regulator_Switching:LT1372HVCN8 +Regulator_Switching:LT1372HVCS8 +Regulator_Switching:LT1373CN8 +Regulator_Switching:LT1373CS8 +Regulator_Switching:LT1373HVCN8 +Regulator_Switching:LT1373HVCS8 +Regulator_Switching:LT1377CN8 +Regulator_Switching:LT1377CS8 +Regulator_Switching:LT1945 +Regulator_Switching:LT3430 +Regulator_Switching:LT3430-1 +Regulator_Switching:LT3439 +Regulator_Switching:LT3471 +Regulator_Switching:LT3472 +Regulator_Switching:LT3483AxS6 +Regulator_Switching:LT3483xS6 +Regulator_Switching:LT3514xUFD +Regulator_Switching:LT3580xDD +Regulator_Switching:LT3580xMS8E +Regulator_Switching:LT3748xMS +Regulator_Switching:LT3757AEDD +Regulator_Switching:LT3757AEMSE +Regulator_Switching:LT3757EDD +Regulator_Switching:LT3757EMSE +Regulator_Switching:LT3988 +Regulator_Switching:LT8303 +Regulator_Switching:LT8306 +Regulator_Switching:LT8610 +Regulator_Switching:LT8610AC +Regulator_Switching:LT8610AC-1 +Regulator_Switching:LT8705AxFE +Regulator_Switching:LTC1436A +Regulator_Switching:LTC1436A-PLL +Regulator_Switching:LTC1437A +Regulator_Switching:LTC1878EMS8 +Regulator_Switching:LTC3105xDD +Regulator_Switching:LTC3105xMS +Regulator_Switching:LTC3245xDE +Regulator_Switching:LTC3245xMSE +Regulator_Switching:LTC3406AES5 +Regulator_Switching:LTC3406B-2ES5 +Regulator_Switching:LTC3406BES5-1.2 +Regulator_Switching:LTC3406ES5 +Regulator_Switching:LTC3406ES5-1.2 +Regulator_Switching:LTC3406ES5-1.5 +Regulator_Switching:LTC3406ES5-1.8 +Regulator_Switching:LTC3429 +Regulator_Switching:LTC3429B +Regulator_Switching:LTC3442 +Regulator_Switching:LTC3525 +Regulator_Switching:LTC3525-3 +Regulator_Switching:LTC3525-3.3 +Regulator_Switching:LTC3525-5 +Regulator_Switching:LTC3525D-3.3 +Regulator_Switching:LTC3525L-3 +Regulator_Switching:LTC3561EDD +Regulator_Switching:LTC3630AxDHC +Regulator_Switching:LTC3630AxMSE +Regulator_Switching:LTC3630xDHC +Regulator_Switching:LTC3630xMSE +Regulator_Switching:LTC3638xMSE +Regulator_Switching:LTC3639xMSE +Regulator_Switching:LTC3886 +Regulator_Switching:LTC7138xMSE +Regulator_Switching:LTM4626 +Regulator_Switching:LTM4637xV +Regulator_Switching:LTM4637xY +Regulator_Switching:LTM4638 +Regulator_Switching:LTM4657 +Regulator_Switching:LTM4668 +Regulator_Switching:LTM4668A +Regulator_Switching:LTM4671 +Regulator_Switching:LTM8049 +Regulator_Switching:LTM8063 +Regulator_Switching:LV2862XDDC +Regulator_Switching:LV2862YDDC +Regulator_Switching:MAX15062A +Regulator_Switching:MAX15062B +Regulator_Switching:MAX15062C +Regulator_Switching:MAX1522 +Regulator_Switching:MAX1523 +Regulator_Switching:MAX1524 +Regulator_Switching:MAX17501AxTB +Regulator_Switching:MAX17501BxTB +Regulator_Switching:MAX17501ExTB +Regulator_Switching:MAX17501FxTB +Regulator_Switching:MAX17501GxTB +Regulator_Switching:MAX17501HxTB +Regulator_Switching:MAX17572 +Regulator_Switching:MAX17574 +Regulator_Switching:MAX17620ATA +Regulator_Switching:MAX1771xSA +Regulator_Switching:MAX5035AUPA +Regulator_Switching:MAX5035AUSA +Regulator_Switching:MAX5035BUPA +Regulator_Switching:MAX5035BUSA +Regulator_Switching:MAX5035CUPA +Regulator_Switching:MAX5035CUSA +Regulator_Switching:MAX5035DUPA +Regulator_Switching:MAX5035DUSA +Regulator_Switching:MAX5035EUSA +Regulator_Switching:MAX777L +Regulator_Switching:MAX77827AEFD +Regulator_Switching:MAX778L +Regulator_Switching:MAX779L +Regulator_Switching:MC33063AD +Regulator_Switching:MC33063AP +Regulator_Switching:MC33063MNTXG +Regulator_Switching:MC34063AD +Regulator_Switching:MC34063AP +Regulator_Switching:MCP1623x-xCHY +Regulator_Switching:MCP1623x-xMC +Regulator_Switching:MCP16301Hx-xCH +Regulator_Switching:MCP16301x-xCH +Regulator_Switching:MCP16311x-xMNY +Regulator_Switching:MCP16311x-xMS +Regulator_Switching:MCP16312x-xMNY +Regulator_Switching:MCP16312x-xMS +Regulator_Switching:MCP16331x-xCH +Regulator_Switching:MCP16331x-xMNY +Regulator_Switching:MCP1640Bx-xCHY +Regulator_Switching:MCP1640Bx-xMC +Regulator_Switching:MCP1640Cx-xCHY +Regulator_Switching:MCP1640Cx-xMC +Regulator_Switching:MCP1640Dx-xCHY +Regulator_Switching:MCP1640Dx-xMC +Regulator_Switching:MCP1640x-xCHY +Regulator_Switching:MCP1640x-xMC +Regulator_Switching:MCP1650x-xMC +Regulator_Switching:MCP1651x-xMC +Regulator_Switching:MCP1652x-xMC +Regulator_Switching:MCP1653x-xUN +Regulator_Switching:MIC2177 +Regulator_Switching:MIC2177-3.3 +Regulator_Switching:MIC2177-5.0 +Regulator_Switching:MIC2178 +Regulator_Switching:MIC2178-3.3 +Regulator_Switching:MIC2178-5.0 +Regulator_Switching:MIC2207 +Regulator_Switching:MIC2253 +Regulator_Switching:MIC2290 +Regulator_Switching:MIC23050-4YML +Regulator_Switching:MIC23050-CYML +Regulator_Switching:MIC23050-GYML +Regulator_Switching:MIC23050-SYML +Regulator_Switching:MIC4684 +Regulator_Switching:MIC4690 +Regulator_Switching:MP1470 +Regulator_Switching:MP171GJ +Regulator_Switching:MP171GS +Regulator_Switching:MP2303ADN +Regulator_Switching:MP2303ADP +Regulator_Switching:MPM3550EGLE +Regulator_Switching:MT3608 +Regulator_Switching:MUN12AD01-SH +Regulator_Switching:MUN12AD03-SH +Regulator_Switching:NBM5100A +Regulator_Switching:NBM7100A +Regulator_Switching:NCP1070P065 +Regulator_Switching:NCP1070P100 +Regulator_Switching:NCP1070P130 +Regulator_Switching:NCP1070STAT +Regulator_Switching:NCP1070STBT +Regulator_Switching:NCP1070STCT +Regulator_Switching:NCP1071P065 +Regulator_Switching:NCP1071P100 +Regulator_Switching:NCP1071P130 +Regulator_Switching:NCP1071STAT +Regulator_Switching:NCP1071STBT +Regulator_Switching:NCP1071STCT +Regulator_Switching:NCP1072P065 +Regulator_Switching:NCP1072P100 +Regulator_Switching:NCP1072P130 +Regulator_Switching:NCP1072STAT +Regulator_Switching:NCP1072STBT +Regulator_Switching:NCP1072STCT +Regulator_Switching:NCP1075P065 +Regulator_Switching:NCP1075P100 +Regulator_Switching:NCP1075P130 +Regulator_Switching:NCP1075STAT +Regulator_Switching:NCP1075STBT +Regulator_Switching:NCP1075STCT +Regulator_Switching:NCP1076P065 +Regulator_Switching:NCP1076P100 +Regulator_Switching:NCP1076P130 +Regulator_Switching:NCP1076STAT +Regulator_Switching:NCP1076STBT +Regulator_Switching:NCP1076STCT +Regulator_Switching:NCP1077P065 +Regulator_Switching:NCP1077P100 +Regulator_Switching:NCP1077P130 +Regulator_Switching:NCP1077STAT +Regulator_Switching:NCP1077STBT +Regulator_Switching:NCP1077STCT +Regulator_Switching:NCP1529A +Regulator_Switching:NCV33063AVD +Regulator_Switching:NID30S24-05 +Regulator_Switching:NID30S24-12 +Regulator_Switching:NID30S24-15 +Regulator_Switching:NID30S48-24 +Regulator_Switching:NID60S24-05 +Regulator_Switching:NID60S24-12 +Regulator_Switching:NID60S24-15 +Regulator_Switching:NID60S48-24 +Regulator_Switching:NMA0505DC +Regulator_Switching:NMA0505SC +Regulator_Switching:NMA0509DC +Regulator_Switching:NMA0509SC +Regulator_Switching:NMA0512DC +Regulator_Switching:NMA0512SC +Regulator_Switching:NMA0515DC +Regulator_Switching:NMA0515SC +Regulator_Switching:NMA1205DC +Regulator_Switching:NMA1205SC +Regulator_Switching:NMA1209DC +Regulator_Switching:NMA1209SC +Regulator_Switching:NMA1212DC +Regulator_Switching:NMA1212SC +Regulator_Switching:NMA1215DC +Regulator_Switching:NMA1215SC +Regulator_Switching:NMA1505DC +Regulator_Switching:NMA1505SC +Regulator_Switching:NMA1512DC +Regulator_Switching:NMA1512SC +Regulator_Switching:NMA1515DC +Regulator_Switching:NMA1515SC +Regulator_Switching:NXE1S0303MC +Regulator_Switching:NXE1S0305MC +Regulator_Switching:NXE1S0505MC +Regulator_Switching:NXE2S0505MC +Regulator_Switching:NXE2S1205MC +Regulator_Switching:NXE2S1212MC +Regulator_Switching:NXE2S1215MC +Regulator_Switching:PAM2301CAAB120 +Regulator_Switching:PAM2301CAAB330 +Regulator_Switching:PAM2301CAABADJ +Regulator_Switching:PAM2305AAB120 +Regulator_Switching:PAM2305AAB150 +Regulator_Switching:PAM2305AAB180 +Regulator_Switching:PAM2305AAB250 +Regulator_Switching:PAM2305AAB280 +Regulator_Switching:PAM2305AAB330 +Regulator_Switching:PAM2305AABADJ +Regulator_Switching:PAM2305BJE120 +Regulator_Switching:PAM2305BJE150 +Regulator_Switching:PAM2305BJE180 +Regulator_Switching:PAM2305BJE250 +Regulator_Switching:PAM2305BJE280 +Regulator_Switching:PAM2305BJE330 +Regulator_Switching:PAM2305BJEADJ +Regulator_Switching:PAM2305CGF120 +Regulator_Switching:PAM2305CGF150 +Regulator_Switching:PAM2305CGF180 +Regulator_Switching:PAM2305CGF250 +Regulator_Switching:PAM2305CGF280 +Regulator_Switching:PAM2305CGF330 +Regulator_Switching:PAM2305CGFADJ +Regulator_Switching:PAM2306AYPAA +Regulator_Switching:PAM2306AYPBB +Regulator_Switching:PAM2306AYPBK +Regulator_Switching:PAM2306AYPCB +Regulator_Switching:PAM2306AYPKB +Regulator_Switching:PAM2306AYPKE +Regulator_Switching:PAM2306DYPAA +Regulator_Switching:R-781.5-0.5 +Regulator_Switching:R-781.8-0.5 +Regulator_Switching:R-781.8-1.0 +Regulator_Switching:R-7812-0.5 +Regulator_Switching:R-7815-0.5 +Regulator_Switching:R-782.5-0.5 +Regulator_Switching:R-782.5-1.0 +Regulator_Switching:R-783.3-0.5 +Regulator_Switching:R-783.3-1.0 +Regulator_Switching:R-785.0-0.5 +Regulator_Switching:R-785.0-1.0 +Regulator_Switching:R-786.5-0.5 +Regulator_Switching:R-78B1.2-2.0 +Regulator_Switching:R-78B1.5-2.0 +Regulator_Switching:R-78B1.8-2.0 +Regulator_Switching:R-78B12-2.0 +Regulator_Switching:R-78B15-2.0 +Regulator_Switching:R-78B2.5-2.0 +Regulator_Switching:R-78B3.3-2.0 +Regulator_Switching:R-78B5.0-2.0 +Regulator_Switching:R-78B9.0-2.0 +Regulator_Switching:R-78C1.8-1.0 +Regulator_Switching:R-78C12-1.0 +Regulator_Switching:R-78C15-1.0 +Regulator_Switching:R-78C3.3-1.0 +Regulator_Switching:R-78C5.0-1.0 +Regulator_Switching:R-78C9.0-1.0 +Regulator_Switching:R-78E12-0.5 +Regulator_Switching:R-78E15-0.5 +Regulator_Switching:R-78E3.3-0.5 +Regulator_Switching:R-78E3.3-1.0 +Regulator_Switching:R-78E5.0-0.5 +Regulator_Switching:R-78E5.0-1.0 +Regulator_Switching:R-78E9.0-0.5 +Regulator_Switching:R-78HB12-0.5 +Regulator_Switching:R-78HB15-0.5 +Regulator_Switching:R-78HB24-0.3 +Regulator_Switching:R-78HB3.3-0.5 +Regulator_Switching:R-78HB5.0-0.5 +Regulator_Switching:R-78HB6.5-0.5 +Regulator_Switching:R-78HB9.0-0.5 +Regulator_Switching:R-78S3.3-0.1 +Regulator_Switching:SC33063AD +Regulator_Switching:SC34063AP +Regulator_Switching:SC4503TSK +Regulator_Switching:SIC431A +Regulator_Switching:SIC431B +Regulator_Switching:SIC431C +Regulator_Switching:SIC431D +Regulator_Switching:SIC437A +Regulator_Switching:SIC437B +Regulator_Switching:SIC437C +Regulator_Switching:SIC437D +Regulator_Switching:SIC438A +Regulator_Switching:SIC438B +Regulator_Switching:SIC438C +Regulator_Switching:SIC438D +Regulator_Switching:ST1S10PHR +Regulator_Switching:ST1S10PUR +Regulator_Switching:ST1S12XX +Regulator_Switching:ST1S14PHR +Regulator_Switching:TDN_5-0910WISM +Regulator_Switching:TDN_5-0911WISM +Regulator_Switching:TDN_5-0912WISM +Regulator_Switching:TDN_5-0913WISM +Regulator_Switching:TDN_5-0915WISM +Regulator_Switching:TDN_5-0919WISM +Regulator_Switching:TDN_5-2410WISM +Regulator_Switching:TDN_5-2411WISM +Regulator_Switching:TDN_5-2412WISM +Regulator_Switching:TDN_5-2413WISM +Regulator_Switching:TDN_5-2415WISM +Regulator_Switching:TDN_5-2419WISM +Regulator_Switching:TDN_5-4810WISM +Regulator_Switching:TDN_5-4811WISM +Regulator_Switching:TDN_5-4812WISM +Regulator_Switching:TDN_5-4813WISM +Regulator_Switching:TDN_5-4815WISM +Regulator_Switching:TDN_5-4819WISM +Regulator_Switching:TL497 +Regulator_Switching:TL497A +Regulator_Switching:TL5001 +Regulator_Switching:TL5001A +Regulator_Switching:TLV61046ADB +Regulator_Switching:TLV61070ADBV +Regulator_Switching:TLV61225DC +Regulator_Switching:TLV62080DSGx +Regulator_Switching:TLV62084ADSGx +Regulator_Switching:TLV62084DSGx +Regulator_Switching:TLV62095RGTx +Regulator_Switching:TLV62565DBVx +Regulator_Switching:TLV62566DBVx +Regulator_Switching:TLV62568ADRL +Regulator_Switching:TLV62568DBV +Regulator_Switching:TLV62568DDC +Regulator_Switching:TLV62568DRL +Regulator_Switching:TLV62569ADRL +Regulator_Switching:TLV62569DBV +Regulator_Switching:TLV62569DDC +Regulator_Switching:TLV62569DRL +Regulator_Switching:TMR_1-0511 +Regulator_Switching:TMR_1-0511SM +Regulator_Switching:TMR_1-0512 +Regulator_Switching:TMR_1-0512SM +Regulator_Switching:TMR_1-0513 +Regulator_Switching:TMR_1-0513SM +Regulator_Switching:TMR_1-0515 +Regulator_Switching:TMR_1-0522 +Regulator_Switching:TMR_1-0522SM +Regulator_Switching:TMR_1-0523 +Regulator_Switching:TMR_1-0523SM +Regulator_Switching:TMR_1-1211 +Regulator_Switching:TMR_1-1211SM +Regulator_Switching:TMR_1-1212 +Regulator_Switching:TMR_1-1212SM +Regulator_Switching:TMR_1-1213 +Regulator_Switching:TMR_1-1213SM +Regulator_Switching:TMR_1-1215 +Regulator_Switching:TMR_1-1222 +Regulator_Switching:TMR_1-1222SM +Regulator_Switching:TMR_1-1223 +Regulator_Switching:TMR_1-1223SM +Regulator_Switching:TMR_1-2411 +Regulator_Switching:TMR_1-2411SM +Regulator_Switching:TMR_1-2412 +Regulator_Switching:TMR_1-2412SM +Regulator_Switching:TMR_1-2413 +Regulator_Switching:TMR_1-2413SM +Regulator_Switching:TMR_1-2415 +Regulator_Switching:TMR_1-2422 +Regulator_Switching:TMR_1-2422SM +Regulator_Switching:TMR_1-2423 +Regulator_Switching:TMR_1-2423SM +Regulator_Switching:TMR_1-4811 +Regulator_Switching:TMR_1-4811SM +Regulator_Switching:TMR_1-4812 +Regulator_Switching:TMR_1-4812SM +Regulator_Switching:TMR_1-4813 +Regulator_Switching:TMR_1-4813SM +Regulator_Switching:TMR_1-4815 +Regulator_Switching:TMR_1-4822 +Regulator_Switching:TMR_1-4822SM +Regulator_Switching:TMR_1-4823 +Regulator_Switching:TMR_1-4823SM +Regulator_Switching:TNY263G +Regulator_Switching:TNY263P +Regulator_Switching:TNY264G +Regulator_Switching:TNY264P +Regulator_Switching:TNY265G +Regulator_Switching:TNY265P +Regulator_Switching:TNY266G +Regulator_Switching:TNY266P +Regulator_Switching:TNY267G +Regulator_Switching:TNY267P +Regulator_Switching:TNY268G +Regulator_Switching:TNY268P +Regulator_Switching:TNY274G +Regulator_Switching:TNY274P +Regulator_Switching:TNY275G +Regulator_Switching:TNY275P +Regulator_Switching:TNY276G +Regulator_Switching:TNY276P +Regulator_Switching:TNY277G +Regulator_Switching:TNY277P +Regulator_Switching:TNY278G +Regulator_Switching:TNY278P +Regulator_Switching:TNY279G +Regulator_Switching:TNY279P +Regulator_Switching:TNY280G +Regulator_Switching:TNY280P +Regulator_Switching:TNY284D +Regulator_Switching:TNY284K +Regulator_Switching:TNY284P +Regulator_Switching:TNY285D +Regulator_Switching:TNY285K +Regulator_Switching:TNY285P +Regulator_Switching:TNY286D +Regulator_Switching:TNY286K +Regulator_Switching:TNY286P +Regulator_Switching:TNY287D +Regulator_Switching:TNY287K +Regulator_Switching:TNY287P +Regulator_Switching:TNY288D +Regulator_Switching:TNY288K +Regulator_Switching:TNY288P +Regulator_Switching:TNY289K +Regulator_Switching:TNY289P +Regulator_Switching:TNY290K +Regulator_Switching:TNY290P +Regulator_Switching:TOP100YN +Regulator_Switching:TOP101YN +Regulator_Switching:TOP102YN +Regulator_Switching:TOP103YN +Regulator_Switching:TOP104YN +Regulator_Switching:TOP200YAI +Regulator_Switching:TOP201YAI +Regulator_Switching:TOP202YAI +Regulator_Switching:TOP203YAI +Regulator_Switching:TOP204YAI +Regulator_Switching:TOP209G +Regulator_Switching:TOP209P +Regulator_Switching:TOP210G +Regulator_Switching:TOP210PFI +Regulator_Switching:TOP214YAI +Regulator_Switching:TOP252EG +Regulator_Switching:TOP252EN +Regulator_Switching:TOP252GN +Regulator_Switching:TOP252MN +Regulator_Switching:TOP252PN +Regulator_Switching:TOP253EG +Regulator_Switching:TOP253EN +Regulator_Switching:TOP253GN +Regulator_Switching:TOP253MN +Regulator_Switching:TOP253PN +Regulator_Switching:TOP254EG +Regulator_Switching:TOP254EN +Regulator_Switching:TOP254GN +Regulator_Switching:TOP254MN +Regulator_Switching:TOP254PN +Regulator_Switching:TOP254YN +Regulator_Switching:TOP255EG +Regulator_Switching:TOP255EN +Regulator_Switching:TOP255GN +Regulator_Switching:TOP255LN +Regulator_Switching:TOP255MN +Regulator_Switching:TOP255PN +Regulator_Switching:TOP255YN +Regulator_Switching:TOP256EG +Regulator_Switching:TOP256EN +Regulator_Switching:TOP256GN +Regulator_Switching:TOP256LN +Regulator_Switching:TOP256MN +Regulator_Switching:TOP256PN +Regulator_Switching:TOP256YN +Regulator_Switching:TOP257EG +Regulator_Switching:TOP257EN +Regulator_Switching:TOP257GN +Regulator_Switching:TOP257LN +Regulator_Switching:TOP257MN +Regulator_Switching:TOP257PN +Regulator_Switching:TOP257YN +Regulator_Switching:TOP258EG +Regulator_Switching:TOP258EN +Regulator_Switching:TOP258GN +Regulator_Switching:TOP258LN +Regulator_Switching:TOP258MN +Regulator_Switching:TOP258PN +Regulator_Switching:TOP258YN +Regulator_Switching:TOP259EG +Regulator_Switching:TOP259EN +Regulator_Switching:TOP259LN +Regulator_Switching:TOP259YN +Regulator_Switching:TOP260EG +Regulator_Switching:TOP260EN +Regulator_Switching:TOP260LN +Regulator_Switching:TOP260YN +Regulator_Switching:TOP261EG +Regulator_Switching:TOP261EN +Regulator_Switching:TOP261LN +Regulator_Switching:TOP261YN +Regulator_Switching:TOP262EN +Regulator_Switching:TOP262LN +Regulator_Switching:TOP264EG +Regulator_Switching:TOP264KG +Regulator_Switching:TOP264VG +Regulator_Switching:TOP265EG +Regulator_Switching:TOP265KG +Regulator_Switching:TOP265VG +Regulator_Switching:TOP266EG +Regulator_Switching:TOP266KG +Regulator_Switching:TOP266VG +Regulator_Switching:TOP267EG +Regulator_Switching:TOP267KG +Regulator_Switching:TOP267VG +Regulator_Switching:TOP268EG +Regulator_Switching:TOP268KG +Regulator_Switching:TOP268VG +Regulator_Switching:TOP269EG +Regulator_Switching:TOP269KG +Regulator_Switching:TOP269VG +Regulator_Switching:TOP270EG +Regulator_Switching:TOP270KG +Regulator_Switching:TOP270VG +Regulator_Switching:TOP271EG +Regulator_Switching:TOP271KG +Regulator_Switching:TOP271VG +Regulator_Switching:TOS06-05SIL +Regulator_Switching:TOS06-12SIL +Regulator_Switching:TPS51363 +Regulator_Switching:TPS5403 +Regulator_Switching:TPS54061DRB +Regulator_Switching:TPS54202DDC +Regulator_Switching:TPS5420D +Regulator_Switching:TPS54233 +Regulator_Switching:TPS54260DGQ +Regulator_Switching:TPS54260DRC +Regulator_Switching:TPS54302 +Regulator_Switching:TPS54308 +Regulator_Switching:TPS5430DDA +Regulator_Switching:TPS5431DDA +Regulator_Switching:TPS54336ADDA +Regulator_Switching:TPS54340DDA +Regulator_Switching:TPS54360DDA +Regulator_Switching:TPS54560BDDA +Regulator_Switching:TPS560200 +Regulator_Switching:TPS562200 +Regulator_Switching:TPS562202 +Regulator_Switching:TPS562202S +Regulator_Switching:TPS562203 +Regulator_Switching:TPS562206 +Regulator_Switching:TPS563200 +Regulator_Switching:TPS563202S +Regulator_Switching:TPS563203 +Regulator_Switching:TPS563206 +Regulator_Switching:TPS563240DDC +Regulator_Switching:TPS563300 +Regulator_Switching:TPS56339DDC +Regulator_Switching:TPS565208 +Regulator_Switching:TPS56528DDA +Regulator_Switching:TPS568215RNN +Regulator_Switching:TPS61040DBV +Regulator_Switching:TPS61040DDC +Regulator_Switching:TPS61040DRV +Regulator_Switching:TPS61041DBV +Regulator_Switching:TPS61041DDC +Regulator_Switching:TPS61041DRV +Regulator_Switching:TPS61085DGK +Regulator_Switching:TPS61085PW +Regulator_Switching:TPS61089 +Regulator_Switching:TPS610891 +Regulator_Switching:TPS61090 +Regulator_Switching:TPS61091 +Regulator_Switching:TPS61092 +Regulator_Switching:TPS610991DRV +Regulator_Switching:TPS610992DRV +Regulator_Switching:TPS610993DRV +Regulator_Switching:TPS610994DRV +Regulator_Switching:TPS610995DRV +Regulator_Switching:TPS610996DRV +Regulator_Switching:TPS610997DRV +Regulator_Switching:TPS61099DRV +Regulator_Switching:TPS61200DRC +Regulator_Switching:TPS61201DRC +Regulator_Switching:TPS61202DRC +Regulator_Switching:TPS61202DSC +Regulator_Switching:TPS61220DCK +Regulator_Switching:TPS61221DCK +Regulator_Switching:TPS61222DCK +Regulator_Switching:TPS61230DRC +Regulator_Switching:TPS61252DSG +Regulator_Switching:TPS613221ADBV +Regulator_Switching:TPS613221ADBZ +Regulator_Switching:TPS613222ADBV +Regulator_Switching:TPS613222ADBZ +Regulator_Switching:TPS613223ADBV +Regulator_Switching:TPS613223ADBZ +Regulator_Switching:TPS613224ADBV +Regulator_Switching:TPS613224ADBZ +Regulator_Switching:TPS613225ADBV +Regulator_Switching:TPS613225ADBZ +Regulator_Switching:TPS613226ADBV +Regulator_Switching:TPS613226ADBZ +Regulator_Switching:TPS61322DBZ +Regulator_Switching:TPS62056DGS +Regulator_Switching:TPS62125DSG +Regulator_Switching:TPS62130 +Regulator_Switching:TPS62130A +Regulator_Switching:TPS62131 +Regulator_Switching:TPS62132 +Regulator_Switching:TPS62133 +Regulator_Switching:TPS62140 +Regulator_Switching:TPS62140A +Regulator_Switching:TPS62141 +Regulator_Switching:TPS62142 +Regulator_Switching:TPS62143 +Regulator_Switching:TPS62150 +Regulator_Switching:TPS62150A +Regulator_Switching:TPS62151 +Regulator_Switching:TPS62152 +Regulator_Switching:TPS62153 +Regulator_Switching:TPS62160DGK +Regulator_Switching:TPS62160DSG +Regulator_Switching:TPS62161DSG +Regulator_Switching:TPS62162DSG +Regulator_Switching:TPS62163DSG +Regulator_Switching:TPS62170DSG +Regulator_Switching:TPS62171DSG +Regulator_Switching:TPS62172DSG +Regulator_Switching:TPS62173DSG +Regulator_Switching:TPS62175DQC +Regulator_Switching:TPS62177DQC +Regulator_Switching:TPS62200DBV +Regulator_Switching:TPS62201DBV +Regulator_Switching:TPS62202DBV +Regulator_Switching:TPS62203DBV +Regulator_Switching:TPS62204DBV +Regulator_Switching:TPS62205DBV +Regulator_Switching:TPS62207DBV +Regulator_Switching:TPS62208DBV +Regulator_Switching:TPS62821DLC +Regulator_Switching:TPS62822DLC +Regulator_Switching:TPS62823DLC +Regulator_Switching:TPS628436DRL +Regulator_Switching:TPS628436YKA +Regulator_Switching:TPS628437DRL +Regulator_Switching:TPS628437YKA +Regulator_Switching:TPS628438DRL +Regulator_Switching:TPS628438YKA +Regulator_Switching:TPS62912 +Regulator_Switching:TPS62913 +Regulator_Switching:TPS62932 +Regulator_Switching:TPS62933 +Regulator_Switching:TPS62933F +Regulator_Switching:TPS62933O +Regulator_Switching:TPS62933P +Regulator_Switching:TPS62A01ADRL +Regulator_Switching:TPS62A01APDDC +Regulator_Switching:TPS62A01DRL +Regulator_Switching:TPS62A01PDDC +Regulator_Switching:TPS62A02ADRL +Regulator_Switching:TPS62A02APDDC +Regulator_Switching:TPS62A02DRL +Regulator_Switching:TPS62A02NADRL +Regulator_Switching:TPS62A02NDRL +Regulator_Switching:TPS62A02PDDC +Regulator_Switching:TPS63000 +Regulator_Switching:TPS63000-Q1 +Regulator_Switching:TPS63001 +Regulator_Switching:TPS63002 +Regulator_Switching:TPS63030DSK +Regulator_Switching:TPS63031DSK +Regulator_Switching:TPS63060 +Regulator_Switching:TPS63061 +Regulator_Switching:TPS63900 +Regulator_Switching:TPS65130RGE +Regulator_Switching:TPS65131RGE +Regulator_Switching:TPS82130 +Regulator_Switching:TPS82140 +Regulator_Switching:TPS82150 +Regulator_Switching:TSR0.6-48120WI +Regulator_Switching:TSR0.6-48150WI +Regulator_Switching:TSR0.6-48240WI +Regulator_Switching:TSR0.6-4833WI +Regulator_Switching:TSR0.6-4850WI +Regulator_Switching:TSR0.6-4865WI +Regulator_Switching:TSR0.6-4890WI +Regulator_Switching:TSR1-2433E +Regulator_Switching:TSR1-2450E +Regulator_Switching:TSR2-24120N +Regulator_Switching:TSR2-2412N +Regulator_Switching:TSR2-24150N +Regulator_Switching:TSR2-2415N +Regulator_Switching:TSR2-2418N +Regulator_Switching:TSR2-2425N +Regulator_Switching:TSR2-2433N +Regulator_Switching:TSR2-2450N +Regulator_Switching:TSR2-2465N +Regulator_Switching:TSR2-2490N +Regulator_Switching:TSR_1-2412 +Regulator_Switching:TSR_1-24120 +Regulator_Switching:TSR_1-2415 +Regulator_Switching:TSR_1-24150 +Regulator_Switching:TSR_1-2418 +Regulator_Switching:TSR_1-2425 +Regulator_Switching:TSR_1-2433 +Regulator_Switching:TSR_1-2450 +Regulator_Switching:TSR_1-2465 +Regulator_Switching:TSR_1-2490 +Regulator_Switching:VIPer22ADIP-E +Regulator_Switching:VIPer22AS +Regulator_Switching:VIPer25HN +Regulator_Switching:VIPer25LN +Regulator_Switching:VIPer26HD +Regulator_Switching:VIPer26HN +Regulator_Switching:VIPer26LD +Regulator_Switching:VIPer26LN +Regulator_Switching:XL1509-12 +Regulator_Switching:XL1509-3.3 +Regulator_Switching:XL1509-5.0 +Regulator_Switching:XL1509-ADJ +Regulator_Switching:XL4015 +Relay:ADW11 +Relay:AZ850-x +Relay:AZ850P1-x +Relay:AZ850P2-x +Relay:AZSR131-1AE-12D +Relay:COTO_3602_Split +Relay:COTO_3650_Split +Relay:COTO_3660_Split +Relay:DIPxx-1Axx-11x +Relay:DIPxx-1Axx-12x +Relay:DIPxx-1Axx-12xD +Relay:DIPxx-1Axx-13x +Relay:DIPxx-1Cxx-51x +Relay:DIPxx-2Axx-21x +Relay:DR-24V +Relay:DR-3V +Relay:DR-5V +Relay:DR-L-3V +Relay:DR-L2-3V_Form1 +Relay:DR-L2-3V_Form2 +Relay:EC2-12NU +Relay:EC2-12SNU +Relay:EC2-12TNU +Relay:EC2-24NU +Relay:EC2-24SNU +Relay:EC2-24TNU +Relay:EC2-3NU +Relay:EC2-3SNU +Relay:EC2-3TNU +Relay:EC2-4.5NU +Relay:EC2-4.5SNU +Relay:EC2-4.5TNU +Relay:EC2-5NU +Relay:EC2-5SNU +Relay:EC2-5TNU +Relay:EE2-12NKX +Relay:EE2-12NU +Relay:EE2-12NUH +Relay:EE2-12NUX +Relay:EE2-12SNU +Relay:EE2-12SNUH +Relay:EE2-12SNUX +Relay:EE2-12TNU +Relay:EE2-12TNUH +Relay:EE2-12TNUX +Relay:EE2-24NU +Relay:EE2-24NUH +Relay:EE2-24NUX +Relay:EE2-24SNU +Relay:EE2-24SNUH +Relay:EE2-24SNUX +Relay:EE2-24TNU +Relay:EE2-24TNUH +Relay:EE2-24TNUX +Relay:EE2-3NKX +Relay:EE2-3NU +Relay:EE2-3NUH +Relay:EE2-3NUX +Relay:EE2-3SNU +Relay:EE2-3SNUH +Relay:EE2-3SNUX +Relay:EE2-3TNU +Relay:EE2-3TNUH +Relay:EE2-3TNUX +Relay:EE2-4.5NKX +Relay:EE2-4.5NU +Relay:EE2-4.5NUH +Relay:EE2-4.5NUX +Relay:EE2-4.5SNU +Relay:EE2-4.5SNUH +Relay:EE2-4.5SNUX +Relay:EE2-4.5TNU +Relay:EE2-4.5TNUH +Relay:EE2-4.5TNUX +Relay:EE2-5NU +Relay:EE2-5NUH +Relay:EE2-5NUX +Relay:EE2-5SNU +Relay:EE2-5SNUH +Relay:EE2-5SNUX +Relay:EE2-5TNU +Relay:EE2-5TNUH +Relay:EE2-5TNUX +Relay:FINDER-30.22 +Relay:FINDER-32.21-x000 +Relay:FINDER-32.21-x300 +Relay:FINDER-34.51 +Relay:FINDER-34.51.7xxx.x019 +Relay:FINDER-36.11 +Relay:FINDER-40.11 +Relay:FINDER-40.11-2016 +Relay:FINDER-40.31 +Relay:FINDER-40.41 +Relay:FINDER-40.51 +Relay:FINDER-40.52 +Relay:FINDER-41.52 +Relay:FINDER-44.52 +Relay:FINDER-44.62 +Relay:FRT5 +Relay:FRT5_separated +Relay:Fujitsu_FTR-F1A +Relay:Fujitsu_FTR-F1C +Relay:Fujitsu_FTR-LYAA005x +Relay:Fujitsu_FTR-LYCA005x +Relay:G2RL-1 +Relay:G2RL-1-E +Relay:G2RL-1-H +Relay:G2RL-1A +Relay:G2RL-1A-E +Relay:G2RL-1A-H +Relay:G2RL-2 +Relay:G2RL-2A +Relay:G5LE-1 +Relay:G5NB +Relay:G5Q-1 +Relay:G5Q-1A +Relay:G5V-1 +Relay:G5V-2 +Relay:G5V-2_Split +Relay:G6A +Relay:G6AK +Relay:G6AU +Relay:G6E +Relay:G6EU +Relay:G6H-2 +Relay:G6HU-2 +Relay:G6K-2 +Relay:G6KU-2 +Relay:G6S-2 +Relay:G6SK-2 +Relay:G6SU-2 +Relay:HF115F-2Z-x4 +Relay:HF3-01 +Relay:HF3-02 +Relay:HF3-03 +Relay:HF3-04 +Relay:HF3-05 +Relay:HF3-06 +Relay:HF3-07 +Relay:HF3-51 +Relay:HF3-52 +Relay:HF3-53 +Relay:HF3-54 +Relay:HF3-55 +Relay:HF3-56 +Relay:HF3-57 +Relay:HK19F-DCxxV-SHC +Relay:HONGFA_HFD2-0xx-x-L2-x +Relay:IM00 +Relay:IM01 +Relay:IM02 +Relay:IM03 +Relay:IM04 +Relay:IM05 +Relay:IM06 +Relay:IM07 +Relay:IM08 +Relay:IM11 +Relay:IM12 +Relay:IM13 +Relay:IM16 +Relay:IM17 +Relay:IM21 +Relay:IM22 +Relay:IM23 +Relay:IM26 +Relay:IM40 +Relay:IM41 +Relay:IM42 +Relay:IM43 +Relay:IM44 +Relay:IM45 +Relay:IM46 +Relay:IM47 +Relay:IM48 +Relay:JQC-3FF-005-1H +Relay:JQC-3FF-005-1Z +Relay:JQC-3FF-006-1H +Relay:JQC-3FF-006-1Z +Relay:JQC-3FF-009-1H +Relay:JQC-3FF-009-1Z +Relay:JQC-3FF-012-1H +Relay:JQC-3FF-012-1Z +Relay:JQC-3FF-018-1H +Relay:JQC-3FF-018-1Z +Relay:JQC-3FF-024-1H +Relay:JQC-3FF-024-1Z +Relay:JQC-3FF-048-1H +Relay:JQC-3FF-048-1Z +Relay:JW2 +Relay:MSxx-1Axx-75 +Relay:MSxx-1Bxx-75 +Relay:Panasonic_ALFG1PF09 +Relay:Panasonic_ALFG1PF091 +Relay:Panasonic_ALFG1PF12 +Relay:Panasonic_ALFG1PF121 +Relay:Panasonic_ALFG1PF18 +Relay:Panasonic_ALFG1PF181 +Relay:Panasonic_ALFG1PF24 +Relay:Panasonic_ALFG1PF241 +Relay:Panasonic_ALFG2PF09 +Relay:Panasonic_ALFG2PF091 +Relay:Panasonic_ALFG2PF12 +Relay:Panasonic_ALFG2PF121 +Relay:Panasonic_ALFG2PF18 +Relay:Panasonic_ALFG2PF181 +Relay:Panasonic_ALFG2PF24 +Relay:Panasonic_ALFG2PF241 +Relay:RAYEX-L90 +Relay:RAYEX-L90A +Relay:RAYEX-L90AS +Relay:RAYEX-L90B +Relay:RAYEX-L90BS +Relay:RAYEX-L90S +Relay:RM50-xx21 +Relay:RM84 +Relay:RSM822 +Relay:RT314A03 +Relay:RT314A05 +Relay:RT314A06 +Relay:RT314A12 +Relay:RT314A24 +Relay:RT42xAxx +Relay:RT42xFxx +Relay:RT42xxxx +Relay:RT44xxxx +Relay:RTE2xAxx +Relay:RTE2xFxx +Relay:RTE2xxxx +Relay:RTE4xxxx +Relay:Relay_DPDT +Relay:Relay_DPDT_Latching_1coil +Relay:Relay_DPDT_Latching_2coil +Relay:Relay_DPST-NC +Relay:Relay_DPST-NO +Relay:Relay_DPST_Latching_1coil +Relay:Relay_DPST_Latching_2coil +Relay:Relay_SPDT +Relay:Relay_SPDT_Latching_1coil +Relay:Relay_SPDT_Latching_2coil +Relay:Relay_SPST-NC +Relay:Relay_SPST-NO +Relay:Relay_SPST_Latching_1coil +Relay:Relay_SPST_Latching_2coil +Relay:SANYOU_SRD_Form_A +Relay:SANYOU_SRD_Form_B +Relay:SANYOU_SRD_Form_C +Relay:SILxx-1Axx-71x +Relay:SILxx-1Bxx-71x +Relay:SILxx-1Cxx-51x +Relay:TE_PCH-1xxx2M +Relay:TIANBO-HJR-4102-L +Relay:UMS05-1A80-75D +Relay:UMS05-1A80-75L +Relay:V23072-Cx061-xxx8 +Relay:V23072-Cx062-xxx8 +Relay:Y14x-1C-xxDS +Relay_SolidState:34.81-7048 +Relay_SolidState:34.81-8240 +Relay_SolidState:34.81-9024 +Relay_SolidState:AQH0213 +Relay_SolidState:AQH0213A +Relay_SolidState:AQH0223 +Relay_SolidState:AQH0223A +Relay_SolidState:AQH1213 +Relay_SolidState:AQH1213A +Relay_SolidState:AQH1223 +Relay_SolidState:AQH1223A +Relay_SolidState:AQH2213 +Relay_SolidState:AQH2213A +Relay_SolidState:AQH2223 +Relay_SolidState:AQH2223A +Relay_SolidState:AQH3213 +Relay_SolidState:AQH3213A +Relay_SolidState:AQH3223 +Relay_SolidState:AQH3223A +Relay_SolidState:ASSR-1218 +Relay_SolidState:BC2213A +Relay_SolidState:CPC1002N +Relay_SolidState:CPC1017N +Relay_SolidState:CPC1117N +Relay_SolidState:FOD420 +Relay_SolidState:FOD4208 +Relay_SolidState:FOD4216 +Relay_SolidState:FOD4218 +Relay_SolidState:FODM3011 +Relay_SolidState:FODM3012 +Relay_SolidState:FODM3022 +Relay_SolidState:FODM3023 +Relay_SolidState:FODM3052 +Relay_SolidState:FODM3053 +Relay_SolidState:HHG1D-1 +Relay_SolidState:LAA110 +Relay_SolidState:LBB110 +Relay_SolidState:LCC110 +Relay_SolidState:MOC3010M +Relay_SolidState:MOC3011M +Relay_SolidState:MOC3012M +Relay_SolidState:MOC3020M +Relay_SolidState:MOC3021M +Relay_SolidState:MOC3022M +Relay_SolidState:MOC3023M +Relay_SolidState:MOC3031M +Relay_SolidState:MOC3032M +Relay_SolidState:MOC3033M +Relay_SolidState:MOC3041M +Relay_SolidState:MOC3042M +Relay_SolidState:MOC3043M +Relay_SolidState:MOC3051M +Relay_SolidState:MOC3052M +Relay_SolidState:MOC3061M +Relay_SolidState:MOC3062M +Relay_SolidState:MOC3063M +Relay_SolidState:MOC3081M +Relay_SolidState:MOC3082M +Relay_SolidState:MOC3083M +Relay_SolidState:MOC3162M +Relay_SolidState:MOC3163M +Relay_SolidState:S102S01 +Relay_SolidState:S102S02 +Relay_SolidState:S112S01 +Relay_SolidState:S116S01 +Relay_SolidState:S116S02 +Relay_SolidState:S202S01 +Relay_SolidState:S202S02 +Relay_SolidState:S212S01 +Relay_SolidState:S216S01 +Relay_SolidState:S216S02 +Relay_SolidState:TLP141G +Relay_SolidState:TLP148G +Relay_SolidState:TLP160G +Relay_SolidState:TLP160J +Relay_SolidState:TLP161G +Relay_SolidState:TLP161J +Relay_SolidState:TLP175A +Relay_SolidState:TLP222A +Relay_SolidState:TLP222A-2 +Relay_SolidState:TLP3123 +Relay_SolidState:TLP3542 +Relay_SolidState:TLP3543 +Relay_SolidState:TLP3544 +Relay_SolidState:TLP3545 +Relay_SolidState:TLP3546 +RF:0900PC15J0013 +RF:ADC-10-1R +RF:ADCH-80 +RF:ADCH-80A +RF:ADL5904 +RF:ADP-2-1W +RF:AMK-2-13 +RF:AX5043 +RF:CC1000 +RF:CC1200 +RF:CC2500 +RF:DC4759J5020AHF-1 +RF:DC4759J5020AHF-2 +RF:DW1000 +RF:F113 +RF:F115 +RF:F117 +RF:HMC394LP4 +RF:HMC431 +RF:LAT-3 +RF:LRPS-2-1 +RF:LTC5507ES6 +RF:MAADSS0008 +RF:MAAVSS0004 +RF:MC12080 +RF:MC12093D +RF:MICRF112YMM +RF:MICRF220AYQS +RF:MRF89XA +RF:NRF24L01 +RF:NRF24L01_Breakout +RF:PAT1220-C-0DB +RF:PAT1220-C-10DB +RF:PAT1220-C-1DB +RF:PAT1220-C-2DB +RF:PAT1220-C-3DB +RF:PAT1220-C-4DB +RF:PAT1220-C-5DB +RF:PAT1220-C-6DB +RF:PAT1220-C-7DB +RF:PAT1220-C-8DB +RF:PAT1220-C-9DB +RF:PD4859J5050S2HF +RF:RMK-3-451 +RF:RMK-5-51 +RF:SE5004L +RF:SX1231IMLTRT +RF:SX1261IMLTRT +RF:SX1262IMLTRT +RF:SX1272 +RF:SX1273 +RF:SX1276 +RF:SX1277 +RF:SX1278 +RF:SX1279 +RF:SYPD-1 +RF:SYPD-2 +RF:SYPD-52 +RF:Si4460 +RF:Si4461 +RF:Si4463 +RF:Si4464 +RF:TCP-2-10X +RF:nRF24L01P +RF_Amplifier:AD8313xRM +RF_Amplifier:ADL5541 +RF_Amplifier:ADL5542 +RF_Amplifier:ADL5610 +RF_Amplifier:BGA2800 +RF_Amplifier:BGA2801 +RF_Amplifier:BGA2803 +RF_Amplifier:BGA2815 +RF_Amplifier:BGA2817 +RF_Amplifier:BGA2818 +RF_Amplifier:BGA2850 +RF_Amplifier:BGA2851 +RF_Amplifier:BGA2865 +RF_Amplifier:BGA2866 +RF_Amplifier:BGA2867 +RF_Amplifier:BGA2869 +RF_Amplifier:BGA2870 +RF_Amplifier:BGA2874 +RF_Amplifier:CMX901 +RF_Amplifier:GALI-1 +RF_Amplifier:GALI-19 +RF_Amplifier:GALI-2 +RF_Amplifier:GALI-21 +RF_Amplifier:GALI-24 +RF_Amplifier:GALI-29 +RF_Amplifier:GALI-3 +RF_Amplifier:GALI-33 +RF_Amplifier:GALI-39 +RF_Amplifier:GALI-4 +RF_Amplifier:GALI-49 +RF_Amplifier:GALI-4F +RF_Amplifier:GALI-5 +RF_Amplifier:GALI-51 +RF_Amplifier:GALI-51F +RF_Amplifier:GALI-52 +RF_Amplifier:GALI-55 +RF_Amplifier:GALI-59 +RF_Amplifier:GALI-5F +RF_Amplifier:GALI-6 +RF_Amplifier:GALI-6F +RF_Amplifier:GALI-74 +RF_Amplifier:GALI-84 +RF_Amplifier:GALI-S66 +RF_Amplifier:GVA-123 +RF_Amplifier:GVA-60 +RF_Amplifier:GVA-62 +RF_Amplifier:GVA-63 +RF_Amplifier:GVA-81 +RF_Amplifier:GVA-82 +RF_Amplifier:GVA-83 +RF_Amplifier:GVA-84 +RF_Amplifier:GVA-93 +RF_Amplifier:HMC1099PM5E +RF_Amplifier:HMC8500PM5E +RF_Amplifier:MAX2679 +RF_Amplifier:MAX2679B +RF_Amplifier:MMZ09332BT1 +RF_Amplifier:PGA-102 +RF_Amplifier:PGA-1021 +RF_Amplifier:PGA-103 +RF_Amplifier:PGA-105 +RF_Amplifier:PGA-106-75 +RF_Amplifier:PGA-106R-75 +RF_Amplifier:PGA-122-75 +RF_Amplifier:PGA-32-75 +RF_Amplifier:PHA-1 +RF_Amplifier:PHA-101 +RF_Amplifier:PHA-13HLN +RF_Amplifier:PHA-13LN +RF_Amplifier:PHA-1H +RF_Amplifier:PHA-23HLN +RF_Amplifier:PHA-23LN +RF_Amplifier:QPL9547 +RF_Amplifier:SGL0622Z +RF_Amplifier:SKY65404 +RF_Amplifier:SPF5189Z +RF_Amplifier:TRF37A73 +RF_AM_FM:LA1185 +RF_AM_FM:MCS3142 +RF_AM_FM:SA605D +RF_AM_FM:SA605DK +RF_AM_FM:SA636DK +RF_AM_FM:Si4362 +RF_AM_FM:Si4730-D60-GU +RF_AM_FM:Si4731-D60-GU +RF_AM_FM:Si4734-D60-GU +RF_AM_FM:Si4735-D60-GU +RF_AM_FM:ZETA-433-SO +RF_AM_FM:ZETA-868-SO +RF_AM_FM:ZETA-915-SO +RF_Bluetooth:BL652 +RF_Bluetooth:BM78SPPS5MC2 +RF_Bluetooth:BM78SPPS5NC2 +RF_Bluetooth:BTM112 +RF_Bluetooth:BTM222 +RF_Bluetooth:MOD-nRF8001 +RF_Bluetooth:Microchip_BM83 +RF_Bluetooth:RFD77101 +RF_Bluetooth:RN42 +RF_Bluetooth:RN42N +RF_Bluetooth:RN4871 +RF_Bluetooth:SPBTLE-RF +RF_Bluetooth:SPBTLE-RF0 +RF_Bluetooth:nRF8001 +RF_Filter:B3715 +RF_Filter:BFCN-1445 +RF_Filter:BFCN-1525 +RF_Filter:BFCN-152W-75 +RF_Filter:BFCN-1560 +RF_Filter:BFCN-1575 +RF_Filter:BFCN-1690 +RF_Filter:BFCN-1840 +RF_Filter:BFCN-1855 +RF_Filter:BFCN-1860 +RF_Filter:BFCN-1900 +RF_Filter:BFCN-1945 +RF_Filter:BFCN-2275 +RF_Filter:BFCN-2360 +RF_Filter:BFCN-2435 +RF_Filter:BFCN-2450 +RF_Filter:BFCN-2500 +RF_Filter:BFCN-2555 +RF_Filter:BFCN-2700 +RF_Filter:BFCN-2840 +RF_Filter:BFCN-2850 +RF_Filter:BFCN-2900 +RF_Filter:BFCN-2910 +RF_Filter:BFCN-2975 +RF_Filter:BFCN-3010 +RF_Filter:BFCN-3085 +RF_Filter:BFCN-3085A +RF_Filter:BFCN-3115 +RF_Filter:BFCN-3600 +RF_Filter:BFCN-3700 +RF_Filter:BFCN-4100 +RF_Filter:BFCN-4440 +RF_Filter:BFCN-4800 +RF_Filter:BFCN-5100 +RF_Filter:BFCN-5200 +RF_Filter:BFCN-5540 +RF_Filter:BFCN-5750 +RF_Filter:BFCN-7200 +RF_Filter:BFCN-7331 +RF_Filter:BFCN-7350 +RF_Filter:BFCN-7500 +RF_Filter:BFCN-7700 +RF_Filter:BFCN-7900 +RF_Filter:BFCN-8000 +RF_Filter:BFCN-8350 +RF_Filter:BFCN-8450 +RF_Filter:BFCN-8650 +RF_Filter:BPF-A355 +RF_Filter:HFCN-1000 +RF_Filter:HFCN-1080 +RF_Filter:HFCN-1100 +RF_Filter:HFCN-1150 +RF_Filter:HFCN-1200 +RF_Filter:HFCN-1200D +RF_Filter:HFCN-1300 +RF_Filter:HFCN-1300D +RF_Filter:HFCN-1320 +RF_Filter:HFCN-1320D +RF_Filter:HFCN-1322 +RF_Filter:HFCN-1500 +RF_Filter:HFCN-1500D +RF_Filter:HFCN-1600 +RF_Filter:HFCN-1600D +RF_Filter:HFCN-1760 +RF_Filter:HFCN-1810 +RF_Filter:HFCN-1810D +RF_Filter:HFCN-1910 +RF_Filter:HFCN-1910D +RF_Filter:HFCN-2000 +RF_Filter:HFCN-2100 +RF_Filter:HFCN-2100D +RF_Filter:HFCN-2275 +RF_Filter:HFCN-2700 +RF_Filter:HFCN-2700A +RF_Filter:HFCN-2700AD +RF_Filter:HFCN-3100 +RF_Filter:HFCN-3100D +RF_Filter:HFCN-3500 +RF_Filter:HFCN-3500D +RF_Filter:HFCN-3800 +RF_Filter:HFCN-3800D +RF_Filter:HFCN-440 +RF_Filter:HFCN-4400 +RF_Filter:HFCN-4400D +RF_Filter:HFCN-4600 +RF_Filter:HFCN-5050 +RF_Filter:HFCN-5500 +RF_Filter:HFCN-5500D +RF_Filter:HFCN-6010 +RF_Filter:HFCN-650 +RF_Filter:HFCN-650D +RF_Filter:HFCN-672 +RF_Filter:HFCN-7150 +RF_Filter:HFCN-740 +RF_Filter:HFCN-740D +RF_Filter:HFCN-7971 +RF_Filter:HFCN-8400 +RF_Filter:HFCN-8400D +RF_Filter:HFCN-880 +RF_Filter:HFCN-880D +RF_Filter:HFCN-9700 +RF_Filter:LFCN-1000 +RF_Filter:LFCN-1000D +RF_Filter:LFCN-105 +RF_Filter:LFCN-113 +RF_Filter:LFCN-120 +RF_Filter:LFCN-1200 +RF_Filter:LFCN-1200D +RF_Filter:LFCN-123 +RF_Filter:LFCN-1282 +RF_Filter:LFCN-1325 +RF_Filter:LFCN-1400 +RF_Filter:LFCN-1400D +RF_Filter:LFCN-1450 +RF_Filter:LFCN-1500 +RF_Filter:LFCN-1500D +RF_Filter:LFCN-1525 +RF_Filter:LFCN-1525D +RF_Filter:LFCN-1575 +RF_Filter:LFCN-1575D +RF_Filter:LFCN-160 +RF_Filter:LFCN-1700 +RF_Filter:LFCN-1700D +RF_Filter:LFCN-180 +RF_Filter:LFCN-1800 +RF_Filter:LFCN-1800D +RF_Filter:LFCN-190 +RF_Filter:LFCN-2000 +RF_Filter:LFCN-2000D +RF_Filter:LFCN-225 +RF_Filter:LFCN-2250 +RF_Filter:LFCN-2250D +RF_Filter:LFCN-225D +RF_Filter:LFCN-2290 +RF_Filter:LFCN-2400 +RF_Filter:LFCN-2400D +RF_Filter:LFCN-2500 +RF_Filter:LFCN-2500D +RF_Filter:LFCN-2600 +RF_Filter:LFCN-2600D +RF_Filter:LFCN-2750 +RF_Filter:LFCN-2750D +RF_Filter:LFCN-2850 +RF_Filter:LFCN-2850D +RF_Filter:LFCN-3000 +RF_Filter:LFCN-3000D +RF_Filter:LFCN-320 +RF_Filter:LFCN-320D +RF_Filter:LFCN-3400 +RF_Filter:LFCN-3400D +RF_Filter:LFCN-3800 +RF_Filter:LFCN-3800D +RF_Filter:LFCN-400 +RF_Filter:LFCN-400D +RF_Filter:LFCN-4400 +RF_Filter:LFCN-4400D +RF_Filter:LFCN-490 +RF_Filter:LFCN-490D +RF_Filter:LFCN-5000 +RF_Filter:LFCN-5000D +RF_Filter:LFCN-530 +RF_Filter:LFCN-530D +RF_Filter:LFCN-5500 +RF_Filter:LFCN-5500D +RF_Filter:LFCN-575 +RF_Filter:LFCN-575D +RF_Filter:LFCN-5850 +RF_Filter:LFCN-5850D +RF_Filter:LFCN-6000 +RF_Filter:LFCN-6000D +RF_Filter:LFCN-630 +RF_Filter:LFCN-630D +RF_Filter:LFCN-6400 +RF_Filter:LFCN-6400D +RF_Filter:LFCN-6700 +RF_Filter:LFCN-6700D +RF_Filter:LFCN-7200 +RF_Filter:LFCN-7200D +RF_Filter:LFCN-722 +RF_Filter:LFCN-80 +RF_Filter:LFCN-800 +RF_Filter:LFCN-800D +RF_Filter:LFCN-8400 +RF_Filter:LFCN-8440 +RF_Filter:LFCN-900 +RF_Filter:LFCN-900D +RF_Filter:LFCN-9170 +RF_Filter:LFCN-95 +RF_Filter:LPF-B0R3 +RF_Filter:RBP-280 +RF_Filter:RBPF-246 +RF_Filter:RLP-30 +RF_Filter:SCHF-31 +RF_Filter:STA0232A +RF_Filter:STA1090EC +RF_Filter:SXBP-100 +RF_Filter:SXBP-140 +RF_Filter:SXBP-202 +RF_Filter:SXBP-27R5 +RF_Filter:TA0232A +RF_Filter:TA0970B +RF_GPS:L70-R +RF_GPS:L80-R +RF_GPS:LEA-M8F +RF_GPS:LEA-M8S +RF_GPS:LEA-M8T +RF_GPS:MAX-8C +RF_GPS:MAX-8Q +RF_GPS:MAX-M10S +RF_GPS:MAX-M8C +RF_GPS:MAX-M8Q +RF_GPS:MAX-M8W +RF_GPS:NEO-8Q +RF_GPS:NEO-M8M +RF_GPS:NEO-M8N +RF_GPS:NEO-M8P +RF_GPS:NEO-M8Q +RF_GPS:NEO-M8T +RF_GPS:NEO-M9N +RF_GPS:RXM-GPS-FM +RF_GPS:RXM-GPS-RM +RF_GPS:SAM-M8Q +RF_GPS:SIM28ML +RF_GPS:ZED-F9P +RF_GPS:ZOE-M8G +RF_GPS:ZOE-M8Q +RF_GSM:BC66 +RF_GSM:BC95 +RF_GSM:BG95-M1 +RF_GSM:BG95-M2 +RF_GSM:BG95-M3 +RF_GSM:BG95-M4 +RF_GSM:BG95-M5 +RF_GSM:BG95-M6 +RF_GSM:BG95-M8 +RF_GSM:BG95-MF +RF_GSM:LENA-R8001 +RF_GSM:M95 +RF_GSM:SARA-U201 +RF_GSM:SARA-U260 +RF_GSM:SARA-U270 +RF_GSM:SARA-U280 +RF_GSM:SE150A4 +RF_GSM:SIM7020C +RF_GSM:SIM7020E +RF_GSM:SIM800C +RF_GSM:SIM900 +RF_GSM:UL865 +RF_Mixer:AD831AP +RF_Mixer:ADE-6 +RF_Mixer:ADEX-10 +RF_Mixer:ADL5801 +RF_Mixer:ADL5802 +RF_Mixer:HMC213A +RF_Mixer:HMC213B +RF_Mixer:LT5560 +RF_Module:AST50147-xx +RF_Module:ATSAMR21G18-MR210UA_NoRFPads +RF_Module:AX-SIP-SFEU +RF_Module:AX-SIP-SFEU-API +RF_Module:Ai-Thinker-Ra-01 +RF_Module:Ai-Thinker-Ra-02 +RF_Module:CMWX1ZZABZ-078 +RF_Module:CMWX1ZZABZ-091 +RF_Module:D52MxxM8 +RF_Module:DCTR-52DA +RF_Module:DCTR-52DAT +RF_Module:DWM1000 +RF_Module:DWM1001 +RF_Module:DWM3000 +RF_Module:E18-MS1-PCB +RF_Module:E73-2G4M04S-52810 +RF_Module:E73-2G4M04S-52832 +RF_Module:ESP-07 +RF_Module:ESP-12E +RF_Module:ESP-12F +RF_Module:ESP-WROOM-02 +RF_Module:ESP32-C3-DevKitM-1 +RF_Module:ESP32-C3-WROOM-02 +RF_Module:ESP32-C3-WROOM-02U +RF_Module:ESP32-C6-MINI-1 +RF_Module:ESP32-S2-WROVER +RF_Module:ESP32-S2-WROVER-I +RF_Module:ESP32-S3-MINI-1 +RF_Module:ESP32-S3-MINI-1U +RF_Module:ESP32-S3-WROOM-1 +RF_Module:ESP32-S3-WROOM-2 +RF_Module:ESP32-WROOM-32 +RF_Module:ESP32-WROOM-32D +RF_Module:ESP32-WROOM-32E +RF_Module:ESP32-WROOM-32E-R2 +RF_Module:ESP32-WROOM-32U +RF_Module:ESP32-WROOM-32UE +RF_Module:ESP32-WROOM-32UE-R2 +RF_Module:HT-CT62 +RF_Module:Jadak_Thingmagic_M6e-Nano +RF_Module:MDBT42Q-512K +RF_Module:MDBT50Q-1MV2 +RF_Module:MDBT50Q-512K +RF_Module:MDBT50Q-P1MV2 +RF_Module:MDBT50Q-P512K +RF_Module:MDBT50Q-U1MV2 +RF_Module:MDBT50Q-U512K +RF_Module:MM002 +RF_Module:Particle_P1 +RF_Module:RAK4200 +RF_Module:RAK811-HF-AS923 +RF_Module:RAK811-HF-AU915 +RF_Module:RAK811-HF-EU868 +RF_Module:RAK811-HF-IN865 +RF_Module:RAK811-HF-KR920 +RF_Module:RAK811-HF-US915 +RF_Module:RAK811-LF-CN470 +RF_Module:RAK811-LF-EU433 +RF_Module:RFM69HCW +RF_Module:RFM69HW +RF_Module:RFM69W +RF_Module:RFM95W-868S2 +RF_Module:RFM95W-915S2 +RF_Module:RFM96W-315S2 +RF_Module:RFM96W-433S2 +RF_Module:RFM97W-868S2 +RF_Module:RFM97W-915S2 +RF_Module:RFM98W-315S2 +RF_Module:RFM98W-433S2 +RF_Module:STM32WB5MMG +RF_Module:TD1205 +RF_Module:TD1208 +RF_Module:TR-52DA +RF_Module:TR-52DAT +RF_Module:TR-72DA +RF_Module:TR-72DAT +RF_Module:WEMOS_C3_mini +RF_Module:WEMOS_D1_mini +RF_Module:iM880A +RF_Module:iM880B +RF_NFC:PN5321A3HN_C1xx +RF_NFC:ST25DV04K-IER8C3 +RF_NFC:ST25DV04K-JFR6D3 +RF_NFC:ST25DV16K-IER8C3 +RF_NFC:ST25DV16K-JFR6D3 +RF_NFC:ST25DV64K-IER8C3 +RF_NFC:ST25DV64K-JFR6D3 +RF_NFC:ST25R3911B-AQF +RF_NFC:ST25R3911B-AQW +RF_RFID:HTRC11001T +RF_RFID:PN5120A0HN1 +RF_Switch:ADG901BCPZ +RF_Switch:ADG901BRMZ +RF_Switch:ADG902BRMZ +RF_Switch:ADG918BCPZ +RF_Switch:ADG918BRM +RF_Switch:ADG919BCPZ +RF_Switch:ADG919BRMZ +RF_Switch:AS179-92LF +RF_Switch:BGS12WN6E6327 +RF_Switch:HMC7992 +RF_Switch:HMC849A +RF_Switch:KSW-2-46 +RF_Switch:KSWA-2-46 +RF_Switch:KSWHA-1-20 +RF_Switch:MASW-007221 +RF_Switch:MASWSS0115 +RF_Switch:MASWSS0136 +RF_Switch:MASWSS0143 +RF_Switch:MASWSS0151 +RF_Switch:MASWSS0166 +RF_Switch:MASWSS0176 +RF_Switch:MASWSS0178 +RF_Switch:MASWSS0179 +RF_Switch:MASWSS0192 +RF_Switch:MSW-2-20 +RF_Switch:MSW2-50 +RF_Switch:MSWA-2-20 +RF_Switch:MSWA2-50 +RF_Switch:SKY13380-350LF +RF_Switch:SKY13575-639LF +RF_WiFi:HF-A11-SMT +RF_WiFi:USR-C322 +RF_ZigBee:CC2520 +RF_ZigBee:MC13192 +RF_ZigBee:MW-R-DP-W +RF_ZigBee:MW-R-WX +RF_ZigBee:TWE-L-DP-W +RF_ZigBee:TWE-L-WX +RF_ZigBee:XBee_SMT +Security:ATAES132A-SH +Security:ATECC508A-MAHDA +Security:ATECC508A-SSHDA +Security:ATECC608A-MAHDA +Security:ATECC608A-SSHDA +Security:ATECC608B-MAHDA +Security:ATECC608B-SSHDA +Sensor:ADE7758 +Sensor:ADE7763xRS +Sensor:ADE7953xCP +Sensor:AM2302 +Sensor:APDS-9960 +Sensor:AS3935 +Sensor:BL0937 +Sensor:BME280 +Sensor:BME680 +Sensor:CHT11 +Sensor:DHT11 +Sensor:INA260 +Sensor:LTC2990 +Sensor:MAX30102 +Sensor:Nuclear-Radiation_Detector +Sensor:RPR-0521RS +Sensor:SHT1x +Sensor_Audio:ICS-43434 +Sensor_Audio:IM69D120 +Sensor_Audio:IM69D130 +Sensor_Audio:IM73A135V01 +Sensor_Audio:MP45DT02 +Sensor_Audio:SPH0641LU4H-1 +Sensor_Audio:SPH0645LM4H +Sensor_Audio:SPM0687LR5H-1 +Sensor_Current:A1363xKTTN-1 +Sensor_Current:A1363xKTTN-10 +Sensor_Current:A1363xKTTN-2 +Sensor_Current:A1363xKTTN-5 +Sensor_Current:A1365xKTTN-1 +Sensor_Current:A1365xKTTN-10 +Sensor_Current:A1365xKTTN-2 +Sensor_Current:A1365xKTTN-5 +Sensor_Current:A1366xKTTN-1 +Sensor_Current:A1366xKTTN-10 +Sensor_Current:A1366xKTTN-2 +Sensor_Current:A1366xKTTN-5 +Sensor_Current:A1367xKTTN-1 +Sensor_Current:A1367xKTTN-10 +Sensor_Current:A1367xKTTN-2 +Sensor_Current:A1367xKTTN-5 +Sensor_Current:A1369xUA-10 +Sensor_Current:A1369xUA-24 +Sensor_Current:ACS706xLC-05C +Sensor_Current:ACS709xLFTR-10BB +Sensor_Current:ACS709xLFTR-20BB +Sensor_Current:ACS709xLFTR-35BB +Sensor_Current:ACS709xLFTR-6BB +Sensor_Current:ACS710xLATR-10BB +Sensor_Current:ACS710xLATR-10BB-NL +Sensor_Current:ACS710xLATR-12BB +Sensor_Current:ACS710xLATR-12BB-NL +Sensor_Current:ACS710xLATR-25BB +Sensor_Current:ACS710xLATR-25BB-NL +Sensor_Current:ACS710xLATR-6BB +Sensor_Current:ACS710xLATR-6BB-NL +Sensor_Current:ACS711xEXLT-15AB +Sensor_Current:ACS711xEXLT-31AB +Sensor_Current:ACS711xLCTR-12AB +Sensor_Current:ACS711xLCTR-25AB +Sensor_Current:ACS712xLCTR-05B +Sensor_Current:ACS712xLCTR-20A +Sensor_Current:ACS712xLCTR-30A +Sensor_Current:ACS713xLCTR-20A +Sensor_Current:ACS713xLCTR-30A +Sensor_Current:ACS714xLCTR-05B +Sensor_Current:ACS714xLCTR-20A +Sensor_Current:ACS714xLCTR-30A +Sensor_Current:ACS714xLCTR-50A +Sensor_Current:ACS715xLCTR-20A +Sensor_Current:ACS715xLCTR-30A +Sensor_Current:ACS716xLATR-12BB +Sensor_Current:ACS716xLATR-12BB-NL +Sensor_Current:ACS716xLATR-25BB +Sensor_Current:ACS716xLATR-25BB-NL +Sensor_Current:ACS716xLATR-6BB +Sensor_Current:ACS716xLATR-6BB-NL +Sensor_Current:ACS717xMATR-10B +Sensor_Current:ACS717xMATR-20B +Sensor_Current:ACS718xMATR-10B +Sensor_Current:ACS718xMATR-20B +Sensor_Current:ACS720xMATR-15B +Sensor_Current:ACS720xMATR-35B +Sensor_Current:ACS720xMATR-65B +Sensor_Current:ACS722xLCTR-05AB +Sensor_Current:ACS722xLCTR-10AB +Sensor_Current:ACS722xLCTR-10AU +Sensor_Current:ACS722xLCTR-20AB +Sensor_Current:ACS722xLCTR-20AU +Sensor_Current:ACS722xLCTR-40AB +Sensor_Current:ACS722xLCTR-40AU +Sensor_Current:ACS722xMATR-10AB +Sensor_Current:ACS722xMATR-20AB +Sensor_Current:ACS722xMATR-40AB +Sensor_Current:ACS723xLCTR-05AB +Sensor_Current:ACS723xLCTR-10AB +Sensor_Current:ACS723xLCTR-10AU +Sensor_Current:ACS723xLCTR-20AB +Sensor_Current:ACS723xLCTR-20AU +Sensor_Current:ACS723xLCTR-40AB +Sensor_Current:ACS723xLCTR-40AU +Sensor_Current:ACS723xMATR-10AB +Sensor_Current:ACS723xMATR-20AB +Sensor_Current:ACS723xMATR-40AB +Sensor_Current:ACS724xLCTR-05AB +Sensor_Current:ACS724xLCTR-10AB +Sensor_Current:ACS724xLCTR-10AU +Sensor_Current:ACS724xLCTR-20AB +Sensor_Current:ACS724xLCTR-20AU +Sensor_Current:ACS724xLCTR-30AB +Sensor_Current:ACS724xLCTR-30AU +Sensor_Current:ACS724xLCTR-50AB +Sensor_Current:ACS724xMATR-12AB +Sensor_Current:ACS724xMATR-20AB +Sensor_Current:ACS724xMATR-30AB +Sensor_Current:ACS724xMATR-30AU +Sensor_Current:ACS724xMATR-65AB +Sensor_Current:ACS725xLCTR-10AU +Sensor_Current:ACS725xLCTR-20AB +Sensor_Current:ACS725xLCTR-20AU +Sensor_Current:ACS725xLCTR-30AB +Sensor_Current:ACS725xLCTR-30AU +Sensor_Current:ACS725xLCTR-40AB +Sensor_Current:ACS725xLCTR-50AB +Sensor_Current:ACS725xMATR-20AB +Sensor_Current:ACS725xMATR-30AB +Sensor_Current:ACS725xMATR-30AU +Sensor_Current:ACS726xLFTR-20B +Sensor_Current:ACS726xLFTR-40B +Sensor_Current:ACS730xLCTR-20AB +Sensor_Current:ACS730xLCTR-40AB +Sensor_Current:ACS730xLCTR-40AU +Sensor_Current:ACS730xLCTR-50AB +Sensor_Current:ACS730xLCTR-80AU +Sensor_Current:ACS732xLATR-40AB +Sensor_Current:ACS73369xUAA-010B5 +Sensor_Current:ACS733xLATR-20AB +Sensor_Current:ACS733xLATR-40AB +Sensor_Current:ACS733xLATR-40AU +Sensor_Current:ACS733xLATR-65AB +Sensor_Current:ACS756xCB-050B-PFF +Sensor_Current:ACS756xCB-100B-PFF +Sensor_Current:ACS758xCB-050B-PFF +Sensor_Current:ACS758xCB-050U-PFF +Sensor_Current:ACS758xCB-100B-PFF +Sensor_Current:ACS758xCB-100B-PSF +Sensor_Current:ACS758xCB-100U-PFF +Sensor_Current:ACS758xCB-150B-PFF +Sensor_Current:ACS758xCB-150B-PSS +Sensor_Current:ACS758xCB-150U-PFF +Sensor_Current:ACS758xCB-150U-PSF +Sensor_Current:ACS758xCB-200B-PFF +Sensor_Current:ACS758xCB-200B-PSF +Sensor_Current:ACS758xCB-200B-PSS +Sensor_Current:ACS758xCB-200U-PFF +Sensor_Current:ACS758xCB-200U-PSF +Sensor_Current:ACS759xCB-050B-PFF +Sensor_Current:ACS759xCB-100B-PFF +Sensor_Current:ACS759xCB-150B-PFF +Sensor_Current:ACS759xCB-150B-PSS +Sensor_Current:ACS759xCB-200B-PFF +Sensor_Current:ACS759xCB-200B-PSS +Sensor_Current:ACS770xCB-050B-PFF +Sensor_Current:ACS770xCB-050U-PFF +Sensor_Current:ACS770xCB-100B-PFF +Sensor_Current:ACS770xCB-100U-PFF +Sensor_Current:ACS770xCB-100U-PSF +Sensor_Current:ACS770xCB-150B-PFF +Sensor_Current:ACS770xCB-150B-PSF +Sensor_Current:ACS770xCB-150U-PFF +Sensor_Current:ACS770xCB-150U-PSF +Sensor_Current:ACS770xCB-200B-PFF +Sensor_Current:ACS770xCB-200B-PSF +Sensor_Current:ACS770xCB-200U-PFF +Sensor_Current:ACS770xCB-200U-PSF +Sensor_Current:ACS780xLRTR-050B +Sensor_Current:ACS780xLRTR-050U +Sensor_Current:ACS780xLRTR-100B +Sensor_Current:ACS780xLRTR-100U +Sensor_Current:ACS780xLRTR-150B +Sensor_Current:ACS780xLRTR-150U +Sensor_Current:ACS781xLRTR-050B +Sensor_Current:ACS781xLRTR-050U +Sensor_Current:ACS781xLRTR-100B +Sensor_Current:ACS781xLRTR-100U +Sensor_Current:ACS781xLRTR-150B +Sensor_Current:ACS781xLRTR-150U +Sensor_Current:CKSR_15-NP +Sensor_Current:CKSR_25-NP +Sensor_Current:CKSR_50-NP +Sensor_Current:CKSR_50-NP-SP1 +Sensor_Current:CKSR_6-NP +Sensor_Current:CKSR_75-NP +Sensor_Current:CQ-2063 +Sensor_Current:CQ-2064 +Sensor_Current:CQ-2065 +Sensor_Current:CQ-206A +Sensor_Current:CQ-206B +Sensor_Current:CQ-2092 +Sensor_Current:CQ-2093 +Sensor_Current:CQ-209A +Sensor_Current:CQ-209B +Sensor_Current:CQ-209D +Sensor_Current:CQ-2232 +Sensor_Current:CQ-2233 +Sensor_Current:CQ-2234 +Sensor_Current:CQ-2235 +Sensor_Current:CQ-2332 +Sensor_Current:CQ-2333 +Sensor_Current:CQ-2334 +Sensor_Current:CQ-2335 +Sensor_Current:CQ-2336 +Sensor_Current:CQ-236B +Sensor_Current:CQ-3200 +Sensor_Current:CQ-3201 +Sensor_Current:CQ-3202 +Sensor_Current:CQ-3203 +Sensor_Current:CQ-3204 +Sensor_Current:CQ-320A +Sensor_Current:CQ-320B +Sensor_Current:CQ-3300 +Sensor_Current:CQ-3301 +Sensor_Current:CQ-3302 +Sensor_Current:CQ-3303 +Sensor_Current:CQ-330A +Sensor_Current:CQ-330B +Sensor_Current:CQ-330E +Sensor_Current:CQ-330F +Sensor_Current:CQ-330G +Sensor_Current:CQ-330H +Sensor_Current:CQ-330J +Sensor_Current:CSLW6B1 +Sensor_Current:CSLW6B200M +Sensor_Current:CSLW6B40M +Sensor_Current:CSLW6B5 +Sensor_Current:CZ-3813 +Sensor_Current:CZ-3814 +Sensor_Current:CZ-3815 +Sensor_Current:HO120-NP +Sensor_Current:HO128-NP +Sensor_Current:HO15-NP +Sensor_Current:HO15-NPxSP33 +Sensor_Current:HO15-NSM +Sensor_Current:HO150-NP +Sensor_Current:HO25-NP +Sensor_Current:HO25-NPxSP33 +Sensor_Current:HO25-NSM +Sensor_Current:HO40-NP +Sensor_Current:HO60-NP +Sensor_Current:HO8-NP +Sensor_Current:HO8-NPxSP33 +Sensor_Current:HO8-NSM +Sensor_Current:HTFS200-P +Sensor_Current:HTFS400-P +Sensor_Current:HTFS600-P +Sensor_Current:HTFS800-P +Sensor_Current:HX02-P +Sensor_Current:HX03-P-SP2 +Sensor_Current:HX04-P +Sensor_Current:HX05-NP +Sensor_Current:HX05-P-SP2 +Sensor_Current:HX06-P +Sensor_Current:HX10-NP +Sensor_Current:HX10-P-SP2 +Sensor_Current:HX15-NP +Sensor_Current:HX15-P-SP2 +Sensor_Current:HX20-P-SP2 +Sensor_Current:HX25-P-SP2 +Sensor_Current:HX50-P-SP2 +Sensor_Current:IR2175 +Sensor_Current:IR21771S +Sensor_Current:IR2177S +Sensor_Current:IR22771S +Sensor_Current:IR2277S +Sensor_Current:IR25750L +Sensor_Current:LA100-P +Sensor_Current:LA25-NP +Sensor_Current:LA25-P +Sensor_Current:LA55-P +Sensor_Current:LTSR15-NP +Sensor_Current:LTSR25-NP +Sensor_Current:LTSR6-NP +Sensor_Current:MCA1101-20-3 +Sensor_Current:MCA1101-20-5 +Sensor_Current:MCA1101-5-3 +Sensor_Current:MCA1101-5-5 +Sensor_Current:MCA1101-50-3 +Sensor_Current:MCA1101-50-5 +Sensor_Current:MCA1101-65-5 +Sensor_Distance:TMF8820 +Sensor_Distance:TMF8821 +Sensor_Distance:TMF8828 +Sensor_Distance:VL53L0CXV0DH1 +Sensor_Distance:VL53L1CXV0FY1 +Sensor_Energy:ATM90E26-YU +Sensor_Energy:INA219AxD +Sensor_Energy:INA219AxDCN +Sensor_Energy:INA219BxD +Sensor_Energy:INA219BxDCN +Sensor_Energy:INA226 +Sensor_Energy:INA228 +Sensor_Energy:INA233 +Sensor_Energy:INA237 +Sensor_Energy:INA238 +Sensor_Energy:LTC4151xMS +Sensor_Energy:MCP39F521 +Sensor_Energy:PAC1931x-xJ6CX +Sensor_Energy:PAC1932x-xJ6CX +Sensor_Energy:PAC1932x-xJQ +Sensor_Energy:PAC1933x-xJ6CX +Sensor_Energy:PAC1933x-xJQ +Sensor_Energy:PAC1934x-xJ6CX +Sensor_Energy:PAC1934x-xJQ +Sensor_Energy:PAC1941x-1x-4MX +Sensor_Energy:PAC1941x-1x-J6CX +Sensor_Energy:PAC1941x-2x-4MX +Sensor_Energy:PAC1941x-2x-J6CX +Sensor_Energy:PAC1942x-1x-4MX +Sensor_Energy:PAC1942x-1x-J6CX +Sensor_Energy:PAC1942x-2x-4MX +Sensor_Energy:PAC1942x-2x-J6CX +Sensor_Energy:PAC1943x-x4MX +Sensor_Energy:PAC1943x-xJ6CX +Sensor_Energy:PAC1944x-x4MX +Sensor_Energy:PAC1944x-xJ6CX +Sensor_Energy:PAC1951x-1x-4MX +Sensor_Energy:PAC1951x-1x-J6CX +Sensor_Energy:PAC1951x-2x-4MX +Sensor_Energy:PAC1951x-2x-J6CX +Sensor_Energy:PAC1952x-1x-4MX +Sensor_Energy:PAC1952x-1x-J6CX +Sensor_Energy:PAC1952x-2x-4MX +Sensor_Energy:PAC1952x-2x-J6CX +Sensor_Energy:PAC1953x-x4MX +Sensor_Energy:PAC1953x-xJ6CX +Sensor_Energy:PAC1954x-x4MX +Sensor_Energy:PAC1954x-xJ6CX +Sensor_Gas:004-0-0010 +Sensor_Gas:004-0-0013 +Sensor_Gas:004-0-0050 +Sensor_Gas:004-0-0053 +Sensor_Gas:004-0-0071 +Sensor_Gas:004-0-0075 +Sensor_Gas:3SP-H2S-50_110-304 +Sensor_Gas:CCS811 +Sensor_Gas:GM-402B +Sensor_Gas:LuminOX_LOX-O2 +Sensor_Gas:MQ-6 +Sensor_Gas:MiCS-5524 +Sensor_Gas:SCD40-D-R2 +Sensor_Gas:SCD41-D-R2 +Sensor_Gas:TGS-5141 +Sensor_Humidity:ENS210 +Sensor_Humidity:HDC1080 +Sensor_Humidity:HDC2080 +Sensor_Humidity:SHT30-DIS +Sensor_Humidity:SHT30A-DIS +Sensor_Humidity:SHT31-DIS +Sensor_Humidity:SHT31A-DIS +Sensor_Humidity:SHT35-DIS +Sensor_Humidity:SHT35A-DIS +Sensor_Humidity:SHT4x +Sensor_Humidity:SHTC1 +Sensor_Humidity:SHTC3 +Sensor_Humidity:Si7020-A20 +Sensor_Humidity:Si7021-A20 +Sensor_Magnetic:A1101ELHL +Sensor_Magnetic:A1101LLHL +Sensor_Magnetic:A1102ELHL +Sensor_Magnetic:A1102LLHL +Sensor_Magnetic:A1103ELHL +Sensor_Magnetic:A1103LLHL +Sensor_Magnetic:A1104LLHL +Sensor_Magnetic:A1106LLHL +Sensor_Magnetic:A1301EUA-T +Sensor_Magnetic:A1301KLHLT-T +Sensor_Magnetic:A1301KUA-T +Sensor_Magnetic:A1302ELHLT-T +Sensor_Magnetic:A1302KLHLT-T +Sensor_Magnetic:A1302KUA-T +Sensor_Magnetic:A3214ELHLT-T +Sensor_Magnetic:AH1806-P +Sensor_Magnetic:AH1806-W +Sensor_Magnetic:AH1806-Z +Sensor_Magnetic:AK7452 +Sensor_Magnetic:AS5045B +Sensor_Magnetic:AS5047D +Sensor_Magnetic:AS5048A +Sensor_Magnetic:AS5048B +Sensor_Magnetic:AS5050A +Sensor_Magnetic:AS5055A +Sensor_Magnetic:BM1422AGMV +Sensor_Magnetic:BMM150 +Sensor_Magnetic:DRV5033AJxDBZ +Sensor_Magnetic:DRV5033AJxLPG +Sensor_Magnetic:DRV5033FAxDBZ +Sensor_Magnetic:DRV5033FAxLPG +Sensor_Magnetic:DRV5055A1xDBZxQ1 +Sensor_Magnetic:DRV5055A1xLPGxQ1 +Sensor_Magnetic:DRV5055A2xDBZxQ1 +Sensor_Magnetic:DRV5055A2xLPGxQ1 +Sensor_Magnetic:DRV5055A3xDBZxQ1 +Sensor_Magnetic:DRV5055A3xLPGxQ1 +Sensor_Magnetic:DRV5055A4xDBZxQ1 +Sensor_Magnetic:DRV5055A4xLPGxQ1 +Sensor_Magnetic:IST8308 +Sensor_Magnetic:IST8310 +Sensor_Magnetic:LIS2MDL +Sensor_Magnetic:LIS3MDL +Sensor_Magnetic:MA730 +Sensor_Magnetic:MMC5633NJL +Sensor_Magnetic:MMC5883MA +Sensor_Magnetic:MT6701CT +Sensor_Magnetic:MT6701QT +Sensor_Magnetic:MT6816CT +Sensor_Magnetic:SM351LT +Sensor_Magnetic:SM353LT +Sensor_Magnetic:Si7210-B-xx-IM2 +Sensor_Magnetic:Si7210-B-xx-IV +Sensor_Magnetic:TLE5012B +Sensor_Magnetic:TLV493D +Sensor_Magnetic:TMAG5110A2xxDBV +Sensor_Magnetic:TMAG5110A4xxDBV +Sensor_Magnetic:TMAG5110B2xxDBV +Sensor_Magnetic:TMAG5110B4xxDBV +Sensor_Magnetic:TMAG5110C2xxDBV +Sensor_Magnetic:TMAG5110C4xxDBV +Sensor_Magnetic:TMAG5111A2xxDBV +Sensor_Magnetic:TMAG5111A4xxDBV +Sensor_Magnetic:TMAG5111B2xxDBV +Sensor_Magnetic:TMAG5111B4xxDBV +Sensor_Magnetic:TMAG5111C2xxDBV +Sensor_Magnetic:TMAG5111C4xxDBV +Sensor_Magnetic:TMAG5170-Q1 +Sensor_Motion:ADXL343 +Sensor_Motion:ADXL363 +Sensor_Motion:BMF055 +Sensor_Motion:BMI160 +Sensor_Motion:BNO055 +Sensor_Motion:ICM-20602 +Sensor_Motion:ICM-20948 +Sensor_Motion:IIM-42652 +Sensor_Motion:IIS3DWB +Sensor_Motion:IPS2200 +Sensor_Motion:ISM330DHCX +Sensor_Motion:KX022-1020 +Sensor_Motion:KX122-1042 +Sensor_Motion:KX222-1054 +Sensor_Motion:KXTJ3-1057 +Sensor_Motion:L3GD20 +Sensor_Motion:LIS2DE12 +Sensor_Motion:LIS2DH +Sensor_Motion:LIS2HH12 +Sensor_Motion:LIS331HH +Sensor_Motion:LIS3DH +Sensor_Motion:LSM303C +Sensor_Motion:LSM303D +Sensor_Motion:LSM303DLHC +Sensor_Motion:LSM6DS3 +Sensor_Motion:LSM6DSL +Sensor_Motion:LSM6DSM +Sensor_Motion:LSM9DS1 +Sensor_Motion:MMA8653FCR1 +Sensor_Motion:MPU-6000 +Sensor_Motion:MPU-6050 +Sensor_Motion:MPU-9150 +Sensor_Motion:MPU-9250 +Sensor_Motion:SC7A20 +Sensor_Optical:A1050 +Sensor_Optical:A1060 +Sensor_Optical:A9013 +Sensor_Optical:A9050 +Sensor_Optical:A9060 +Sensor_Optical:APDS-9251-001 +Sensor_Optical:APDS-9301 +Sensor_Optical:APDS-9306 +Sensor_Optical:APDS-9306-065 +Sensor_Optical:AS7261 +Sensor_Optical:AS7262 +Sensor_Optical:AS7263 +Sensor_Optical:AS72651 +Sensor_Optical:AS7341DLG +Sensor_Optical:AS7343xDLG +Sensor_Optical:BP103 +Sensor_Optical:BP103B +Sensor_Optical:BP103BF +Sensor_Optical:BP104 +Sensor_Optical:BP104-SMD +Sensor_Optical:BPW21 +Sensor_Optical:BPW34 +Sensor_Optical:BPW34-SMD +Sensor_Optical:BPW40 +Sensor_Optical:BPW42 +Sensor_Optical:BPW82 +Sensor_Optical:BPW85 +Sensor_Optical:BPW85A +Sensor_Optical:BPW85B +Sensor_Optical:BPW85C +Sensor_Optical:BPX61 +Sensor_Optical:BPX65 +Sensor_Optical:BPY62 +Sensor_Optical:C12880MA +Sensor_Optical:Flir_LEPTON +Sensor_Optical:ISL29035 +Sensor_Optical:KPS-3227 +Sensor_Optical:KPS-5130 +Sensor_Optical:LDR03 +Sensor_Optical:LDR07 +Sensor_Optical:LPT80A +Sensor_Optical:LTR-303ALS-01 +Sensor_Optical:M9960 +Sensor_Optical:NOA1305 +Sensor_Optical:PMTx08Dyn +Sensor_Optical:PMTx08Dyn_Shld +Sensor_Optical:S13360-3025CS +Sensor_Optical:S13360-3050CS +Sensor_Optical:S13360-3075CS +Sensor_Optical:S5971 +Sensor_Optical:S5972 +Sensor_Optical:S5973 +Sensor_Optical:SFH203 +Sensor_Optical:SFH203FA +Sensor_Optical:SFH205F +Sensor_Optical:SFH205FA +Sensor_Optical:SFH206K +Sensor_Optical:SFH216 +Sensor_Optical:SFH225FA +Sensor_Optical:SFH235FA +Sensor_Optical:SFH2400 +Sensor_Optical:SFH2430 +Sensor_Optical:SFH2440 +Sensor_Optical:SFH2701 +Sensor_Optical:SFH300 +Sensor_Optical:SFH309 +Sensor_Optical:SFH320 +Sensor_Optical:SFH3201 +Sensor_Optical:TEPT4400 +Sensor_Optical:TSL2550D +Sensor_Optical:TSL2550T +Sensor_Optical:TSL25911FN +Sensor_Optical:VT93xx +Sensor_Pressure:40PC015G +Sensor_Pressure:40PC100G +Sensor_Pressure:40PC150G +Sensor_Pressure:40PC250G +Sensor_Pressure:BMP280 +Sensor_Pressure:LPS22DF +Sensor_Pressure:LPS22HB +Sensor_Pressure:LPS22HH +Sensor_Pressure:LPS25HB +Sensor_Pressure:MPL115A1 +Sensor_Pressure:MPL3115A2 +Sensor_Pressure:MPXA6115A +Sensor_Pressure:MPXAZ6115A +Sensor_Pressure:MPXH6115A +Sensor_Pressure:MPXHZ6115A +Sensor_Pressure:MS5525DSO +Sensor_Pressure:MS5607-02BA +Sensor_Pressure:MS5611-01BA +Sensor_Pressure:MS5837-xxBA +Sensor_Pressure:WSEN-PADS_2511020213301 +Sensor_Pressure:XGZP6897D +Sensor_Pressure:XGZP6899D +Sensor_Proximity:AD7150BRMZ +Sensor_Proximity:AD7151BRMZ +Sensor_Proximity:APDS-9160-003 +Sensor_Proximity:BPR-105 +Sensor_Proximity:BPR-105F +Sensor_Proximity:BPR-205 +Sensor_Proximity:CNY70 +Sensor_Proximity:GP2S700HCP +Sensor_Proximity:ITR1201SR10AR +Sensor_Proximity:ITR8307 +Sensor_Proximity:ITR8307-F43 +Sensor_Proximity:ITR8307-L24-TR8 +Sensor_Proximity:ITR8307-S17-TR8 +Sensor_Proximity:ITR9608-F +Sensor_Proximity:KRC011 +Sensor_Proximity:LDC1312 +Sensor_Proximity:LDC1314 +Sensor_Proximity:LDC1612 +Sensor_Proximity:LDC1614 +Sensor_Proximity:LG206D +Sensor_Proximity:LG206L +Sensor_Proximity:QRE1113 +Sensor_Proximity:QRE1113GR +Sensor_Proximity:RPR-0720 +Sensor_Proximity:SFH900 +Sensor_Proximity:SFH9201 +Sensor_Proximity:SFH9202 +Sensor_Proximity:SFH9206 +Sensor_Proximity:SG-105 +Sensor_Proximity:SG-105F +Sensor_Proximity:SG-107 +Sensor_Proximity:SG-107F +Sensor_Proximity:TSSP58038 +Sensor_Proximity:TSSP58038SS1XB +Sensor_Proximity:TSSP58P38 +Sensor_Temperature:AD8494 +Sensor_Temperature:AD8495 +Sensor_Temperature:AD8496 +Sensor_Temperature:AD8497 +Sensor_Temperature:BD1020HFV +Sensor_Temperature:DS1621 +Sensor_Temperature:DS1621S +Sensor_Temperature:DS1621V +Sensor_Temperature:DS1804 +Sensor_Temperature:DS1821C +Sensor_Temperature:DS1822 +Sensor_Temperature:DS1822-PAR +Sensor_Temperature:DS1822Z +Sensor_Temperature:DS1825 +Sensor_Temperature:DS18B20 +Sensor_Temperature:DS18B20-PAR +Sensor_Temperature:DS18B20U +Sensor_Temperature:DS18B20Z +Sensor_Temperature:DS18S20 +Sensor_Temperature:DS18S20-PAR +Sensor_Temperature:DS18S20Z +Sensor_Temperature:DS28EA00 +Sensor_Temperature:KT100 +Sensor_Temperature:KTY10 +Sensor_Temperature:KTY81 +Sensor_Temperature:KTY82 +Sensor_Temperature:KTY83 +Sensor_Temperature:KTY84 +Sensor_Temperature:KTY85 +Sensor_Temperature:LM20BIM7 +Sensor_Temperature:LM20CIM7 +Sensor_Temperature:LM35-D +Sensor_Temperature:LM35-LP +Sensor_Temperature:LM35-NEB +Sensor_Temperature:LM73 +Sensor_Temperature:LM73-1 +Sensor_Temperature:LM74CIM +Sensor_Temperature:LM74CITP +Sensor_Temperature:LM75B +Sensor_Temperature:LM75C +Sensor_Temperature:LM92CIM +Sensor_Temperature:LM94021 +Sensor_Temperature:LMT01DQX +Sensor_Temperature:LMT01LPG +Sensor_Temperature:LMT84DCK +Sensor_Temperature:LMT85DCK +Sensor_Temperature:LMT86DCK +Sensor_Temperature:LMT87DCK +Sensor_Temperature:LTC2983 +Sensor_Temperature:MAX31820 +Sensor_Temperature:MAX31820PAR +Sensor_Temperature:MAX31826 +Sensor_Temperature:MAX31855EASA +Sensor_Temperature:MAX31855JASA +Sensor_Temperature:MAX31855KASA +Sensor_Temperature:MAX31855NASA +Sensor_Temperature:MAX31855RASA +Sensor_Temperature:MAX31855SASA +Sensor_Temperature:MAX31855TASA +Sensor_Temperature:MAX31856 +Sensor_Temperature:MAX31865xAP +Sensor_Temperature:MAX31865xTP +Sensor_Temperature:MAX6654 +Sensor_Temperature:MCP9501 +Sensor_Temperature:MCP9502 +Sensor_Temperature:MCP9503 +Sensor_Temperature:MCP9504 +Sensor_Temperature:MCP9700Ax-ELT +Sensor_Temperature:MCP9700Ax-ETT +Sensor_Temperature:MCP9700Ax-HLT +Sensor_Temperature:MCP9700Ax-HTT +Sensor_Temperature:MCP9700x-ELT +Sensor_Temperature:MCP9700x-ETT +Sensor_Temperature:MCP9700x-HLT +Sensor_Temperature:MCP9700x-HTT +Sensor_Temperature:MCP9800Ax-xOT +Sensor_Temperature:MCP9802Ax-xOT +Sensor_Temperature:MCP9804_DFN +Sensor_Temperature:MCP9804_MSOP +Sensor_Temperature:MCP9808_DFN +Sensor_Temperature:MCP9808_MSOP +Sensor_Temperature:MCP9844x-xMN +Sensor_Temperature:PCT2075D +Sensor_Temperature:PCT2075DP +Sensor_Temperature:PT100 +Sensor_Temperature:PT1000 +Sensor_Temperature:PT500 +Sensor_Temperature:Si7050-A20 +Sensor_Temperature:Si7051-A20 +Sensor_Temperature:Si7053-A20 +Sensor_Temperature:Si7054-A20 +Sensor_Temperature:Si7055-A20 +Sensor_Temperature:TC1047AxNB +Sensor_Temperature:TC1047xNB +Sensor_Temperature:TMP100 +Sensor_Temperature:TMP101 +Sensor_Temperature:TMP102xxDRL +Sensor_Temperature:TMP1075D +Sensor_Temperature:TMP1075DGK +Sensor_Temperature:TMP1075DSG +Sensor_Temperature:TMP110D +Sensor_Temperature:TMP112xxDRL +Sensor_Temperature:TMP114 +Sensor_Temperature:TMP116xxDRV +Sensor_Temperature:TMP117xxDRV +Sensor_Temperature:TMP117xxYBG +Sensor_Temperature:TMP119AIYBGR +Sensor_Temperature:TMP20AIDCK +Sensor_Temperature:TMP20AIDRL +Sensor_Temperature:TMP36xS +Sensor_Temperature:TMP411 +Sensor_Temperature:TMP461xxRUN +Sensor_Temperature:TMP464xxRGT +Sensor_Temperature:TMP468xxRGT +Sensor_Temperature:TSIC206-SO8 +Sensor_Temperature:TSIC206-TO92 +Sensor_Temperature:TSIC306-SO8 +Sensor_Temperature:TSIC306-TO92 +Sensor_Touch:AT42QT1010-M +Sensor_Touch:AT42QT1010-TSHR +Sensor_Touch:AT42QT1011-M +Sensor_Touch:AT42QT1011-TSHR +Sensor_Touch:AT42QT1012-M +Sensor_Touch:AT42QT1012-T +Sensor_Touch:AT42QT1040-M +Sensor_Touch:AT42QT1050-M +Sensor_Touch:AT42QT1050-U +Sensor_Touch:AT42QT1060-M +Sensor_Touch:AT42QT1070-M +Sensor_Touch:AT42QT1070-S +Sensor_Touch:AT42QT1110-M +Sensor_Touch:CAP1206-x-AIA +Sensor_Touch:CAP1206-x-SL +Sensor_Touch:CY8CMBR3002 +Sensor_Touch:CY8CMBR3102 +Sensor_Touch:CY8CMBR3106S +Sensor_Touch:CY8CMBR3108 +Sensor_Touch:CY8CMBR3110 +Sensor_Touch:CY8CMBR3116 +Sensor_Touch:MPR121QR2 +Sensor_Touch:PCA8886 +Sensor_Touch:PCF8883 +Sensor_Voltage:LV25-P +Simulation_SPICE:0 +Simulation_SPICE:BSOURCE +Simulation_SPICE:D +Simulation_SPICE:ESOURCE +Simulation_SPICE:GSOURCE +Simulation_SPICE:IAM +Simulation_SPICE:IBIS_DEVICE +Simulation_SPICE:IBIS_DEVICE_DIFF +Simulation_SPICE:IBIS_DRIVER +Simulation_SPICE:IBIS_DRIVER_DIFF +Simulation_SPICE:IDC +Simulation_SPICE:IEXP +Simulation_SPICE:IPULSE +Simulation_SPICE:IPWL +Simulation_SPICE:ISFFM +Simulation_SPICE:ISIN +Simulation_SPICE:ITRNOISE +Simulation_SPICE:ITRRANDOM +Simulation_SPICE:NJFET +Simulation_SPICE:NMOS +Simulation_SPICE:NMOS_Substrate +Simulation_SPICE:NPN +Simulation_SPICE:NPN_Substrate +Simulation_SPICE:OPAMP +Simulation_SPICE:PJFET +Simulation_SPICE:PMOS +Simulation_SPICE:PMOS_Substrate +Simulation_SPICE:PNP +Simulation_SPICE:PNP_Substrate +Simulation_SPICE:SWITCH +Simulation_SPICE:TLINE +Simulation_SPICE:VAM +Simulation_SPICE:VDC +Simulation_SPICE:VEXP +Simulation_SPICE:VOLTMETER_DIFF +Simulation_SPICE:VPULSE +Simulation_SPICE:VPWL +Simulation_SPICE:VSFFM +Simulation_SPICE:VSIN +Simulation_SPICE:VTRNOISE +Simulation_SPICE:VTRRANDOM +Switch:CK_KMS2xxG +Switch:CK_KMS2xxGP +Switch:SW_Coded +Switch:SW_Coded_SH-7010 +Switch:SW_Coded_SH-7030 +Switch:SW_Coded_SH-7040 +Switch:SW_Coded_SH-7050 +Switch:SW_Coded_SH-7070 +Switch:SW_Coded_SH-7080 +Switch:SW_DIP_x01 +Switch:SW_DIP_x02 +Switch:SW_DIP_x03 +Switch:SW_DIP_x04 +Switch:SW_DIP_x05 +Switch:SW_DIP_x06 +Switch:SW_DIP_x07 +Switch:SW_DIP_x08 +Switch:SW_DIP_x09 +Switch:SW_DIP_x10 +Switch:SW_DIP_x11 +Switch:SW_DIP_x12 +Switch:SW_DP3T +Switch:SW_DPDT_x2 +Switch:SW_DPST +Switch:SW_DPST_Temperature +Switch:SW_DPST_x2 +Switch:SW_E3_SA3216 +Switch:SW_E3_SA3624 +Switch:SW_E3_SA6432 +Switch:SW_MEC_5E +Switch:SW_MEC_5G +Switch:SW_MEC_5G_2LED +Switch:SW_MEC_5G_LED +Switch:SW_MMI_Q5-100 +Switch:SW_NKK_GW12LJPCF +Switch:SW_Nidec_CAS-120A1 +Switch:SW_Omron_B3FS +Switch:SW_Push +Switch:SW_Push_45deg +Switch:SW_Push_DPDT +Switch:SW_Push_Dual +Switch:SW_Push_Dual_x2 +Switch:SW_Push_LED +Switch:SW_Push_Lamp +Switch:SW_Push_Open +Switch:SW_Push_Open_Dual +Switch:SW_Push_Open_Dual_x2 +Switch:SW_Push_SPDT +Switch:SW_Push_Shielded +Switch:SW_Reed +Switch:SW_Reed_Opener +Switch:SW_Reed_SPDT +Switch:SW_Rotary_1x12 +Switch:SW_Rotary_1x3_MP +Switch:SW_Rotary_1x4_MP +Switch:SW_Rotary_1x5_MP +Switch:SW_Rotary_1x6_MP +Switch:SW_Rotary_1x7_MP +Switch:SW_Rotary_1x8_MP +Switch:SW_Rotary_1x9_MP +Switch:SW_Rotary_2x6 +Switch:SW_Rotary_3x4 +Switch:SW_Rotary_4x3 +Switch:SW_SP3T +Switch:SW_SP3T_NR01103 +Switch:SW_SP4T_NR01104 +Switch:SW_SP5T_NR01105 +Switch:SW_SPDT +Switch:SW_SPDT_312 +Switch:SW_SPDT_321 +Switch:SW_SPDT_MSM +Switch:SW_SPDT_XKB_DMx-xxxx-1 +Switch:SW_SPST +Switch:SW_SPST_LED +Switch:SW_SPST_Lamp +Switch:SW_SPST_Temperature +Switch:SW_Slide_DPDT +Switch:SW_Wuerth_450301014042 +Timer:8253 +Timer:8254 +Timer:8284 +Timer:82C54 +Timer:82C54_PLCC +Timer:AD9513 +Timer:AD9514 +Timer:AD9515 +Timer:CD4541BE +Timer:CD4541BM +Timer:CD4541BPW +Timer:DS1023S +Timer:ICM7209 +Timer:ICM7555xB +Timer:ICM7555xP +Timer:ICM7556 +Timer:LM555xM +Timer:LM555xMM +Timer:LM555xN +Timer:LM556 +Timer:LMC555xM +Timer:LMC555xMM +Timer:LMC555xN +Timer:LMC555xTP +Timer:LTC6902 +Timer:LTC6909 +Timer:LTC6993xS6-1 +Timer:LTC6993xS6-2 +Timer:LTC6993xS6-3 +Timer:LTC6993xS6-4 +Timer:LTC6994xDCB-1 +Timer:LTC6994xDCB-2 +Timer:LTC6994xS6-1 +Timer:LTC6994xS6-2 +Timer:MC14541BD +Timer:MC14541BDT +Timer:MC1455B +Timer:MC1455P +Timer:MN3101 +Timer:MN3102 +Timer:NA555D +Timer:NA555P +Timer:NA556 +Timer:NE555D +Timer:NE555P +Timer:NE556 +Timer:NE567 +Timer:NLV14541BD +Timer:NLV14541BDT +Timer:PL611-01-xxxT +Timer:SA555D +Timer:SA555P +Timer:SA556 +Timer:SE555D +Timer:SE555P +Timer:SE556 +Timer:SE567 +Timer:SY58031U +Timer:SY58032U +Timer:SY58033U +Timer:TLC555xD +Timer:TLC555xP +Timer:TLC555xPS +Timer:TLC555xPW +Timer:TPL5010 +Timer:TPL5110 +Timer:TPL5111 +Timer_PLL:ADF4002BCPZ +Timer_PLL:ADF4002BRUZ +Timer_PLL:ADF4158 +Timer_PLL:ADF4350 +Timer_PLL:ADF4351 +Timer_PLL:CDCVF2505 +Timer_PLL:CS2000-CP +Timer_PLL:ICS525-01R +Timer_PLL:ICS525R-02 +Timer_PLL:Si5342A-D +Timer_PLL:Si5342B-D +Timer_PLL:Si5342C-D +Timer_PLL:Si5342D-D +Timer_PLL:Si5344A-D +Timer_PLL:Si5344B-D +Timer_PLL:Si5344C-D +Timer_PLL:Si5344D-D +Timer_PLL:Si5345A-D +Timer_PLL:Si5345B-D +Timer_PLL:Si5345C-D +Timer_PLL:Si5345D-D +Timer_RTC:AB0805 +Timer_RTC:AB0815 +Timer_RTC:AB1805 +Timer_RTC:AB1815 +Timer_RTC:BQ32000 +Timer_RTC:BQ32002 +Timer_RTC:DS1302+ +Timer_RTC:DS1302N+ +Timer_RTC:DS1302S+ +Timer_RTC:DS1302SN+ +Timer_RTC:DS1302Z+ +Timer_RTC:DS1302ZN+ +Timer_RTC:DS1307+ +Timer_RTC:DS1307N+ +Timer_RTC:DS1307Z+ +Timer_RTC:DS1307ZN+ +Timer_RTC:DS1602 +Timer_RTC:DS3231M +Timer_RTC:DS3231MZ +Timer_RTC:DS3232M +Timer_RTC:M41T62Q +Timer_RTC:MCP7940N-xMNY +Timer_RTC:MCP7940N-xMS +Timer_RTC:MCP7940N-xP +Timer_RTC:MCP7940N-xSN +Timer_RTC:MCP7940N-xST +Timer_RTC:PCF85063ATL +Timer_RTC:PCF8523T +Timer_RTC:PCF8523TK +Timer_RTC:PCF8523TS +Timer_RTC:PCF85263AT +Timer_RTC:PCF85263ATL +Timer_RTC:PCF85263ATT +Timer_RTC:PCF85263ATT1 +Timer_RTC:PCF85363ATT +Timer_RTC:PCF85363ATT1 +Timer_RTC:PCF8563T +Timer_RTC:PCF8563TS +Timer_RTC:RV-1805-C3 +Timer_RTC:RV-3028-C7 +Timer_RTC:RV-8523-C3 +Timer_RTC:RX8901CE +Transformer:0433BM15A0001 +Transformer:0868BM15C0001 +Transformer:0896BM15A0001 +Transformer:0915BM15A0001 +Transformer:30F-51NL +Transformer:5400BL15B050 +Transformer:ADT1-1 +Transformer:ADT1-1WT +Transformer:ADT1-1WT-1 +Transformer:ADT1-6T +Transformer:ADT1.5-1 +Transformer:ADT1.5-122 +Transformer:ADT1.5-17 +Transformer:ADT1.5-2 +Transformer:ADT16-1T +Transformer:ADT16-6 +Transformer:ADT16-6T +Transformer:ADT2-1T +Transformer:ADT2-1T-1P +Transformer:ADT2-71T +Transformer:ADT3-1T +Transformer:ADT3-1T-75 +Transformer:ADT3-6T +Transformer:ADT4-1T +Transformer:ADT4-1WT +Transformer:ADT4-5WT +Transformer:ADT4-6 +Transformer:ADT4-6T +Transformer:ADT4-6WT +Transformer:ADT8-1T +Transformer:ADT9-1T +Transformer:ADTL1-12 +Transformer:ADTL1-15-75 +Transformer:ADTL1-18-75 +Transformer:ADTL1-4-75 +Transformer:ADTL2-18 +Transformer:ADTT1-1 +Transformer:ADTT1-6 +Transformer:ADTT1.5-1 +Transformer:ADTT3-2 +Transformer:ADTT4-1 +Transformer:B0322J5050AHF +Transformer:CST1 +Transformer:CST1_Split +Transformer:CST2 +Transformer:CST2010 +Transformer:CST2010_Split +Transformer:CST2_Split +Transformer:ED8_4 +Transformer:ETC1-1-13 +Transformer:LL1587 +Transformer:P0544NL +Transformer:P0926NL +Transformer:PA0173NL +Transformer:PA0184NL +Transformer:PA0184NL1 +Transformer:PA0185NL +Transformer:PA0185NL1 +Transformer:PA0264NL +Transformer:PA0297NL +Transformer:PA0510NL +Transformer:PA1323NL +Transformer:PA2001NL +Transformer:PA2002NL +Transformer:PA2004NL +Transformer:PA2005NL +Transformer:PA2006NL +Transformer:PA2007NL +Transformer:PA2008NL +Transformer:PA2009NL +Transformer:PA2777NL +Transformer:PA3493NL +Transformer:PE-68386NL +Transformer:PT61017PEL +Transformer:PT61020EL +Transformer:TC1-1-13M+ +Transformer:TEZ0.5-D-1 +Transformer:TEZ0.5-D-2 +Transformer:TEZ1.5-D-1 +Transformer:TEZ1.5-D-2 +Transformer:TEZ10.0-D-1 +Transformer:TEZ10.0-D-2 +Transformer:TEZ16.0-D-1 +Transformer:TEZ16.0-D-2 +Transformer:TEZ2.0-D-1 +Transformer:TEZ2.0-D-2 +Transformer:TEZ2.5-D-1 +Transformer:TEZ2.5-D-2 +Transformer:TEZ2.6-D-1 +Transformer:TEZ2.6-D-2 +Transformer:TEZ4.0-D-1 +Transformer:TEZ4.0-D-2 +Transformer:TEZ6.0-D-1 +Transformer:TEZ6.0-D-2 +Transformer:TG110-E050N5xx +Transformer:TG110-S050N2xx +Transformer:TG111-MSC13LF +Transformer:TR1-SO8 +Transformer:TR60_FC +Transformer:TR60_IC +Transformer:TR60_IC2 +Transformer:TRANSF1 +Transformer:TRANSF2 +Transformer:TRANSF3 +Transformer:TRANSF4 +Transformer:TRANSF5 +Transformer:TRANSF6 +Transformer:TRANSF7 +Transformer:TRANSF8 +Transformer:Triad_VPP16-310 +Transformer:Wuerth_749013011A +Transformer:Wuerth_750315371 +Transformer:Wuerth_750343373 +Transformer:Wuerth_760871131 +Transformer:Wurth_750319177 +Transformer:ZMCT103C +Transformer:ZMPT101K +Transistor_Array:A2982 +Transistor_Array:MC1413BD +Transistor_Array:MC1413BP +Transistor_Array:MC1413D +Transistor_Array:MC1413P +Transistor_Array:NCV1413B +Transistor_Array:SN75468 +Transistor_Array:SN75469 +Transistor_Array:TBD62783A +Transistor_Array:TBD62785AFWG +Transistor_Array:TBD62785APG +Transistor_Array:ULN2002 +Transistor_Array:ULN2002A +Transistor_Array:ULN2003 +Transistor_Array:ULN2003A +Transistor_Array:ULN2004 +Transistor_Array:ULN2004A +Transistor_Array:ULN2801A +Transistor_Array:ULN2802A +Transistor_Array:ULN2803A +Transistor_Array:ULN2804A +Transistor_Array:ULN2805A +Transistor_Array:ULQ2003A +Transistor_Array:ULQ2004A +Transistor_BJT:2N2219 +Transistor_BJT:2N2646 +Transistor_BJT:2N2647 +Transistor_BJT:2N3055 +Transistor_BJT:2N3904 +Transistor_BJT:2N3905 +Transistor_BJT:2N3906 +Transistor_BJT:2SA1015 +Transistor_BJT:2SB631 +Transistor_BJT:2SB817 +Transistor_BJT:2SC1815 +Transistor_BJT:2SC1941 +Transistor_BJT:2SC1945 +Transistor_BJT:2SC4213 +Transistor_BJT:2SD1047 +Transistor_BJT:2SD600 +Transistor_BJT:320S14-U +Transistor_BJT:BC107 +Transistor_BJT:BC108 +Transistor_BJT:BC109 +Transistor_BJT:BC140 +Transistor_BJT:BC141 +Transistor_BJT:BC160 +Transistor_BJT:BC161 +Transistor_BJT:BC212 +Transistor_BJT:BC237 +Transistor_BJT:BC240 +Transistor_BJT:BC307 +Transistor_BJT:BC327 +Transistor_BJT:BC328 +Transistor_BJT:BC337 +Transistor_BJT:BC338 +Transistor_BJT:BC413 +Transistor_BJT:BC413B +Transistor_BJT:BC413C +Transistor_BJT:BC414 +Transistor_BJT:BC414B +Transistor_BJT:BC414C +Transistor_BJT:BC516 +Transistor_BJT:BC517 +Transistor_BJT:BC546 +Transistor_BJT:BC547 +Transistor_BJT:BC548 +Transistor_BJT:BC549 +Transistor_BJT:BC550 +Transistor_BJT:BC556 +Transistor_BJT:BC557 +Transistor_BJT:BC558 +Transistor_BJT:BC559 +Transistor_BJT:BC560 +Transistor_BJT:BC636 +Transistor_BJT:BC807 +Transistor_BJT:BC807W +Transistor_BJT:BC808 +Transistor_BJT:BC808W +Transistor_BJT:BC817 +Transistor_BJT:BC817W +Transistor_BJT:BC818 +Transistor_BJT:BC818W +Transistor_BJT:BC846 +Transistor_BJT:BC846BDW1 +Transistor_BJT:BC846BPDW1 +Transistor_BJT:BC846BPN +Transistor_BJT:BC846BS +Transistor_BJT:BC847 +Transistor_BJT:BC847BDW1 +Transistor_BJT:BC847BPDW1 +Transistor_BJT:BC847BPN +Transistor_BJT:BC847BS +Transistor_BJT:BC847W +Transistor_BJT:BC848 +Transistor_BJT:BC848W +Transistor_BJT:BC849 +Transistor_BJT:BC849W +Transistor_BJT:BC850 +Transistor_BJT:BC850W +Transistor_BJT:BC856 +Transistor_BJT:BC856BDW1 +Transistor_BJT:BC856BS +Transistor_BJT:BC856W +Transistor_BJT:BC857 +Transistor_BJT:BC857BDW1 +Transistor_BJT:BC857BS +Transistor_BJT:BC857W +Transistor_BJT:BC858 +Transistor_BJT:BC858W +Transistor_BJT:BC859 +Transistor_BJT:BC859W +Transistor_BJT:BC860 +Transistor_BJT:BC860W +Transistor_BJT:BCP51 +Transistor_BJT:BCP53 +Transistor_BJT:BCP56 +Transistor_BJT:BCV29 +Transistor_BJT:BCV49 +Transistor_BJT:BCV61 +Transistor_BJT:BCV62 +Transistor_BJT:BCX51 +Transistor_BJT:BCX52 +Transistor_BJT:BCX53 +Transistor_BJT:BCX56 +Transistor_BJT:BD135 +Transistor_BJT:BD136 +Transistor_BJT:BD137 +Transistor_BJT:BD138 +Transistor_BJT:BD139 +Transistor_BJT:BD140 +Transistor_BJT:BD233 +Transistor_BJT:BD234 +Transistor_BJT:BD235 +Transistor_BJT:BD236 +Transistor_BJT:BD237 +Transistor_BJT:BD238 +Transistor_BJT:BD249 +Transistor_BJT:BD249A +Transistor_BJT:BD249B +Transistor_BJT:BD249C +Transistor_BJT:BD250 +Transistor_BJT:BD250A +Transistor_BJT:BD250B +Transistor_BJT:BD250C +Transistor_BJT:BD433 +Transistor_BJT:BD434 +Transistor_BJT:BD435 +Transistor_BJT:BD436 +Transistor_BJT:BD437 +Transistor_BJT:BD438 +Transistor_BJT:BD439 +Transistor_BJT:BD440 +Transistor_BJT:BD441 +Transistor_BJT:BD442 +Transistor_BJT:BD909 +Transistor_BJT:BD910 +Transistor_BJT:BD911 +Transistor_BJT:BD912 +Transistor_BJT:BDW93 +Transistor_BJT:BDW93A +Transistor_BJT:BDW93B +Transistor_BJT:BDW93C +Transistor_BJT:BDW94 +Transistor_BJT:BDW94A +Transistor_BJT:BDW94B +Transistor_BJT:BDW94C +Transistor_BJT:BF199 +Transistor_BJT:BF457 +Transistor_BJT:BF458 +Transistor_BJT:BF459 +Transistor_BJT:BFR92 +Transistor_BJT:BFT92 +Transistor_BJT:BUT11 +Transistor_BJT:BUT11A +Transistor_BJT:DMMT5401 +Transistor_BJT:DTA113T +Transistor_BJT:DTA113Z +Transistor_BJT:DTA114E +Transistor_BJT:DTA114G +Transistor_BJT:DTA114T +Transistor_BJT:DTA114W +Transistor_BJT:DTA114Y +Transistor_BJT:DTA115E +Transistor_BJT:DTA115G +Transistor_BJT:DTA115T +Transistor_BJT:DTA115U +Transistor_BJT:DTA123E +Transistor_BJT:DTA123J +Transistor_BJT:DTA123Y +Transistor_BJT:DTA124E +Transistor_BJT:DTA124G +Transistor_BJT:DTA124T +Transistor_BJT:DTA124X +Transistor_BJT:DTA125T +Transistor_BJT:DTA143E +Transistor_BJT:DTA143T +Transistor_BJT:DTA143X +Transistor_BJT:DTA143Y +Transistor_BJT:DTA143Z +Transistor_BJT:DTA144E +Transistor_BJT:DTA144G +Transistor_BJT:DTA144T +Transistor_BJT:DTA144V +Transistor_BJT:DTA144W +Transistor_BJT:DTA1D3R +Transistor_BJT:DTA214Y +Transistor_BJT:DTB113E +Transistor_BJT:DTB113Z +Transistor_BJT:DTB114E +Transistor_BJT:DTB114G +Transistor_BJT:DTB114T +Transistor_BJT:DTB122J +Transistor_BJT:DTB123E +Transistor_BJT:DTB123T +Transistor_BJT:DTB123Y +Transistor_BJT:DTB133H +Transistor_BJT:DTB143T +Transistor_BJT:DTB163T +Transistor_BJT:DTC113T +Transistor_BJT:DTC113Z +Transistor_BJT:DTC114E +Transistor_BJT:DTC114G +Transistor_BJT:DTC114T +Transistor_BJT:DTC114W +Transistor_BJT:DTC114Y +Transistor_BJT:DTC115E +Transistor_BJT:DTC115G +Transistor_BJT:DTC115T +Transistor_BJT:DTC115U +Transistor_BJT:DTC123E +Transistor_BJT:DTC123J +Transistor_BJT:DTC123Y +Transistor_BJT:DTC124E +Transistor_BJT:DTC124G +Transistor_BJT:DTC124T +Transistor_BJT:DTC124X +Transistor_BJT:DTC125T +Transistor_BJT:DTC143E +Transistor_BJT:DTC143T +Transistor_BJT:DTC143X +Transistor_BJT:DTC143Y +Transistor_BJT:DTC143Z +Transistor_BJT:DTC144E +Transistor_BJT:DTC144G +Transistor_BJT:DTC144T +Transistor_BJT:DTC144V +Transistor_BJT:DTC144W +Transistor_BJT:DTC1D3R +Transistor_BJT:DTC214Y +Transistor_BJT:DTD113E +Transistor_BJT:DTD113Z +Transistor_BJT:DTD114E +Transistor_BJT:DTD114G +Transistor_BJT:DTD114T +Transistor_BJT:DTD122J +Transistor_BJT:DTD123E +Transistor_BJT:DTD123T +Transistor_BJT:DTD123Y +Transistor_BJT:DTD133H +Transistor_BJT:DTD143T +Transistor_BJT:DTD163T +Transistor_BJT:EMH3 +Transistor_BJT:FFB2222A +Transistor_BJT:FFB2227A +Transistor_BJT:FFB3904 +Transistor_BJT:FFB3906 +Transistor_BJT:FFB3946 +Transistor_BJT:FFB5551 +Transistor_BJT:FMB2227A +Transistor_BJT:FMB3946 +Transistor_BJT:IMH3A +Transistor_BJT:KTD1624 +Transistor_BJT:MAT02 +Transistor_BJT:MBT2222ADW1T1 +Transistor_BJT:MBT3904DW1 +Transistor_BJT:MBT3906DW1 +Transistor_BJT:MBT3946DW1T1 +Transistor_BJT:MJ2955 +Transistor_BJT:MJE13003 +Transistor_BJT:MJE13005G +Transistor_BJT:MJE13007G +Transistor_BJT:MJE13009G +Transistor_BJT:MMBT2222A +Transistor_BJT:MMBT3904 +Transistor_BJT:MMBT3906 +Transistor_BJT:MMBT5550L +Transistor_BJT:MMBT5551L +Transistor_BJT:MMBTA06 +Transistor_BJT:MMBTA42 +Transistor_BJT:MMBTA44 +Transistor_BJT:MMBTA56 +Transistor_BJT:MMBTA92 +Transistor_BJT:MMBTA94 +Transistor_BJT:MMDT2222A +Transistor_BJT:MMDT3904 +Transistor_BJT:MMDT3906 +Transistor_BJT:MMDT3946 +Transistor_BJT:MMDT5401 +Transistor_BJT:MMDT5551 +Transistor_BJT:MMDTA06 +Transistor_BJT:MPSA42 +Transistor_BJT:MPSA92 +Transistor_BJT:MUN5111DW1 +Transistor_BJT:MUN5112DW1 +Transistor_BJT:MUN5113DW1 +Transistor_BJT:MUN5114DW1 +Transistor_BJT:MUN5211DW1 +Transistor_BJT:MUN5212DW1 +Transistor_BJT:MUN5213DW1 +Transistor_BJT:MUN5214DW1 +Transistor_BJT:MUN5311DW1 +Transistor_BJT:MUN5312DW1 +Transistor_BJT:MUN5313DW1 +Transistor_BJT:MUN5314DW1 +Transistor_BJT:MUN5330DW1 +Transistor_BJT:MUN5331DW1 +Transistor_BJT:MUN5332DW1 +Transistor_BJT:MUN5333DW1 +Transistor_BJT:MUN5334DW1 +Transistor_BJT:MUN5335DW1 +Transistor_BJT:MUN5336DW1 +Transistor_BJT:PBSS301PZ +Transistor_BJT:PMBT2222A +Transistor_BJT:PMBT2222AYS +Transistor_BJT:PMBT3904YS +Transistor_BJT:PMBT3906YS +Transistor_BJT:PMBT3946YPN +Transistor_BJT:PN2222A +Transistor_BJT:PUMT1 +Transistor_BJT:PUMX1 +Transistor_BJT:PZT2222A +Transistor_BJT:PZT3904 +Transistor_BJT:PZT3906 +Transistor_BJT:PZTA42 +Transistor_BJT:PZTA92 +Transistor_BJT:Q_Dual_NPN_C2C1E1E2 +Transistor_BJT:Q_Dual_NPN_NPN_B1E2B2C2E1C1 +Transistor_BJT:Q_Dual_NPN_NPN_BRT_E1B1C2E2B2C1 +Transistor_BJT:Q_Dual_NPN_NPN_BRT_No_R2_C1B2E2C2B1E1 +Transistor_BJT:Q_Dual_NPN_NPN_BRT_No_R2_E1B1C2E2B2C1 +Transistor_BJT:Q_Dual_NPN_NPN_C1E1C2E2B2B1 +Transistor_BJT:Q_Dual_NPN_NPN_E1B1C2E2B2C1 +Transistor_BJT:Q_Dual_NPN_PNP_B1E2B2C2E1C1 +Transistor_BJT:Q_Dual_NPN_PNP_BRT_E1B1C2E2B2C1 +Transistor_BJT:Q_Dual_NPN_PNP_E1B1C2E2B2C1 +Transistor_BJT:Q_Dual_PNP_C2C1E1E2 +Transistor_BJT:Q_Dual_PNP_NPN_BRT_E1B1C2E2B2C1 +Transistor_BJT:Q_Dual_PNP_PNP_BRT_E1B1C2E2B2C1 +Transistor_BJT:Q_Dual_PNP_PNP_C1B1B2C2E2E1 +Transistor_BJT:Q_Dual_PNP_PNP_C1E1C2E2B2B1 +Transistor_BJT:Q_Dual_PNP_PNP_E1B1C2E2B2C1 +Transistor_BJT:Q_NPN_BCE +Transistor_BJT:Q_NPN_BCEC +Transistor_BJT:Q_NPN_BEC +Transistor_BJT:Q_NPN_BRT_BEC +Transistor_BJT:Q_NPN_BRT_ECB +Transistor_BJT:Q_NPN_CBE +Transistor_BJT:Q_NPN_CEB +Transistor_BJT:Q_NPN_Darlington_BCE +Transistor_BJT:Q_NPN_Darlington_BCEC +Transistor_BJT:Q_NPN_Darlington_BEC +Transistor_BJT:Q_NPN_Darlington_CBE +Transistor_BJT:Q_NPN_Darlington_CEB +Transistor_BJT:Q_NPN_Darlington_EBC +Transistor_BJT:Q_NPN_Darlington_ECB +Transistor_BJT:Q_NPN_Darlington_ECBC +Transistor_BJT:Q_NPN_EBC +Transistor_BJT:Q_NPN_ECB +Transistor_BJT:Q_NPN_ECBC +Transistor_BJT:Q_PNP_BCE +Transistor_BJT:Q_PNP_BCEC +Transistor_BJT:Q_PNP_BEC +Transistor_BJT:Q_PNP_BRT_BEC +Transistor_BJT:Q_PNP_BRT_ECB +Transistor_BJT:Q_PNP_CBE +Transistor_BJT:Q_PNP_CEB +Transistor_BJT:Q_PNP_Darlington_BCE +Transistor_BJT:Q_PNP_Darlington_BCEC +Transistor_BJT:Q_PNP_Darlington_BEC +Transistor_BJT:Q_PNP_Darlington_CBE +Transistor_BJT:Q_PNP_Darlington_CEB +Transistor_BJT:Q_PNP_Darlington_EBC +Transistor_BJT:Q_PNP_Darlington_ECB +Transistor_BJT:Q_PNP_Darlington_ECBC +Transistor_BJT:Q_PNP_EBC +Transistor_BJT:Q_PNP_ECB +Transistor_BJT:Q_PNP_ECBC +Transistor_BJT:S8050 +Transistor_BJT:S8550 +Transistor_BJT:SS8050 +Transistor_BJT:SS8550 +Transistor_BJT:SSM2210 +Transistor_BJT:SSM2220 +Transistor_BJT:TIP120 +Transistor_BJT:TIP121 +Transistor_BJT:TIP122 +Transistor_BJT:TIP125 +Transistor_BJT:TIP126 +Transistor_BJT:TIP127 +Transistor_BJT:TIP2955 +Transistor_BJT:TIP2955G +Transistor_BJT:TIP3055 +Transistor_BJT:TIP3055G +Transistor_BJT:TIP41 +Transistor_BJT:TIP41A +Transistor_BJT:TIP41B +Transistor_BJT:TIP41C +Transistor_BJT:TIP42 +Transistor_BJT:TIP42A +Transistor_BJT:TIP42B +Transistor_BJT:TIP42C +Transistor_BJT:UMH3N +Transistor_FET:2N3819 +Transistor_FET:2N7000 +Transistor_FET:2N7002 +Transistor_FET:2N7002E +Transistor_FET:2N7002H +Transistor_FET:2N7002K +Transistor_FET:3SK263 +Transistor_FET:AO3400A +Transistor_FET:AO3401A +Transistor_FET:AO4842 +Transistor_FET:AO4892 +Transistor_FET:AON6411 +Transistor_FET:BF244A +Transistor_FET:BF244B +Transistor_FET:BF244C +Transistor_FET:BF245A +Transistor_FET:BF245B +Transistor_FET:BF245C +Transistor_FET:BF545A +Transistor_FET:BF545B +Transistor_FET:BF545C +Transistor_FET:BF994S +Transistor_FET:BS107 +Transistor_FET:BS108 +Transistor_FET:BS170 +Transistor_FET:BS170F +Transistor_FET:BS250 +Transistor_FET:BS870 +Transistor_FET:BSB008NE2LX +Transistor_FET:BSB012NE2LXI +Transistor_FET:BSB013NE2LXI +Transistor_FET:BSB014N04LX3 +Transistor_FET:BSB015N04NX3 +Transistor_FET:BSB028N06NN3 +Transistor_FET:BSB044N08NN3 +Transistor_FET:BSB056N10NN3 +Transistor_FET:BSB104N08NP3 +Transistor_FET:BSB165N15NZ3 +Transistor_FET:BSB280N15NZ3 +Transistor_FET:BSC026N08NS5 +Transistor_FET:BSC028N06LS3 +Transistor_FET:BSC030N08NS5 +Transistor_FET:BSC035N10NS5 +Transistor_FET:BSC037N08NS5 +Transistor_FET:BSC040N08NS5 +Transistor_FET:BSC040N10NS5 +Transistor_FET:BSC046N10NS3G +Transistor_FET:BSC047N08NS3G +Transistor_FET:BSC052N08NS5 +Transistor_FET:BSC057N08NS3G +Transistor_FET:BSC060N10NS3G +Transistor_FET:BSC061N08NS5 +Transistor_FET:BSC070N10NS3G +Transistor_FET:BSC070N10NS5 +Transistor_FET:BSC072N08NS5 +Transistor_FET:BSC079N10NSG +Transistor_FET:BSC082N10LSG +Transistor_FET:BSC098N10NS5 +Transistor_FET:BSC100N10NSFG +Transistor_FET:BSC105N10LSFG +Transistor_FET:BSC109N10NS3G +Transistor_FET:BSC117N08NS5 +Transistor_FET:BSC118N10NSG +Transistor_FET:BSC123N08NS3G +Transistor_FET:BSC123N10LSG +Transistor_FET:BSC13DN30NSFD +Transistor_FET:BSC159N10LSFG +Transistor_FET:BSC160N10NS3G +Transistor_FET:BSC196N10NSG +Transistor_FET:BSC252N10NSFG +Transistor_FET:BSC265N10LSFG +Transistor_FET:BSC340N08NS3G +Transistor_FET:BSC440N10NS3G +Transistor_FET:BSD235C +Transistor_FET:BSD840N +Transistor_FET:BSF030NE2LQ +Transistor_FET:BSF035NE2LQ +Transistor_FET:BSF450NE7NH3 +Transistor_FET:BSN20 +Transistor_FET:BSP129 +Transistor_FET:BSP89 +Transistor_FET:BSR56 +Transistor_FET:BSR57 +Transistor_FET:BSR58 +Transistor_FET:BSS123 +Transistor_FET:BSS127S +Transistor_FET:BSS138 +Transistor_FET:BSS214NW +Transistor_FET:BSS83P +Transistor_FET:BSS84 +Transistor_FET:BUK7880-55A +Transistor_FET:BUK7M10-40EX +Transistor_FET:BUK7M12-40EX +Transistor_FET:BUK7M12-60EX +Transistor_FET:BUK7M15-60EX +Transistor_FET:BUK7M17-80EX +Transistor_FET:BUK7M19-60EX +Transistor_FET:BUK7M21-40EX +Transistor_FET:BUK7M22-80EX +Transistor_FET:BUK7M27-80EX +Transistor_FET:BUK7M33-60EX +Transistor_FET:BUK7M42-60EX +Transistor_FET:BUK7M45-40EX +Transistor_FET:BUK7M67-60EX +Transistor_FET:BUK7M6R3-40EX +Transistor_FET:BUK7M8R0-40EX +Transistor_FET:BUK7M9R9-60EX +Transistor_FET:BUK9832-55A +Transistor_FET:BUK9M10-30EX +Transistor_FET:BUK9M11-40EX +Transistor_FET:BUK9M12-60EX +Transistor_FET:BUK9M120-100EX +Transistor_FET:BUK9M14-40EX +Transistor_FET:BUK9M15-60EX +Transistor_FET:BUK9M156-100EX +Transistor_FET:BUK9M17-30EX +Transistor_FET:BUK9M19-60EX +Transistor_FET:BUK9M23-80EX +Transistor_FET:BUK9M24-40EX +Transistor_FET:BUK9M24-60EX +Transistor_FET:BUK9M28-80EX +Transistor_FET:BUK9M34-100EX +Transistor_FET:BUK9M35-80EX +Transistor_FET:BUK9M42-60EX +Transistor_FET:BUK9M43-100EX +Transistor_FET:BUK9M52-40EX +Transistor_FET:BUK9M53-60EX +Transistor_FET:BUK9M5R2-30EX +Transistor_FET:BUK9M6R6-30EX +Transistor_FET:BUK9M7R2-40EX +Transistor_FET:BUK9M85-60EX +Transistor_FET:BUK9M9R1-40EX +Transistor_FET:BUZ11 +Transistor_FET:C2M0025120D +Transistor_FET:C2M0040120D +Transistor_FET:C2M0045170D +Transistor_FET:C2M0080120D +Transistor_FET:C2M0160120D +Transistor_FET:C2M0280120D +Transistor_FET:C2M1000170D +Transistor_FET:C2M1000170J +Transistor_FET:C3M0030090K +Transistor_FET:C3M0065090D +Transistor_FET:C3M0065090J +Transistor_FET:C3M0065100J +Transistor_FET:C3M0065100K +Transistor_FET:C3M0075120J +Transistor_FET:C3M0075120K +Transistor_FET:C3M0120090D +Transistor_FET:C3M0120090J +Transistor_FET:C3M0120100J +Transistor_FET:C3M0120100K +Transistor_FET:C3M0280090D +Transistor_FET:C3M0280090J +Transistor_FET:CSD13380F3 +Transistor_FET:CSD16301Q2 +Transistor_FET:CSD16321Q5 +Transistor_FET:CSD16322Q5 +Transistor_FET:CSD16325Q5 +Transistor_FET:CSD16327Q3 +Transistor_FET:CSD16342Q5A +Transistor_FET:CSD16401Q5 +Transistor_FET:CSD16403Q5A +Transistor_FET:CSD16404Q5A +Transistor_FET:CSD16407Q5 +Transistor_FET:CSD16408Q5 +Transistor_FET:CSD16410Q5A +Transistor_FET:CSD16412Q5A +Transistor_FET:CSD16413Q5A +Transistor_FET:CSD16414Q5 +Transistor_FET:CSD16415Q5 +Transistor_FET:CSD16570Q5B +Transistor_FET:CSD17301Q5A +Transistor_FET:CSD17302Q5A +Transistor_FET:CSD17303Q5 +Transistor_FET:CSD17305Q5A +Transistor_FET:CSD17306Q5A +Transistor_FET:CSD17307Q5A +Transistor_FET:CSD17310Q5A +Transistor_FET:CSD17311Q5 +Transistor_FET:CSD17312Q5 +Transistor_FET:CSD17313Q2 +Transistor_FET:CSD17322Q5A +Transistor_FET:CSD17327Q5A +Transistor_FET:CSD17501Q5A +Transistor_FET:CSD17505Q5A +Transistor_FET:CSD17506Q5A +Transistor_FET:CSD17507Q5A +Transistor_FET:CSD17510Q5A +Transistor_FET:CSD17522Q5A +Transistor_FET:CSD17527Q5A +Transistor_FET:CSD17551Q5A +Transistor_FET:CSD17552Q5A +Transistor_FET:CSD17553Q5A +Transistor_FET:CSD17555Q5A +Transistor_FET:CSD17556Q5B +Transistor_FET:CSD17559Q5 +Transistor_FET:CSD17570Q5B +Transistor_FET:CSD17573Q5B +Transistor_FET:CSD17576Q5B +Transistor_FET:CSD17577Q3A +Transistor_FET:CSD17577Q5A +Transistor_FET:CSD17578Q5A +Transistor_FET:CSD17579Q5A +Transistor_FET:CSD17581Q3A +Transistor_FET:CSD18501Q5A +Transistor_FET:CSD18502Q5B +Transistor_FET:CSD18503Q5A +Transistor_FET:CSD18504Q5A +Transistor_FET:CSD18509Q5B +Transistor_FET:CSD18531Q5A +Transistor_FET:CSD18532NQ5B +Transistor_FET:CSD18532Q5B +Transistor_FET:CSD18533Q5A +Transistor_FET:CSD18534Q5A +Transistor_FET:CSD18537NQ5A +Transistor_FET:CSD18540Q5B +Transistor_FET:CSD18543Q3A +Transistor_FET:CSD18563Q5A +Transistor_FET:CSD19502Q5B +Transistor_FET:CSD19531Q5A +Transistor_FET:CSD19532Q5B +Transistor_FET:CSD19533Q5A +Transistor_FET:CSD19534Q5A +Transistor_FET:CSD19537Q3 +Transistor_FET:CSD25302Q2 +Transistor_FET:CSD25402Q3A +Transistor_FET:CSD25480F3 +Transistor_FET:DMC2053UVT +Transistor_FET:DMC3071LVT +Transistor_FET:DMG1012T +Transistor_FET:DMG2301L +Transistor_FET:DMG2302U +Transistor_FET:DMG3402L +Transistor_FET:DMG3404L +Transistor_FET:DMG3406L +Transistor_FET:DMG3414U +Transistor_FET:DMG3418L +Transistor_FET:DMG9926UDM +Transistor_FET:DMN10H220L +Transistor_FET:DMN10H700S +Transistor_FET:DMN13H750S +Transistor_FET:DMN2040U +Transistor_FET:DMN2041L +Transistor_FET:DMN2050L +Transistor_FET:DMN2056U +Transistor_FET:DMN2058U +Transistor_FET:DMN2075U +Transistor_FET:DMN2230U +Transistor_FET:DMN24H11DS +Transistor_FET:DMN24H3D5L +Transistor_FET:DMN3008SFG +Transistor_FET:DMN3033LDM +Transistor_FET:DMN3042L +Transistor_FET:DMN3051L +Transistor_FET:DMN30H4D0L +Transistor_FET:DMN3110S +Transistor_FET:DMN3150L +Transistor_FET:DMN32D2LDF +Transistor_FET:DMN3300U +Transistor_FET:DMN3404L +Transistor_FET:DMN6075S +Transistor_FET:DMN60H080DS +Transistor_FET:DMN6140L +Transistor_FET:DMN61D8LQ +Transistor_FET:DMN67D7L +Transistor_FET:DMN67D8L +Transistor_FET:DMP3013SFV +Transistor_FET:DMP6050SSD +Transistor_FET:DMT6008LFG +Transistor_FET:EPC2035 +Transistor_FET:EPC2036 +Transistor_FET:EPC2037 +Transistor_FET:EPC2038 +Transistor_FET:EPC2203 +Transistor_FET:EPC2219 +Transistor_FET:FDC2512 +Transistor_FET:FDC6330L +Transistor_FET:FDC86244 +Transistor_FET:FDG1024NZ +Transistor_FET:FDG6335N +Transistor_FET:FDMC8032L +Transistor_FET:FDMS8050 +Transistor_FET:FDMS8050ET30 +Transistor_FET:FDMS8350L +Transistor_FET:FDMS8350LET40 +Transistor_FET:FDMS86150 +Transistor_FET:FDMS86150ET100 +Transistor_FET:FDMS86152 +Transistor_FET:FDMS86202 +Transistor_FET:FDMS86202ET120 +Transistor_FET:FDMS86255 +Transistor_FET:FDMS86255ET150 +Transistor_FET:FDMS86350 +Transistor_FET:FDMS86350ET80 +Transistor_FET:FDMS86550 +Transistor_FET:FDMS86550ET60 +Transistor_FET:FDMT800100DC +Transistor_FET:FDMT800120DC +Transistor_FET:FDMT800150DC +Transistor_FET:FDMT800152DC +Transistor_FET:FDMT80060DC +Transistor_FET:FDMT80080DC +Transistor_FET:FDN340P +Transistor_FET:FDS2734 +Transistor_FET:FDS4559 +Transistor_FET:FDS4897AC +Transistor_FET:FDS4897C +Transistor_FET:FDS6630A +Transistor_FET:FDS6890A +Transistor_FET:FDS6892A +Transistor_FET:FDS6898A +Transistor_FET:FDS6930A +Transistor_FET:FDS6930B +Transistor_FET:FDS8960C +Transistor_FET:FDS9435A +Transistor_FET:FDS9926A +Transistor_FET:FDS9934C +Transistor_FET:FQP27P06 +Transistor_FET:GS66502B +Transistor_FET:GS66504B +Transistor_FET:GS66508B +Transistor_FET:IF3602 +Transistor_FET:IGLD60R070D1 +Transistor_FET:IGLD60R190D1 +Transistor_FET:IGO60R070D1 +Transistor_FET:IGOT60R070D1 +Transistor_FET:IGT40R070D1_E8220 +Transistor_FET:IGT60R070D1 +Transistor_FET:IGT60R190D1S +Transistor_FET:IPB180N10S4-02 +Transistor_FET:IPD50R380CE +Transistor_FET:IPD50R3K0CE +Transistor_FET:IPDD60R050G7 +Transistor_FET:IPDD60R080G7 +Transistor_FET:IPDD60R102G7 +Transistor_FET:IPDD60R125G7 +Transistor_FET:IPDD60R150G7 +Transistor_FET:IPDD60R190G7 +Transistor_FET:IPP060N06N +Transistor_FET:IPT012N08N5 +Transistor_FET:IPT015N10N5 +Transistor_FET:IPT020N10N3 +Transistor_FET:IRF3205 +Transistor_FET:IRF40DM229 +Transistor_FET:IRF4905 +Transistor_FET:IRF540N +Transistor_FET:IRF60DM206 +Transistor_FET:IRF6613 +Transistor_FET:IRF6614 +Transistor_FET:IRF6616 +Transistor_FET:IRF6617 +Transistor_FET:IRF6618 +Transistor_FET:IRF6620 +Transistor_FET:IRF6621 +Transistor_FET:IRF6622 +Transistor_FET:IRF6623 +Transistor_FET:IRF6628 +Transistor_FET:IRF6631 +Transistor_FET:IRF6635 +Transistor_FET:IRF6636 +Transistor_FET:IRF6637 +Transistor_FET:IRF6641 +Transistor_FET:IRF6643 +Transistor_FET:IRF6644 +Transistor_FET:IRF6646 +Transistor_FET:IRF6648 +Transistor_FET:IRF6655 +Transistor_FET:IRF6662 +Transistor_FET:IRF6665 +Transistor_FET:IRF6668 +Transistor_FET:IRF6674 +Transistor_FET:IRF6710S2 +Transistor_FET:IRF6711S +Transistor_FET:IRF6712S +Transistor_FET:IRF6713S +Transistor_FET:IRF6714M +Transistor_FET:IRF6715M +Transistor_FET:IRF6716M +Transistor_FET:IRF6717M +Transistor_FET:IRF6718L2 +Transistor_FET:IRF6721S +Transistor_FET:IRF6722M +Transistor_FET:IRF6724M +Transistor_FET:IRF6725M +Transistor_FET:IRF6726M +Transistor_FET:IRF6727M +Transistor_FET:IRF6728M +Transistor_FET:IRF6775M +Transistor_FET:IRF6785 +Transistor_FET:IRF6795M +Transistor_FET:IRF6797M +Transistor_FET:IRF6798M +Transistor_FET:IRF6802SD +Transistor_FET:IRF6810S +Transistor_FET:IRF6811S +Transistor_FET:IRF6892S +Transistor_FET:IRF6893M +Transistor_FET:IRF6894M +Transistor_FET:IRF6898M +Transistor_FET:IRF7171M +Transistor_FET:IRF7309IPBF +Transistor_FET:IRF7324 +Transistor_FET:IRF7343PBF +Transistor_FET:IRF740 +Transistor_FET:IRF7403 +Transistor_FET:IRF7404 +Transistor_FET:IRF7480M +Transistor_FET:IRF7483M +Transistor_FET:IRF7486M +Transistor_FET:IRF7580M +Transistor_FET:IRF7606PBF +Transistor_FET:IRF7607PBF +Transistor_FET:IRF7665S2 +Transistor_FET:IRF7739L1 +Transistor_FET:IRF7748L1 +Transistor_FET:IRF7759L2 +Transistor_FET:IRF7769L1 +Transistor_FET:IRF7779L2 +Transistor_FET:IRF7780M +Transistor_FET:IRF7799L2 +Transistor_FET:IRF7946 +Transistor_FET:IRF8301M +Transistor_FET:IRF8302M +Transistor_FET:IRF8304M +Transistor_FET:IRF8306M +Transistor_FET:IRF8308M +Transistor_FET:IRF8327S +Transistor_FET:IRF8721PBF-1 +Transistor_FET:IRF9383M +Transistor_FET:IRF9540N +Transistor_FET:IRFI4019H +Transistor_FET:IRFI4020H +Transistor_FET:IRFI4212H +Transistor_FET:IRFP4468PbF +Transistor_FET:IRFP4668PbF +Transistor_FET:IRFS4115 +Transistor_FET:IRFS4127 +Transistor_FET:IRFS4227 +Transistor_FET:IRFS4229 +Transistor_FET:IRFS4310Z +Transistor_FET:IRFS4321 +Transistor_FET:IRFTS9342PBF +Transistor_FET:IRL6283M +Transistor_FET:IRL6297SD +Transistor_FET:IRL7472L1 +Transistor_FET:IRLB8721PBF +Transistor_FET:IRLIZ44N +Transistor_FET:IRLML0030 +Transistor_FET:IRLML2060 +Transistor_FET:IRLML5203 +Transistor_FET:IRLML6244 +Transistor_FET:IRLML6401 +Transistor_FET:IRLML6402 +Transistor_FET:IRLML9301 +Transistor_FET:IRLZ24 +Transistor_FET:IRLZ34N +Transistor_FET:IRLZ44N +Transistor_FET:JFE150DBV +Transistor_FET:JFE150DCK +Transistor_FET:JFE2140D +Transistor_FET:MMBF170 +Transistor_FET:MMBF4391 +Transistor_FET:MMBF4392 +Transistor_FET:MMBF4393 +Transistor_FET:MMBFJ111 +Transistor_FET:MMBFJ112 +Transistor_FET:MMBFJ113 +Transistor_FET:NDT3055L +Transistor_FET:NTR2101P +Transistor_FET:PGA26E07BA +Transistor_FET:PGA26E19BA +Transistor_FET:PMDT290UCE +Transistor_FET:PMN48XP +Transistor_FET:PSMN5R2-60YL +Transistor_FET:QM6006D +Transistor_FET:QM6015D +Transistor_FET:Q_Dual_NMOS_G1S2G2D2S1D1 +Transistor_FET:Q_Dual_NMOS_S1G1D2S2G2D1 +Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D1 +Transistor_FET:Q_Dual_NMOS_S1G1S2G2D2D2D1D1 +Transistor_FET:Q_Dual_PMOS_G1S2G2D2S1D1 +Transistor_FET:Q_Dual_PMOS_S1G1D2S2G2D1 +Transistor_FET:Q_Dual_PMOS_S1G1S2G2D2D2D1D1 +Transistor_FET:Q_NMOS_DGS +Transistor_FET:Q_NMOS_DSG +Transistor_FET:Q_NMOS_GDS +Transistor_FET:Q_NMOS_GDSD +Transistor_FET:Q_NMOS_GSD +Transistor_FET:Q_NMOS_SDGD +Transistor_FET:Q_NMOS_SGD +Transistor_FET:Q_PMOS_DGS +Transistor_FET:Q_PMOS_DSG +Transistor_FET:Q_PMOS_GDS +Transistor_FET:Q_PMOS_GDSD +Transistor_FET:Q_PMOS_GSD +Transistor_FET:Q_PMOS_SDG +Transistor_FET:Q_PMOS_SDGD +Transistor_FET:Q_PMOS_SGD +Transistor_FET:RQ6E080AJ +Transistor_FET:RS9N50D +Transistor_FET:RSQ030N08HZG +Transistor_FET:SCTL35N65G2V +Transistor_FET:SGT65R65AL +Transistor_FET:SQJQ100E +Transistor_FET:SQJQ100EL +Transistor_FET:SQJQ112E +Transistor_FET:SQJQ114EL +Transistor_FET:SQJQ116EL +Transistor_FET:SQJQ130EL +Transistor_FET:SQJQ140E +Transistor_FET:SQJQ142E +Transistor_FET:SQJQ144AE +Transistor_FET:SQJQ146E +Transistor_FET:SQJQ148E +Transistor_FET:SQJQ150E +Transistor_FET:SQJQ160E +Transistor_FET:SQJQ160EL +Transistor_FET:SQJQ184E +Transistor_FET:SQJQ186E +Transistor_FET:SQJQ402E +Transistor_FET:SQJQ404E +Transistor_FET:SQJQ410EL +Transistor_FET:SQJQ466E +Transistor_FET:SQJQ480E +Transistor_FET:STB15N80K5 +Transistor_FET:STB33N65M2 +Transistor_FET:STB40N60M2 +Transistor_FET:STD7NK40Z +Transistor_FET:STS2DNE60 +Transistor_FET:SUD08P06-155L +Transistor_FET:SUD09P10-195 +Transistor_FET:SUD19P06-60 +Transistor_FET:SUD45P03-09 +Transistor_FET:SUD50P04-08 +Transistor_FET:SUD50P06-15 +Transistor_FET:SUD50P08-25L +Transistor_FET:SUD50P10-43L +Transistor_FET:Si1308EDL +Transistor_FET:Si1442DH +Transistor_FET:Si2319CDS +Transistor_FET:Si2371EDS +Transistor_FET:Si3456DDV +Transistor_FET:Si4162DY +Transistor_FET:Si4532DY +Transistor_FET:Si4542DY +Transistor_FET:Si7141DP +Transistor_FET:Si7336ADP +Transistor_FET:Si7450DP +Transistor_FET:Si7617DN +Transistor_FET:SiA449DJ +Transistor_FET:SiA453EDJ +Transistor_FET:SiA462DJ +Transistor_FET:SiR696DP +Transistor_FET:SiS415DNT +Transistor_FET:SiS443DN +Transistor_FET:SiS454DN +Transistor_FET:SiSS27DN +Transistor_FET:T2N7002AK +Transistor_FET:TP0610L +Transistor_FET:TP0610T +Transistor_FET:TSM2301ACX +Transistor_FET:TSM2302CX +Transistor_FET:VN10LF +Transistor_FET:VNP10N07 +Transistor_FET:VP0610L +Transistor_FET:VP0610T +Transistor_FET:ZVN3306F +Transistor_FET:ZVN3310F +Transistor_FET:ZVN3320F +Transistor_FET:ZVN4106F +Transistor_FET:ZXM61N02F +Transistor_FET:ZXM61N03F +Transistor_FET:ZXMN10A07F +Transistor_FET:ZXMN2A01F +Transistor_FET:ZXMN2A14F +Transistor_FET:ZXMN2B01F +Transistor_FET:ZXMN2B14FH +Transistor_FET:ZXMN2F30FH +Transistor_FET:ZXMN2F34FH +Transistor_FET:ZXMN3A01F +Transistor_FET:ZXMN3A14F +Transistor_FET:ZXMN3B01F +Transistor_FET:ZXMN3B14F +Transistor_FET:ZXMN3F30FH +Transistor_FET:ZXMN6A07F +Transistor_FET:ZXMP4A16G +Transistor_FET_Other:DN2540N3-G +Transistor_FET_Other:DN2540N5-G +Transistor_FET_Other:DN2540N8-G +Transistor_FET_Other:Q_NMOS_Depletion_DGS +Transistor_FET_Other:Q_NMOS_Depletion_DSG +Transistor_FET_Other:Q_NMOS_Depletion_GDS +Transistor_FET_Other:Q_NMOS_Depletion_GSD +Transistor_FET_Other:Q_NMOS_Depletion_SDG +Transistor_FET_Other:Q_NMOS_Depletion_SGD +Transistor_IGBT:IRG4PF50W +Transistor_IGBT:STGP7NC60HD +Transistor_Power_Module:A2C25S12M3 +Transistor_Power_Module:A2C25S12M3-F +Transistor_Power_Module:A2C35S12M3 +Transistor_Power_Module:A2C35S12M3-F +Transistor_Power_Module:A2C50S65M2 +Transistor_Power_Module:A2C50S65M2-F +Transistor_Power_Module:FP10R06W1E3 +Transistor_Power_Module:FP15R06W1E3 +Transistor_Power_Module:FP15R12W2T4 +Transistor_Power_Module:FP25R12W2T4 +Transistor_Power_Module:FP25R12W2T4P +Transistor_Power_Module:FP35R12W2T4 +Transistor_Power_Module:FP35R12W2T4P +Transistor_Power_Module:FP50R06W2E3 +Transistor_Power_Module:FS75R07N2E4 +Transistor_Power_Module:MG12100W-XN2MM +Transistor_Power_Module:MG1215H-XBN2MM +Transistor_Power_Module:MG1225H-XBN2MM +Transistor_Power_Module:MG1225H-XN2MM +Transistor_Power_Module:MG1240H-XBN2MM +Transistor_Power_Module:MG1250H-XN2MM +Transistor_Power_Module:MG1250W-XBN2MM +Transistor_Power_Module:MG1275W-XBN2MM +Transistor_Power_Module:MG1275W-XN2MM +Transistor_Power_Module:STGIPS10C60-H +Transistor_Power_Module:STGIPS10K60A +Transistor_Power_Module:STGIPS10K60A2 +Transistor_Power_Module:STGIPS10K60T +Transistor_Power_Module:STGIPS14K60 +Transistor_Power_Module:STGIPS14K60T +Transistor_Power_Module:STGIPS20K60 +Triac_Thyristor:BT136-500 +Triac_Thyristor:BT136-600 +Triac_Thyristor:BT136-800 +Triac_Thyristor:BT138-600 +Triac_Thyristor:BT138-800 +Triac_Thyristor:BT139-600 +Triac_Thyristor:BT169B +Triac_Thyristor:BT169D +Triac_Thyristor:BT169G +Triac_Thyristor:BTA16-600B +Triac_Thyristor:BTA16-600BW +Triac_Thyristor:BTA16-600C +Triac_Thyristor:BTA16-600CW +Triac_Thyristor:BTA16-600SW +Triac_Thyristor:BTA16-800B +Triac_Thyristor:BTA16-800BW +Triac_Thyristor:BTA16-800C +Triac_Thyristor:BTA16-800CW +Triac_Thyristor:BTA16-800SW +Triac_Thyristor:BTB16-600B +Triac_Thyristor:BTB16-600BW +Triac_Thyristor:BTB16-600C +Triac_Thyristor:BTB16-600CW +Triac_Thyristor:BTB16-600SW +Triac_Thyristor:BTB16-800B +Triac_Thyristor:BTB16-800BW +Triac_Thyristor:BTB16-800C +Triac_Thyristor:BTB16-800CW +Triac_Thyristor:BTB16-800SW +Triac_Thyristor:CT401T +Triac_Thyristor:Generic_Triac_A1A2G +Triac_Thyristor:Generic_Triac_A1GA2 +Triac_Thyristor:Generic_Triac_A2A1G +Triac_Thyristor:Generic_Triac_A2GA1 +Triac_Thyristor:Generic_Triac_GA1A2 +Triac_Thyristor:Generic_Triac_GA2A1 +Triac_Thyristor:TIC106 +Triac_Thyristor:TIC116 +Triac_Thyristor:TIC126 +Triac_Thyristor:TIC206 +Triac_Thyristor:TIC216 +Triac_Thyristor:TIC226 +Triac_Thyristor:X0202MN +Triac_Thyristor:X0202NN +Triac_Thyristor:Z0103MN +Triac_Thyristor:Z0103NN +Triac_Thyristor:Z0107MN +Triac_Thyristor:Z0107NN +Triac_Thyristor:Z0109MN +Triac_Thyristor:Z0109NN +Triac_Thyristor:Z0110MN +Triac_Thyristor:Z0110NN +Valve:6AK8 +Valve:9AK8 +Valve:CK548DX +Valve:CK6418 +Valve:EABC80 +Valve:EC92 +Valve:ECC81 +Valve:ECC83 +Valve:ECC88 +Valve:ECH81 +Valve:ECL82 +Valve:ECL86 +Valve:EF80 +Valve:EF83 +Valve:EF85 +Valve:EF86 +Valve:EL34 +Valve:EL84 +Valve:EM84 +Valve:JAN6418 +Valve:NOS-6418 +Valve:PABC80 +Valve:STABI +Valve:UABC80 +Video:AD725 +Video:AD9708AR +Video:AD9891 +Video:AD9895 +Video:AD9984AKST +Video:ADA4430-1WYRTZ +Video:ADA4430-1YKSZ +Video:ADV7280xCP +Video:ADV7390BCPZ +Video:ADV7391BCPZ +Video:AV9173 +Video:CX7930 +Video:CXD3400N +Video:HD63484 +Video:HD63484_PLCC +Video:ICX415AQ +Video:ISL59885 +Video:LM1881 +Video:MAX310 +Video:MAX311 +Video:MB88303P +Video:S178 +Video:SAA7182 +Video:SI582 +Video:TDA1950 +Video:TDA1950F +Video:TDA2593 +Video:TDA7260 +Video:TDA8501 +Video:TDA8702 +Video:TDA8702T +Video:TDA8772 +Video:TDA9500 +Video:TDA9503 +Video:TDA9513 +Video:TEA2014 +Video:TEA5115 +Video:TFP410PAP diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..40eee9f7 --- /dev/null +++ b/rector.php @@ -0,0 +1,81 @@ +symfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml'); + $rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php'); + + //Import class names instead of using fully qualified class names + $rectorConfig->importNames(); + //But keep the fully qualified class names for classes in the global namespace + $rectorConfig->importShortClasses(false); + + $rectorConfig->paths([ + __DIR__ . '/config', + __DIR__ . '/public', + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + // register a single rule + //$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); + + $rectorConfig->rules([ + DeclareStrictTypesRector::class, + ]); + + // define sets of rules + $rectorConfig->sets([ + //PHP rules + SetList::CODE_QUALITY, + LevelSetList::UP_TO_PHP_81, + + //Symfony rules + SymfonySetList::SYMFONY_CODE_QUALITY, + SymfonySetList::SYMFONY_64, + + //Doctrine rules + DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, + DoctrineSetList::DOCTRINE_CODE_QUALITY, + + //PHPUnit rules + PHPUnitSetList::PHPUNIT_CODE_QUALITY, + PHPUnitSetList::PHPUNIT_90, + ]); + + $rectorConfig->skip([ + CountArrayToEmptyArrayComparisonRector::class, + //Leave our !== null checks alone + FlipTypeControlToUseExclusiveTypeRector::class, + //Leave our PartList TableAction alone + ActionSuffixRemoverRector::class, + //We declare event listeners via attributes, therefore no need to migrate them to subscribers + EventListenerToEventSubscriberRector::class, + PreferPHPUnitThisCallRector::class, + //Do not replace 'GET' with class constant, + LiteralGetToRequestClassConstantRector::class, + ]); + + //Do not apply rules to Symfony own files + $rectorConfig->skip([ + __DIR__ . '/public/index.php', + __DIR__ . '/src/Kernel.php', + __DIR__ . '/config/preload.php', + __DIR__ . '/config/bundles.php', + ]); +}; diff --git a/src/ApiPlatform/DocumentedAPIProperties/DocumentedAPIProperty.php b/src/ApiPlatform/DocumentedAPIProperties/DocumentedAPIProperty.php new file mode 100644 index 00000000..57d275be --- /dev/null +++ b/src/ApiPlatform/DocumentedAPIProperties/DocumentedAPIProperty.php @@ -0,0 +1,120 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\DocumentedAPIProperties; + +use ApiPlatform\Metadata\ApiProperty; + +/** + * When this attribute is applied to a class, an property will be added to the API documentation using the given parameters. + * This is useful for adding properties to the API documentation, that are not existing in the entity class itself, + * but get added by a normalizer. + */ +#[\Attribute(\Attribute::TARGET_CLASS| \Attribute::IS_REPEATABLE)] +final class DocumentedAPIProperty +{ + public function __construct( + /** + * @param string $schemaName The name of the schema to add the property to (e.g. "Part-Read") + */ + public readonly string $schemaName, + /** + * @var string $property The name of the property to add to the schema + */ + public readonly string $property, + public readonly string $type = 'string', + public readonly bool $nullable = true, + /** + * @var string $description The description of the property + */ + public readonly ?string $description = null, + /** + * @var bool True if the property is readable, false otherwise + */ + public readonly bool $readable = true, + /** + * @var bool True if the property is writable, false otherwise + */ + public readonly bool $writeable = false, + /** + * @var string|null The deprecation reason of the property + */ + public readonly ?string $deprecationReason = null, + /** @var mixed The default value of this property */ + public readonly mixed $default = null, + public readonly mixed $example = null, + ) + { + } + + public function toAPIProperty(bool $use_swagger = false): ApiProperty + { + $openApiContext = []; + + if (false === $this->writeable) { + $openApiContext['readOnly'] = true; + } + if (!$use_swagger && false === $this->readable) { + $openApiContext['writeOnly'] = true; + } + if (null !== $description = $this->description) { + $openApiContext['description'] = $description; + } + + $deprecationReason = $this->deprecationReason; + + // see https://github.com/json-schema-org/json-schema-spec/pull/737 + if (!$use_swagger && null !== $deprecationReason) { + $openApiContext['deprecated'] = true; + } + + if (!empty($default = $this->default)) { + if ($default instanceof \BackedEnum) { + $default = $default->value; + } + $openApiContext['default'] = $default; + } + + if (!empty($example = $this->example)) { + $openApiContext['example'] = $example; + } + + if (!isset($openApiContext['example']) && isset($openApiContext['default'])) { + $openApiContext['example'] = $openApiContext['default']; + } + + $openApiContext['type'] = $this->type; + $openApiContext['nullable'] = $this->nullable; + + + + return new ApiProperty( + description: $this->description, + readable: $this->readable, + writable: $this->writeable, + openapiContext: $openApiContext, + types: $this->type, + property: $this->property + ); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/DocumentedAPIProperties/PropertyMetadataFactory.php b/src/ApiPlatform/DocumentedAPIProperties/PropertyMetadataFactory.php new file mode 100644 index 00000000..49e9a031 --- /dev/null +++ b/src/ApiPlatform/DocumentedAPIProperties/PropertyMetadataFactory.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\DocumentedAPIProperties; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ReflectionClass; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +/** + * This decorator adds the virtual properties defined by the DocumentedAPIProperty attribute to the property metadata + * which then get picked up by the openapi schema generator + */ +#[AsDecorator('api_platform.metadata.property.metadata_factory')] +class PropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct(private PropertyMetadataFactoryInterface $decorated) + { + } + + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + $metadata = $this->decorated->create($resourceClass, $property, $options); + + //Only become active in the context of the openapi schema generation + if (!isset($options['schema_type'])) { + return $metadata; + } + + if (!class_exists($resourceClass)) { + return $metadata; + } + + $refClass = new ReflectionClass($resourceClass); + $attributes = $refClass->getAttributes(DocumentedAPIProperty::class); + + //Look for the DocumentedAPIProperty attribute with the given property name + foreach ($attributes as $attribute) { + /** @var DocumentedAPIProperty $api_property */ + $api_property = $attribute->newInstance(); + //If attribute not matches the property name, skip it + if ($api_property->property !== $property) { + continue; + } + + //Return the virtual property + return $api_property->toAPIProperty(); + } + + return $metadata; + } +} \ No newline at end of file diff --git a/src/ApiPlatform/DocumentedAPIProperties/PropertyNameCollectionFactory.php b/src/ApiPlatform/DocumentedAPIProperties/PropertyNameCollectionFactory.php new file mode 100644 index 00000000..3157cbf3 --- /dev/null +++ b/src/ApiPlatform/DocumentedAPIProperties/PropertyNameCollectionFactory.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\DocumentedAPIProperties; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ReflectionClass; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +/** + * This decorator adds the virtual property names defined by the DocumentedAPIProperty attribute to the property name collection + * which then get picked up by the openapi schema generator + */ +#[AsDecorator('api_platform.metadata.property.name_collection_factory')] +class PropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated) + { + } + + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + // Get the default properties from the decorated service + $propertyNames = $this->decorated->create($resourceClass, $options); + + //Only become active in the context of the openapi schema generation + if (!isset($options['schema_type'])) { + return $propertyNames; + } + + if (!class_exists($resourceClass)) { + return $propertyNames; + } + + $properties = iterator_to_array($propertyNames); + + $refClass = new ReflectionClass($resourceClass); + + foreach ($refClass->getAttributes(DocumentedAPIProperty::class) as $attribute) { + /** @var DocumentedAPIProperty $instance */ + $instance = $attribute->newInstance(); + $properties[] = $instance->property; + } + + return new PropertyNameCollection($properties); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/ErrorHandler.php b/src/ApiPlatform/ErrorHandler.php new file mode 100644 index 00000000..7704347d --- /dev/null +++ b/src/ApiPlatform/ErrorHandler.php @@ -0,0 +1,75 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use Doctrine\ORM\ORMInvalidArgumentException; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use ApiPlatform\State\ApiResource\Error; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * This class adds a custom error if the user tries to create a new entity through a relation, and suggests to do reference it through an IRI instead. + * This class decorates the default error handler of API Platform. + */ +#[AsDecorator('api_platform.state.error_provider')] +final class ErrorHandler implements ProviderInterface +{ + public function __construct(private readonly ProviderInterface $decorated, #[Autowire('%kernel.debug%')] private readonly bool $debug) + { + + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $request = $context['request']; + $format = $request->getRequestFormat(); + $exception = $request->attributes->get('exception'); + + //Check if the exception is a ORM InvalidArgument exception and complains about a not-persisted entity through relation + if ($exception instanceof ORMInvalidArgumentException && str_contains($exception->getMessage(), 'A new entity was found through the relationship')) { + //Extract the entity class and property name from the exception message + $matches = []; + preg_match('/A new entity was found through the relationship \'(?.*)\'/i', $exception->getMessage(), $matches); + + $property = $matches['property'] ?? "unknown"; + + //Create a new error response + $error = Error::createFromException($exception, 400); + + //Return the error response + $detail = "You tried to create a new entity through the relation '$property', but this is not allowed. Please create the entity first and then reference it through an IRI!"; + //If we are in debug mode, add the exception message to the error response + if ($this->debug) { + $detail .= " Original exception message: " . $exception->getMessage(); + } + $error->setDetail($detail); + return $error; + } + + + return $this->decorated->provide($operation, $uriVariables, $context); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/Filter/EntityFilter.php b/src/ApiPlatform/Filter/EntityFilter.php new file mode 100644 index 00000000..85bc3833 --- /dev/null +++ b/src/ApiPlatform/Filter/EntityFilter.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ManagerRegistry; +use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +class EntityFilter extends AbstractFilter +{ + + public function __construct( + ManagerRegistry $managerRegistry, + private readonly EntityFilterHelper $filter_helper, + ?LoggerInterface $logger = null, + ?array $properties = null, + ?NameConverterInterface $nameConverter = null + ) { + parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + } + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + if ( + !$this->isPropertyEnabled($property, $resourceClass) || + !$this->isPropertyMapped($property, $resourceClass, true) + ) { + return; + } + + $metadata = $this->getClassMetadata($resourceClass); + $target_class = $metadata->getAssociationTargetClass($property); + //If it is not an association we can not filter the property + if (!$target_class) { + return; + } + + $elements = $this->filter_helper->valueToEntityArray($value, $target_class); + + $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters + $queryBuilder + ->andWhere(sprintf('o.%s IN (:%s)', $property, $parameterName)) + ->setParameter($parameterName, $elements); + } + + + + public function getDescription(string $resourceClass): array + { + return $this->filter_helper->getDescription($this->properties); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/Filter/EntityFilterHelper.php b/src/ApiPlatform/Filter/EntityFilterHelper.php new file mode 100644 index 00000000..45e04fde --- /dev/null +++ b/src/ApiPlatform/Filter/EntityFilterHelper.php @@ -0,0 +1,99 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\Filter; + +use App\Entity\Base\AbstractStructuralDBElement; +use App\Services\Trees\NodesListBuilder; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\PropertyInfo\Type; + +class EntityFilterHelper +{ + public function __construct( + private readonly NodesListBuilder $nodesListBuilder, + private readonly EntityManagerInterface $entityManager) + { + + } + + public function valueToEntityArray(string $value, string $target_class): array + { + //Convert value to IDs: + $elements = []; + + //Split the given value by comm + foreach (explode(',', $value) as $id) { + if (trim($id) === '') { + continue; + } + + //Check if the given value ends with a plus, then we want to include all direct children + $include_children = false; + $include_recursive = false; + if (str_ends_with($id, '++')) { //Plus Plus means include all children recursively + $id = substr($id, 0, -2); + $include_recursive = true; + } elseif (str_ends_with($id, '+')) { + $id = substr($id, 0, -1); + $include_children = true; + } + + //Get a (shallow) reference to the entitity + $element = $this->entityManager->getReference($target_class, (int) $id); + $elements[] = $element; + + //If $element is not structural we are done + if (!is_a($element, AbstractStructuralDBElement::class)) { + continue; + } + + //Get the recursive list of children + if ($include_recursive) { + $elements = array_merge($elements, $this->nodesListBuilder->getChildrenFlatList($element)); + } elseif ($include_children) { + $elements = array_merge($elements, $element->getChildren()->toArray()); + } + } + + return $elements; + } + + public function getDescription(array $properties): array + { + if ($properties === []) { + return []; + } + + $description = []; + foreach (array_keys($properties) as $property) { + $description[(string)$property] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + 'description' => 'Filter using a comma seperated list of element IDs. Use + to include all direct children and ++ to include all children recursively.', + ]; + } + return $description; + } +} \ No newline at end of file diff --git a/src/ApiPlatform/Filter/LikeFilter.php b/src/ApiPlatform/Filter/LikeFilter.php new file mode 100644 index 00000000..a8e96eb9 --- /dev/null +++ b/src/ApiPlatform/Filter/LikeFilter.php @@ -0,0 +1,74 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\PropertyInfo\Type; + +final class LikeFilter extends AbstractFilter +{ + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + // Otherwise filter is applied to order and page as well + if ( + !$this->isPropertyEnabled($property, $resourceClass) || + !$this->isPropertyMapped($property, $resourceClass) + ) { + return; + } + $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters + $queryBuilder + ->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName)) + ->setParameter($parameterName, $value); + } + + public function getDescription(string $resourceClass): array + { + if (!$this->properties) { + return []; + } + + $description = []; + foreach (array_keys($this->properties) as $property) { + $description[(string)$property] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + 'description' => 'Filter using a LIKE SQL expression. Use % as wildcard for multiple characters and _ for single characters. For example, to search for all items containing foo, use foo. To search for all items starting with foo, use foo%. To search for all items ending with foo, use %foo', + ]; + } + return $description; + } +} \ No newline at end of file diff --git a/src/ApiPlatform/Filter/PartStoragelocationFilter.php b/src/ApiPlatform/Filter/PartStoragelocationFilter.php new file mode 100644 index 00000000..4d0ad2df --- /dev/null +++ b/src/ApiPlatform/Filter/PartStoragelocationFilter.php @@ -0,0 +1,79 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use App\Entity\Parts\StorageLocation; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ManagerRegistry; +use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +class PartStoragelocationFilter extends AbstractFilter +{ + + public function __construct( + ManagerRegistry $managerRegistry, + private readonly EntityFilterHelper $filter_helper, + ?LoggerInterface $logger = null, + ?array $properties = null, + ?NameConverterInterface $nameConverter = null + ) { + parent::__construct($managerRegistry, $logger, $properties, $nameConverter); + } + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + //Do not check for mapping here, as we are using a virtual property + if ( + !$this->isPropertyEnabled($property, $resourceClass) + ) { + return; + } + + $elements = $this->filter_helper->valueToEntityArray($value, StorageLocation::class); + + $parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters + $queryBuilder + ->leftJoin('o.partLots', 'partLots') + ->andWhere(sprintf('partLots.storage_location IN (:%s)', $parameterName)) + ->setParameter($parameterName, $elements); + } + + + + public function getDescription(string $resourceClass): array + { + return $this->filter_helper->getDescription($this->properties); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/Filter/TagFilter.php b/src/ApiPlatform/Filter/TagFilter.php new file mode 100644 index 00000000..98648ee9 --- /dev/null +++ b/src/ApiPlatform/Filter/TagFilter.php @@ -0,0 +1,96 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\PropertyInfo\Type; + +/** + * Due to their nature, tags are stored in a single string, separated by commas, which requires some more complex search logic. + * This filter allows to easily search for tags in a part entity. + */ +final class TagFilter extends AbstractFilter +{ + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + // Ignore filter if property is not enabled or mapped + if ( + !$this->isPropertyEnabled($property, $resourceClass) || + !$this->isPropertyMapped($property, $resourceClass) + ) { + return; + } + + //Escape any %, _ or \ in the tag + $value = addcslashes($value, '%_\\'); + + $tag_identifier_prefix = $queryNameGenerator->generateParameterName($property); + + $expr = $queryBuilder->expr(); + + $tmp = $expr->orX( + 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE', + 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE', + 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE', + 'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE', + ); + + $queryBuilder->andWhere($tmp); + + //Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag) + $queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $value . ',%'); + $queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $value); + $queryBuilder->setParameter($tag_identifier_prefix . '_3', $value . ',%'); + $queryBuilder->setParameter($tag_identifier_prefix . '_4', $value); + } + + public function getDescription(string $resourceClass): array + { + if (!$this->properties) { + return []; + } + + $description = []; + foreach (array_keys($this->properties) as $property) { + $description[(string)$property] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + 'description' => 'Filter for tags of a part', + ]; + } + return $description; + } +} \ No newline at end of file diff --git a/src/ApiPlatform/FixInheritanceMappingMetadataFacory.php b/src/ApiPlatform/FixInheritanceMappingMetadataFacory.php new file mode 100644 index 00000000..c65e57a0 --- /dev/null +++ b/src/ApiPlatform/FixInheritanceMappingMetadataFacory.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform; + +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use App\Entity\Attachments\Attachment; +use App\Entity\Parameters\AbstractParameter; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +/** + * API Platform has problems with single table inheritance, as it assumes that they all have different endpoints. + * This decorator fixes this problem by using the parent class for the metadata collection. + */ +#[AsDecorator('api_platform.metadata.resource.metadata_collection_factory')] +class FixInheritanceMappingMetadataFacory implements ResourceMetadataCollectionFactoryInterface +{ + private const SINGLE_INHERITANCE_ENTITY_CLASSES = [ + Attachment::class, + AbstractParameter::class, + ]; + + private array $cache = []; + + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated) + { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + //If we already have a cached value, we can return it + if (isset($this->cache[$resourceClass])) { + return $this->decorated->create($this->cache[$resourceClass]); + } + + //Check if the resourceClass is a single inheritance class, then we can use the parent class to access it + foreach (self::SINGLE_INHERITANCE_ENTITY_CLASSES as $class) { + if (is_a($resourceClass, $class, true)) { + $this->cache[$resourceClass] = $class; + break; + } + } + + //If it was not found in the list of single inheritance classes, we can use the original class + if (!isset($this->cache[$resourceClass])) { + $this->cache[$resourceClass] = $resourceClass; + } + + return $this->decorated->create($this->cache[$resourceClass] ?? $resourceClass); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/HandleAttachmentsUploadsProcessor.php b/src/ApiPlatform/HandleAttachmentsUploadsProcessor.php new file mode 100644 index 00000000..b5149442 --- /dev/null +++ b/src/ApiPlatform/HandleAttachmentsUploadsProcessor.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform; + +use ApiPlatform\Metadata\DeleteOperationInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\Entity\Attachments\Attachment; +use App\Services\Attachments\AttachmentSubmitHandler; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * This state processor handles the upload property set on the deserialized attachment entity and + * calls the upload handler service to handle the upload. + */ +final class HandleAttachmentsUploadsProcessor implements ProcessorInterface +{ + public function __construct( + #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] + private readonly ProcessorInterface $persistProcessor, + #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] + private readonly ProcessorInterface $removeProcessor, + private readonly AttachmentSubmitHandler $attachmentSubmitHandler + ) { + + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed + { + if ($operation instanceof DeleteOperationInterface) { + return $this->removeProcessor->process($data, $operation, $uriVariables, $context); + } + + //Check if the attachment has any upload data we need to handle + //This have to happen before the persist processor is called, because the changes on the entity must be saved! + if ($data instanceof Attachment && $data->getUpload()) { + $upload = $data->getUpload(); + //Reset the upload data + $data->setUpload(null); + + $this->attachmentSubmitHandler->handleUpload($data, $upload); + } + + return $this->persistProcessor->process($data, $operation, $uriVariables, $context); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php b/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php new file mode 100644 index 00000000..c6a8220e --- /dev/null +++ b/src/ApiPlatform/NormalizePropertyNameCollectionFactory.php @@ -0,0 +1,77 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use function Symfony\Component\String\u; + +/** + * This decorator removes all camelCase property names from the property name collection, if a snake_case version exists. + * This is a fix for https://github.com/Part-DB/Part-DB-server/issues/862, as the openapi schema generator wrongly collects + * both camelCase and snake_case property names, which leads to duplicate properties in the schema. + * This seems to come from the fact that the openapi schema generator uses no serializerContext, which seems then to collect + * the getters too... + */ +#[AsDecorator('api_platform.metadata.property.name_collection_factory')] +class NormalizePropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated) + { + } + + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + // Get the default properties from the decorated service + $propertyNames = $this->decorated->create($resourceClass, $options); + + //Only become active in the context of the openapi schema generation + if (!isset($options['schema_type'])) { + return $propertyNames; + } + + //If we are not in the jsonapi generator (which sets no serializer groups), return the property names as is + if (isset($options['serializer_groups'])) { + return $propertyNames; + } + + //Remove all camelCase property names from the collection, if a snake_case version exists + $properties = iterator_to_array($propertyNames); + + foreach ($properties as $property) { + if (str_contains($property, '_')) { + $camelized = u($property)->camel()->toString(); + + //If the camelized version exists, remove it from the collection + $index = array_search($camelized, $properties, true); + if ($index !== false) { + unset($properties[$index]); + } + } + } + + return new PropertyNameCollection($properties); + } +} \ No newline at end of file diff --git a/src/ApiPlatform/OpenApiFactoryDecorator.php b/src/ApiPlatform/OpenApiFactoryDecorator.php new file mode 100644 index 00000000..af213e14 --- /dev/null +++ b/src/ApiPlatform/OpenApiFactoryDecorator.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform; + +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\Model\SecurityScheme; +use ApiPlatform\OpenApi\OpenApi; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +#[AsDecorator('api_platform.openapi.factory')] +class OpenApiFactoryDecorator implements OpenApiFactoryInterface +{ + public function __construct(private readonly OpenApiFactoryInterface $decorated) + { + } + + public function __invoke(array $context = []): OpenApi + { + $openApi = $this->decorated->__invoke($context); + $securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject(); + $securitySchemes['access_token'] = new SecurityScheme( + type: 'http', + description: 'Use an API token to authenticate', + name: 'Authorization', + scheme: 'bearer', + ); + return $openApi; + } +} \ No newline at end of file diff --git a/src/ApiResource/PartDBInfo.php b/src/ApiResource/PartDBInfo.php new file mode 100644 index 00000000..25aed05e --- /dev/null +++ b/src/ApiResource/PartDBInfo.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiResource; + +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\State\PartDBInfoProvider; + +/** + * This class is used to provide various information about the system. + */ +#[ApiResource( + uriTemplate: '/info.{_format}', + description: 'Basic information about Part-DB like version, title, etc.', + operations: [new Get(openapi: new Operation(summary: 'Get basic information about the installed Part-DB instance.'))], + provider: PartDBInfoProvider::class +)] +#[ApiFilter(PropertyFilter::class)] +class PartDBInfo +{ + public function __construct( + /** The installed Part-DB version */ + public readonly string $version, + /** The Git branch name of the Part-DB version (or null, if not installed via git) */ + public readonly string|null $git_branch, + /** The Git branch commit of the Part-DB version (or null, if not installed via git) */ + public readonly string|null $git_commit, + /** The name of this Part-DB instance */ + public readonly string $title, + /** The banner, shown on homepage (markdown encoded) */ + public readonly string $banner, + /** The configured default URI for Part-DB */ + public readonly string $default_uri, + /** The global timezone of this Part-DB */ + public readonly string $global_timezone, + /** The base currency of Part-DB, used as internal representation of monetary values */ + public readonly string $base_currency, + /** The configured default language of Part-DB */ + public readonly string $global_locale, + ) { + + } +} \ No newline at end of file diff --git a/src/Command/Attachments/CleanAttachmentsCommand.php b/src/Command/Attachments/CleanAttachmentsCommand.php index 40f568bf..59bc99ee 100644 --- a/src/Command/Attachments/CleanAttachmentsCommand.php +++ b/src/Command/Attachments/CleanAttachmentsCommand.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Command\Attachments; +use Symfony\Component\Console\Attribute\AsCommand; use App\Services\Attachments\AttachmentManager; use App\Services\Attachments\AttachmentPathResolver; use App\Services\Attachments\AttachmentReverseSearch; @@ -40,30 +41,21 @@ use function count; use const DIRECTORY_SEPARATOR; +#[AsCommand('partdb:attachments:clean-unused|app:clean-attachments', 'Lists (and deletes if wanted) attachments files that are not used anymore (abandoned files).')] class CleanAttachmentsCommand extends Command { - protected static $defaultName = 'partdb:attachments:clean-unused|app:clean-attachments'; - - protected AttachmentManager $attachment_helper; - protected AttachmentReverseSearch $reverseSearch; protected MimeTypes $mimeTypeGuesser; - protected AttachmentPathResolver $pathResolver; - public function __construct(AttachmentManager $attachmentHelper, AttachmentReverseSearch $reverseSearch, AttachmentPathResolver $pathResolver) + public function __construct(protected AttachmentManager $attachment_helper, protected AttachmentReverseSearch $reverseSearch, protected AttachmentPathResolver $pathResolver) { - $this->attachment_helper = $attachmentHelper; - $this->pathResolver = $pathResolver; - $this->reverseSearch = $reverseSearch; $this->mimeTypeGuesser = new MimeTypes(); parent::__construct(); } protected function configure(): void { - $this - ->setDescription('Lists (and deletes if wanted) attachments files that are not used anymore (abandoned files).') - ->setHelp('This command allows to find all files in the media folder which are not associated with an attachment anymore.'. - ' These files are not needed and can eventually deleted.'); + $this->setHelp('This command allows to find all files in the media folder which are not associated with an attachment anymore.'. + ' These files are not needed and can eventually deleted.'); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -81,6 +73,9 @@ class CleanAttachmentsCommand extends Command //Ignore image cache folder $finder->exclude('cache'); + //Ignore automigration folder + $finder->exclude('.automigration-backup'); + $fs = new Filesystem(); $file_list = []; @@ -91,7 +86,7 @@ class CleanAttachmentsCommand extends Command foreach ($finder as $file) { //If not attachment object uses this file, print it - if (0 === count($this->reverseSearch->findAttachmentsByFile($file))) { + if ([] === $this->reverseSearch->findAttachmentsByFile($file)) { $file_list[] = $file; $table->addRow([ $fs->makePathRelative($file->getPathname(), $mediaPath), @@ -101,14 +96,14 @@ class CleanAttachmentsCommand extends Command } } - if (count($file_list) > 0) { + if ($file_list !== []) { $table->render(); $continue = $io->confirm(sprintf('Found %d abandoned files. Do you want to delete them? This can not be undone!', count($file_list)), false); if (!$continue) { //We are finished here, when no files should be deleted - return 0; + return Command::SUCCESS; } //Delete the files @@ -121,7 +116,7 @@ class CleanAttachmentsCommand extends Command $io->success('No abandoned files found.'); } - return 0; + return Command::SUCCESS; } /** diff --git a/src/Command/Attachments/DownloadAttachmentsCommand.php b/src/Command/Attachments/DownloadAttachmentsCommand.php new file mode 100644 index 00000000..34deef0e --- /dev/null +++ b/src/Command/Attachments/DownloadAttachmentsCommand.php @@ -0,0 +1,136 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Command\Attachments; + +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentUpload; +use App\Exceptions\AttachmentDownloadException; +use App\Services\Attachments\AttachmentManager; +use App\Services\Attachments\AttachmentSubmitHandler; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand('partdb:attachments:download', "Downloads all attachments which have only an external URL to the local filesystem.")] +class DownloadAttachmentsCommand extends Command +{ + public function __construct(private readonly AttachmentSubmitHandler $attachmentSubmitHandler, + private EntityManagerInterface $entityManager) + { + parent::__construct(); + } + + public function configure(): void + { + $this->setHelp('This command downloads all attachments, which only have an external URL, to the local filesystem, so that you have an offline copy of the attachments.'); + $this->addOption('--private', null, null, 'If set, the attachments will be downloaded to the private storage.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('attachment') + ->from(Attachment::class, 'attachment') + ->where('attachment.external_path IS NOT NULL') + ->andWhere('attachment.external_path != \'\'') + ->andWhere('attachment.internal_path IS NULL'); + + $query = $qb->getQuery(); + $attachments = $query->getResult(); + + if (count($attachments) === 0) { + $io->success('No attachments with external URL found.'); + return Command::SUCCESS; + } + + $io->note('Found ' . count($attachments) . ' attachments with external URL, that will be downloaded.'); + + //If the option --private is set, the attachments will be downloaded to the private storage. + $private = $input->getOption('private'); + if ($private) { + if (!$io->confirm('Attachments will be downloaded to the private storage. Continue?')) { + return Command::SUCCESS; + } + } else { + if (!$io->confirm('Attachments will be downloaded to the public storage, where everybody knowing the correct URL can access it. Continue?')){ + return Command::SUCCESS; + } + } + + $progressBar = $io->createProgressBar(count($attachments)); + $progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% \n%message%"); + + $progressBar->setMessage('Starting download...'); + $progressBar->start(); + + + $errors = []; + + foreach ($attachments as $attachment) { + /** @var Attachment $attachment */ + $progressBar->setMessage(sprintf('%s (ID: %s) from %s', $attachment->getName(), $attachment->getID(), $attachment->getHost())); + $progressBar->advance(); + + try { + $attachmentUpload = new AttachmentUpload(file: null, downloadUrl: true, private: $private); + $this->attachmentSubmitHandler->handleUpload($attachment, $attachmentUpload); + + //Write changes to the database + $this->entityManager->flush(); + } catch (AttachmentDownloadException $e) { + $errors[] = [ + 'attachment' => $attachment, + 'error' => $e->getMessage() + ]; + } + } + + $progressBar->finish(); + + //Fix the line break after the progress bar + $io->newLine(); + $io->newLine(); + + if (count($errors) > 0) { + $io->warning('Some attachments could not be downloaded:'); + foreach ($errors as $error) { + $io->warning(sprintf("Attachment %s (ID %s) could not be downloaded from %s:\n%s", + $error['attachment']->getName(), + $error['attachment']->getID(), + $error['attachment']->getExternalPath(), + $error['error']) + ); + } + } else { + $io->success('All attachments downloaded successfully.'); + } + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Command/Attachments/SanitizeSVGAttachmentsCommand.php b/src/Command/Attachments/SanitizeSVGAttachmentsCommand.php new file mode 100644 index 00000000..7f6550f0 --- /dev/null +++ b/src/Command/Attachments/SanitizeSVGAttachmentsCommand.php @@ -0,0 +1,90 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Command\Attachments; + +use App\Entity\Attachments\Attachment; +use App\Services\Attachments\AttachmentSubmitHandler; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand('partdb:attachments:sanitize-svg', "Sanitize uploaded SVG files.")] +class SanitizeSVGAttachmentsCommand extends Command +{ + public function __construct(private readonly EntityManagerInterface $entityManager, private readonly AttachmentSubmitHandler $attachmentSubmitHandler, ?string $name = null) + { + parent::__construct($name); + } + + public function configure(): void + { + $this->setHelp('This command allows to sanitize SVG files uploaded via attachments. This happens automatically since version 1.17.1, this command is intended to be used for older files.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->info('This command will sanitize all uploaded SVG files. This is only required if you have uploaded (untrusted) SVG files before version 1.17.1. If you are running a newer version, you don\'t need to run this command (again).'); + if (!$io->confirm('Do you want to continue?', false)) { + $io->success('Command aborted.'); + return Command::FAILURE; + } + + $io->info('Sanitizing SVG files...'); + + //Finding all attachments with svg files + $qb = $this->entityManager->createQueryBuilder(); + $qb->select('a') + ->from(Attachment::class, 'a') + ->where('a.internal_path LIKE :pattern ESCAPE \'#\'') + ->orWhere('a.original_filename LIKE :pattern ESCAPE \'#\'') + ->setParameter('pattern', '%.svg'); + + $attachments = $qb->getQuery()->getResult(); + $io->note('Found '.count($attachments).' attachments with SVG files.'); + + if (count($attachments) === 0) { + $io->success('No SVG files found.'); + return Command::FAILURE; + } + + $io->info('Sanitizing SVG files...'); + $io->progressStart(count($attachments)); + foreach ($attachments as $attachment) { + /** @var Attachment $attachment */ + $io->note('Sanitizing attachment '.$attachment->getId().' ('.($attachment->getFilename() ?? '???').')'); + $this->attachmentSubmitHandler->sanitizeSVGAttachment($attachment); + $io->progressAdvance(); + + } + $io->progressFinish(); + + $io->success('Sanitization finished. All SVG files have been sanitized.'); + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Command/BackupCommand.php b/src/Command/BackupCommand.php index 7ee535c6..085c552a 100644 --- a/src/Command/BackupCommand.php +++ b/src/Command/BackupCommand.php @@ -1,7 +1,13 @@ project_dir = $project_dir; - $this->entityManager = $entityManager; - parent::__construct(); } @@ -55,6 +52,7 @@ class BackupCommand extends Command $backup_attachments = $input->getOption('attachments'); $backup_config = $input->getOption('config'); $backup_full = $input->getOption('full'); + $overwrite = $input->getOption('overwrite'); if ($backup_full) { $backup_database = true; @@ -72,13 +70,12 @@ class BackupCommand extends Command $io->info('Backup Part-DB to '.$output_filepath); //Check if the file already exists - if (file_exists($output_filepath)) { - //Then ask the user, if he wants to overwrite the file - if (!$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) { - $io->error('Backup aborted!'); - - return Command::FAILURE; - } + //Then ask the user, if he wants to overwrite the file + if (!$overwrite + && file_exists($output_filepath) + && !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) { + $io->error('Backup aborted!'); + return Command::FAILURE; } $io->note('Starting backup...'); @@ -116,8 +113,6 @@ class BackupCommand extends Command /** * Constructs the MySQL PDO DSN. * Taken from https://github.com/doctrine/dbal/blob/3.5.x/src/Driver/PDO/MySQL/Driver.php - * - * @param array $params */ private function configureDumper(array $params, DbDumper $dumper): void { @@ -146,30 +141,42 @@ class BackupCommand extends Command } } + private function runSQLDumper(DbDumper $dumper, ZipFile $zip, array $connectionParams): void + { + $this->configureDumper($connectionParams, $dumper); + + $tmp_file = tempnam(sys_get_temp_dir(), 'partdb_sql_dump'); + + $dumper->dumpToFile($tmp_file); + $zip->addFile($tmp_file, 'database.sql'); + } + protected function backupDatabase(ZipFile $zip, SymfonyStyle $io): void { $io->note('Backup database...'); //Determine if we use MySQL or SQLite $connection = $this->entityManager->getConnection(); - if ($connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + $params = $connection->getParams(); + $platform = $connection->getDatabasePlatform(); + if ($platform instanceof AbstractMySQLPlatform) { try { $io->note('MySQL database detected. Dump DB to SQL using mysqldump...'); - $params = $connection->getParams(); - $dumper = MySql::create(); - $this->configureDumper($params, $dumper); - - $tmp_file = tempnam(sys_get_temp_dir(), 'partdb_sql_dump'); - - $dumper->dumpToFile($tmp_file); - $zip->addFile($tmp_file, 'mysql_dump.sql'); + $this->runSQLDumper(MySql::create(), $zip, $params); } catch (\Exception $e) { $io->error('Could not dump database: '.$e->getMessage()); $io->error('This can maybe be fixed by installing the mysqldump binary and adding it to the PATH variable!'); } - } elseif ($connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SqlitePlatform) { + } elseif ($platform instanceof PostgreSQLPlatform) { + try { + $io->note('PostgreSQL database detected. Dump DB to SQL using pg_dump...'); + $this->runSQLDumper(PostgreSql::create(), $zip, $params); + } catch (\Exception $e) { + $io->error('Could not dump database: '.$e->getMessage()); + $io->error('This can maybe be fixed by installing the pg_dump binary and adding it to the PATH variable!'); + } + } elseif ($platform instanceof SQLitePlatform) { $io->note('SQLite database detected. Copy DB file to ZIP...'); - $params = $connection->getParams(); $zip->addFile($params['path'], 'var/app.db'); } else { $io->error('Unknown database platform. Could not backup database!'); diff --git a/src/Command/CheckRequirementsCommand.php b/src/Command/CheckRequirementsCommand.php index 4d9f9d96..5e15e8e2 100644 --- a/src/Command/CheckRequirementsCommand.php +++ b/src/Command/CheckRequirementsCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -27,23 +30,17 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; +#[AsCommand('partdb:check-requirements', 'Checks if the requirements Part-DB needs or recommends are fulfilled.')] class CheckRequirementsCommand extends Command { - protected static $defaultName = 'partdb:check-requirements'; - - protected ContainerBagInterface $params; - - public function __construct(ContainerBagInterface $params) + public function __construct(protected ContainerBagInterface $params) { - $this->params = $params; parent::__construct(); } protected function configure(): void { - $this - ->setDescription('Checks if the requirements Part-DB needs or recommends are fulfilled.') - ->addOption('only_issues', 'i', InputOption::VALUE_NONE, 'Only show issues, not success messages.') + $this->addOption('only_issues', 'i', InputOption::VALUE_NONE, 'Only show issues, not success messages.') ; } @@ -66,105 +63,135 @@ class CheckRequirementsCommand extends Command } - protected function checkPHP(SymfonyStyle $io, $only_issues = false): void + protected function checkPHP(SymfonyStyle $io, bool $only_issues = false): void { //Check PHP versions - $io->isVerbose() && $io->comment('Checking PHP version...'); - if (PHP_VERSION_ID < 80100) { + if ($io->isVerbose()) { + $io->comment('Checking PHP version...'); + } + //We recommend PHP 8.2, but 8.1 is the minimum + if (PHP_VERSION_ID < 80200) { $io->warning('You are using PHP '. PHP_VERSION .'. This will work, but a newer version is recommended.'); + } elseif (!$only_issues) { + $io->success('PHP version is sufficient.'); + } + + //Checking 32-bit system + if (PHP_INT_SIZE === 4) { + $io->warning('You are using a 32-bit system. You will have problems with working with dates after the year 2038, therefore a 64-bit system is recommended.'); + } elseif (PHP_INT_SIZE === 8) { //@phpstan-ignore-line //PHP_INT_SIZE is always 4 or 8 + if (!$only_issues) { + $io->success('You are using a 64-bit system.'); + } } else { - !$only_issues && $io->success('PHP version is sufficient.'); + $io->warning('You are using a system with an unknown bit size. That is interesting xD'); } //Check if opcache is enabled - $io->isVerbose() && $io->comment('Checking Opcache...'); + if ($io->isVerbose()) { + $io->comment('Checking Opcache...'); + } $opcache_enabled = ini_get('opcache.enable') === '1'; if (!$opcache_enabled) { $io->warning('Opcache is not enabled. This will work, but performance will be better with opcache enabled. Set opcache.enable=1 in your php.ini to enable it'); - } else { - !$only_issues && $io->success('Opcache is enabled.'); + } elseif (!$only_issues) { + $io->success('Opcache is enabled.'); } //Check if opcache is configured correctly - $io->isVerbose() && $io->comment('Checking Opcache configuration...'); + if ($io->isVerbose()) { + $io->comment('Checking Opcache configuration...'); + } if ($opcache_enabled && (ini_get('opcache.memory_consumption') < 256 || ini_get('opcache.max_accelerated_files') < 20000)) { $io->warning('Opcache configuration can be improved. See https://symfony.com/doc/current/performance.html for more info.'); - } else { - !$only_issues && $io->success('Opcache configuration is already performance optimized.'); + } elseif (!$only_issues) { + $io->success('Opcache configuration is already performance optimized.'); } } - protected function checkPartDBConfig(SymfonyStyle $io, $only_issues = false): void + protected function checkPartDBConfig(SymfonyStyle $io, bool $only_issues = false): void { //Check if APP_ENV is set to prod - $io->isVerbose() && $io->comment('Checking debug mode...'); - if($this->params->get('kernel.debug')) { + if ($io->isVerbose()) { + $io->comment('Checking debug mode...'); + } + if ($this->params->get('kernel.debug')) { $io->warning('You have activated debug mode, this is will leak informations in a production environment.'); - } else { - !$only_issues && $io->success('Debug mode disabled.'); + } elseif (!$only_issues) { + $io->success('Debug mode disabled.'); } } - protected function checkPHPExtensions(SymfonyStyle $io, $only_issues = false): void + protected function checkPHPExtensions(SymfonyStyle $io, bool $only_issues = false): void { //Get all installed PHP extensions $extensions = get_loaded_extensions(); - $io->isVerbose() && $io->comment('Your PHP installation has '. count($extensions) .' extensions installed: '. implode(', ', $extensions)); + if ($io->isVerbose()) { + $io->comment('Your PHP installation has '. count($extensions) .' extensions installed: '. implode(', ', $extensions)); + } $db_drivers_count = 0; - if(!in_array('pdo_mysql', $extensions)) { + if(!in_array('pdo_mysql', $extensions, true)) { $io->error('pdo_mysql is not installed. You will not be able to use MySQL databases.'); } else { - !$only_issues && $io->success('PHP extension pdo_mysql is installed.'); + if (!$only_issues) { + $io->success('PHP extension pdo_mysql is installed.'); + } $db_drivers_count++; } - if(!in_array('pdo_sqlite', $extensions)) { + if(!in_array('pdo_sqlite', $extensions, true)) { $io->error('pdo_sqlite is not installed. You will not be able to use SQLite. databases'); } else { - !$only_issues && $io->success('PHP extension pdo_sqlite is installed.'); + if (!$only_issues) { + $io->success('PHP extension pdo_sqlite is installed.'); + } $db_drivers_count++; } - $io->isVerbose() && $io->comment('You have '. $db_drivers_count .' database drivers installed.'); + if ($io->isVerbose()) { + $io->comment('You have '. $db_drivers_count .' database drivers installed.'); + } if ($db_drivers_count === 0) { $io->error('You have no database drivers installed. You have to install at least one database driver!'); } - if(!in_array('curl', $extensions)) { + if (!in_array('curl', $extensions, true)) { $io->warning('curl extension is not installed. Install curl extension for better performance'); - } else { - !$only_issues && $io->success('PHP extension curl is installed.'); + } elseif (!$only_issues) { + $io->success('PHP extension curl is installed.'); } - $gd_installed = in_array('gd', $extensions); - if(!$gd_installed) { + $gd_installed = in_array('gd', $extensions, true); + if (!$gd_installed) { $io->error('GD is not installed. GD is required for image processing.'); - } else { - !$only_issues && $io->success('PHP extension GD is installed.'); + } elseif (!$only_issues) { + $io->success('PHP extension GD is installed.'); } //Check if GD has jpeg support - $io->isVerbose() && $io->comment('Checking if GD has jpeg support...'); + if ($io->isVerbose()) { + $io->comment('Checking if GD has jpeg support...'); + } if ($gd_installed) { $gd_info = gd_info(); - if($gd_info['JPEG Support'] === false) { + if ($gd_info['JPEG Support'] === false) { $io->warning('Your GD does not have jpeg support. You will not be able to generate thumbnails of jpeg images.'); - } else { - !$only_issues && $io->success('GD has jpeg support.'); + } elseif (!$only_issues) { + $io->success('GD has jpeg support.'); } - if($gd_info['PNG Support'] === false) { + if ($gd_info['PNG Support'] === false) { $io->warning('Your GD does not have png support. You will not be able to generate thumbnails of png images.'); - } else { - !$only_issues && $io->success('GD has png support.'); + } elseif (!$only_issues) { + $io->success('GD has png support.'); } - if($gd_info['WebP Support'] === false) { + if ($gd_info['WebP Support'] === false) { $io->warning('Your GD does not have WebP support. You will not be able to generate thumbnails of WebP images.'); - } else { - !$only_issues && $io->success('GD has WebP support.'); + } elseif (!$only_issues) { + $io->success('GD has WebP support.'); } } @@ -173,4 +200,4 @@ class CheckRequirementsCommand extends Command } -} \ No newline at end of file +} diff --git a/src/Command/Currencies/UpdateExchangeRatesCommand.php b/src/Command/Currencies/UpdateExchangeRatesCommand.php index b0735cbc..0f3eb11f 100644 --- a/src/Command/Currencies/UpdateExchangeRatesCommand.php +++ b/src/Command/Currencies/UpdateExchangeRatesCommand.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Command\Currencies; +use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\PriceInformations\Currency; use App\Services\Tools\ExchangeRateUpdater; use Doctrine\ORM\EntityManagerInterface; @@ -35,30 +36,17 @@ use Symfony\Component\Console\Style\SymfonyStyle; use function count; use function strlen; +#[AsCommand('partdb:currencies:update-exchange-rates|partdb:update-exchange-rates|app:update-exchange-rates', 'Updates the currency exchange rates.')] class UpdateExchangeRatesCommand extends Command { - protected static $defaultName = 'partdb:currencies:update-exchange-rates|partdb:update-exchange-rates|app:update-exchange-rates'; - - protected string $base_current; - protected EntityManagerInterface $em; - protected ExchangeRateUpdater $exchangeRateUpdater; - - public function __construct(string $base_current, EntityManagerInterface $entityManager, ExchangeRateUpdater $exchangeRateUpdater) + public function __construct(protected string $base_current, protected EntityManagerInterface $em, protected ExchangeRateUpdater $exchangeRateUpdater) { - //$this->swap = $swap; - $this->base_current = $base_current; - - $this->em = $entityManager; - $this->exchangeRateUpdater = $exchangeRateUpdater; - parent::__construct(); } protected function configure(): void { - $this - ->setDescription('Updates the currency exchange rates.') - ->addArgument('iso_code', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'The ISO Codes of the currencies that should be updated.'); + $this->addArgument('iso_code', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'The ISO Codes of the currencies that should be updated.'); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -69,7 +57,7 @@ class UpdateExchangeRatesCommand extends Command if (3 !== strlen($this->base_current)) { $io->error('Chosen Base current is not valid. Check your settings!'); - return 1; + return Command::FAILURE; } $io->note('Update currency exchange rates with base currency: '.$this->base_current); @@ -78,11 +66,7 @@ class UpdateExchangeRatesCommand extends Command $iso_code = $input->getArgument('iso_code'); $repo = $this->em->getRepository(Currency::class); - if (!empty($iso_code)) { - $candidates = $repo->findBy(['iso_code' => $iso_code]); - } else { - $candidates = $repo->findAll(); - } + $candidates = empty($iso_code) ? $repo->findAll() : $repo->findBy(['iso_code' => $iso_code]); $success_counter = 0; @@ -106,6 +90,6 @@ class UpdateExchangeRatesCommand extends Command $io->success(sprintf('%d (of %d) currency exchange rates were updated.', $success_counter, count($candidates))); - return 0; + return Command::SUCCESS; } } diff --git a/src/Command/LoadFixturesCommand.php b/src/Command/LoadFixturesCommand.php new file mode 100644 index 00000000..d01d19c3 --- /dev/null +++ b/src/Command/LoadFixturesCommand.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Command; + +use App\Doctrine\Purger\ResetAutoIncrementPurgerFactory; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * This command does basically the same as doctrine:fixtures:load, but it purges the database before loading the fixtures. + * It does so in another transaction, so we can modify the purger to reset the autoincrement, which would not be possible + * because the implicit commit otherwise. + */ +#[AsCommand(name: 'partdb:fixtures:load', description: 'Load test fixtures into the database and allows to reset the autoincrement before loading the fixtures.', hidden: true)] +class LoadFixturesCommand extends Command +{ + public function __construct(private readonly EntityManagerInterface $entityManager) + { + parent::__construct(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $ui = new SymfonyStyle($input, $output); + + $ui->warning('This command is for development and testing purposes only. It will purge the database and load fixtures afterwards. Do not use in production!'); + + if (! $ui->confirm(sprintf('Careful, database "%s" will be purged. Do you want to continue?', $this->entityManager->getConnection()->getDatabase()), ! $input->isInteractive())) { + return 0; + } + + $factory = new ResetAutoIncrementPurgerFactory(); + $purger = $factory->createForEntityManager(null, $this->entityManager); + + $purger->purge(); + + //Afterwards run the load fixtures command as normal, but with the --append option + $new_input = new ArrayInput([ + 'command' => 'doctrine:fixtures:load', + '--append' => true, + ]); + + $returnCode = $this->getApplication()?->doRun($new_input, $output); + + return $returnCode ?? Command::FAILURE; + } +} \ No newline at end of file diff --git a/src/Command/Logs/ShowEventLogCommand.php b/src/Command/Logs/ShowEventLogCommand.php index 7eef7a2d..505b1275 100644 --- a/src/Command/Logs/ShowEventLogCommand.php +++ b/src/Command/Logs/ShowEventLogCommand.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\Command\Logs; +use Symfony\Component\Console\Attribute\AsCommand; +use App\Entity\UserSystem\User; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\LogSystem\AbstractLogEntry; use App\Repository\LogEntryRepository; @@ -36,23 +38,14 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Contracts\Translation\TranslatorInterface; +#[AsCommand('partdb:logs:show|app:show-logs', 'List the last event log entries.')] class ShowEventLogCommand extends Command { - protected static $defaultName = 'partdb:logs:show|app:show-logs'; - protected EntityManagerInterface $entityManager; - protected TranslatorInterface $translator; - protected ElementTypeNameGenerator $elementTypeNameGenerator; protected LogEntryRepository $repo; - protected LogEntryExtraFormatter $formatter; - public function __construct(EntityManagerInterface $entityManager, - TranslatorInterface $translator, ElementTypeNameGenerator $elementTypeNameGenerator, LogEntryExtraFormatter $formatter) + public function __construct(protected EntityManagerInterface $entityManager, + protected TranslatorInterface $translator, protected ElementTypeNameGenerator $elementTypeNameGenerator, protected LogEntryExtraFormatter $formatter) { - $this->entityManager = $entityManager; - $this->translator = $translator; - $this->elementTypeNameGenerator = $elementTypeNameGenerator; - $this->formatter = $formatter; - $this->repo = $this->entityManager->getRepository(AbstractLogEntry::class); parent::__construct(); } @@ -72,33 +65,31 @@ class ShowEventLogCommand extends Command $max_page = (int) ceil($total_count / $limit); if ($page > $max_page && $max_page > 0) { - $io->error("There is no page ${page}! The maximum page is ${max_page}."); + $io->error("There is no page $page! The maximum page is $max_page."); - return 1; + return Command::FAILURE; } - $io->note("There are a total of ${total_count} log entries in the DB."); + $io->note("There are a total of $total_count log entries in the DB."); $continue = true; while ($continue && $page <= $max_page) { $this->showPage($output, $desc, $limit, $page, $max_page, $showExtra); if ($onePage) { - return 0; + return Command::SUCCESS; } $continue = $io->confirm('Do you want to show the next page?'); ++$page; } - return 0; + return Command::SUCCESS; } protected function configure(): void { - $this - ->setDescription('List the last event log entries.') - ->addOption('count', 'c', InputOption::VALUE_REQUIRED, 'How many log entries should be shown per page.', 50) + $this->addOption('count', 'c', InputOption::VALUE_REQUIRED, 'How many log entries should be shown per page.', 50) ->addOption('oldest_first', null, InputOption::VALUE_NONE, 'Show older entries first.') ->addOption('page', 'p', InputOption::VALUE_REQUIRED, 'Which page should be shown?', 1) ->addOption('onePage', null, InputOption::VALUE_NONE, 'Show only one page (dont ask to go to next).') @@ -114,7 +105,7 @@ class ShowEventLogCommand extends Command $entries = $this->repo->getLogsOrderedByTimestamp($sorting, $limit, $offset); $table = new Table($output); - $table->setHeaderTitle("Page ${page} / ${max_page}"); + $table->setHeaderTitle("Page $page / $max_page"); $headers = ['ID', 'Timestamp', 'Type', 'User', 'Target Type', 'Target']; if ($showExtra) { $headers[] = 'Extra data'; @@ -147,14 +138,12 @@ class ShowEventLogCommand extends Command $target_class = $this->elementTypeNameGenerator->getLocalizedTypeLabel($entry->getTargetClass()); } - if ($entry->getUser()) { + if ($entry->getUser() instanceof User) { $user = $entry->getUser()->getFullName(true); + } elseif ($entry->isCLIEntry()) { + $user = $entry->getCLIUsername() . ' [CLI]'; } else { - if ($entry->isCLIEntry()) { - $user = $entry->getCLIUsername() . ' [CLI]'; - } else { - $user = $entry->getUsername() . ' [deleted]'; - } + $user = $entry->getUsername() . ' [deleted]'; } $row = [ diff --git a/src/Command/Migrations/ConvertBBCodeCommand.php b/src/Command/Migrations/ConvertBBCodeCommand.php index 6b2b3fd7..201263ff 100644 --- a/src/Command/Migrations/ConvertBBCodeCommand.php +++ b/src/Command/Migrations/ConvertBBCodeCommand.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Command\Migrations; +use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\ProjectSystem\Project; @@ -29,7 +30,7 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\UserSystem\Group; @@ -45,30 +46,23 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use function count; /** - * This command converts the BBCode used by old Part-DB versions (<1.0), to the current used markdown format. + * This command converts the BBCode used by old Part-DB versions (<1.0), to the current used Markdown format. */ +#[AsCommand('partdb:migrations:convert-bbcode|app:convert-bbcode', 'Converts BBCode used in old Part-DB versions to newly used Markdown')] class ConvertBBCodeCommand extends Command { /** - * @var string The LIKE criteria used to detect on SQL server if a entry contains BBCode + * @var string The LIKE criteria used to detect on SQL server if an entry contains BBCode */ protected const BBCODE_CRITERIA = '%[%]%[/%]%'; /** * @var string The regex (performed in PHP) used to check if a property really contains BBCODE */ protected const BBCODE_REGEX = '/\\[.+\\].*\\[\\/.+\\]/'; - - protected static $defaultName = 'partdb:migrations:convert-bbcode|app:convert-bbcode'; - - protected EntityManagerInterface $em; - protected PropertyAccessorInterface $propertyAccessor; protected BBCodeToMarkdownConverter $converter; - public function __construct(EntityManagerInterface $entityManager, PropertyAccessorInterface $propertyAccessor) + public function __construct(protected EntityManagerInterface $em, protected PropertyAccessorInterface $propertyAccessor) { - $this->em = $entityManager; - $this->propertyAccessor = $propertyAccessor; - $this->converter = new BBCodeToMarkdownConverter(); parent::__construct(); @@ -76,9 +70,7 @@ class ConvertBBCodeCommand extends Command protected function configure(): void { - $this - ->setDescription('Converts BBCode used in old Part-DB versions to newly used Markdown') - ->setHelp('Older versions of Part-DB (<1.0) used BBCode for rich text formatting. + $this->setHelp('Older versions of Part-DB (<1.0) used BBCode for rich text formatting. Part-DB now uses Markdown which offers more features but is incompatible with BBCode. When you upgrade from an pre 1.0 version you have to run this command to convert your comment fields'); @@ -87,13 +79,14 @@ class ConvertBBCodeCommand extends Command /** * Returns a list which entities and which properties need to be checked. + * @return array, string[]> */ protected function getTargetsLists(): array { return [ Part::class => ['description', 'comment'], AttachmentType::class => ['comment'], - Storelocation::class => ['comment'], + StorageLocation::class => ['comment'], Project::class => ['comment'], Category::class => ['comment'], Manufacturer::class => ['comment'], @@ -117,7 +110,6 @@ class ConvertBBCodeCommand extends Command $class )); //Determine which entities of this type we need to modify - /** @var EntityRepository $repo */ $repo = $this->em->getRepository($class); $qb = $repo->createQueryBuilder('e') ->select('e'); @@ -129,25 +121,25 @@ class ConvertBBCodeCommand extends Command //Fetch resulting classes $results = $qb->getQuery()->getResult(); - $io->note(sprintf('Found %d entities, that need to be converted!', count($results))); + $io->note(sprintf('Found %d entities, that need to be converted!', is_countable($results) ? count($results) : 0)); //In verbose mode print the names of the entities foreach ($results as $result) { /** @var AbstractNamedDBElement $result */ $io->writeln( - 'Convert entity: '.$result->getName().' ('.get_class($result).': '.$result->getID().')', + 'Convert entity: '.$result->getName().' ('.$result::class.': '.$result->getID().')', OutputInterface::VERBOSITY_VERBOSE ); foreach ($properties as $property) { //Retrieve bbcode from entity $bbcode = $this->propertyAccessor->getValue($result, $property); //Check if the current property really contains BBCode - if (!preg_match(static::BBCODE_REGEX, $bbcode)) { + if (!preg_match(static::BBCODE_REGEX, (string) $bbcode)) { continue; } $io->writeln( 'BBCode (old): ' - .str_replace('\n', ' ', substr($bbcode, 0, 255)), + .str_replace('\n', ' ', substr((string) $bbcode, 0, 255)), OutputInterface::VERBOSITY_VERY_VERBOSE ); $markdown = $this->converter->convert($bbcode); @@ -168,6 +160,6 @@ class ConvertBBCodeCommand extends Command $io->success('Changes saved to DB successfully!'); } - return 0; + return Command::SUCCESS; } } diff --git a/src/Command/Migrations/ImportPartKeeprCommand.php b/src/Command/Migrations/ImportPartKeeprCommand.php index ec5010e4..98272440 100644 --- a/src/Command/Migrations/ImportPartKeeprCommand.php +++ b/src/Command/Migrations/ImportPartKeeprCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command\Migrations; +use Symfony\Component\Console\Attribute\AsCommand; use App\Services\ImportExportSystem\PartKeeprImporter\PKDatastructureImporter; use App\Services\ImportExportSystem\PartKeeprImporter\MySQLDumpXMLConverter; use App\Services\ImportExportSystem\PartKeeprImporter\PKImportHelper; @@ -33,34 +36,19 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +#[AsCommand('partdb:migrations:import-partkeepr', 'Import a PartKeepr database XML dump into Part-DB')] class ImportPartKeeprCommand extends Command { - protected static $defaultName = 'partdb:migrations:import-partkeepr'; - - protected EntityManagerInterface $em; - protected MySQLDumpXMLConverter $xml_converter; - protected PKDatastructureImporter $datastructureImporter; - protected PKImportHelper $importHelper; - protected PKPartImporter $partImporter; - protected PKOptionalImporter $optionalImporter; - - public function __construct(EntityManagerInterface $em, MySQLDumpXMLConverter $xml_converter, - PKDatastructureImporter $datastructureImporter, PKPartImporter $partImporter, PKImportHelper $importHelper, - PKOptionalImporter $optionalImporter) + public function __construct(protected EntityManagerInterface $em, protected MySQLDumpXMLConverter $xml_converter, + protected PKDatastructureImporter $datastructureImporter, protected PKPartImporter $partImporter, protected PKImportHelper $importHelper, + protected PKOptionalImporter $optionalImporter) { parent::__construct(self::$defaultName); - $this->em = $em; - $this->datastructureImporter = $datastructureImporter; - $this->importHelper = $importHelper; - $this->partImporter = $partImporter; - $this->xml_converter = $xml_converter; - $this->optionalImporter = $optionalImporter; } - protected function configure() + protected function configure(): void { - $this->setDescription('Import a PartKeepr database XML dump into Part-DB'); $this->setHelp('This command allows you to import a PartKeepr database exported by mysqldump as XML file into Part-DB'); $this->addArgument('file', InputArgument::REQUIRED, 'The file to which should be imported.'); @@ -69,7 +57,7 @@ class ImportPartKeeprCommand extends Command $this->addOption('--import-users', null, InputOption::VALUE_NONE, 'Import users (passwords will not be imported).'); } - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -100,7 +88,7 @@ class ImportPartKeeprCommand extends Command if (!$this->importHelper->checkVersion($data)) { $db_version = $this->importHelper->getDatabaseSchemaVersion($data); $io->error('The version of the imported database is not supported! (Version: '.$db_version.')'); - return 1; + return Command::FAILURE; } //Import the mandatory data @@ -118,7 +106,7 @@ class ImportPartKeeprCommand extends Command $io->success('Imported '.$count.' users.'); } - return 0; + return Command::SUCCESS; } private function doImport(SymfonyStyle $io, array $data): void @@ -155,4 +143,4 @@ class ImportPartKeeprCommand extends Command $io->success('Imported '.$count.' parts.'); } -} \ No newline at end of file +} diff --git a/src/Command/User/ConvertToSAMLUserCommand.php b/src/Command/User/ConvertToSAMLUserCommand.php index df48ce06..98892ecd 100644 --- a/src/Command/User/ConvertToSAMLUserCommand.php +++ b/src/Command/User/ConvertToSAMLUserCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command\User; +use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\UserSystem\User; use App\Security\SamlUserFactory; use Doctrine\ORM\EntityManagerInterface; @@ -30,25 +33,17 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +#[AsCommand('partdb:user:convert-to-saml-user|partdb:users:convert-to-saml-user', 'Converts a local user to a SAML user (and vice versa)')] class ConvertToSAMLUserCommand extends Command { - protected static $defaultName = 'partdb:user:convert-to-saml-user|partdb:users:convert-to-saml-user'; - - protected EntityManagerInterface $entityManager; - protected bool $saml_enabled; - - public function __construct(EntityManagerInterface $entityManager, bool $saml_enabled) + public function __construct(protected EntityManagerInterface $entityManager, protected bool $saml_enabled) { parent::__construct(); - $this->entityManager = $entityManager; - $this->saml_enabled = $saml_enabled; } protected function configure(): void { - $this - ->setDescription('Converts a local user to a SAML user (and vice versa)') - ->setHelp('This converts a local user, which can login via the login form, to a SAML user, which can only login via SAML. This is useful if you want to migrate from a local user system to a SAML user system.') + $this->setHelp('This converts a local user, which can login via the login form, to a SAML user, which can only login via SAML. This is useful if you want to migrate from a local user system to a SAML user system.') ->addArgument('user', InputArgument::REQUIRED, 'The username (or email) of the user') ->addOption('to-local', null, InputOption::VALUE_NONE, 'Converts a SAML user to a local user') ; @@ -70,7 +65,7 @@ class ConvertToSAMLUserCommand extends Command if (!$user) { $io->error('User not found!'); - return 1; + return Command::FAILURE; } $io->info('User found: '.$user->getFullName(true) . ': '.$user->getEmail().' [ID: ' . $user->getID() . ']'); @@ -87,7 +82,7 @@ class ConvertToSAMLUserCommand extends Command $io->confirm('You are going to convert a SAML user to a local user. This means, that the user can only login via the login form. ' . 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?'); - $user->setSAMLUser(false); + $user->setSamlUser(false); $user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER); $this->entityManager->flush(); @@ -102,7 +97,7 @@ class ConvertToSAMLUserCommand extends Command $io->confirm('You are going to convert a local user to a SAML user. This means, that the user can only login via SAML afterwards. The password in the DB will be removed. ' . 'The permissions and groups settings of the user will remain unchanged. Do you really want to continue?'); - $user->setSAMLUser(true); + $user->setSamlUser(true); $user->setPassword(SamlUserFactory::SAML_PASSWORD_PLACEHOLDER); $this->entityManager->flush(); @@ -112,4 +107,4 @@ class ConvertToSAMLUserCommand extends Command return 0; } -} \ No newline at end of file +} diff --git a/src/Command/User/SetPasswordCommand.php b/src/Command/User/SetPasswordCommand.php index 78724ecf..30ef867b 100644 --- a/src/Command/User/SetPasswordCommand.php +++ b/src/Command/User/SetPasswordCommand.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Command\User; +use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\UserSystem\User; use App\Events\SecurityEvent; use App\Events\SecurityEvents; @@ -34,28 +35,17 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +#[AsCommand('partdb:users:set-password|app:set-password|users:set-password|partdb:user:set-password', 'Sets the password of a user')] class SetPasswordCommand extends Command { - protected static $defaultName = 'partdb:users:set-password|app:set-password|users:set-password|partdb:user:set-password'; - - protected EntityManagerInterface $entityManager; - protected UserPasswordHasherInterface $encoder; - protected EventDispatcherInterface $eventDispatcher; - - public function __construct(EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordEncoder, EventDispatcherInterface $eventDispatcher) + public function __construct(protected EntityManagerInterface $entityManager, protected UserPasswordHasherInterface $encoder, protected EventDispatcherInterface $eventDispatcher) { - $this->entityManager = $entityManager; - $this->encoder = $passwordEncoder; - $this->eventDispatcher = $eventDispatcher; - parent::__construct(); } protected function configure(): void { - $this - ->setDescription('Sets the password of a user') - ->setHelp('This password allows you to set the password of a user, without knowing the old password.') + $this->setHelp('This password allows you to set the password of a user, without knowing the old password.') ->addArgument('user', InputArgument::REQUIRED, 'The username or email of the user') ; } @@ -67,17 +57,17 @@ class SetPasswordCommand extends Command $user = $this->entityManager->getRepository(User::class)->findByEmailOrName($user_name); - if (!$user) { + if (!$user instanceof User) { $io->error(sprintf('No user with the given username %s found in the database!', $user_name)); - return 1; + return Command::FAILURE; } $io->note('User found!'); if ($user->isSamlUser()) { $io->error('This user is a SAML user, so you can not change the password!'); - return 1; + return Command::FAILURE; } $proceed = $io->confirm( @@ -85,7 +75,7 @@ class SetPasswordCommand extends Command $user->getFullName(true), $user->getID())); if (!$proceed) { - return 1; + return Command::FAILURE; } $success = false; @@ -93,6 +83,19 @@ class SetPasswordCommand extends Command while (!$success) { $pw1 = $io->askHidden('Please enter new password:'); + + if ($pw1 === null) { + $io->error('No password entered! Please try again.'); + + //If we are in non-interactive mode, we can not ask again + if (!$input->isInteractive()) { + $io->warning('Non-interactive mode detected. No password can be entered that way! If you are using docker exec, please use -it flag.'); + return Command::FAILURE; + } + + continue; + } + $pw2 = $io->askHidden('Please confirm:'); if ($pw1 !== $pw2) { $io->error('The entered password did not match! Please try again.'); @@ -116,6 +119,6 @@ class SetPasswordCommand extends Command $security_event = new SecurityEvent($user); $this->eventDispatcher->dispatch($security_event, SecurityEvents::PASSWORD_CHANGED); - return 0; + return Command::SUCCESS; } } diff --git a/src/Command/User/UpgradePermissionsSchemaCommand.php b/src/Command/User/UpgradePermissionsSchemaCommand.php index ae5adfff..4947fd5c 100644 --- a/src/Command/User/UpgradePermissionsSchemaCommand.php +++ b/src/Command/User/UpgradePermissionsSchemaCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command\User; +use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\PermissionData; use App\Entity\UserSystem\User; @@ -27,28 +30,16 @@ use App\Services\LogSystem\EventCommentHelper; use App\Services\UserSystem\PermissionSchemaUpdater; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +#[AsCommand('partdb:users:upgrade-permissions-schema', '(Manually) upgrades the permissions schema of all users to the latest version.')] final class UpgradePermissionsSchemaCommand extends Command { - protected static $defaultName = 'partdb:users:upgrade-permissions-schema'; - protected static $defaultDescription = '(Manually) upgrades the permissions schema of all users to the latest version.'; - - private PermissionSchemaUpdater $permissionSchemaUpdater; - private EntityManagerInterface $em; - private EventCommentHelper $eventCommentHelper; - - public function __construct(PermissionSchemaUpdater $permissionSchemaUpdater, EntityManagerInterface $entityManager, EventCommentHelper $eventCommentHelper) + public function __construct(private readonly PermissionSchemaUpdater $permissionSchemaUpdater, private readonly EntityManagerInterface $em, private readonly EventCommentHelper $eventCommentHelper) { parent::__construct(self::$defaultName); - - $this->permissionSchemaUpdater = $permissionSchemaUpdater; - $this->eventCommentHelper = $eventCommentHelper; - $this->em = $entityManager; } protected function configure(): void @@ -83,26 +74,22 @@ final class UpgradePermissionsSchemaCommand extends Command } $io->info('Found '. count($groups_to_upgrade) .' groups and '. count($users_to_upgrade) .' users that need an update.'); - if (empty($groups_to_upgrade) && empty($users_to_upgrade)) { + if ($groups_to_upgrade === [] && $users_to_upgrade === []) { $io->success('All users and group permissions schemas are up-to-date. No update needed.'); - return 0; + return Command::SUCCESS; } //List all users and groups that need an update $io->section('Groups that need an update:'); - $io->listing(array_map(function (Group $group) { - return $group->getName() . ' (ID: '. $group->getID() .', Current version: ' . $group->getPermissions()->getSchemaVersion() . ')'; - }, $groups_to_upgrade)); + $io->listing(array_map(static fn(Group $group): string => $group->getName() . ' (ID: '. $group->getID() .', Current version: ' . $group->getPermissions()->getSchemaVersion() . ')', $groups_to_upgrade)); $io->section('Users that need an update:'); - $io->listing(array_map(function (User $user) { - return $user->getUsername() . ' (ID: '. $user->getID() .', Current version: ' . $user->getPermissions()->getSchemaVersion() . ')'; - }, $users_to_upgrade)); + $io->listing(array_map(static fn(User $user): string => $user->getUsername() . ' (ID: '. $user->getID() .', Current version: ' . $user->getPermissions()->getSchemaVersion() . ')', $users_to_upgrade)); if(!$io->confirm('Continue with the update?', false)) { $io->warning('Update aborted.'); - return 0; + return Command::SUCCESS; } //Update all users and groups diff --git a/src/Command/User/UserEnableCommand.php b/src/Command/User/UserEnableCommand.php index 39878a91..51ff2280 100644 --- a/src/Command/User/UserEnableCommand.php +++ b/src/Command/User/UserEnableCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command\User; +use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\UserSystem\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; @@ -29,24 +32,17 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +#[AsCommand('partdb:users:enable|partdb:user:enable', 'Enables/Disable the login of one or more users')] class UserEnableCommand extends Command { - protected static $defaultName = 'partdb:users:enable|partdb:user:enable'; - - protected EntityManagerInterface $entityManager; - - public function __construct(EntityManagerInterface $entityManager, string $name = null) + public function __construct(protected EntityManagerInterface $entityManager, ?string $name = null) { - $this->entityManager = $entityManager; - parent::__construct($name); } protected function configure(): void { - $this - ->setDescription('Enables/Disable the login of one or more users') - ->setHelp('This allows you to allow or prevent the login of certain user. Use the --disable option to disable the login for the given users') + $this->setHelp('This allows you to allow or prevent the login of certain user. Use the --disable option to disable the login for the given users') ->addArgument('users', InputArgument::IS_ARRAY, 'The usernames of the users to use') ->addOption('all', 'a', InputOption::VALUE_NONE, 'Enable/Disable all users') ->addOption('disable', 'd', InputOption::VALUE_NONE, 'Disable the login of the given users') @@ -73,7 +69,7 @@ class UserEnableCommand extends Command } else { //Otherwise, fetch the users from DB foreach ($usernames as $username) { $user = $repo->findByEmailOrName($username); - if ($user === null) { + if (!$user instanceof User) { $io->error('No user found with username: '.$username); return self::FAILURE; } @@ -87,9 +83,7 @@ class UserEnableCommand extends Command $io->note('The following users will be enabled:'); } $io->table(['Username', 'Enabled/Disabled'], - array_map(function(User $user) { - return [$user->getFullName(true), $user->isDisabled() ? 'Disabled' : 'Enabled']; - }, $users)); + array_map(static fn(User $user) => [$user->getFullName(true), $user->isDisabled() ? 'Disabled' : 'Enabled'], $users)); if(!$io->confirm('Do you want to continue?')) { $io->warning('Aborting!'); @@ -107,4 +101,4 @@ class UserEnableCommand extends Command return self::SUCCESS; } -} \ No newline at end of file +} diff --git a/src/Command/User/UserListCommand.php b/src/Command/User/UserListCommand.php index 66265dd8..11d50fbf 100644 --- a/src/Command/User/UserListCommand.php +++ b/src/Command/User/UserListCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command\User; +use Symfony\Component\Console\Attribute\AsCommand; +use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; @@ -28,24 +32,17 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +#[AsCommand('partdb:users:list|users:list', 'Lists all users')] class UserListCommand extends Command { - protected static $defaultName = 'partdb:users:list|users:list'; - - protected EntityManagerInterface $entityManager; - - public function __construct(EntityManagerInterface $entityManager) + public function __construct(protected EntityManagerInterface $entityManager) { - $this->entityManager = $entityManager; - parent::__construct(); } protected function configure(): void { - $this - ->setDescription('Lists all users') - ->setHelp('This command lists all users in the database.') + $this->setHelp('This command lists all users in the database.') ->addOption('local', 'l', null, 'Only list local users') ->addOption('saml', 's', null, 'Only list SAML users') ; @@ -82,13 +79,13 @@ class UserListCommand extends Command foreach ($users as $user) { $table->addRow([ - $user->getId(), + $user->getID(), $user->getUsername(), $user->getFullName(), $user->getEmail(), - $user->getGroup() !== null ? $user->getGroup()->getName() . ' (ID: ' . $user->getGroup()->getID() . ')' : 'No group', + $user->getGroup() instanceof Group ? $user->getGroup()->getName() . ' (ID: ' . $user->getGroup()->getID() . ')' : 'No group', $user->isDisabled() ? 'Yes' : 'No', - $user->isSAMLUser() ? 'SAML' : 'Local', + $user->isSamlUser() ? 'SAML' : 'Local', ]); } @@ -98,4 +95,4 @@ class UserListCommand extends Command return self::SUCCESS; } -} \ No newline at end of file +} diff --git a/src/Command/User/UsersPermissionsCommand.php b/src/Command/User/UsersPermissionsCommand.php index 3d57a4dc..6408e9c9 100644 --- a/src/Command/User/UsersPermissionsCommand.php +++ b/src/Command/User/UsersPermissionsCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command\User; +use Symfony\Component\Console\Attribute\AsCommand; use App\Entity\UserSystem\User; use App\Repository\UserRepository; use App\Services\UserSystem\PermissionManager; @@ -34,22 +37,14 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Contracts\Translation\TranslatorInterface; +#[AsCommand('partdb:users:permissions|partdb:user:permissions', 'View and edit the permissions of a given user')] class UsersPermissionsCommand extends Command { - protected static $defaultName = 'partdb:users:permissions|partdb:user:permissions'; - protected static $defaultDescription = 'View and edit the permissions of a given user'; - - protected EntityManagerInterface $entityManager; protected UserRepository $userRepository; - protected PermissionManager $permissionResolver; - protected TranslatorInterface $translator; - public function __construct(EntityManagerInterface $entityManager, PermissionManager $permissionResolver, TranslatorInterface $translator) + public function __construct(protected EntityManagerInterface $entityManager, protected PermissionManager $permissionResolver, protected TranslatorInterface $translator) { - $this->entityManager = $entityManager; $this->userRepository = $entityManager->getRepository(User::class); - $this->permissionResolver = $permissionResolver; - $this->translator = $translator; parent::__construct(self::$defaultName); } @@ -73,12 +68,12 @@ class UsersPermissionsCommand extends Command //Find user $io->note('Finding user with username: ' . $username); $user = $this->userRepository->findByEmailOrName($username); - if ($user === null) { + if (!$user instanceof User) { $io->error('No user found with username: ' . $username); return Command::FAILURE; } - $io->note(sprintf('Found user %s with ID %d', $user->getFullName(true), $user->getId())); + $io->note(sprintf('Found user %s with ID %d', $user->getFullName(true), $user->getID())); $edit_mapping = $this->renderPermissionTable($output, $user, $inherit); @@ -102,7 +97,7 @@ class UsersPermissionsCommand extends Command $new_value_str = $io->ask('Enter the new value for the permission (A = allow, D = disallow, I = inherit)'); - switch (strtolower($new_value_str)) { + switch (strtolower((string) $new_value_str)) { case 'a': case 'allow': $new_value = true; @@ -209,14 +204,17 @@ class UsersPermissionsCommand extends Command if ($permission_value === true) { return 'Allow'; - } else if ($permission_value === false) { + } elseif ($permission_value === false) { return 'Disallow'; - } else if ($permission_value === null && !$inherit) { + } + // Permission value is null by this point + elseif (!$inherit) { return 'Inherit'; - } else if ($permission_value === null && $inherit) { + } elseif ($inherit) { return 'Disallow (Inherited)'; } + //@phpstan-ignore-next-line This line is never reached, but PHPstorm complains otherwise return '???'; } } diff --git a/src/Command/VersionCommand.php b/src/Command/VersionCommand.php index a5437684..d2ce75e1 100644 --- a/src/Command/VersionCommand.php +++ b/src/Command/VersionCommand.php @@ -1,4 +1,7 @@ . */ - namespace App\Command; +use Symfony\Component\Console\Attribute\AsCommand; use App\Services\Misc\GitVersionInfo; use Shivas\VersioningBundle\Service\VersionManagerInterface; use Symfony\Component\Console\Command\Command; @@ -27,25 +30,16 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +#[AsCommand('partdb:version|app:version', 'Shows the currently installed version of Part-DB.')] class VersionCommand extends Command { - protected static $defaultName = 'partdb:version|app:version'; - - protected VersionManagerInterface $versionManager; - protected GitVersionInfo $gitVersionInfo; - - public function __construct(VersionManagerInterface $versionManager, GitVersionInfo $gitVersionInfo) + public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfo $gitVersionInfo) { - $this->versionManager = $versionManager; - $this->gitVersionInfo = $gitVersionInfo; parent::__construct(); } protected function configure(): void { - $this - ->setDescription('Shows the currently installed version of Part-DB.') - ; } protected function execute(InputInterface $input, OutputInterface $output): int @@ -66,6 +60,6 @@ class VersionCommand extends Command $io->info('OS: '. php_uname()); $io->info('PHP extension: '. implode(', ', get_loaded_extensions())); - return 0; + return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/src/Configuration/PermissionsConfiguration.php b/src/Configuration/PermissionsConfiguration.php index 307e3794..c069ab24 100644 --- a/src/Configuration/PermissionsConfiguration.php +++ b/src/Configuration/PermissionsConfiguration.php @@ -55,6 +55,7 @@ final class PermissionsConfiguration implements ConfigurationInterface ->scalarNode('name')->end() ->scalarNode('label')->end() ->scalarNode('bit')->end() + ->scalarNode('apiTokenRole')->defaultNull()->end() ->arrayNode('alsoSet') ->beforeNormalization()->castToArray()->end()->scalarPrototype()->end(); diff --git a/src/Controller/AdminPages/AttachmentTypeController.php b/src/Controller/AdminPages/AttachmentTypeController.php index 6ff5d930..08c10da1 100644 --- a/src/Controller/AdminPages/AttachmentTypeController.php +++ b/src/Controller/AdminPages/AttachmentTypeController.php @@ -34,11 +34,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/attachment_type") + * @see \App\Tests\Controller\AdminPages\AttachmentTypeControllerTest */ +#[Route(path: '/attachment_type')] class AttachmentTypeController extends BaseAdminController { protected string $entity_class = AttachmentType::class; @@ -48,44 +49,34 @@ class AttachmentTypeController extends BaseAdminController protected string $attachment_class = AttachmentTypeAttachment::class; protected ?string $parameter_class = AttachmentTypeParameter::class; - /** - * @Route("/{id}", name="attachment_type_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'attachment_type_delete', methods: ['DELETE'])] public function delete(Request $request, AttachmentType $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="attachment_type_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'attachment_type_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(AttachmentType $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="attachment_type_new") - * @Route("/{id}/clone", name="attachment_type_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'attachment_type_new')] + #[Route(path: '/{id}/clone', name: 'attachment_type_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AttachmentType $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="attachment_type_export_all") - */ + #[Route(path: '/export', name: 'attachment_type_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="attachment_type_export") - */ + #[Route(path: '/{id}/export', name: 'attachment_type_export')] public function exportEntity(AttachmentType $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index 0921d42a..edc5917a 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -24,15 +24,18 @@ namespace App\Controller\AdminPages; use App\DataTables\LogDataTable; use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentContainingDBElement; +use App\Entity\Attachments\AttachmentUpload; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractPartsContainingDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\PartsContainingRepositoryInterface; +use App\Entity\LabelSystem\LabelProcessMode; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; -use App\Entity\UserSystem\User; use App\Exceptions\AttachmentDownloadException; +use App\Exceptions\TwigModeException; use App\Form\AdminPages\ImportType; use App\Form\AdminPages\MassCreationForm; use App\Repository\AbstractPartsContainingRepository; @@ -50,8 +53,8 @@ use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -59,7 +62,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Contracts\Translation\TranslatorInterface; use function Symfony\Component\Translation\t; @@ -73,28 +77,11 @@ abstract class BaseAdminController extends AbstractController protected string $attachment_class = ''; protected ?string $parameter_class = ''; - protected UserPasswordHasherInterface $passwordEncoder; - protected TranslatorInterface $translator; - protected AttachmentSubmitHandler $attachmentSubmitHandler; - protected EventCommentHelper $commentHelper; - - protected HistoryHelper $historyHelper; - protected TimeTravel $timeTravel; - protected DataTableFactory $dataTableFactory; - /** - * @var EventDispatcher|EventDispatcherInterface - */ - protected $eventDispatcher; - protected LabelGenerator $labelGenerator; - protected LabelExampleElementsGenerator $barcodeExampleGenerator; - - protected EntityManagerInterface $entityManager; - - public function __construct(TranslatorInterface $translator, UserPasswordHasherInterface $passwordEncoder, - AttachmentSubmitHandler $attachmentSubmitHandler, - EventCommentHelper $commentHelper, HistoryHelper $historyHelper, TimeTravel $timeTravel, - DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, LabelExampleElementsGenerator $barcodeExampleGenerator, - LabelGenerator $labelGenerator, EntityManagerInterface $entityManager) + public function __construct(protected TranslatorInterface $translator, protected UserPasswordHasherInterface $passwordEncoder, + protected AttachmentSubmitHandler $attachmentSubmitHandler, + protected EventCommentHelper $commentHelper, protected HistoryHelper $historyHelper, protected TimeTravel $timeTravel, + protected DataTableFactory $dataTableFactory, protected EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator, + protected LabelGenerator $labelGenerator, protected EntityManagerInterface $entityManager) { if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) { throw new InvalidArgumentException('You have to override the $entity_class, $form_class, $route_base and $twig_template value in your subclasss!'); @@ -107,18 +94,6 @@ abstract class BaseAdminController extends AbstractController if ('' === $this->parameter_class || ($this->parameter_class && !is_a($this->parameter_class, AbstractParameter::class, true))) { throw new InvalidArgumentException('You have to override the $parameter_class value with a valid Parameter class in your subclass!'); } - - $this->translator = $translator; - $this->passwordEncoder = $passwordEncoder; - $this->attachmentSubmitHandler = $attachmentSubmitHandler; - $this->commentHelper = $commentHelper; - $this->historyHelper = $historyHelper; - $this->timeTravel = $timeTravel; - $this->dataTableFactory = $dataTableFactory; - $this->eventDispatcher = $eventDispatcher; - $this->barcodeExampleGenerator = $barcodeExampleGenerator; - $this->labelGenerator = $labelGenerator; - $this->entityManager = $entityManager; } protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?DateTime @@ -177,13 +152,13 @@ abstract class BaseAdminController extends AbstractController $form_options = [ 'attachment_class' => $this->attachment_class, 'parameter_class' => $this->parameter_class, - 'disabled' => null !== $timeTravel_timestamp, + 'disabled' => $timeTravel_timestamp instanceof \DateTime, ]; //Disable editing of options, if user is not allowed to use twig... if ( $entity instanceof LabelProfile - && 'twig' === $entity->getOptions()->getLinesMode() + && LabelProcessMode::TWIG === $entity->getOptions()->getProcessMode() && !$this->isGranted('@labels.use_twig') ) { $form_options['disable_options'] = true; @@ -198,16 +173,10 @@ abstract class BaseAdminController extends AbstractController $attachments = $form['attachments']; foreach ($attachments as $attachment) { /** @var FormInterface $attachment */ - $options = [ - 'secure_attachment' => $attachment['secureFile']->getData(), - 'download_url' => $attachment['downloadURL']->getData(), - ]; - try { - $this->attachmentSubmitHandler->handleFormSubmit( + $this->attachmentSubmitHandler->handleUpload( $attachment->getData(), - $attachment['file']->getData(), - $options + AttachmentUpload::fromAttachmentForm($attachment) ); } catch (AttachmentDownloadException $attachmentDownloadException) { $this->addFlash( @@ -219,6 +188,11 @@ abstract class BaseAdminController extends AbstractController } } + //Ensure that the master picture is still part of the attachments + if ($entity instanceof AttachmentContainingDBElement && ($entity->getMasterPictureAttachment() !== null && !$entity->getAttachments()->contains($entity->getMasterPictureAttachment()))) { + $entity->setMasterPictureAttachment(null); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); $em->persist($entity); @@ -239,13 +213,17 @@ abstract class BaseAdminController extends AbstractController //Show preview for LabelProfile if needed. if ($entity instanceof LabelProfile) { $example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement()); - $pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example); + $pdf_data = null; + try { + $pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example); + } catch (TwigModeException $exception) { + $form->get('options')->get('lines')->addError(new FormError($exception->getSafeMessage())); + } } - /** @var AbstractPartsContainingRepository $repo */ $repo = $this->entityManager->getRepository($this->entity_class); - return $this->renderForm($this->twig_template, [ + return $this->render($this->twig_template, [ 'entity' => $entity, 'form' => $form, 'route_base' => $this->route_base, @@ -267,16 +245,9 @@ abstract class BaseAdminController extends AbstractController return true; } - protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AbstractNamedDBElement $entity = null) + protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AbstractNamedDBElement $entity = null): Response { - $master_picture_backup = null; - if (null === $entity) { - /** @var AbstractStructuralDBElement|User $new_entity */ - $new_entity = new $this->entity_class(); - } else { - /** @var AbstractStructuralDBElement|User $new_entity */ - $new_entity = clone $entity; - } + $new_entity = $entity instanceof AbstractNamedDBElement ? clone $entity : new $this->entity_class(); $this->denyAccessUnlessGranted('read', $new_entity); @@ -288,42 +259,38 @@ abstract class BaseAdminController extends AbstractController $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - //Perform additional actions - if ($this->additionalActionNew($form, $new_entity)) { - //Upload passed files - $attachments = $form['attachments']; - foreach ($attachments as $attachment) { - /** @var FormInterface $attachment */ - $options = [ - 'secure_attachment' => $attachment['secureFile']->getData(), - 'download_url' => $attachment['downloadURL']->getData(), - ]; + //Perform additional actions + if ($form->isSubmitted() && $form->isValid() && $this->additionalActionNew($form, $new_entity)) { + //Upload passed files + $attachments = $form['attachments']; + foreach ($attachments as $attachment) { + /** @var FormInterface $attachment */ - try { - $this->attachmentSubmitHandler->handleFormSubmit( - $attachment->getData(), - $attachment['file']->getData(), - $options - ); - } catch (AttachmentDownloadException $attachmentDownloadException) { - $this->addFlash( - 'error', - $this->translator->trans( - 'attachment.download_failed' - ).' '.$attachmentDownloadException->getMessage() - ); - } + try { + $this->attachmentSubmitHandler->handleUpload( + $attachment->getData(), + AttachmentUpload::fromAttachmentForm($attachment) + ); + } catch (AttachmentDownloadException $attachmentDownloadException) { + $this->addFlash( + 'error', + $this->translator->trans( + 'attachment.download_failed' + ).' '.$attachmentDownloadException->getMessage() + ); } - - $this->commentHelper->setMessage($form['log_comment']->getData()); - - $em->persist($new_entity); - $em->flush(); - $this->addFlash('success', 'entity.created_flash'); - - return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]); } + + //Ensure that the master picture is still part of the attachments + if ($new_entity instanceof AttachmentContainingDBElement && ($new_entity->getMasterPictureAttachment() !== null && !$new_entity->getAttachments()->contains($new_entity->getMasterPictureAttachment()))) { + $new_entity->setMasterPictureAttachment(null); + } + + $this->commentHelper->setMessage($form['log_comment']->getData()); + $em->persist($new_entity); + $em->flush(); + $this->addFlash('success', 'entity.created_flash'); + return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]); } if ($form->isSubmitted() && !$form->isValid()) { @@ -363,18 +330,18 @@ abstract class BaseAdminController extends AbstractController try { $errors = $importer->importFileAndPersistToDB($file, $options); - /** @var ConstraintViolationList $error */ - foreach ($errors as $name => $error) { - foreach ($error['violations'] as $violation) { + foreach ($errors as $name => ['violations' => $violations]) { + foreach ($violations as $violation) { $this->addFlash('error', $name.': '.$violation->getMessage()); } } } - catch (UnexpectedValueException $e) { + catch (UnexpectedValueException) { $this->addFlash('error', 'parts.import.flash.error.invalid_file'); } } + ret: //Mass creation form $mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]); $mass_creation_form->handleRequest($request); @@ -387,11 +354,14 @@ abstract class BaseAdminController extends AbstractController $results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'] ?? null, $errors); //Show errors to user: - foreach ($errors as $error) { - if ($error['entity'] instanceof AbstractStructuralDBElement) { - $this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']); - } else { //When we dont have a structural element, we can only show the name - $this->addFlash('error', $error['entity']->getName().':'.$error['violations']); + foreach ($errors as ['entity' => $new_entity, 'violations' => $violations]) { + /** @var ConstraintViolationInterface $violation */ + foreach ($violations as $violation) { + if ($new_entity instanceof AbstractStructuralDBElement) { + $this->addFlash('error', $new_entity->getFullPath().':'.$violation->getMessage()); + } else { //When we don't have a structural element, we can only show the name + $this->addFlash('error', $new_entity->getName().':'.$violation->getMessage()); + } } } @@ -400,10 +370,13 @@ abstract class BaseAdminController extends AbstractController $em->persist($result); } $em->flush(); + + if (count($results) > 0) { + $this->addFlash('success', t('entity.mass_creation_flash', ['%COUNT%' => count($results)])); + } } - ret: - return $this->renderForm($this->twig_template, [ + return $this->render($this->twig_template, [ 'entity' => $new_entity, 'form' => $form, 'import_form' => $import_form, @@ -413,17 +386,17 @@ abstract class BaseAdminController extends AbstractController } /** - * Performs checks if the element can be deleted safely. Otherwise an flash message is added. + * Performs checks if the element can be deleted safely. Otherwise, a flash message is added. * * @param AbstractNamedDBElement $entity the element that should be checked * - * @return bool True if the the element can be deleted, false if not + * @return bool True if the element can be deleted, false if not */ protected function deleteCheck(AbstractNamedDBElement $entity): bool { if ($entity instanceof AbstractPartsContainingDBElement) { /** @var AbstractPartsContainingRepository $repo */ - $repo = $this->entityManager->getRepository($this->entity_class); + $repo = $this->entityManager->getRepository($this->entity_class); //@phpstan-ignore-line if ($repo->getPartsCount($entity) > 0) { $this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()])); @@ -438,7 +411,7 @@ abstract class BaseAdminController extends AbstractController { $this->denyAccessUnlessGranted('delete', $entity); - if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('delete'.$entity->getID(), $request->request->get('_token'))) { $entityManager = $this->entityManager; @@ -494,6 +467,11 @@ abstract class BaseAdminController extends AbstractController $this->denyAccessUnlessGranted('read', $entity); $entities = $em->getRepository($this->entity_class)->findAll(); + if (count($entities) === 0) { + $this->addFlash('error', 'entity.export.flash.error.no_entities'); + return $this->redirectToRoute($this->route_base.'_new'); + } + return $exporter->exportEntityFromRequest($entities, $request); } diff --git a/src/Controller/AdminPages/CategoryController.php b/src/Controller/AdminPages/CategoryController.php index 4f247a1d..361df625 100644 --- a/src/Controller/AdminPages/CategoryController.php +++ b/src/Controller/AdminPages/CategoryController.php @@ -33,11 +33,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/category") + * @see \App\Tests\Controller\AdminPages\CategoryControllerTest */ +#[Route(path: '/category')] class CategoryController extends BaseAdminController { protected string $entity_class = Category::class; @@ -47,44 +48,34 @@ class CategoryController extends BaseAdminController protected string $attachment_class = CategoryAttachment::class; protected ?string $parameter_class = CategoryParameter::class; - /** - * @Route("/{id}", name="category_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'category_delete', methods: ['DELETE'])] public function delete(Request $request, Category $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="category_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'category_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(Category $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="category_new") - * @Route("/{id}/clone", name="category_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'category_new')] + #[Route(path: '/{id}/clone', name: 'category_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Category $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="category_export_all") - */ + #[Route(path: '/export', name: 'category_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="category_export") - */ + #[Route(path: '/{id}/export', name: 'category_export')] public function exportEntity(Category $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/CurrencyController.php b/src/Controller/AdminPages/CurrencyController.php index bda992c5..4450e157 100644 --- a/src/Controller/AdminPages/CurrencyController.php +++ b/src/Controller/AdminPages/CurrencyController.php @@ -38,7 +38,6 @@ use App\Services\LogSystem\HistoryHelper; use App\Services\LogSystem\TimeTravel; use App\Services\Trees\StructuralElementRecursionHelper; use Doctrine\ORM\EntityManagerInterface; -use Exchanger\Exception\ChainException; use Exchanger\Exception\Exception; use Exchanger\Exception\UnsupportedCurrencyPairException; use Omines\DataTablesBundle\DataTableFactory; @@ -48,14 +47,13 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Contracts\Translation\TranslatorInterface; /** - * @Route("/currency") - * * Class CurrencyController */ +#[Route(path: '/currency')] class CurrencyController extends BaseAdminController { protected string $entity_class = Currency::class; @@ -65,8 +63,6 @@ class CurrencyController extends BaseAdminController protected string $attachment_class = CurrencyAttachment::class; protected ?string $parameter_class = CurrencyParameter::class; - protected ExchangeRateUpdater $exchangeRateUpdater; - public function __construct( TranslatorInterface $translator, UserPasswordHasherInterface $passwordEncoder, @@ -79,10 +75,8 @@ class CurrencyController extends BaseAdminController LabelExampleElementsGenerator $barcodeExampleGenerator, LabelGenerator $labelGenerator, EntityManagerInterface $entityManager, - ExchangeRateUpdater $exchangeRateUpdater + protected ExchangeRateUpdater $exchangeRateUpdater ) { - $this->exchangeRateUpdater = $exchangeRateUpdater; - parent::__construct( $translator, $passwordEncoder, @@ -98,9 +92,7 @@ class CurrencyController extends BaseAdminController ); } - /** - * @Route("/{id}", name="currency_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'currency_delete', methods: ['DELETE'])] public function delete(Request $request, Currency $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); @@ -131,36 +123,28 @@ class CurrencyController extends BaseAdminController return true; } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="currency_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'currency_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(Currency $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="currency_new") - * @Route("/{id}/clone", name="currency_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'currency_new')] + #[Route(path: '/{id}/clone', name: 'currency_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Currency $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="currency_export_all") - */ + #[Route(path: '/export', name: 'currency_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="currency_export") - */ + #[Route(path: '/{id}/export', name: 'currency_export')] public function exportEntity(Currency $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/FootprintController.php b/src/Controller/AdminPages/FootprintController.php index ba37256c..3932e939 100644 --- a/src/Controller/AdminPages/FootprintController.php +++ b/src/Controller/AdminPages/FootprintController.php @@ -34,11 +34,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/footprint") + * @see \App\Tests\Controller\AdminPages\FootprintControllerTest */ +#[Route(path: '/footprint')] class FootprintController extends BaseAdminController { protected string $entity_class = Footprint::class; @@ -48,44 +49,34 @@ class FootprintController extends BaseAdminController protected string $attachment_class = FootprintAttachment::class; protected ?string $parameter_class = FootprintParameter::class; - /** - * @Route("/{id}", name="footprint_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'footprint_delete', methods: ['DELETE'])] public function delete(Request $request, Footprint $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="footprint_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'footprint_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(Footprint $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="footprint_new") - * @Route("/{id}/clone", name="footprint_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'footprint_new')] + #[Route(path: '/{id}/clone', name: 'footprint_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Footprint $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="footprint_export_all") - */ + #[Route(path: '/export', name: 'footprint_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="footprint_export") - */ + #[Route(path: '/{id}/export', name: 'footprint_export')] public function exportEntity(AttachmentType $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/LabelProfileController.php b/src/Controller/AdminPages/LabelProfileController.php index 1b3579db..7e7dfa1c 100644 --- a/src/Controller/AdminPages/LabelProfileController.php +++ b/src/Controller/AdminPages/LabelProfileController.php @@ -22,10 +22,8 @@ declare(strict_types=1); namespace App\Controller\AdminPages; -use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\LabelAttachment; use App\Entity\LabelSystem\LabelProfile; -use App\Entity\Parameters\AbstractParameter; use App\Form\AdminPages\LabelProfileAdminForm; use App\Services\ImportExportSystem\EntityExporter; use App\Services\ImportExportSystem\EntityImporter; @@ -34,10 +32,11 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/label_profile") + * @see \App\Tests\Controller\AdminPages\LabelProfileControllerTest */ +#[Route(path: '/label_profile')] class LabelProfileController extends BaseAdminController { protected string $entity_class = LabelProfile::class; @@ -48,44 +47,34 @@ class LabelProfileController extends BaseAdminController //Just a placeholder protected ?string $parameter_class = null; - /** - * @Route("/{id}", name="label_profile_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'label_profile_delete', methods: ['DELETE'])] public function delete(Request $request, LabelProfile $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="label_profile_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'label_profile_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(LabelProfile $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="label_profile_new") - * @Route("/{id}/clone", name="label_profile_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'label_profile_new')] + #[Route(path: '/{id}/clone', name: 'label_profile_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?LabelProfile $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="label_profile_export_all") - */ + #[Route(path: '/export', name: 'label_profile_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="label_profile_export") - */ + #[Route(path: '/{id}/export', name: 'label_profile_export')] public function exportEntity(LabelProfile $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/ManufacturerController.php b/src/Controller/AdminPages/ManufacturerController.php index 2ded7d10..5fc4d4ac 100644 --- a/src/Controller/AdminPages/ManufacturerController.php +++ b/src/Controller/AdminPages/ManufacturerController.php @@ -33,11 +33,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/manufacturer") + * @see \App\Tests\Controller\AdminPages\ManufacturerControllerTest */ +#[Route(path: '/manufacturer')] class ManufacturerController extends BaseAdminController { protected string $entity_class = Manufacturer::class; @@ -47,46 +48,34 @@ class ManufacturerController extends BaseAdminController protected string $attachment_class = ManufacturerAttachment::class; protected ?string $parameter_class = ManufacturerParameter::class; - /** - * @Route("/{id}", name="manufacturer_delete", methods={"DELETE"}) - * - * @return RedirectResponse - */ + #[Route(path: '/{id}', name: 'manufacturer_delete', methods: ['DELETE'])] public function delete(Request $request, Manufacturer $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="manufacturer_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'manufacturer_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(Manufacturer $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="manufacturer_new") - * @Route("/{id}/clone", name="manufacturer_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'manufacturer_new')] + #[Route(path: '/{id}/clone', name: 'manufacturer_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Manufacturer $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="manufacturer_export_all") - */ + #[Route(path: '/export', name: 'manufacturer_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="manufacturer_export") - */ + #[Route(path: '/{id}/export', name: 'manufacturer_export')] public function exportEntity(Manufacturer $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/MeasurementUnitController.php b/src/Controller/AdminPages/MeasurementUnitController.php index 402c2018..59c38ba4 100644 --- a/src/Controller/AdminPages/MeasurementUnitController.php +++ b/src/Controller/AdminPages/MeasurementUnitController.php @@ -34,11 +34,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/measurement_unit") + * @see \App\Tests\Controller\AdminPages\MeasurementUnitControllerTest */ +#[Route(path: '/measurement_unit')] class MeasurementUnitController extends BaseAdminController { protected string $entity_class = MeasurementUnit::class; @@ -48,44 +49,34 @@ class MeasurementUnitController extends BaseAdminController protected string $attachment_class = MeasurementUnitAttachment::class; protected ?string $parameter_class = MeasurementUnitParameter::class; - /** - * @Route("/{id}", name="measurement_unit_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'measurement_unit_delete', methods: ['DELETE'])] public function delete(Request $request, MeasurementUnit $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="measurement_unit_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'measurement_unit_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(MeasurementUnit $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="measurement_unit_new") - * @Route("/{id}/clone", name="measurement_unit_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'measurement_unit_new')] + #[Route(path: '/{id}/clone', name: 'measurement_unit_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?MeasurementUnit $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="measurement_unit_export_all") - */ + #[Route(path: '/export', name: 'measurement_unit_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="measurement_unit_export") - */ + #[Route(path: '/{id}/export', name: 'measurement_unit_export')] public function exportEntity(AttachmentType $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/ProjectAdminController.php b/src/Controller/AdminPages/ProjectAdminController.php index ae14ea27..41287b69 100644 --- a/src/Controller/AdminPages/ProjectAdminController.php +++ b/src/Controller/AdminPages/ProjectAdminController.php @@ -25,7 +25,6 @@ namespace App\Controller\AdminPages; use App\Entity\Attachments\ProjectAttachment; use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\ProjectParameter; -use App\Form\AdminPages\BaseEntityAdminForm; use App\Form\AdminPages\ProjectAdminForm; use App\Services\ImportExportSystem\EntityExporter; use App\Services\ImportExportSystem\EntityImporter; @@ -34,11 +33,9 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; -/** - * @Route("/project") - */ +#[Route(path: '/project')] class ProjectAdminController extends BaseAdminController { protected string $entity_class = Project::class; @@ -48,44 +45,34 @@ class ProjectAdminController extends BaseAdminController protected string $attachment_class = ProjectAttachment::class; protected ?string $parameter_class = ProjectParameter::class; - /** - * @Route("/{id}", name="project_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'project_delete', methods: ['DELETE'])] public function delete(Request $request, Project $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="project_edit") - * @Route("/{id}/edit", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'project_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}/edit', requirements: ['id' => '\d+'])] public function edit(Project $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="project_new") - * @Route("/{id}/clone", name="device_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'project_new')] + #[Route(path: '/{id}/clone', name: 'device_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Project $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="project_export_all") - */ + #[Route(path: '/export', name: 'project_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="project_export") - */ + #[Route(path: '/{id}/export', name: 'project_export')] public function exportEntity(Project $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AdminPages/StorelocationController.php b/src/Controller/AdminPages/StorageLocationController.php similarity index 57% rename from src/Controller/AdminPages/StorelocationController.php rename to src/Controller/AdminPages/StorageLocationController.php index 080690ab..def6d3c6 100644 --- a/src/Controller/AdminPages/StorelocationController.php +++ b/src/Controller/AdminPages/StorageLocationController.php @@ -22,9 +22,9 @@ declare(strict_types=1); namespace App\Controller\AdminPages; -use App\Entity\Attachments\StorelocationAttachment; -use App\Entity\Parameters\StorelocationParameter; -use App\Entity\Parts\Storelocation; +use App\Entity\Attachments\StorageLocationAttachment; +use App\Entity\Parameters\StorageLocationParameter; +use App\Entity\Parts\StorageLocation; use App\Form\AdminPages\StorelocationAdminForm; use App\Services\ImportExportSystem\EntityExporter; use App\Services\ImportExportSystem\EntityImporter; @@ -33,59 +33,50 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/store_location") + * @see \App\Tests\Controller\AdminPages\StorelocationControllerTest */ -class StorelocationController extends BaseAdminController +#[Route(path: '/store_location')] +class StorageLocationController extends BaseAdminController { - protected string $entity_class = Storelocation::class; + protected string $entity_class = StorageLocation::class; protected string $twig_template = 'admin/storelocation_admin.html.twig'; protected string $form_class = StorelocationAdminForm::class; protected string $route_base = 'store_location'; - protected string $attachment_class = StorelocationAttachment::class; - protected ?string $parameter_class = StorelocationParameter::class; + protected string $attachment_class = StorageLocationAttachment::class; + protected ?string $parameter_class = StorageLocationParameter::class; - /** - * @Route("/{id}", name="store_location_delete", methods={"DELETE"}) - */ - public function delete(Request $request, Storelocation $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + #[Route(path: '/{id}', name: 'store_location_delete', methods: ['DELETE'])] + public function delete(Request $request, StorageLocation $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="store_location_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ - public function edit(Storelocation $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + #[Route(path: '/{id}/edit/{timestamp}', name: 'store_location_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] + public function edit(StorageLocation $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="store_location_new") - * @Route("/{id}/clone", name="store_location_clone") - * @Route("/") - */ - public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Storelocation $entity = null): Response + #[Route(path: '/new', name: 'store_location_new')] + #[Route(path: '/{id}/clone', name: 'store_location_clone')] + #[Route(path: '/')] + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?StorageLocation $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="store_location_export_all") - */ + #[Route(path: '/export', name: 'store_location_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="store_location_export") - */ - public function exportEntity(Storelocation $entity, EntityExporter $exporter, Request $request): Response + #[Route(path: '/{id}/export', name: 'store_location_export')] + public function exportEntity(StorageLocation $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); } diff --git a/src/Controller/AdminPages/SupplierController.php b/src/Controller/AdminPages/SupplierController.php index f7505c6a..195b9e18 100644 --- a/src/Controller/AdminPages/SupplierController.php +++ b/src/Controller/AdminPages/SupplierController.php @@ -33,11 +33,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/supplier") + * @see \App\Tests\Controller\AdminPages\SupplierControllerTest */ +#[Route(path: '/supplier')] class SupplierController extends BaseAdminController { protected string $entity_class = Supplier::class; @@ -47,44 +48,34 @@ class SupplierController extends BaseAdminController protected string $attachment_class = SupplierAttachment::class; protected ?string $parameter_class = SupplierParameter::class; - /** - * @Route("/{id}", name="supplier_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'supplier_delete', methods: ['DELETE'])] public function delete(Request $request, Supplier $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="supplier_edit") - * @Route("/{id}", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'supplier_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function edit(Supplier $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="supplier_new") - * @Route("/{id}/clone", name="supplier_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'supplier_new')] + #[Route(path: '/{id}/clone', name: 'supplier_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Supplier $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/export", name="supplier_export_all") - */ + #[Route(path: '/export', name: 'supplier_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="supplier_export") - */ + #[Route(path: '/{id}/export', name: 'supplier_export')] public function exportEntity(Supplier $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index 952237d8..d8bd8d87 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -24,31 +24,25 @@ namespace App\Controller; use App\DataTables\AttachmentDataTable; use App\DataTables\Filters\AttachmentFilter; -use App\DataTables\Filters\PartFilter; -use App\DataTables\PartsDataTable; use App\Entity\Attachments\Attachment; -use App\Entity\Attachments\PartAttachment; use App\Form\Filters\AttachmentFilterType; -use App\Form\Filters\PartFilterType; use App\Services\Attachments\AttachmentManager; use App\Services\Trees\NodesListBuilder; use Omines\DataTablesBundle\DataTableFactory; use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\BinaryFileResponse; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; class AttachmentFileController extends AbstractController { /** * Download the selected attachment. - * - * @Route("/attachment/{id}/download", name="attachment_download") */ + #[Route(path: '/attachment/{id}/download', name: 'attachment_download')] public function download(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse { $this->denyAccessUnlessGranted('read', $attachment); @@ -57,15 +51,15 @@ class AttachmentFileController extends AbstractController $this->denyAccessUnlessGranted('show_private', $attachment); } - if ($attachment->isExternal()) { - throw new RuntimeException('You can not download external attachments!'); + if (!$attachment->hasInternal()) { + throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); } - if (!$helper->isFileExisting($attachment)) { - throw new RuntimeException('The file associated with the attachment is not existing!'); + if (!$helper->isInternalFileExisting($attachment)) { + throw $this->createNotFoundException('The file associated with the attachment is not existing!'); } - $file_path = $helper->toAbsoluteFilePath($attachment); + $file_path = $helper->toAbsoluteInternalFilePath($attachment); $response = new BinaryFileResponse($file_path); //Set header content disposition, so that the file will be downloaded @@ -76,9 +70,8 @@ class AttachmentFileController extends AbstractController /** * View the attachment. - * - * @Route("/attachment/{id}/view", name="attachment_view") */ + #[Route(path: '/attachment/{id}/view', name: 'attachment_view')] public function view(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse { $this->denyAccessUnlessGranted('read', $attachment); @@ -87,15 +80,15 @@ class AttachmentFileController extends AbstractController $this->denyAccessUnlessGranted('show_private', $attachment); } - if ($attachment->isExternal()) { - throw new RuntimeException('You can not download external attachments!'); + if (!$attachment->hasInternal()) { + throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); } - if (!$helper->isFileExisting($attachment)) { - throw new RuntimeException('The file associated with the attachment is not existing!'); + if (!$helper->isInternalFileExisting($attachment)) { + throw $this->createNotFoundException('The file associated with the attachment is not existing!'); } - $file_path = $helper->toAbsoluteFilePath($attachment); + $file_path = $helper->toAbsoluteInternalFilePath($attachment); $response = new BinaryFileResponse($file_path); //Set header content disposition, so that the file will be downloaded @@ -104,12 +97,8 @@ class AttachmentFileController extends AbstractController return $response; } - /** - * @Route("/attachment/list", name="attachment_list") - * - * @return JsonResponse|Response - */ - public function attachmentsTable(Request $request, DataTableFactory $dataTableFactory, NodesListBuilder $nodesListBuilder) + #[Route(path: '/attachment/list', name: 'attachment_list')] + public function attachmentsTable(Request $request, DataTableFactory $dataTableFactory, NodesListBuilder $nodesListBuilder): Response { $this->denyAccessUnlessGranted('@attachments.list_attachments'); @@ -130,7 +119,7 @@ class AttachmentFileController extends AbstractController return $this->render('attachment_list.html.twig', [ 'datatable' => $table, - 'filterForm' => $filterForm->createView(), + 'filterForm' => $filterForm, ]); } } diff --git a/src/Controller/ErrorHandling/FixedErrorController.php b/src/Controller/ErrorHandling/FixedErrorController.php new file mode 100644 index 00000000..610a07b1 --- /dev/null +++ b/src/Controller/ErrorHandling/FixedErrorController.php @@ -0,0 +1,64 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Controller\ErrorHandling; + +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ErrorController; + +/** + * This class decorates the default error decorator and changes the content type of responses, if it went through a + * Turbo request. + * The problem is, that the default error controller returns in the format of the preferred content type of the request. + * This is turbo-stream. This causes Turbo to try to integrate it into the content frame and not trigger the ajax failed + * events to show the error in a popup like intended. + */ +#[AsDecorator("error_controller")] +class FixedErrorController +{ + public function __construct(private readonly ErrorController $decorated) + {} + + public function __invoke(\Throwable $exception): Response + { + $response = ($this->decorated)($exception); + + //Check the content type of the response + $contentType = $response->headers->get('Content-Type'); + + //If the content type is turbo stream, change the content type to html + //This prevents Turbo to render the response as a turbo stream, and forces to render it in the popup + if ($contentType === 'text/vnd.turbo-stream.html') { + $response->headers->set('Content-Type', 'text/html'); + } + + return $response; + } + + public function preview(Request $request, int $code): Response + { + return ($this->decorated)->preview($request, $code); + } +} \ No newline at end of file diff --git a/src/Controller/GroupController.php b/src/Controller/GroupController.php index 1eb10d76..cf7f0ab8 100644 --- a/src/Controller/GroupController.php +++ b/src/Controller/GroupController.php @@ -37,11 +37,9 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; -/** - * @Route("/group") - */ +#[Route(path: '/group')] class GroupController extends BaseAdminController { protected string $entity_class = Group::class; @@ -51,10 +49,8 @@ class GroupController extends BaseAdminController protected string $attachment_class = GroupAttachment::class; protected ?string $parameter_class = GroupParameter::class; - /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="group_edit") - * @Route("/{id}/", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/edit/{timestamp}', name: 'group_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}/', requirements: ['id' => '\d+'])] public function edit(Group $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper, PermissionSchemaUpdater $permissionSchemaUpdater, ?string $timestamp = null): Response { //Do an upgrade of the permission schema if needed (so the user can see the permissions a user get on next request (even if it was not done yet) @@ -63,7 +59,7 @@ class GroupController extends BaseAdminController //Handle permissions presets if ($request->request->has('permission_preset')) { $this->denyAccessUnlessGranted('edit_permissions', $entity); - if ($this->isCsrfTokenValid('group'.$entity->getId(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('group'.$entity->getID(), $request->request->get('_token'))) { $preset = $request->request->get('permission_preset'); $permissionPresetsHelper->applyPreset($entity, $preset); @@ -74,43 +70,35 @@ class GroupController extends BaseAdminController //We need to stop the execution here, or our permissions changes will be overwritten by the form values return $this->redirectToRoute('group_edit', ['id' => $entity->getID()]); - } else { - $this->addFlash('danger', 'csfr_invalid'); } + + $this->addFlash('danger', 'csfr_invalid'); } return $this->_edit($entity, $request, $em, $timestamp); } - /** - * @Route("/new", name="group_new") - * @Route("/{id}/clone", name="group_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'group_new')] + #[Route(path: '/{id}/clone', name: 'group_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Group $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/{id}", name="group_delete", methods={"DELETE"}) - */ + #[Route(path: '/{id}', name: 'group_delete', methods: ['DELETE'])] public function delete(Request $request, Group $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/export", name="group_export_all") - */ + #[Route(path: '/export', name: 'group_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="group_export") - */ + #[Route(path: '/{id}/export', name: 'group_export')] public function exportEntity(Group $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php index 51447c17..076e790b 100644 --- a/src/Controller/HomepageController.php +++ b/src/Controller/HomepageController.php @@ -25,47 +25,29 @@ namespace App\Controller; use App\DataTables\LogDataTable; use App\Entity\Parts\Part; use App\Services\Misc\GitVersionInfo; +use App\Services\System\BannerHelper; +use App\Services\System\UpdateAvailableManager; use Doctrine\ORM\EntityManagerInterface; -use const DIRECTORY_SEPARATOR; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\Routing\Annotation\Route; -use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Component\Routing\Attribute\Route; class HomepageController extends AbstractController { - protected CacheInterface $cache; - protected KernelInterface $kernel; - protected DataTableFactory $dataTable; - - public function __construct(CacheInterface $cache, KernelInterface $kernel, DataTableFactory $dataTable) + public function __construct(private readonly DataTableFactory $dataTable, private readonly BannerHelper $bannerHelper) { - $this->cache = $cache; - $this->kernel = $kernel; - $this->dataTable = $dataTable; } - public function getBanner(): string + + + #[Route(path: '/', name: 'homepage')] + public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager, + UpdateAvailableManager $updateAvailableManager): Response { - $banner = $this->getParameter('partdb.banner'); - if (empty($banner)) { - $banner_path = $this->kernel->getProjectDir() - .DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'banner.md'; + $this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS'); - return file_get_contents($banner_path); - } - - return $banner; - } - - /** - * @Route("/", name="homepage") - */ - public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager): Response - { if ($this->isGranted('@tools.lastActivity')) { $table = $this->dataTable->createFromType( LogDataTable::class, @@ -94,11 +76,14 @@ class HomepageController extends AbstractController } return $this->render('homepage.html.twig', [ - 'banner' => $this->getBanner(), + 'banner' => $this->bannerHelper->getBanner(), 'git_branch' => $versionInfo->getGitBranchName(), 'git_commit' => $versionInfo->getGitCommitHash(), 'show_first_steps' => $show_first_steps, 'datatable' => $table, + 'new_version_available' => $updateAvailableManager->isUpdateAvailable(), + 'new_version' => $updateAvailableManager->getLatestVersionString(), + 'new_version_url' => $updateAvailableManager->getLatestVersionUrl(), ]); } } diff --git a/src/Controller/InfoProviderController.php b/src/Controller/InfoProviderController.php new file mode 100644 index 00000000..a6ce3f1b --- /dev/null +++ b/src/Controller/InfoProviderController.php @@ -0,0 +1,131 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Controller; + +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\Part; +use App\Form\InfoProviderSystem\PartSearchType; +use App\Services\InfoProviderSystem\ExistingPartFinder; +use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\ProviderRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +use function Symfony\Component\Translation\t; + +#[Route('/tools/info_providers')] +class InfoProviderController extends AbstractController +{ + + public function __construct(private readonly ProviderRegistry $providerRegistry, + private readonly PartInfoRetriever $infoRetriever, + private readonly ExistingPartFinder $existingPartFinder + ) + { + + } + + #[Route('/providers', name: 'info_providers_list')] + public function listProviders(): Response + { + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + + return $this->render('info_providers/providers_list/providers_list.html.twig', [ + 'active_providers' => $this->providerRegistry->getActiveProviders(), + 'disabled_providers' => $this->providerRegistry->getDisabledProviders(), + ]); + } + + #[Route('/search', name: 'info_providers_search')] + #[Route('/update/{target}', name: 'info_providers_update_part_search')] + public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target, LoggerInterface $exceptionLogger): Response + { + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + + $form = $this->createForm(PartSearchType::class); + $form->handleRequest($request); + + $results = null; + + //When we are updating a part, use its name as keyword, to make searching easier + //However we can only do this, if the form was not submitted yet + if ($update_target !== null && !$form->isSubmitted()) { + //Use the provider reference if available, otherwise use the manufacturer product number + $keyword = $update_target->getProviderReference()->getProviderId() ?? $update_target->getManufacturerProductNumber(); + //Or the name if both are not available + if ($keyword === "") { + $keyword = $update_target->getName(); + } + + $form->get('keyword')->setData($keyword); + + //If we are updating a part, which already has a provider, preselect that provider in the form + if ($update_target->getProviderReference()->getProviderKey() !== null) { + try { + $form->get('providers')->setData([$this->providerRegistry->getProviderByKey($update_target->getProviderReference()->getProviderKey())]); + } catch (\InvalidArgumentException $e) { + //If the provider is not found, just ignore it + } + } + } + + if ($form->isSubmitted() && $form->isValid()) { + $keyword = $form->get('keyword')->getData(); + $providers = $form->get('providers')->getData(); + + $dtos = []; + + try { + $dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers); + } catch (ClientException $e) { + $this->addFlash('error', t('info_providers.search.error.client_exception')); + $this->addFlash('error',$e->getMessage()); + //Log the exception + $exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]); + } + + // modify the array to an array of arrays that has a field for a matching local Part + // the advantage to use that format even when we don't look for local parts is that we + // always work with the same interface + $results = array_map(function ($result) {return ['dto' => $result, 'localPart' => null];}, $dtos); + if(!$update_target) { + foreach ($results as $index => $result) { + $results[$index]['localPart'] = $this->existingPartFinder->findFirstExisting($result['dto']); + } + } + } + + return $this->render('info_providers/search/part_search.html.twig', [ + 'form' => $form, + 'results' => $results, + 'update_target' => $update_target + ]); + } +} \ No newline at end of file diff --git a/src/Controller/KiCadApiController.php b/src/Controller/KiCadApiController.php new file mode 100644 index 00000000..c28e87a6 --- /dev/null +++ b/src/Controller/KiCadApiController.php @@ -0,0 +1,85 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Controller; + +use App\Entity\Parts\Category; +use App\Entity\Parts\Part; +use App\Services\EDA\KiCadHelper; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +/** + * @see \App\Tests\Controller\KiCadApiControllerTest + */ +#[Route('/kicad-api/v1')] +class KiCadApiController extends AbstractController +{ + public function __construct( + private readonly KiCadHelper $kiCADHelper, + ) + { + } + + #[Route('/', name: 'kicad_api_root')] + public function root(): Response + { + $this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS'); + + //The API documentation says this can be either blank or the URL to the endpoints + return $this->json([ + 'categories' => '', + 'parts' => '', + ]); + } + + #[Route('/categories.json', name: 'kicad_api_categories')] + public function categories(): Response + { + $this->denyAccessUnlessGranted('@categories.read'); + + return $this->json($this->kiCADHelper->getCategories()); + } + + #[Route('/parts/category/{category}.json', name: 'kicad_api_category')] + public function categoryParts(?Category $category): Response + { + if ($category !== null) { + $this->denyAccessUnlessGranted('read', $category); + } else { + $this->denyAccessUnlessGranted('@categories.read'); + } + $this->denyAccessUnlessGranted('@parts.read'); + + return $this->json($this->kiCADHelper->getCategoryParts($category)); + } + + #[Route('/parts/{part}.json', name: 'kicad_api_part')] + public function partDetails(Part $part): Response + { + $this->denyAccessUnlessGranted('read', $part); + + return $this->json($this->kiCADHelper->getKiCADPart($part)); + } +} \ No newline at end of file diff --git a/src/Controller/LabelController.php b/src/Controller/LabelController.php index 769639d4..4950628b 100644 --- a/src/Controller/LabelController.php +++ b/src/Controller/LabelController.php @@ -43,7 +43,9 @@ namespace App\Controller; use App\Entity\Base\AbstractDBElement; use App\Entity\LabelSystem\LabelOptions; +use App\Entity\LabelSystem\LabelProcessMode; use App\Entity\LabelSystem\LabelProfile; +use App\Entity\LabelSystem\LabelSupportedElement; use App\Exceptions\TwigModeException; use App\Form\LabelSystem\LabelDialogType; use App\Repository\DBElementRepository; @@ -51,67 +53,46 @@ use App\Services\ElementTypeNameGenerator; use App\Services\LabelSystem\LabelGenerator; use App\Services\Misc\RangeParser; use Doctrine\ORM\EntityManagerInterface; -use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Contracts\Translation\TranslatorInterface; -/** - * @Route("/label") - */ +#[Route(path: '/label')] class LabelController extends AbstractController { - protected LabelGenerator $labelGenerator; - protected EntityManagerInterface $em; - protected ElementTypeNameGenerator $elementTypeNameGenerator; - protected RangeParser $rangeParser; - protected TranslatorInterface $translator; - - public function __construct(LabelGenerator $labelGenerator, EntityManagerInterface $em, ElementTypeNameGenerator $elementTypeNameGenerator, - RangeParser $rangeParser, TranslatorInterface $translator) + public function __construct(protected LabelGenerator $labelGenerator, protected EntityManagerInterface $em, protected ElementTypeNameGenerator $elementTypeNameGenerator, protected RangeParser $rangeParser, protected TranslatorInterface $translator) { - $this->labelGenerator = $labelGenerator; - $this->em = $em; - $this->elementTypeNameGenerator = $elementTypeNameGenerator; - $this->rangeParser = $rangeParser; - $this->translator = $translator; } - /** - * @Route("/dialog", name="label_dialog") - * @Route("/{profile}/dialog", name="label_dialog_profile") - */ + #[Route(path: '/dialog', name: 'label_dialog')] + #[Route(path: '/{profile}/dialog', name: 'label_dialog_profile')] public function generator(Request $request, ?LabelProfile $profile = null): Response { $this->denyAccessUnlessGranted('@labels.create_labels'); //If we inherit a LabelProfile, the user need to have access to it... - if (null !== $profile) { + if ($profile instanceof LabelProfile) { $this->denyAccessUnlessGranted('read', $profile); } - if ($profile) { - $label_options = $profile->getOptions(); - } else { - $label_options = new LabelOptions(); - } + $label_options = $profile instanceof LabelProfile ? $profile->getOptions() : new LabelOptions(); //We have to disable the options, if twig mode is selected and user is not allowed to use it. - $disable_options = 'twig' === $label_options->getLinesMode() && !$this->isGranted('@labels.use_twig'); + $disable_options = (LabelProcessMode::TWIG === $label_options->getProcessMode()) && !$this->isGranted('@labels.use_twig'); $form = $this->createForm(LabelDialogType::class, null, [ 'disable_options' => $disable_options, ]); //Try to parse given target_type and target_id - $target_type = $request->query->get('target_type', null); + $target_type = $request->query->getEnum('target_type', LabelSupportedElement::class, null); $target_id = $request->query->get('target_id', null); $generate = $request->query->getBoolean('generate', false); - if (null === $profile && is_string($target_type)) { + if (!$profile instanceof LabelProfile && $target_type instanceof LabelSupportedElement) { $label_options->setSupportedElement($target_type); } if (is_string($target_id)) { @@ -127,16 +108,39 @@ class LabelController extends AbstractController $pdf_data = null; $filename = 'invalid.pdf'; - //Generate PDF either when the form is submitted and valid, or the form was not submit yet, and generate is set - if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && null !== $profile)) { + if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && $profile instanceof LabelProfile)) { + + //Check if the label should be saved as profile + if ($form->get('save_profile')->isClicked() && $this->isGranted('@labels.create_profiles')) { //@phpstan-ignore-line Phpstan does not recognize the isClicked method + //Retrieve the profile name from the form + $new_name = $form->get('save_profile_name')->getData(); + //ensure that the name is not empty + if ($new_name === '' || $new_name === null) { + $form->get('save_profile_name')->addError(new FormError($this->translator->trans('label_generator.profile_name_empty'))); + goto render; + } + + $profile = new LabelProfile(); + $profile->setName($form->get('save_profile_name')->getData()); + $profile->setOptions($form_options); + $this->em->persist($profile); + $this->em->flush(); + $this->addFlash('success', 'label_generator.profile_saved'); + + return $this->redirectToRoute('label_dialog_profile', [ + 'profile' => $profile->getID(), + 'target_id' => (string) $form->get('target_id')->getData() + ]); + } + $target_id = (string) $form->get('target_id')->getData(); $targets = $this->findObjects($form_options->getSupportedElement(), $target_id); - if (!empty($targets)) { + if ($targets !== []) { try { $pdf_data = $this->labelGenerator->generateLabel($form_options, $targets); $filename = $this->getLabelName($targets[0], $profile); } catch (TwigModeException $exception) { - $form->get('options')->get('lines')->addError(new FormError($exception->getMessage())); + $form->get('options')->get('lines')->addError(new FormError($exception->getSafeMessage())); } } else { //$this->addFlash('warning', 'label_generator.no_entities_found'); @@ -144,9 +148,15 @@ class LabelController extends AbstractController new FormError($this->translator->trans('label_generator.no_entities_found')) ); } + + //When the profile lines are empty, show a notice flash + if (trim($form_options->getLines()) === '') { + $this->addFlash('notice', 'label_generator.no_lines_given'); + } } - return $this->renderForm('label_system/dialog.html.twig', [ + render: + return $this->render('label_system/dialog.html.twig', [ 'form' => $form, 'pdf_data' => $pdf_data, 'filename' => $filename, @@ -162,16 +172,12 @@ class LabelController extends AbstractController return $ret.'.pdf'; } - protected function findObjects(string $type, string $ids): array + protected function findObjects(LabelSupportedElement $type, string $ids): array { - if (!isset(LabelGenerator::CLASS_SUPPORT_MAPPING[$type])) { - throw new InvalidArgumentException('The given type is not known and can not be mapped to a class!'); - } - $id_array = $this->rangeParser->parse($ids); - /** @var DBElementRepository $repo */ - $repo = $this->em->getRepository(LabelGenerator::CLASS_SUPPORT_MAPPING[$type]); + /** @var DBElementRepository $repo */ + $repo = $this->em->getRepository($type->getEntityClass()); return $repo->getElementsFromIDArray($id_array); } diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php index 91b37269..a849539d 100644 --- a/src/Controller/LogController.php +++ b/src/Controller/LogController.php @@ -33,40 +33,32 @@ use App\Entity\LogSystem\ElementEditedLogEntry; use App\Form\Filters\LogFilterType; use App\Repository\DBElementRepository; use App\Services\LogSystem\EventUndoHelper; +use App\Services\LogSystem\EventUndoMode; +use App\Services\LogSystem\LogEntryExtraFormatter; +use App\Services\LogSystem\LogLevelHelper; +use App\Services\LogSystem\LogTargetHelper; use App\Services\LogSystem\TimeTravel; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityRepository; use InvalidArgumentException; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; -/** - * @Route("/log") - */ +#[Route(path: '/log')] class LogController extends AbstractController { - protected EntityManagerInterface $entityManager; - protected TimeTravel $timeTravel; protected DBElementRepository $dbRepository; - public function __construct(EntityManagerInterface $entityManager, TimeTravel $timeTravel) + public function __construct(protected EntityManagerInterface $entityManager, protected TimeTravel $timeTravel) { - $this->entityManager = $entityManager; - $this->timeTravel = $timeTravel; $this->dbRepository = $entityManager->getRepository(AbstractDBElement::class); } - /** - * @Route("/", name="log_view") - * - * @return JsonResponse|Response - */ - public function showLogs(Request $request, DataTableFactory $dataTable) + #[Route(path: '/', name: 'log_view')] + public function showLogs(Request $request, DataTableFactory $dataTable): Response { $this->denyAccessUnlessGranted('@system.show_logs'); @@ -89,26 +81,66 @@ class LogController extends AbstractController return $this->render('log_system/log_list.html.twig', [ 'datatable' => $table, - 'filterForm' => $filterForm->createView(), + 'filterForm' => $filterForm, ]); } - /** - * @Route("/undo", name="log_undo", methods={"POST"}) - */ + #[Route(path: '/{id}/details', name: 'log_details')] + public function logDetails(AbstractLogEntry $logEntry, LogEntryExtraFormatter $logEntryExtraFormatter, + LogLevelHelper $logLevelHelper, LogTargetHelper $logTargetHelper, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('show_details', $logEntry); + + $extra_html = $logEntryExtraFormatter->format($logEntry); + $target_html = $logTargetHelper->formatTarget($logEntry); + + $repo = $entityManager->getRepository(AbstractLogEntry::class); + $target_element = $repo->getTargetElement($logEntry); + + return $this->render('log_system/details/log_details.html.twig', [ + 'log_entry' => $logEntry, + 'target_element' => $target_element, + 'extra_html' => $extra_html, + 'target_html' => $target_html, + 'log_level_helper' => $logLevelHelper, + ]); + } + + #[Route(path: '/{id}/delete', name: 'log_delete', methods: ['DELETE'])] + public function deleteLogEntry(Request $request, AbstractLogEntry $logEntry, EntityManagerInterface $entityManager): RedirectResponse + { + $this->denyAccessUnlessGranted('delete', $logEntry); + + if ($this->isCsrfTokenValid('delete'.$logEntry->getID(), $request->request->get('_token'))) { + //Remove part + $entityManager->remove($logEntry); + //Flush changes + $entityManager->flush(); + $this->addFlash('success', 'log.delete.success'); + } + + return $this->redirectToRoute('homepage'); + } + + + #[Route(path: '/undo', name: 'log_undo', methods: ['POST'])] public function undoRevertLog(Request $request, EventUndoHelper $eventUndoHelper): RedirectResponse { - $mode = EventUndoHelper::MODE_UNDO; - $id = $request->request->get('undo'); + $mode = EventUndoMode::UNDO; + $id = $request->request->getInt('undo'); //If no undo value was set check if a revert was set - if (null === $id) { - $id = $request->get('revert'); - $mode = EventUndoHelper::MODE_REVERT; + if (0 === $id) { + $id = $request->request->getInt('revert'); + $mode = EventUndoMode::REVERT; + } + + if (0 === $id) { + throw new InvalidArgumentException('No log entry ID was given!'); } $log_element = $this->entityManager->find(AbstractLogEntry::class, $id); - if (null === $log_element) { + if (!$log_element instanceof AbstractLogEntry) { throw new InvalidArgumentException('No log entry with the given ID is existing!'); } @@ -117,9 +149,9 @@ class LogController extends AbstractController $eventUndoHelper->setMode($mode); $eventUndoHelper->setUndoneEvent($log_element); - if (EventUndoHelper::MODE_UNDO === $mode) { + if (EventUndoMode::UNDO === $mode) { $this->undoLog($log_element); - } elseif (EventUndoHelper::MODE_REVERT === $mode) { + } else { $this->revertLog($log_element); } diff --git a/src/Controller/OAuthClientController.php b/src/Controller/OAuthClientController.php new file mode 100644 index 00000000..9606a4e4 --- /dev/null +++ b/src/Controller/OAuthClientController.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Controller; + +use App\Services\OAuth\OAuthTokenManager; +use KnpU\OAuth2ClientBundle\Client\ClientRegistry; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +use function Symfony\Component\Translation\t; + +#[Route('/oauth/client')] +class OAuthClientController extends AbstractController +{ + public function __construct(private readonly ClientRegistry $clientRegistry, private readonly OAuthTokenManager $tokenManager) + { + + } + + #[Route('/{name}/connect', name: 'oauth_client_connect')] + public function connect(string $name): Response + { + $this->denyAccessUnlessGranted('@system.manage_oauth_tokens'); + + return $this->clientRegistry + ->getClient($name) // key used in config/packages/knpu_oauth2_client.yaml + ->redirect([], []); + } + + #[Route('/{name}/check', name: 'oauth_client_check')] + public function check(string $name): Response + { + $this->denyAccessUnlessGranted('@system.manage_oauth_tokens'); + + $client = $this->clientRegistry->getClient($name); + + $access_token = $client->getAccessToken(); + $this->tokenManager->saveToken($name, $access_token); + + $this->addFlash('success', t('oauth_client.flash.connection_successful')); + + return $this->redirectToRoute('homepage'); + } +} \ No newline at end of file diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index f0d6fdfe..b11a5c90 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -23,12 +23,13 @@ declare(strict_types=1); namespace App\Controller; use App\DataTables\LogDataTable; +use App\Entity\Attachments\AttachmentUpload; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Orderdetail; use App\Entity\ProjectSystem\Project; @@ -36,6 +37,8 @@ use App\Exceptions\AttachmentDownloadException; use App\Form\Part\PartBaseType; use App\Services\Attachments\AttachmentSubmitHandler; use App\Services\Attachments\PartPreviewGenerator; +use App\Services\EntityMergers\Mergers\PartMerger; +use App\Services\InfoProviderSystem\PartInfoRetriever; use App\Services\LogSystem\EventCommentHelper; use App\Services\LogSystem\HistoryHelper; use App\Services\LogSystem\TimeTravel; @@ -47,42 +50,35 @@ use DateTime; use Doctrine\ORM\EntityManagerInterface; use Exception; use Omines\DataTablesBundle\DataTableFactory; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Contracts\Translation\TranslatorInterface; use function Symfony\Component\Translation\t; -/** - * @Route("/part") - */ +#[Route(path: '/part')] class PartController extends AbstractController { - protected PricedetailHelper $pricedetailHelper; - protected PartPreviewGenerator $partPreviewGenerator; - protected EventCommentHelper $commentHelper; - - public function __construct(PricedetailHelper $pricedetailHelper, - PartPreviewGenerator $partPreviewGenerator, EventCommentHelper $commentHelper) + public function __construct(protected PricedetailHelper $pricedetailHelper, + protected PartPreviewGenerator $partPreviewGenerator, + private readonly TranslatorInterface $translator, + private readonly AttachmentSubmitHandler $attachmentSubmitHandler, private readonly EntityManagerInterface $em, + protected EventCommentHelper $commentHelper) { - $this->pricedetailHelper = $pricedetailHelper; - $this->partPreviewGenerator = $partPreviewGenerator; - $this->commentHelper = $commentHelper; } /** - * @Route("/{id}/info/{timestamp}", name="part_info") - * @Route("/{id}", requirements={"id"="\d+"}) * * @throws Exception */ + #[Route(path: '/{id}/info/{timestamp}', name: 'part_info')] + #[Route(path: '/{id}', requirements: ['id' => '\d+'])] public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper, DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, PartLotWithdrawAddHelper $withdrawAddHelper, ?string $timestamp = null): Response { @@ -130,82 +126,28 @@ class PartController extends AbstractController ); } - /** - * @Route("/{id}/edit", name="part_edit") - */ - public function edit(Part $part, Request $request, EntityManagerInterface $em, TranslatorInterface $translator, - AttachmentSubmitHandler $attachmentSubmitHandler): Response + #[Route(path: '/{id}/edit', name: 'part_edit')] + public function edit(Part $part, Request $request): Response { $this->denyAccessUnlessGranted('edit', $part); - $form = $this->createForm(PartBaseType::class, $part); - - $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - //Upload passed files - $attachments = $form['attachments']; - foreach ($attachments as $attachment) { - /** @var FormInterface $attachment */ - $options = [ - 'secure_attachment' => $attachment['secureFile']->getData(), - 'download_url' => $attachment['downloadURL']->getData(), - ]; - - try { - $attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options); - } catch (AttachmentDownloadException $attachmentDownloadException) { - $this->addFlash( - 'error', - $translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage() - ); - } - } - - $this->commentHelper->setMessage($form['log_comment']->getData()); - - $em->persist($part); - $em->flush(); - $this->addFlash('success', 'part.edited_flash'); - - //Redirect to clone page if user wished that... - //@phpstan-ignore-next-line - if ('save_and_clone' === $form->getClickedButton()->getName()) { - return $this->redirectToRoute('part_clone', ['id' => $part->getID()]); - } - //@phpstan-ignore-next-line - if ('save_and_new' === $form->getClickedButton()->getName()) { - return $this->redirectToRoute('part_new'); - } - - //Reload form, so the SIUnitType entries use the new part unit - $form = $this->createForm(PartBaseType::class, $part); - } elseif ($form->isSubmitted() && !$form->isValid()) { - $this->addFlash('error', 'part.edited_flash.invalid'); - } - - return $this->renderForm('parts/edit/edit_part_info.html.twig', - [ - 'part' => $part, - 'form' => $form, - ]); + return $this->renderPartForm('edit', $request, $part); } - /** - * @Route("/{id}/delete", name="part_delete", methods={"DELETE"}) - */ - public function delete(Request $request, Part $part, EntityManagerInterface $entityManager): RedirectResponse + #[Route(path: '/{id}/delete', name: 'part_delete', methods: ['DELETE'])] + public function delete(Request $request, Part $part): RedirectResponse { $this->denyAccessUnlessGranted('delete', $part); - if ($this->isCsrfTokenValid('delete'.$part->getId(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('delete'.$part->getID(), $request->request->get('_token'))) { $this->commentHelper->setMessage($request->request->get('log_comment', null)); //Remove part - $entityManager->remove($part); + $this->em->remove($part); //Flush changes - $entityManager->flush(); + $this->em->flush(); $this->addFlash('success', 'part.deleted'); } @@ -213,23 +155,22 @@ class PartController extends AbstractController return $this->redirectToRoute('homepage'); } - /** - * @Route("/new", name="part_new") - * @Route("/{id}/clone", name="part_clone") - * @Route("/new_build_part/{project_id}", name="part_new_build_part") - * @ParamConverter("part", options={"id" = "id"}) - * @ParamConverter("project", options={"id" = "project_id"}) - */ + #[Route(path: '/new', name: 'part_new')] + #[Route(path: '/{id}/clone', name: 'part_clone')] + #[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')] public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator, AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper, - ?Part $part = null, ?Project $project = null): Response + #[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null, + #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response { - if ($part) { //Clone part + if ($part instanceof Part) { + //Clone part $new_part = clone $part; - } else if ($project) { //Initialize a new part for a build part from the given project + } elseif ($project instanceof Project) { + //Initialize a new part for a build part from the given project //Ensure that the project has not already a build part - if ($project->getBuildPart() !== null) { + if ($project->getBuildPart() instanceof Part) { $this->addFlash('error', 'part.new_build_part.error.build_part_already_exists'); return $this->redirectToRoute('part_edit', ['id' => $project->getBuildPart()->getID()]); } @@ -242,7 +183,7 @@ class PartController extends AbstractController $cid = $request->get('category', null); $category = $cid ? $em->find(Category::class, $cid) : null; - if (null !== $category && null === $new_part->getCategory()) { + if ($category instanceof Category && !$new_part->getCategory() instanceof Category) { $new_part->setCategory($category); $new_part->setDescription($category->getDefaultDescription()); $new_part->setComment($category->getDefaultComment()); @@ -250,19 +191,19 @@ class PartController extends AbstractController $fid = $request->get('footprint', null); $footprint = $fid ? $em->find(Footprint::class, $fid) : null; - if (null !== $footprint && null === $new_part->getFootprint()) { + if ($footprint instanceof Footprint && !$new_part->getFootprint() instanceof Footprint) { $new_part->setFootprint($footprint); } $mid = $request->get('manufacturer', null); $manufacturer = $mid ? $em->find(Manufacturer::class, $mid) : null; - if (null !== $manufacturer && null === $new_part->getManufacturer()) { + if ($manufacturer instanceof Manufacturer && !$new_part->getManufacturer() instanceof Manufacturer) { $new_part->setManufacturer($manufacturer); } $store_id = $request->get('storelocation', null); - $storelocation = $store_id ? $em->find(Storelocation::class, $store_id) : null; - if (null !== $storelocation && $new_part->getPartLots()->isEmpty()) { + $storelocation = $store_id ? $em->find(StorageLocation::class, $store_id) : null; + if ($storelocation instanceof StorageLocation && $new_part->getPartLots()->isEmpty()) { $partLot = new PartLot(); $partLot->setStorageLocation($storelocation); $partLot->setInstockUnknown(true); @@ -271,13 +212,91 @@ class PartController extends AbstractController $supplier_id = $request->get('supplier', null); $supplier = $supplier_id ? $em->find(Supplier::class, $supplier_id) : null; - if (null !== $supplier && $new_part->getOrderdetails()->isEmpty()) { + if ($supplier instanceof Supplier && $new_part->getOrderdetails()->isEmpty()) { $orderdetail = new Orderdetail(); $orderdetail->setSupplier($supplier); $new_part->addOrderdetail($orderdetail); } - $form = $this->createForm(PartBaseType::class, $new_part); + return $this->renderPartForm('new', $request, $new_part); + } + + #[Route('/from_info_provider/{providerKey}/{providerId}/create', name: 'info_providers_create_part', requirements: ['providerId' => '.+'])] + public function createFromInfoProvider(Request $request, string $providerKey, string $providerId, PartInfoRetriever $infoRetriever): Response + { + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + + $dto = $infoRetriever->getDetails($providerKey, $providerId); + $new_part = $infoRetriever->dtoToPart($dto); + + if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) { + $this->addFlash('warning', t("part.create_from_info_provider.no_category_yet")); + } + + return $this->renderPartForm('new', $request, $new_part, [ + 'info_provider_dto' => $dto, + ]); + } + + #[Route('/{target}/merge/{other}', name: 'part_merge')] + public function merge(Request $request, Part $target, Part $other, PartMerger $partMerger): Response + { + $this->denyAccessUnlessGranted('edit', $target); + $this->denyAccessUnlessGranted('delete', $other); + + //Save the old name of the target part for the template + $target_name = $target->getName(); + + $this->addFlash('notice', t('part.merge.flash.please_review')); + + $merged = $partMerger->merge($target, $other); + return $this->renderPartForm('merge', $request, $merged, [], [ + 'tname_before' => $target_name, + 'other_part' => $other, + ]); + } + + #[Route(path: '/{id}/from_info_provider/{providerKey}/{providerId}/update', name: 'info_providers_update_part', requirements: ['providerId' => '.+'])] + public function updateFromInfoProvider(Part $part, Request $request, string $providerKey, string $providerId, + PartInfoRetriever $infoRetriever, PartMerger $partMerger): Response + { + $this->denyAccessUnlessGranted('edit', $part); + $this->denyAccessUnlessGranted('@info_providers.create_parts'); + + //Save the old name of the target part for the template + $old_name = $part->getName(); + + $dto = $infoRetriever->getDetails($providerKey, $providerId); + $provider_part = $infoRetriever->dtoToPart($dto); + + $part = $partMerger->merge($part, $provider_part); + + $this->addFlash('notice', t('part.merge.flash.please_review')); + + return $this->renderPartForm('update_from_ip', $request, $part, [ + 'info_provider_dto' => $dto, + ], [ + 'tname_before' => $old_name + ]); + } + + /** + * This function provides a common implementation for methods, which use the part form. + * @param Request $request + * @param Part $data + * @param array $form_options + * @return Response + */ + private function renderPartForm(string $mode, Request $request, Part $data, array $form_options = [], array $merge_infos = []): Response + { + //Ensure that mode is either 'new' or 'edit + if (!in_array($mode, ['new', 'edit', 'merge', 'update_from_ip'], true)) { + throw new \InvalidArgumentException('Invalid mode given'); + } + + $new_part = $data; + + $form = $this->createForm(PartBaseType::class, $new_part, $form_options); $form->handleRequest($request); @@ -286,26 +305,37 @@ class PartController extends AbstractController $attachments = $form['attachments']; foreach ($attachments as $attachment) { /** @var FormInterface $attachment */ - $options = [ - 'secure_attachment' => $attachment['secureFile']->getData(), - 'download_url' => $attachment['downloadURL']->getData(), - ]; try { - $attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options); + $this->attachmentSubmitHandler->handleUpload($attachment->getData(), AttachmentUpload::fromAttachmentForm($attachment)); } catch (AttachmentDownloadException $attachmentDownloadException) { $this->addFlash( 'error', - $translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage() + $this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage() ); } } + //Ensure that the master picture is still part of the attachments + if ($new_part->getMasterPictureAttachment() !== null && !$new_part->getAttachments()->contains($new_part->getMasterPictureAttachment())) { + $new_part->setMasterPictureAttachment(null); + } + $this->commentHelper->setMessage($form['log_comment']->getData()); - $em->persist($new_part); - $em->flush(); - $this->addFlash('success', 'part.created_flash'); + $this->em->persist($new_part); + + //When we are in merge mode, we have to remove the other part + if ($mode === 'merge') { + $this->em->remove($merge_infos['other_part']); + } + + $this->em->flush(); + if ($mode === 'new') { + $this->addFlash('success', 'part.created_flash'); + } elseif ($mode === 'edit') { + $this->addFlash('success', 'part.edited_flash'); + } //If a redirect URL was given, redirect there if ($request->query->get('_redirect')) { @@ -329,30 +359,44 @@ class PartController extends AbstractController $this->addFlash('error', 'part.created_flash.invalid'); } - return $this->renderForm('parts/edit/new_part.html.twig', + $template = ''; + if ($mode === 'new') { + $template = 'parts/edit/new_part.html.twig'; + } elseif ($mode === 'edit') { + $template = 'parts/edit/edit_part_info.html.twig'; + } elseif ($mode === 'merge') { + $template = 'parts/edit/merge_parts.html.twig'; + } elseif ($mode === 'update_from_ip') { + $template = 'parts/edit/update_from_ip.html.twig'; + } + + return $this->render($template, [ 'part' => $new_part, 'form' => $form, + 'merge_old_name' => $merge_infos['tname_before'] ?? null, + 'merge_other' => $merge_infos['other_part'] ?? null ]); } - /** - * @Route("/{id}/add_withdraw", name="part_add_withdraw", methods={"POST"}) - */ + + #[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])] public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response { if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) { //Retrieve partlot from the request $partLot = $em->find(PartLot::class, $request->request->get('lot_id')); - if($partLot === null) { + if(!$partLot instanceof PartLot) { throw new \RuntimeException('Part lot not found!'); } //Ensure that the partlot belongs to the part if($partLot->getPart() !== $part) { throw new \RuntimeException("The origin partlot does not belong to the part!"); } - //Try to determine the target lot (used for move actions) - $targetLot = $em->find(PartLot::class, $request->request->get('target_id')); + + //Try to determine the target lot (used for move actions), if the parameter is existing + $targetId = $request->request->get('target_id', null); + $targetLot = $targetId ? $em->find(PartLot::class, $targetId) : null; if ($targetLot && $targetLot->getPart() !== $part) { throw new \RuntimeException("The target partlot does not belong to the part!"); } @@ -361,28 +405,46 @@ class PartController extends AbstractController $amount = (float) $request->request->get('amount'); $comment = $request->request->get('comment'); $action = $request->request->get('action'); + $delete_lot_if_empty = $request->request->getBoolean('delete_lot_if_empty', false); + $timestamp = null; + $timestamp_str = $request->request->getString('timestamp', ''); + //Try to parse the timestamp + if($timestamp_str !== '') { + $timestamp = new DateTime($timestamp_str); + } + + //Ensure that the timestamp is not in the future + if($timestamp !== null && $timestamp > new DateTime("+20min")) { + throw new \LogicException("The timestamp must not be in the future!"); + } + + //Ensure that the amount is not null or negative + if ($amount <= 0) { + $this->addFlash('warning', 'part.withdraw.zero_amount'); + goto err; + } try { switch ($action) { case "withdraw": case "remove": $this->denyAccessUnlessGranted('withdraw', $partLot); - $withdrawAddHelper->withdraw($partLot, $amount, $comment); + $withdrawAddHelper->withdraw($partLot, $amount, $comment, $timestamp, $delete_lot_if_empty); break; case "add": $this->denyAccessUnlessGranted('add', $partLot); - $withdrawAddHelper->add($partLot, $amount, $comment); + $withdrawAddHelper->add($partLot, $amount, $comment, $timestamp); break; case "move": $this->denyAccessUnlessGranted('move', $partLot); $this->denyAccessUnlessGranted('move', $targetLot); - $withdrawAddHelper->move($partLot, $targetLot, $amount, $comment); + $withdrawAddHelper->move($partLot, $targetLot, $amount, $comment, $timestamp, $delete_lot_if_empty); break; default: throw new \RuntimeException("Unknown action!"); } - } catch (AccessDeniedException $exception) { + } catch (AccessDeniedException) { $this->addFlash('error', t('part.withdraw.access_denied')); goto err; } @@ -396,7 +458,7 @@ class PartController extends AbstractController } err: - //If an redirect was passed, then redirect there + //If a redirect was passed, then redirect there if($request->request->get('_redirect')) { return $this->redirect($request->request->get('_redirect')); } diff --git a/src/Controller/PartImportExportController.php b/src/Controller/PartImportExportController.php index 22b5b528..45f90d75 100644 --- a/src/Controller/PartImportExportController.php +++ b/src/Controller/PartImportExportController.php @@ -1,4 +1,7 @@ . */ - namespace App\Controller; use App\Entity\Parts\Part; @@ -26,35 +28,21 @@ use App\Services\ImportExportSystem\EntityExporter; use App\Services\ImportExportSystem\EntityImporter; use App\Services\LogSystem\EventCommentHelper; use App\Services\Parts\PartsTableActionHandler; -use Doctrine\ORM\EntityManagerInterface; -use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use UnexpectedValueException; class PartImportExportController extends AbstractController { - private PartsTableActionHandler $partsTableActionHandler; - private EntityImporter $entityImporter; - private EventCommentHelper $commentHelper; - - public function __construct(PartsTableActionHandler $partsTableActionHandler, - EntityImporter $entityImporter, EventCommentHelper $commentHelper) + public function __construct(private readonly PartsTableActionHandler $partsTableActionHandler, private readonly EntityImporter $entityImporter, private readonly EventCommentHelper $commentHelper) { - $this->partsTableActionHandler = $partsTableActionHandler; - $this->entityImporter = $entityImporter; - $this->commentHelper = $commentHelper; } - /** - * @Route("/parts/import", name="parts_import") - * @param Request $request - * @return Response - */ + #[Route(path: '/parts/import', name: 'parts_import')] public function importParts(Request $request): Response { $this->denyAccessUnlessGranted('@parts.import'); @@ -111,24 +99,22 @@ class PartImportExportController extends AbstractController ret: - return $this->renderForm('parts/import/parts_import.html.twig', [ + return $this->render('parts/import/parts_import.html.twig', [ 'import_form' => $import_form, 'imported_entities' => $entities ?? [], 'import_errors' => $errors ?? [], ]); } - /** - * @Route("/parts/export", name="parts_export", methods={"GET"}) - * @return Response - */ + #[Route(path: '/parts/export', name: 'parts_export', methods: ['GET'])] public function exportParts(Request $request, EntityExporter $entityExporter): Response { $ids = $request->query->get('ids', ''); $parts = $this->partsTableActionHandler->idStringToArray($ids); - if (empty($parts)) { - throw new \RuntimeException('No parts found!'); + if (count($parts) === 0) { + $this->addFlash('error', 'entity.export.flash.error.no_entities'); + return $this->redirectToRoute('homepage'); } //Ensure that we have access to the parts @@ -138,4 +124,4 @@ class PartImportExportController extends AbstractController return $entityExporter->exportEntityFromRequest($parts, $request); } -} \ No newline at end of file +} diff --git a/src/Controller/PartListsController.php b/src/Controller/PartListsController.php index ab23c0f2..48995228 100644 --- a/src/Controller/PartListsController.php +++ b/src/Controller/PartListsController.php @@ -22,42 +22,40 @@ declare(strict_types=1); namespace App\Controller; +use App\DataTables\ErrorDataTable; use App\DataTables\Filters\PartFilter; use App\DataTables\Filters\PartSearchFilter; use App\DataTables\PartsDataTable; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\Part; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; +use App\Exceptions\InvalidRegexException; use App\Form\Filters\PartFilterType; use App\Services\Parts\PartsTableActionHandler; use App\Services\Trees\NodesListBuilder; +use Doctrine\DBAL\Exception\DriverException; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatorInterface; + +use function Symfony\Component\Translation\t; class PartListsController extends AbstractController { - private EntityManagerInterface $entityManager; - private NodesListBuilder $nodesListBuilder; - private DataTableFactory $dataTableFactory; - - public function __construct(EntityManagerInterface $entityManager, NodesListBuilder $nodesListBuilder, DataTableFactory $dataTableFactory) + public function __construct(private readonly EntityManagerInterface $entityManager, private readonly NodesListBuilder $nodesListBuilder, private readonly DataTableFactory $dataTableFactory, private readonly TranslatorInterface $translator) { - $this->entityManager = $entityManager; - $this->nodesListBuilder = $nodesListBuilder; - $this->dataTableFactory = $dataTableFactory; } - /** - * @Route("/table/action", name="table_action", methods={"POST"}) - */ + #[Route(path: '/table/action', name: 'table_action', methods: ['POST'])] public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response { $this->denyAccessUnlessGranted('@parts.edit'); @@ -66,6 +64,7 @@ class PartListsController extends AbstractController $ids = $request->request->get('ids'); $action = $request->request->get('action'); $target = $request->request->get('target'); + $redirectResponse = null; if (!$this->isCsrfTokenValid('table_action', $request->request->get('_token'))) { $this->addFlash('error', 'csfr_invalid'); @@ -76,17 +75,36 @@ class PartListsController extends AbstractController if (null === $action || null === $ids) { $this->addFlash('error', 'part.table.actions.no_params_given'); } else { + $errors = []; + $parts = $actionHandler->idStringToArray($ids); - $redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect); + $redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect, $errors); //Save changes $this->entityManager->flush(); - $this->addFlash('success', 'part.table.actions.success'); + if (count($errors) === 0) { + $this->addFlash('success', 'part.table.actions.success'); + } else { + $this->addFlash('error', t('part.table.actions.error', ['%count%' => count($errors)])); + //Create a flash message for each error + foreach ($errors as $error) { + /** @var Part $part */ + $part = $error['part']; + + $this->addFlash('error', + t('part.table.actions.error_detail', [ + '%part_name%' => $part->getName(), + '%part_id%' => $part->getID(), + '%message%' => $error['message'] + ]) + ); + } + } } //If the action handler returned a response, we use it, otherwise we redirect back to the previous page. - if (isset($redirectResponse) && $redirectResponse instanceof Response) { + if ($redirectResponse !== null) { return $redirectResponse; } @@ -95,8 +113,6 @@ class PartListsController extends AbstractController /** * Disable the given form interface after creation of the form by removing and reattaching the form. - * @param FormInterface $form - * @return void */ private function disableFormFieldAfterCreation(FormInterface $form, bool $disabled = true): void { @@ -104,12 +120,12 @@ class PartListsController extends AbstractController $attrs['disabled'] = $disabled; $parent = $form->getParent(); - if ($parent === null) { + if (!$parent instanceof FormInterface) { throw new \RuntimeException('This function can only be used on form fields that are children of another form!'); } $parent->remove($form->getName()); - $parent->add($form->getName(), get_class($form->getConfig()->getType()->getInnerType()), $attrs); + $parent->add($form->getName(), $form->getConfig()->getType()->getInnerType()::class, $attrs); } /** @@ -120,7 +136,6 @@ class PartListsController extends AbstractController * @param callable|null $form_changer A function that is called with the form object as parameter. This function can be used to customize the form * @param array $additonal_template_vars Any additional template variables that should be passed to the template * @param array $additional_table_vars Any additional variables that should be passed to the table creation - * @return Response */ protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response { @@ -140,11 +155,29 @@ class PartListsController extends AbstractController $filterForm->handleRequest($formRequest); - $table = $this->dataTableFactory->createFromType(PartsDataTable::class, array_merge(['filter' => $filter], $additional_table_vars)) + $table = $this->dataTableFactory->createFromType( + PartsDataTable::class, + array_merge(['filter' => $filter], $additional_table_vars), + ['lengthMenu' => PartsDataTable::LENGTH_MENU] + ) ->handleRequest($request); if ($table->isCallback()) { - return $table->getResponse(); + try { + try { + return $table->getResponse(); + } catch (DriverException $driverException) { + if ($driverException->getCode() === 1139) { + //Convert the driver exception to InvalidRegexException so it has the same hanlder as for SQLite + throw InvalidRegexException::fromDriverException($driverException); + } else { + throw $driverException; + } + } + } catch (InvalidRegexException $exception) { + $errors = $this->translator->trans('part.table.invalid_regex').': '.$exception->getReason(); + return ErrorDataTable::errorTable($this->dataTableFactory, $request, $errors); + } } return $this->render($template, array_merge([ @@ -153,19 +186,15 @@ class PartListsController extends AbstractController ], $additonal_template_vars)); } - /** - * @Route("/category/{id}/parts", name="part_list_category") - * - * @return JsonResponse|Response - */ - public function showCategory(Category $category, Request $request) + #[Route(path: '/category/{id}/parts', name: 'part_list_category')] + public function showCategory(Category $category, Request $request): Response { $this->denyAccessUnlessGranted('@categories.read'); return $this->showListWithFilter($request, 'parts/lists/category_list.html.twig', function (PartFilter $filter) use ($category) { - $filter->getCategory()->setOperator('INCLUDING_CHILDREN')->setValue($category); + $filter->category->setOperator('INCLUDING_CHILDREN')->setValue($category); }, function (FormInterface $filterForm) { $this->disableFormFieldAfterCreation($filterForm->get('category')->get('value')); }, [ @@ -175,19 +204,15 @@ class PartListsController extends AbstractController ); } - /** - * @Route("/footprint/{id}/parts", name="part_list_footprint") - * - * @return JsonResponse|Response - */ - public function showFootprint(Footprint $footprint, Request $request) + #[Route(path: '/footprint/{id}/parts', name: 'part_list_footprint')] + public function showFootprint(Footprint $footprint, Request $request): Response { $this->denyAccessUnlessGranted('@footprints.read'); return $this->showListWithFilter($request, 'parts/lists/footprint_list.html.twig', function (PartFilter $filter) use ($footprint) { - $filter->getFootprint()->setOperator('INCLUDING_CHILDREN')->setValue($footprint); + $filter->footprint->setOperator('INCLUDING_CHILDREN')->setValue($footprint); }, function (FormInterface $filterForm) { $this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value')); }, [ @@ -197,19 +222,15 @@ class PartListsController extends AbstractController ); } - /** - * @Route("/manufacturer/{id}/parts", name="part_list_manufacturer") - * - * @return JsonResponse|Response - */ - public function showManufacturer(Manufacturer $manufacturer, Request $request) + #[Route(path: '/manufacturer/{id}/parts', name: 'part_list_manufacturer')] + public function showManufacturer(Manufacturer $manufacturer, Request $request): Response { $this->denyAccessUnlessGranted('@manufacturers.read'); return $this->showListWithFilter($request, 'parts/lists/manufacturer_list.html.twig', function (PartFilter $filter) use ($manufacturer) { - $filter->getManufacturer()->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer); + $filter->manufacturer->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer); }, function (FormInterface $filterForm) { $this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value')); }, [ @@ -219,41 +240,33 @@ class PartListsController extends AbstractController ); } - /** - * @Route("/store_location/{id}/parts", name="part_list_store_location") - * - * @return JsonResponse|Response - */ - public function showStorelocation(Storelocation $storelocation, Request $request) + #[Route(path: '/store_location/{id}/parts', name: 'part_list_store_location')] + public function showStorelocation(StorageLocation $storelocation, Request $request): Response { $this->denyAccessUnlessGranted('@storelocations.read'); return $this->showListWithFilter($request, 'parts/lists/store_location_list.html.twig', function (PartFilter $filter) use ($storelocation) { - $filter->getStorelocation()->setOperator('INCLUDING_CHILDREN')->setValue($storelocation); + $filter->storelocation->setOperator('INCLUDING_CHILDREN')->setValue($storelocation); }, function (FormInterface $filterForm) { $this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value')); }, [ 'entity' => $storelocation, - 'repo' => $this->entityManager->getRepository(Storelocation::class), + 'repo' => $this->entityManager->getRepository(StorageLocation::class), ] ); } - /** - * @Route("/supplier/{id}/parts", name="part_list_supplier") - * - * @return JsonResponse|Response - */ - public function showSupplier(Supplier $supplier, Request $request) + #[Route(path: '/supplier/{id}/parts', name: 'part_list_supplier')] + public function showSupplier(Supplier $supplier, Request $request): Response { $this->denyAccessUnlessGranted('@suppliers.read'); return $this->showListWithFilter($request, 'parts/lists/supplier_list.html.twig', function (PartFilter $filter) use ($supplier) { - $filter->getSupplier()->setOperator('INCLUDING_CHILDREN')->setValue($supplier); + $filter->supplier->setOperator('INCLUDING_CHILDREN')->setValue($supplier); }, function (FormInterface $filterForm) { $this->disableFormFieldAfterCreation($filterForm->get('supplier')->get('value')); }, [ @@ -263,19 +276,15 @@ class PartListsController extends AbstractController ); } - /** - * @Route("/parts/by_tag/{tag}", name="part_list_tags", requirements={"tag": ".*"}) - * - * @return JsonResponse|Response - */ - public function showTag(string $tag, Request $request, DataTableFactory $dataTable) + #[Route(path: '/parts/by_tag/{tag}', name: 'part_list_tags', requirements: ['tag' => '.*'])] + public function showTag(string $tag, Request $request): Response { $tag = trim($tag); return $this->showListWithFilter($request, 'parts/lists/tags_list.html.twig', function (PartFilter $filter) use ($tag) { - $filter->getTags()->setOperator('ANY')->setValue($tag); + $filter->tags->setOperator('ANY')->setValue($tag); }, function (FormInterface $filterForm) { $this->disableFormFieldAfterCreation($filterForm->get('tags')->get('value')); }, [ @@ -288,29 +297,28 @@ class PartListsController extends AbstractController { $filter = new PartSearchFilter($request->query->get('keyword', '')); - $filter->setName($request->query->getBoolean('name', true)); - $filter->setCategory($request->query->getBoolean('category', true)); - $filter->setDescription($request->query->getBoolean('description', true)); - $filter->setTags($request->query->getBoolean('tags', true)); - $filter->setStorelocation($request->query->getBoolean('storelocation', true)); - $filter->setComment($request->query->getBoolean('comment', true)); - $filter->setIPN($request->query->getBoolean('ipn', true)); - $filter->setOrdernr($request->query->getBoolean('ordernr', true)); - $filter->setSupplier($request->query->getBoolean('supplier', false)); - $filter->setManufacturer($request->query->getBoolean('manufacturer', false)); - $filter->setFootprint($request->query->getBoolean('footprint', false)); + //As an unchecked checkbox is not set in the query, the default value for all bools have to be false (which is the default argument value)! + $filter->setName($request->query->getBoolean('name')); + $filter->setCategory($request->query->getBoolean('category')); + $filter->setDescription($request->query->getBoolean('description')); + $filter->setMpn($request->query->getBoolean('mpn')); + $filter->setTags($request->query->getBoolean('tags')); + $filter->setStorelocation($request->query->getBoolean('storelocation')); + $filter->setComment($request->query->getBoolean('comment')); + $filter->setIPN($request->query->getBoolean('ipn')); + $filter->setOrdernr($request->query->getBoolean('ordernr')); + $filter->setSupplier($request->query->getBoolean('supplier')); + $filter->setManufacturer($request->query->getBoolean('manufacturer')); + $filter->setFootprint($request->query->getBoolean('footprint')); - $filter->setRegex($request->query->getBoolean('regex', false)); + + $filter->setRegex($request->query->getBoolean('regex')); return $filter; } - /** - * @Route("/parts/search", name="parts_search") - * - * @return JsonResponse|Response - */ - public function showSearch(Request $request, DataTableFactory $dataTable) + #[Route(path: '/parts/search', name: 'parts_search')] + public function showSearch(Request $request, DataTableFactory $dataTable): Response { $searchFilter = $this->searchRequestToFilter($request); @@ -328,12 +336,8 @@ class PartListsController extends AbstractController ); } - /** - * @Route("/parts", name="parts_show_all") - * - * @return JsonResponse|Response - */ - public function showAll(Request $request, DataTableFactory $dataTable) + #[Route(path: '/parts', name: 'parts_show_all')] + public function showAll(Request $request): Response { return $this->showListWithFilter($request,'parts/lists/all_list.html.twig'); } diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 81833f82..761e498c 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -1,4 +1,7 @@ . */ - namespace App\Controller; use App\DataTables\ProjectBomEntriesDataTable; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Form\ProjectSystem\ProjectBOMEntryCollectionType; +use App\Form\ProjectSystem\ProjectAddPartsType; use App\Form\ProjectSystem\ProjectBuildType; -use App\Form\Type\StructuralEntityType; use App\Helpers\Projects\ProjectBuildRequest; use App\Services\ImportExportSystem\BOMImporter; use App\Services\ProjectSystem\ProjectBuildHelper; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; -use League\Csv\Exception; use League\Csv\SyntaxError; use Omines\DataTablesBundle\DataTableFactory; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -43,28 +42,20 @@ use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Validator\Validator\ValidatorInterface; use function Symfony\Component\Translation\t; -/** - * @Route("/project") - */ +#[Route(path: '/project')] class ProjectController extends AbstractController { - private DataTableFactory $dataTableFactory; - - public function __construct(DataTableFactory $dataTableFactory) + public function __construct(private readonly DataTableFactory $dataTableFactory) { - $this->dataTableFactory = $dataTableFactory; } - /** - * @Route("/{id}/info", name="project_info", requirements={"id"="\d+"}) - */ - public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper) + #[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])] + public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper): Response { $this->denyAccessUnlessGranted('read', $project); @@ -82,9 +73,7 @@ class ProjectController extends AbstractController ]); } - /** - * @Route("/{id}/build", name="project_build", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/build', name: 'project_build', requirements: ['id' => '\d+'])] public function build(Project $project, Request $request, ProjectBuildHelper $buildHelper, EntityManagerInterface $entityManager): Response { $this->denyAccessUnlessGranted('read', $project); @@ -114,12 +103,12 @@ class ProjectController extends AbstractController $request->get('_redirect', $this->generateUrl('project_info', ['id' => $project->getID()] ))); - } else { - $this->addFlash('error', 'project.build.flash.invalid_input'); } + + $this->addFlash('error', 'project.build.flash.invalid_input'); } - return $this->renderForm('projects/build/build.html.twig', [ + return $this->render('projects/build/build.html.twig', [ 'buildHelper' => $buildHelper, 'project' => $project, 'build_request' => $projectBuildRequest, @@ -128,9 +117,7 @@ class ProjectController extends AbstractController ]); } - /** - * @Route("/{id}/import_bom", name="project_import_bom", requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}/import_bom', name: 'project_import_bom', requirements: ['id' => '\d+'])] public function importBOM(Request $request, EntityManagerInterface $entityManager, Project $project, BOMImporter $BOMImporter, ValidatorInterface $validator): Response { @@ -187,54 +174,44 @@ class ProjectController extends AbstractController return $this->redirectToRoute('project_edit', ['id' => $project->getID()]); } - if (count ($errors) > 0) { - $this->addFlash('error', t('project.bom_import.flash.invalid_entries')); - } - } catch (\UnexpectedValueException $e) { - $this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); - } catch (SyntaxError $e) { + //When we get here, there were validation errors + $this->addFlash('error', t('project.bom_import.flash.invalid_entries')); + + } catch (\UnexpectedValueException|SyntaxError $e) { $this->addFlash('error', t('project.bom_import.flash.invalid_file', ['%message%' => $e->getMessage()])); } } - return $this->renderForm('projects/import_bom.html.twig', [ + return $this->render('projects/import_bom.html.twig', [ 'project' => $project, 'form' => $form, 'errors' => $errors ?? null, ]); } - /** - * @Route("/add_parts", name="project_add_parts_no_id") - * @Route("/{id}/add_parts", name="project_add_parts", requirements={"id"="\d+"}) - * @param Request $request - * @param Project|null $project - */ + #[Route(path: '/add_parts', name: 'project_add_parts_no_id')] + #[Route(path: '/{id}/add_parts', name: 'project_add_parts', requirements: ['id' => '\d+'])] public function addPart(Request $request, EntityManagerInterface $entityManager, ?Project $project): Response { - if($project) { + if($project instanceof Project) { $this->denyAccessUnlessGranted('edit', $project); } else { $this->denyAccessUnlessGranted('@projects.edit'); } - $builder = $this->createFormBuilder(); - $builder->add('project', StructuralEntityType::class, [ - 'class' => Project::class, - 'required' => true, - 'disabled' => $project !== null, //If a project is given, disable the field - 'data' => $project, - 'constraints' => [ - new NotNull() - ] + $form = $this->createForm(ProjectAddPartsType::class, null, [ + 'project' => $project, ]); - $builder->add('bom_entries', ProjectBOMEntryCollectionType::class); - $builder->add('submit', SubmitType::class, ['label' => 'save']); - $form = $builder->getForm(); + //Preset the BOM entries with the selected parts, when the form was not submitted yet $preset_data = new ArrayCollection(); - foreach (explode(',', $request->get('parts', '')) as $part_id) { + foreach (explode(',', (string) $request->get('parts', '')) as $part_id) { + //Skip empty part IDs. Postgres seems to be especially sensitive to empty strings, as it does not allow them in integer columns + if ($part_id === '') { + continue; + } + $part = $entityManager->getRepository(Part::class)->find($part_id); if (null !== $part) { //If there is already a BOM entry for this part, we use this one (we edit it then) @@ -242,7 +219,7 @@ class ProjectController extends AbstractController 'project' => $project, 'part' => $part ]); - if ($bom_entry) { + if ($bom_entry !== null) { $preset_data->add($bom_entry); } else { //Otherwise create an empty one $entry = new ProjectBOMEntry(); @@ -266,8 +243,11 @@ class ProjectController extends AbstractController foreach ($bom_entries as $bom_entry){ $target_project->addBOMEntry($bom_entry); } + + $entityManager->flush(); + //If a redirect query parameter is set, redirect to this page if ($request->query->get('_redirect')) { return $this->redirect($request->query->get('_redirect')); @@ -276,9 +256,9 @@ class ProjectController extends AbstractController return $this->redirectToRoute('project_info', ['id' => $target_project->getID()]); } - return $this->renderForm('projects/add_parts.html.twig', [ + return $this->render('projects/add_parts.html.twig', [ 'project' => $project, 'form' => $form, ]); } -} \ No newline at end of file +} diff --git a/src/Controller/RedirectController.php b/src/Controller/RedirectController.php index 632399c1..65bd78f5 100644 --- a/src/Controller/RedirectController.php +++ b/src/Controller/RedirectController.php @@ -28,22 +28,15 @@ use function in_array; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @see \App\Tests\Controller\RedirectControllerTest + */ class RedirectController extends AbstractController { - protected string $default_locale; - protected TranslatorInterface $translator; - protected SessionInterface $session; - protected bool $enforce_index_php; - - public function __construct(string $default_locale, TranslatorInterface $translator, SessionInterface $session, bool $enforce_index_php) + public function __construct(protected string $default_locale, protected TranslatorInterface $translator, protected bool $enforce_index_php) { - $this->default_locale = $default_locale; - $this->session = $session; - $this->translator = $translator; - $this->enforce_index_php = $enforce_index_php; } /** @@ -52,25 +45,27 @@ class RedirectController extends AbstractController */ public function addLocalePart(Request $request): RedirectResponse { - //By default we use the global default locale + //By default, we use the global default locale $locale = $this->default_locale; //Check if a user has set a preferred language setting: $user = $this->getUser(); - if (($user instanceof User) && !empty($user->getLanguage())) { + if (($user instanceof User) && ($user->getLanguage() !== null && $user->getLanguage() !== '')) { $locale = $user->getLanguage(); } - //$new_url = str_replace($request->getPathInfo(), '/' . $locale . $request->getPathInfo(), $request->getUri()); $new_url = $request->getUriForPath('/'.$locale.$request->getPathInfo()); //If either mod_rewrite is not enabled or the index.php version is enforced, add index.php to the string if (($this->enforce_index_php || !$this->checkIfModRewriteAvailable()) - && false === strpos($new_url, 'index.php')) { + && !str_contains($new_url, 'index.php')) { //Like Request::getUriForPath only with index.php $new_url = $request->getSchemeAndHttpHost().$request->getBaseUrl().'/index.php/'.$locale.$request->getPathInfo(); } + //Add the query string + $new_url .= $request->getQueryString() ? '?'.$request->getQueryString() : ''; + return $this->redirect($new_url); } @@ -78,6 +73,7 @@ class RedirectController extends AbstractController * Check if mod_rewrite is available (URL rewriting is possible). * If this is true, we can redirect to /en, otherwise we have to redirect to index.php/en. * When the PHP is not used via Apache SAPI, we just assume that URL rewriting is available. + * @noinspection PhpUndefinedFunctionInspection */ public function checkIfModRewriteAvailable(): bool { @@ -88,6 +84,6 @@ class RedirectController extends AbstractController } //Check if the mod_rewrite module is loaded - return in_array('mod_rewrite', apache_get_modules(), false); + return in_array('mod_rewrite', apache_get_modules(), true); } } diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index 8c0c9ad8..aebadd89 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -42,57 +42,65 @@ declare(strict_types=1); namespace App\Controller; use App\Form\LabelSystem\ScanDialogType; -use App\Services\LabelSystem\Barcodes\BarcodeNormalizer; -use App\Services\LabelSystem\Barcodes\BarcodeRedirector; +use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector; +use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper; +use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType; +use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult; use Doctrine\ORM\EntityNotFoundException; use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\Routing\Attribute\Route; /** - * @Route("/scan") + * @see \App\Tests\Controller\ScanControllerTest */ +#[Route(path: '/scan')] class ScanController extends AbstractController { - protected BarcodeRedirector $barcodeParser; - protected BarcodeNormalizer $barcodeNormalizer; - - public function __construct(BarcodeRedirector $barcodeParser, BarcodeNormalizer $barcodeNormalizer) + public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeScanHelper $barcodeNormalizer) { - $this->barcodeParser = $barcodeParser; - $this->barcodeNormalizer = $barcodeNormalizer; } - /** - * @Route("", name="scan_dialog") - */ - public function dialog(Request $request): Response + #[Route(path: '', name: 'scan_dialog')] + public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response { $this->denyAccessUnlessGranted('@tools.label_scanner'); $form = $this->createForm(ScanDialogType::class); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { + if ($input === null && $form->isSubmitted() && $form->isValid()) { $input = $form['input']->getData(); + $mode = $form['mode']->getData(); + } + $infoModeData = null; + + if ($input !== null) { try { - [$type, $id] = $this->barcodeNormalizer->normalizeBarcodeContent($input); + $scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null); + //Perform a redirect if the info mode is not enabled + if (!$form['info_mode']->getData()) { + try { + return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); + } catch (EntityNotFoundException) { + $this->addFlash('success', 'scan.qr_not_found'); + } + } else { //Otherwise retrieve infoModeData + $infoModeData = $scan_result->getDecodedForInfoMode(); - try { - return $this->redirect($this->barcodeParser->getRedirectURL($type, $id)); - } catch (EntityNotFoundException $exception) { - $this->addFlash('success', 'scan.qr_not_found'); } - } catch (InvalidArgumentException $exception) { + } catch (InvalidArgumentException) { $this->addFlash('error', 'scan.format_unknown'); } } - return $this->renderForm('label_system/scanner/scanner.html.twig', [ + return $this->render('label_system/scanner/scanner.html.twig', [ 'form' => $form, + 'infoModeData' => $infoModeData, ]); } @@ -101,11 +109,24 @@ class ScanController extends AbstractController */ public function scanQRCode(string $type, int $id): Response { + $type = strtolower($type); + try { $this->addFlash('success', 'scan.qr_success'); - return $this->redirect($this->barcodeParser->getRedirectURL($type, $id)); - } catch (EntityNotFoundException $exception) { + if (!isset(BarcodeScanHelper::QR_TYPE_MAP[$type])) { + throw new InvalidArgumentException('Unknown type: '.$type); + } + //Construct the scan result manually, as we don't have a barcode here + $scan_result = new LocalBarcodeScanResult( + target_type: BarcodeScanHelper::QR_TYPE_MAP[$type], + target_id: $id, + //The routes are only used on the internal generated QR codes + source_type: BarcodeSourceType::INTERNAL + ); + + return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); + } catch (EntityNotFoundException) { $this->addFlash('success', 'scan.qr_not_found'); return $this->redirectToRoute('homepage'); diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 026e18c1..4e2077c4 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -39,7 +39,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; @@ -48,18 +48,11 @@ use Symfony\Contracts\Translation\TranslatorInterface; class SecurityController extends AbstractController { - protected TranslatorInterface $translator; - protected bool $allow_email_pw_reset; - - public function __construct(TranslatorInterface $translator, bool $allow_email_pw_reset) + public function __construct(protected TranslatorInterface $translator, protected bool $allow_email_pw_reset) { - $this->translator = $translator; - $this->allow_email_pw_reset = $allow_email_pw_reset; } - /** - * @Route("/login", name="login", methods={"GET", "POST"}) - */ + #[Route(path: '/login', name: 'login', methods: ['GET', 'POST'])] public function login(AuthenticationUtils $authenticationUtils): Response { // get the login error if there is one @@ -75,11 +68,10 @@ class SecurityController extends AbstractController } /** - * @Route("/pw_reset/request", name="pw_reset_request") - * * @return RedirectResponse|Response */ - public function requestPwReset(PasswordResetManager $passwordReset, Request $request) + #[Route(path: '/pw_reset/request', name: 'pw_reset_request')] + public function requestPwReset(PasswordResetManager $passwordReset, Request $request): RedirectResponse|Response { if (!$this->allow_email_pw_reset) { throw new AccessDeniedHttpException('The password reset via email is disabled!'); @@ -113,17 +105,16 @@ class SecurityController extends AbstractController return $this->redirectToRoute('login'); } - return $this->renderForm('security/pw_reset_request.html.twig', [ + return $this->render('security/pw_reset_request.html.twig', [ 'form' => $form, ]); } /** - * @Route("/pw_reset/new_pw/{user}/{token}", name="pw_reset_new_pw") - * * @return RedirectResponse|Response */ - public function pwResetNewPw(PasswordResetManager $passwordReset, Request $request, EntityManagerInterface $em, EventDispatcherInterface $eventDispatcher, ?string $user = null, ?string $token = null) + #[Route(path: '/pw_reset/new_pw/{user}/{token}', name: 'pw_reset_new_pw')] + public function pwResetNewPw(PasswordResetManager $passwordReset, Request $request, EntityManagerInterface $em, EventDispatcherInterface $eventDispatcher, ?string $user = null, ?string $token = null): RedirectResponse|Response { if (!$this->allow_email_pw_reset) { throw new AccessDeniedHttpException('The password reset via email is disabled!'); @@ -150,6 +141,7 @@ class SecurityController extends AbstractController 'type' => PasswordType::class, 'first_options' => [ 'label' => 'user.settings.pw_new.label', + 'password_estimator' => true, ], 'second_options' => [ 'label' => 'user.settings.pw_confirm.label', @@ -178,7 +170,7 @@ class SecurityController extends AbstractController $this->addFlash('success', 'pw_reset.new_pw.success'); $repo = $em->getRepository(User::class); - $u = $repo->findOneBy(['name' => $data['username']]); + $u = $repo->findByUsername($data['username']); $event = new SecurityEvent($u); /** @var EventDispatcher $eventDispatcher */ $eventDispatcher->dispatch($event, SecurityEvents::PASSWORD_RESET); @@ -187,15 +179,13 @@ class SecurityController extends AbstractController } } - return $this->renderForm('security/pw_reset_new_pw.html.twig', [ + return $this->render('security/pw_reset_new_pw.html.twig', [ 'form' => $form, ]); } - /** - * @Route("/logout", name="logout") - */ - public function logout(): void + #[Route(path: '/logout', name: 'logout')] + public function logout(): never { throw new RuntimeException('Will be intercepted before getting here'); } diff --git a/src/Controller/SelectAPIController.php b/src/Controller/SelectAPIController.php index 1b7784e4..c1e682c8 100644 --- a/src/Controller/SelectAPIController.php +++ b/src/Controller/SelectAPIController.php @@ -1,4 +1,7 @@ . */ - namespace App\Controller; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\Contracts\NamedElementInterface; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\StorageLocation; use App\Entity\ProjectSystem\Project; use App\Form\Type\Helper\StructuralEntityChoiceHelper; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Contracts\Translation\TranslatorInterface; /** - * @Route("/select_api") - * * This endpoint is used by the select2 library to dynamically load data (used in the multiselect action helper in parts lists) */ +#[Route(path: '/select_api')] class SelectAPIController extends AbstractController { - private NodesListBuilder $nodesListBuilder; - private TranslatorInterface $translator; - private StructuralEntityChoiceHelper $choiceHelper; - - public function __construct(NodesListBuilder $nodesListBuilder, TranslatorInterface $translator, StructuralEntityChoiceHelper $choiceHelper) + public function __construct(private readonly NodesListBuilder $nodesListBuilder, private readonly TranslatorInterface $translator, private readonly StructuralEntityChoiceHelper $choiceHelper) { - $this->nodesListBuilder = $nodesListBuilder; - $this->translator = $translator; - $this->choiceHelper = $choiceHelper; } - /** - * @Route("/category", name="select_category") - */ + #[Route(path: '/category', name: 'select_category')] public function category(): Response { return $this->getResponseForClass(Category::class); } - /** - * @Route("/footprint", name="select_footprint") - */ + #[Route(path: '/footprint', name: 'select_footprint')] public function footprint(): Response { return $this->getResponseForClass(Footprint::class, true); } - /** - * @Route("/manufacturer", name="select_manufacturer") - */ + #[Route(path: '/manufacturer', name: 'select_manufacturer')] public function manufacturer(): Response { return $this->getResponseForClass(Manufacturer::class, true); } - /** - * @Route("/measurement_unit", name="select_measurement_unit") - */ + #[Route(path: '/measurement_unit', name: 'select_measurement_unit')] public function measurement_unit(): Response { return $this->getResponseForClass(MeasurementUnit::class, true); } - /** - * @Route("/project", name="select_project") - */ + #[Route(path: '/project', name: 'select_project')] public function projects(): Response { return $this->getResponseForClass(Project::class, false); } - /** - * @Route("/export_level", name="select_export_level") - */ + #[Route(path: '/storage_location', name: 'select_storage_location')] + public function locations(): Response + { + return $this->getResponseForClass(StorageLocation::class, true); + } + + #[Route(path: '/export_level', name: 'select_export_level')] public function exportLevel(): Response { $entries = [ @@ -106,18 +94,13 @@ class SelectAPIController extends AbstractController 3 => $this->translator->trans('export.level.full'), ]; - return $this->json(array_map(function ($key, $value) { - return [ - 'text' => $value, - 'value' => $key, - ]; - }, array_keys($entries), $entries)); + return $this->json(array_map(static fn($key, $value) => [ + 'text' => $value, + 'value' => $key, + ], array_keys($entries), $entries)); } - /** - * @Route("/label_profiles", name="select_label_profiles") - * @return Response - */ + #[Route(path: '/label_profiles', name: 'select_label_profiles')] public function labelProfiles(EntityManagerInterface $entityManager): Response { $this->denyAccessUnlessGranted('@labels.create_labels'); @@ -135,10 +118,7 @@ class SelectAPIController extends AbstractController return $this->json($nodes); } - /** - * @Route("/label_profiles_lot", name="select_label_profiles_lot") - * @return Response - */ + #[Route(path: '/label_profiles_lot', name: 'select_label_profiles_lot')] public function labelProfilesLot(EntityManagerInterface $entityManager): Response { $this->denyAccessUnlessGranted('@labels.create_labels'); @@ -198,8 +178,8 @@ class SelectAPIController extends AbstractController ]); //Remove the data-* prefix for each key $data = array_combine( - array_map(function ($key) { - if (strpos($key, 'data-') === 0) { + array_map(static function ($key) { + if (str_starts_with($key, 'data-')) { return substr($key, 5); } return $key; diff --git a/src/Controller/StatisticsController.php b/src/Controller/StatisticsController.php index d9c467f4..67c29781 100644 --- a/src/Controller/StatisticsController.php +++ b/src/Controller/StatisticsController.php @@ -44,13 +44,11 @@ namespace App\Controller; use App\Services\Tools\StatisticsHelper; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; class StatisticsController extends AbstractController { - /** - * @Route("/statistics", name="statistics_view") - */ + #[Route(path: '/statistics', name: 'statistics_view')] public function showStatistics(StatisticsHelper $helper): Response { $this->denyAccessUnlessGranted('@tools.statistics'); diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index 3ef68b8f..dbcb91a1 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -1,4 +1,7 @@ . */ - namespace App\Controller; -use App\Services\Attachments\AttachmentPathResolver; use App\Services\Attachments\AttachmentSubmitHandler; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; +use App\Services\Doctrine\DBInfoHelper; +use App\Services\Doctrine\NatsortDebugHelper; use App\Services\Misc\GitVersionInfo; -use App\Services\Misc\DBInfoHelper; -use Doctrine\ORM\EntityManagerInterface; +use App\Services\System\UpdateAvailableManager; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Runtime\SymfonyRuntime; -/** - * @Route("/tools") - */ +#[Route(path: '/tools')] class ToolsController extends AbstractController { - /** - * @Route("/reel_calc", name="tools_reel_calculator") - */ + #[Route(path: '/reel_calc', name: 'tools_reel_calculator')] public function reelCalculator(): Response { $this->denyAccessUnlessGranted('@tools.reel_calculator'); @@ -47,11 +45,9 @@ class ToolsController extends AbstractController return $this->render('tools/reel_calculator/reel_calculator.html.twig'); } - /** - * @Route("/server_infos", name="tools_server_infos") - */ - public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, - AttachmentSubmitHandler $attachmentSubmitHandler): Response + #[Route(path: '/server_infos', name: 'tools_server_infos')] + public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper, + AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager): Response { $this->denyAccessUnlessGranted('@system.server_infos'); @@ -65,10 +61,10 @@ class ToolsController extends AbstractController 'default_theme' => $this->getParameter('partdb.global_theme'), 'enabled_locales' => $this->getParameter('partdb.locale_menu'), 'demo_mode' => $this->getParameter('partdb.demo_mode'), - 'gpdr_compliance' => $this->getParameter('partdb.gpdr_compliance'), + 'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'), 'use_gravatar' => $this->getParameter('partdb.users.use_gravatar'), 'email_password_reset' => $this->getParameter('partdb.users.email_pw_reset'), - 'enviroment' => $this->getParameter('kernel.environment'), + 'environment' => $this->getParameter('kernel.environment'), 'is_debug' => $this->getParameter('kernel.debug'), 'email_sender' => $this->getParameter('partdb.mail.sender_email'), 'email_sender_name' => $this->getParameter('partdb.mail.sender_name'), @@ -83,10 +79,14 @@ class ToolsController extends AbstractController 'php_version' => PHP_VERSION, 'php_uname' => php_uname('a'), 'php_sapi' => PHP_SAPI, - 'php_extensions' => array_merge(get_loaded_extensions()), + 'php_bit_size' => PHP_INT_SIZE * 8, + 'php_extensions' => [...get_loaded_extensions()], 'php_opcache_enabled' => ini_get('opcache.enable'), 'php_upload_max_filesize' => ini_get('upload_max_filesize'), 'php_post_max_size' => ini_get('post_max_size'), + 'kernel_runtime_environment' => $this->getParameter('kernel.runtime_environment'), + 'kernel_runtime_mode' => $this->getParameter('kernel.runtime_mode'), + 'kernel_runtime' => $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? SymfonyRuntime::class, //DB section 'db_type' => $DBInfoHelper->getDatabaseType() ?? 'Unknown', @@ -94,36 +94,33 @@ class ToolsController extends AbstractController 'db_size' => $DBInfoHelper->getDatabaseSize(), 'db_name' => $DBInfoHelper->getDatabaseName() ?? 'Unknown', 'db_user' => $DBInfoHelper->getDatabaseUsername() ?? 'Unknown', + 'db_natsort_method' => $natsortDebugHelper->getNaturalSortMethod(), + 'db_natsort_slow_allowed' => $natsortDebugHelper->isSlowNaturalSortAllowed(), + + //New version section + 'new_version_available' => $updateAvailableManager->isUpdateAvailable(), + 'new_version' => $updateAvailableManager->getLatestVersionString(), + 'new_version_url' => $updateAvailableManager->getLatestVersionUrl(), ]); } - /** - * @Route("/builtin_footprints", name="tools_builtin_footprints_viewer") - * @return Response - */ + #[Route(path: '/builtin_footprints', name: 'tools_builtin_footprints_viewer')] public function builtInFootprintsViewer(BuiltinAttachmentsFinder $builtinAttachmentsFinder, AttachmentURLGenerator $urlGenerator): Response { $this->denyAccessUnlessGranted('@tools.builtin_footprints_viewer'); $grouped_footprints = $builtinAttachmentsFinder->getListOfFootprintsGroupedByFolder(); - $grouped_footprints = array_map(function($group) use ($urlGenerator) { - return array_map(function($placeholder_filepath) use ($urlGenerator) { - return [ - 'filename' => basename($placeholder_filepath), - 'assets_path' => $urlGenerator->placeholderPathToAssetPath($placeholder_filepath), - ]; - }, $group); - }, $grouped_footprints); + $grouped_footprints = array_map(static fn($group) => array_map(static fn($placeholder_filepath) => [ + 'filename' => basename((string) $placeholder_filepath), + 'assets_path' => $urlGenerator->placeholderPathToAssetPath($placeholder_filepath), + ], $group), $grouped_footprints); return $this->render('tools/builtin_footprints_viewer/builtin_footprints_viewer.html.twig', [ 'grouped_footprints' => $grouped_footprints, ]); } - /** - * @Route("/ic_logos", name="tools_ic_logos") - * @return Response - */ + #[Route(path: '/ic_logos', name: 'tools_ic_logos')] public function icLogos(): Response { $this->denyAccessUnlessGranted('@tools.ic_logos'); diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index 6ab3b420..71f8ba5c 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -22,35 +22,30 @@ declare(strict_types=1); namespace App\Controller; +use Symfony\Component\HttpFoundation\Response; use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Services\Trees\ToolsTreeBuilder; use App\Services\Trees\TreeViewGenerator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; /** * This controller has the purpose to provide the data for all treeviews. - * - * @Route("/tree") */ +#[Route(path: '/tree')] class TreeController extends AbstractController { - protected TreeViewGenerator $treeGenerator; - - public function __construct(TreeViewGenerator $treeGenerator) + public function __construct(protected TreeViewGenerator $treeGenerator) { - $this->treeGenerator = $treeGenerator; } - /** - * @Route("/tools", name="tree_tools") - */ + #[Route(path: '/tools', name: 'tree_tools')] public function tools(ToolsTreeBuilder $builder): JsonResponse { $tree = $builder->getTree(); @@ -58,90 +53,78 @@ class TreeController extends AbstractController return new JsonResponse($tree); } - /** - * @Route("/category/{id}", name="tree_category") - * @Route("/categories", name="tree_category_root") - */ + #[Route(path: '/category/{id}', name: 'tree_category')] + #[Route(path: '/categories', name: 'tree_category_root')] public function categoryTree(?Category $category = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@categories.read')) { $tree = $this->treeGenerator->getTreeView(Category::class, $category, 'list_parts_root'); } else { - return new JsonResponse("Access denied", 403); + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); } return new JsonResponse($tree); } - /** - * @Route("/footprint/{id}", name="tree_footprint") - * @Route("/footprints", name="tree_footprint_root") - */ + #[Route(path: '/footprint/{id}', name: 'tree_footprint')] + #[Route(path: '/footprints', name: 'tree_footprint_root')] public function footprintTree(?Footprint $footprint = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@footprints.read')) { $tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint, 'list_parts_root'); } else { - return new JsonResponse("Access denied", 403); + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); } return new JsonResponse($tree); } - /** - * @Route("/location/{id}", name="tree_location") - * @Route("/locations", name="tree_location_root") - */ - public function locationTree(?Storelocation $location = null): JsonResponse + #[Route(path: '/location/{id}', name: 'tree_location')] + #[Route(path: '/locations', name: 'tree_location_root')] + public function locationTree(?StorageLocation $location = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@storelocations.read')) { - $tree = $this->treeGenerator->getTreeView(Storelocation::class, $location, 'list_parts_root'); + $tree = $this->treeGenerator->getTreeView(StorageLocation::class, $location, 'list_parts_root'); } else { - return new JsonResponse("Access denied", 403); + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); } return new JsonResponse($tree); } - /** - * @Route("/manufacturer/{id}", name="tree_manufacturer") - * @Route("/manufacturers", name="tree_manufacturer_root") - */ + #[Route(path: '/manufacturer/{id}', name: 'tree_manufacturer')] + #[Route(path: '/manufacturers', name: 'tree_manufacturer_root')] public function manufacturerTree(?Manufacturer $manufacturer = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@manufacturers.read')) { $tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer, 'list_parts_root'); } else { - return new JsonResponse("Access denied", 403); + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); } return new JsonResponse($tree); } - /** - * @Route("/supplier/{id}", name="tree_supplier") - * @Route("/suppliers", name="tree_supplier_root") - */ + #[Route(path: '/supplier/{id}', name: 'tree_supplier')] + #[Route(path: '/suppliers', name: 'tree_supplier_root')] public function supplierTree(?Supplier $supplier = null): JsonResponse { if ($this->isGranted('@parts.read') && $this->isGranted('@suppliers.read')) { $tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier, 'list_parts_root'); } else { - return new JsonResponse("Access denied", 403); + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); } return new JsonResponse($tree); } - /** - * @Route("/device/{id}", name="tree_device") - * @Route("/devices", name="tree_device_root") - */ + #[Route(path: '/device/{id}', name: 'tree_device')] + #[Route(path: '/devices', name: 'tree_device_root')] public function deviceTree(?Project $device = null): JsonResponse { if ($this->isGranted('@projects.read')) { $tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices'); } else { - return new JsonResponse("Access denied", 403); + return new JsonResponse("Access denied", Response::HTTP_FORBIDDEN); } return new JsonResponse($tree); diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 37ff6ec1..89eac7ff 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,6 +22,11 @@ declare(strict_types=1); namespace App\Controller; +use App\Entity\Parameters\AbstractParameter; +use Symfony\Component\HttpFoundation\Response; +use App\Entity\Attachments\Attachment; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; use App\Entity\Parameters\ProjectParameter; @@ -30,7 +35,7 @@ use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; use App\Entity\Parameters\MeasurementUnitParameter; use App\Entity\Parameters\PartParameter; -use App\Entity\Parameters\StorelocationParameter; +use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Currency; @@ -44,30 +49,22 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; /** * In this controller the endpoints for the typeaheads are collected. - * - * @Route("/typeahead") */ +#[Route(path: '/typeahead')] class TypeaheadController extends AbstractController { - protected AttachmentURLGenerator $urlGenerator; - protected Packages $assets; - - public function __construct(AttachmentURLGenerator $URLGenerator, Packages $assets) + public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets) { - $this->urlGenerator = $URLGenerator; - $this->assets = $assets; } - /** - * @Route("/builtInResources/search", name="typeahead_builtInRessources") - */ + #[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')] public function builtInResources(Request $request, BuiltinAttachmentsFinder $finder): JsonResponse { $query = $request->get('query'); @@ -91,51 +88,32 @@ class TypeaheadController extends AbstractController $serializer = new Serializer($normalizers, $encoders); $data = $serializer->serialize($result, 'json'); - return new JsonResponse($data, 200, [], true); + return new JsonResponse($data, Response::HTTP_OK, [], true); } /** - * This functions map the parameter type to the class, so we can access its repository - * @param string $type - * @return class-string + * This function map the parameter type to the class, so we can access its repository + * @return class-string */ private function typeToParameterClass(string $type): string { - switch ($type) { - case 'category': - return CategoryParameter::class; - case 'part': - return PartParameter::class; - case 'device': - return ProjectParameter::class; - case 'footprint': - return FootprintParameter::class; - case 'manufacturer': - return ManufacturerParameter::class; - case 'storelocation': - return StorelocationParameter::class; - case 'supplier': - return SupplierParameter::class; - case 'attachment_type': - return AttachmentTypeParameter::class; - case 'group': - return GroupParameter::class; - case 'measurement_unit': - return MeasurementUnitParameter::class; - case 'currency': - return Currency::class; - - default: - throw new \InvalidArgumentException('Invalid parameter type: '.$type); - } + return match ($type) { + 'category' => CategoryParameter::class, + 'part' => PartParameter::class, + 'device' => ProjectParameter::class, + 'footprint' => FootprintParameter::class, + 'manufacturer' => ManufacturerParameter::class, + 'storelocation' => StorageLocationParameter::class, + 'supplier' => SupplierParameter::class, + 'attachment_type' => AttachmentTypeParameter::class, + 'group' => GroupParameter::class, + 'measurement_unit' => MeasurementUnitParameter::class, + 'currency' => Currency::class, + default => throw new \InvalidArgumentException('Invalid parameter type: '.$type), + }; } - /** - * @Route("/parts/search/{query}", name="typeahead_parts") - * @param string $query - * @param EntityManagerInterface $entityManager - * @return JsonResponse - */ + #[Route(path: '/parts/search/{query}', name: 'typeahead_parts')] public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator, AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse { @@ -149,7 +127,7 @@ class TypeaheadController extends AbstractController foreach ($parts as $part) { //Determine the picture to show: $preview_attachment = $previewGenerator->getTablePreviewAttachment($part); - if($preview_attachment !== null) { + if($preview_attachment instanceof Attachment) { $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); } else { $preview_url = ''; @@ -159,8 +137,8 @@ class TypeaheadController extends AbstractController $data[] = [ 'id' => $part->getID(), 'name' => $part->getName(), - 'category' => $part->getCategory() ? $part->getCategory()->getName() : 'Unknown', - 'footprint' => $part->getFootprint() ? $part->getFootprint()->getName() : '', + 'category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : 'Unknown', + 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), 'image' => $preview_url, ]; @@ -169,11 +147,7 @@ class TypeaheadController extends AbstractController return new JsonResponse($data); } - /** - * @Route("/parameters/{type}/search/{query}", name="typeahead_parameters", requirements={"type" = ".+"}) - * @param string $query - * @return JsonResponse - */ + #[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])] public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse { $class = $this->typeToParameterClass($type); @@ -182,7 +156,7 @@ class TypeaheadController extends AbstractController //Ensure user has the correct permissions $this->denyAccessUnlessGranted('read', $test_obj); - /** @var ParameterRepository $repository */ + /** @var ParameterRepository $repository */ $repository = $entityManager->getRepository($class); $data = $repository->autocompleteParamName($query); @@ -190,9 +164,7 @@ class TypeaheadController extends AbstractController return new JsonResponse($data); } - /** - * @Route("/tags/search/{query}", name="typeahead_tags", requirements={"query"= ".+"}) - */ + #[Route(path: '/tags/search/{query}', name: 'typeahead_tags', requirements: ['query' => '.+'])] public function tags(string $query, TagFinder $finder): JsonResponse { $this->denyAccessUnlessGranted('@parts.read'); @@ -209,6 +181,6 @@ class TypeaheadController extends AbstractController $serializer = new Serializer($normalizers, $encoders); $data = $serializer->serialize($array, 'json'); - return new JsonResponse($data, 200, [], true); + return new JsonResponse($data, Response::HTTP_OK, [], true); } } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 9949b8c7..968bd1e3 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -22,10 +22,10 @@ declare(strict_types=1); namespace App\Controller; +use App\Controller\AdminPages\BaseAdminController; use App\DataTables\LogDataTable; use App\Entity\Attachments\UserAttachment; use App\Entity\Base\AbstractNamedDBElement; -use App\Entity\Parameters\AbstractParameter; use App\Entity\UserSystem\User; use App\Events\SecurityEvent; use App\Events\SecurityEvents; @@ -45,13 +45,11 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Validator\Validator\ValidatorInterface; -/** - * @Route("/user") - * Class UserController - */ -class UserController extends AdminPages\BaseAdminController +#[Route(path: '/user')] +class UserController extends BaseAdminController { protected string $entity_class = User::class; protected string $twig_template = 'admin/user_admin.html.twig'; @@ -62,11 +60,11 @@ class UserController extends AdminPages\BaseAdminController protected function additionalActionEdit(FormInterface $form, AbstractNamedDBElement $entity): bool { - //Check if we editing a user and if we need to change the password of it + //Check if we're editing a user and if we need to change the password of it if ($entity instanceof User && !empty($form['new_password']->getData())) { $password = $this->passwordEncoder->hashPassword($entity, $form['new_password']->getData()); $entity->setPassword($password); - //By default the user must change the password afterwards + //By default, the user must change the password afterward $entity->setNeedPwChange(true); $event = new SecurityEvent($entity); @@ -77,12 +75,13 @@ class UserController extends AdminPages\BaseAdminController } /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="user_edit") - * @Route("/{id}/", requirements={"id"="\d+"}) * * @throws Exception */ - public function edit(User $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper, PermissionSchemaUpdater $permissionSchemaUpdater, ?string $timestamp = null): Response + #[Route(path: '/{id}/edit/{timestamp}', name: 'user_edit', requirements: ['id' => '\d+'])] + #[Route(path: '/{id}/', requirements: ['id' => '\d+'])] + public function edit(User $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper, + PermissionSchemaUpdater $permissionSchemaUpdater, ValidatorInterface $validator, ?string $timestamp = null): Response { //Do an upgrade of the permission schema if needed (so the user can see the permissions a user get on next request (even if it was not done yet) $permissionSchemaUpdater->userUpgradeSchemaRecursively($entity); @@ -91,7 +90,7 @@ class UserController extends AdminPages\BaseAdminController if ($request->request->has('reset_2fa')) { //Check if the admin has the needed permissions $this->denyAccessUnlessGranted('set_password', $entity); - if ($this->isCsrfTokenValid('reset_2fa'.$entity->getId(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('reset_2fa'.$entity->getID(), $request->request->get('_token'))) { //Disable Google authenticator $entity->setGoogleAuthenticatorSecret(null); $entity->setBackupCodes([]); @@ -99,7 +98,7 @@ class UserController extends AdminPages\BaseAdminController foreach ($entity->getLegacyU2FKeys() as $key) { $em->remove($key); } - foreach ($entity->getWebAuthnKeys() as $key) { + foreach ($entity->getWebauthnKeys() as $key) { $em->remove($key); } //Invalidate trusted devices @@ -111,26 +110,36 @@ class UserController extends AdminPages\BaseAdminController $this->addFlash('success', 'user.edit.reset_success'); } else { - $this->addFlash('danger', 'csfr_invalid'); + $this->addFlash('error', 'csfr_invalid'); } } //Handle permissions presets if ($request->request->has('permission_preset')) { $this->denyAccessUnlessGranted('edit_permissions', $entity); - if ($this->isCsrfTokenValid('reset_2fa'.$entity->getId(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('reset_2fa'.$entity->getID(), $request->request->get('_token'))) { $preset = $request->request->get('permission_preset'); $permissionPresetsHelper->applyPreset($entity, $preset); - $em->flush(); + //Ensure that the user is valid after applying the preset + $errors = $validator->validate($entity); + if (count($errors) > 0) { + $this->addFlash('error', 'validator.noLockout'); + //Refresh the entity to remove the changes + $em->refresh($entity); + } else { + $em->flush(); + + $this->addFlash('success', 'user.edit.permission_success'); + + //We need to stop the execution here, or our permissions changes will be overwritten by the form values + return $this->redirectToRoute('user_edit', ['id' => $entity->getID()]); + } - $this->addFlash('success', 'user.edit.permission_success'); - //We need to stop the execution here, or our permissions changes will be overwritten by the form values - return $this->redirectToRoute('user_edit', ['id' => $entity->getID()]); } else { - $this->addFlash('danger', 'csfr_invalid'); + $this->addFlash('error', 'csfr_invalid'); } } @@ -142,59 +151,55 @@ class UserController extends AdminPages\BaseAdminController if ($entity instanceof User && !empty($form['new_password']->getData())) { $password = $this->passwordEncoder->hashPassword($entity, $form['new_password']->getData()); $entity->setPassword($password); - //By default the user must change the password afterwards + //By default, the user must change the password afterward $entity->setNeedPwChange(true); } return true; } - /** - * @Route("/new", name="user_new") - * @Route("/{id}/clone", name="user_clone") - * @Route("/") - */ + #[Route(path: '/new', name: 'user_new')] + #[Route(path: '/{id}/clone', name: 'user_clone')] + #[Route(path: '/')] public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?User $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } - /** - * @Route("/{id}", name="user_delete", methods={"DELETE"}, requirements={"id"="\d+"}) - */ + #[Route(path: '/{id}', name: 'user_delete', requirements: ['id' => '\d+'], methods: ['DELETE'])] public function delete(Request $request, User $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { + //Disallow deleting the anonymous user if (User::ID_ANONYMOUS === $entity->getID()) { - throw new InvalidArgumentException('You can not delete the anonymous user! It is needed for permission checking without a logged in user'); + throw new \LogicException('You can not delete the anonymous user! It is needed for permission checking without a logged in user'); + } + + //Disallow deleting the current logged-in user + if ($entity === $this->getUser()) { + throw new \LogicException('You can not delete your own user account!'); } return $this->_delete($request, $entity, $recursionHelper); } - /** - * @Route("/export", name="user_export_all") - */ + #[Route(path: '/export', name: 'user_export_all')] public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { return $this->_exportAll($em, $exporter, $request); } - /** - * @Route("/{id}/export", name="user_export") - */ + #[Route(path: '/{id}/export', name: 'user_export')] public function exportEntity(User $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); } - /** - * @Route("/info", name="user_info_self") - * @Route("/{id}/info", name="user_info") - */ + #[Route(path: '/info', name: 'user_info_self')] + #[Route(path: '/{id}/info', name: 'user_info')] public function userInfo(?User $user, Packages $packages, Request $request, DataTableFactory $dataTableFactory): Response { //If no user id was passed, then we show info about the current user - if (null === $user) { + if (!$user instanceof User) { $tmp = $this->getUser(); if (!$tmp instanceof User) { throw new InvalidArgumentException('Userinfo only works for database users!'); @@ -230,7 +235,7 @@ class UserController extends AdminPages\BaseAdminController 'data' => $user, ]); - return $this->renderForm('users/user_info.html.twig', [ + return $this->render('users/user_info.html.twig', [ 'user' => $user, 'form' => $builder->getForm(), 'datatable' => $table ?? null, diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php index 80353ac9..4e56015a 100644 --- a/src/Controller/UserSettingsController.php +++ b/src/Controller/UserSettingsController.php @@ -22,6 +22,9 @@ declare(strict_types=1); namespace App\Controller; +use App\Entity\Attachments\Attachment; +use App\Entity\UserSystem\ApiToken; +use App\Entity\UserSystem\ApiTokenLevel; use App\Entity\UserSystem\U2FKey; use App\Entity\UserSystem\User; use App\Entity\UserSystem\WebauthnKey; @@ -33,10 +36,11 @@ use App\Services\UserSystem\TFA\BackupCodeManager; use App\Services\UserSystem\UserAvatarHelper; use Doctrine\ORM\EntityManagerInterface; use RuntimeException; -use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -47,32 +51,19 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; use Symfony\Component\Validator\Constraints\Length; -/** - * @Route("/user") - */ +#[Route(path: '/user')] class UserSettingsController extends AbstractController { - protected bool $demo_mode; - - /** - * @var EventDispatcher|EventDispatcherInterface - */ - protected $eventDispatcher; - - public function __construct(bool $demo_mode, EventDispatcherInterface $eventDispatcher) + public function __construct(protected bool $demo_mode, protected EventDispatcherInterface $eventDispatcher) { - $this->demo_mode = $demo_mode; - $this->eventDispatcher = $eventDispatcher; } - /** - * @Route("/2fa_backup_codes", name="show_backup_codes") - */ - public function showBackupCodes() + #[Route(path: '/2fa_backup_codes', name: 'show_backup_codes')] + public function showBackupCodes(): Response { $user = $this->getUser(); @@ -80,14 +71,14 @@ class UserSettingsController extends AbstractController $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); if (!$user instanceof User) { - return new RuntimeException('This controller only works only for Part-DB User objects!'); + throw new RuntimeException('This controller only works only for Part-DB User objects!'); } if ($user->isSamlUser()) { throw new RuntimeException('You can not remove U2F keys from SAML users!'); } - if (empty($user->getBackupCodes())) { + if ($user->getBackupCodes() === []) { $this->addFlash('error', 'tfa_backup.no_codes_enabled'); throw new RuntimeException('You do not have any backup codes enabled, therefore you can not view them!'); @@ -98,9 +89,7 @@ class UserSettingsController extends AbstractController ]); } - /** - * @Route("/u2f_delete", name="u2f_delete", methods={"DELETE"}) - */ + #[Route(path: '/u2f_delete', name: 'u2f_delete', methods: ['DELETE'])] public function removeU2FToken(Request $request, EntityManagerInterface $entityManager, BackupCodeManager $backupCodeManager): RedirectResponse { if ($this->demo_mode) { @@ -120,56 +109,50 @@ class UserSettingsController extends AbstractController throw new RuntimeException('You can not remove U2F keys from SAML users!'); } - if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('delete'.$user->getID(), $request->request->get('_token'))) { //Handle U2F key removal if ($request->request->has('key_id')) { $key_id = $request->request->get('key_id'); $key_repo = $entityManager->getRepository(U2FKey::class); /** @var U2FKey|null $u2f */ $u2f = $key_repo->find($key_id); - if (null === $u2f) { + if (!$u2f instanceof U2FKey) { $this->addFlash('danger', 'tfa_u2f.u2f_delete.not_existing'); return $this->redirectToRoute('user_settings'); } - //User can only delete its own U2F keys if ($u2f->getUser() !== $user) { $this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied'); return $this->redirectToRoute('user_settings'); } - $backupCodeManager->disableBackupCodesIfUnused($user); $entityManager->remove($u2f); $entityManager->flush(); $this->addFlash('success', 'tfa.u2f.u2f_delete.success'); - $security_event = new SecurityEvent($user); $this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED); - } else if ($request->request->has('webauthn_key_id')) { + } elseif ($request->request->has('webauthn_key_id')) { $key_id = $request->request->get('webauthn_key_id'); $key_repo = $entityManager->getRepository(WebauthnKey::class); /** @var WebauthnKey|null $key */ $key = $key_repo->find($key_id); - if (null === $key) { + if (!$key instanceof WebauthnKey) { $this->addFlash('error', 'tfa_u2f.u2f_delete.not_existing'); return $this->redirectToRoute('user_settings'); } - //User can only delete its own U2F keys if ($key->getUser() !== $user) { $this->addFlash('error', 'tfa_u2f.u2f_delete.access_denied'); return $this->redirectToRoute('user_settings'); } - $backupCodeManager->disableBackupCodesIfUnused($user); $entityManager->remove($key); $entityManager->flush(); $this->addFlash('success', 'tfa.u2f.u2f_delete.success'); - $security_event = new SecurityEvent($user); $this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED); } @@ -180,12 +163,8 @@ class UserSettingsController extends AbstractController return $this->redirectToRoute('user_settings'); } - /** - * @Route("/invalidate_trustedDevices", name="tfa_trustedDevices_invalidate", methods={"DELETE"}) - * - * @return RuntimeException|RedirectResponse - */ - public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager) + #[Route(path: '/invalidate_trustedDevices', name: 'tfa_trustedDevices_invalidate', methods: ['DELETE'])] + public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager): \RuntimeException|RedirectResponse { if ($this->demo_mode) { throw new RuntimeException('You can not do 2FA things in demo mode'); @@ -204,7 +183,7 @@ class UserSettingsController extends AbstractController throw new RuntimeException('You can not remove U2F keys from SAML users!'); } - if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) { + if ($this->isCsrfTokenValid('devices_reset'.$user->getID(), $request->request->get('_token'))) { $user->invalidateTrustedDeviceTokens(); $entityManager->flush(); $this->addFlash('success', 'tfa_trustedDevice.invalidate.success'); @@ -219,13 +198,13 @@ class UserSettingsController extends AbstractController } /** - * @Route("/settings", name="user_settings") - * * @return RedirectResponse|Response */ - public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager, FormFactoryInterface $formFactory, UserAvatarHelper $avatarHelper) + #[Route(path: '/settings', name: 'user_settings')] + public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordEncoder, + GoogleAuthenticatorInterface $googleAuthenticator, BackupCodeManager $backupCodeManager, FormFactoryInterface $formFactory, UserAvatarHelper $avatarHelper): RedirectResponse|Response { - /** @var User */ + /** @var User $user */ $user = $this->getUser(); $page_need_reload = false; @@ -261,14 +240,15 @@ class UserSettingsController extends AbstractController $page_need_reload = true; } - /** @var Form $form We need an form implementation for the next calls */ - if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName()) { - //Remove the avatar attachment from the user if requested - if ($user->getMasterPictureAttachment() !== null) { - $em->remove($user->getMasterPictureAttachment()); - $user->setMasterPictureAttachment(null); - $page_need_reload = true; - } + if (!$form instanceof Form) { + throw new RuntimeException('Form is not an instance of Form, so we cannot retrieve the clicked button!'); + } + + //Remove the avatar attachment from the user if requested + if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) { + $em->remove($user->getMasterPictureAttachment()); + $user->setMasterPictureAttachment(null); + $page_need_reload = true; } $em->flush(); @@ -304,6 +284,7 @@ class UserSettingsController extends AbstractController 'type' => PasswordType::class, 'first_options' => [ 'label' => 'user.settings.pw_new.label', + 'password_estimator' => true, ], 'second_options' => [ 'label' => 'user.settings.pw_confirm.label', @@ -327,7 +308,7 @@ class UserSettingsController extends AbstractController $pw_form->handleRequest($request); - //Check if password if everything was correct, then save it to User and DB + //Check if everything was correct, then save it to User and DB if (!$this->demo_mode && $pw_form->isSubmitted() && $pw_form->isValid()) { $password = $passwordEncoder->hashPassword($user, $pw_form['new_password']->getData()); $user->setPassword($password); @@ -346,7 +327,7 @@ class UserSettingsController extends AbstractController 'disabled' => $this->demo_mode || $user->isSamlUser(), ]); $google_enabled = $user->isGoogleAuthenticatorEnabled(); - if (!$google_enabled && !$form->isSubmitted()) { + if (!$google_enabled && !$google_form->isSubmitted()) { $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret()); $google_form->get('googleAuthenticatorSecret')->setData($user->getGoogleAuthenticatorSecret()); } @@ -382,7 +363,7 @@ class UserSettingsController extends AbstractController 'attr' => [ 'class' => 'btn-danger', ], - 'disabled' => empty($user->getBackupCodes()), + 'disabled' => $user->getBackupCodes() === [], ])->getForm(); $backup_form->handleRequest($request); @@ -397,7 +378,7 @@ class UserSettingsController extends AbstractController * Output both forms *****************************/ - return $this->renderForm('users/user_settings.html.twig', [ + return $this->render('users/user_settings.html.twig', [ 'user' => $user, 'settings_form' => $form, 'pw_form' => $pw_form, @@ -413,4 +394,99 @@ class UserSettingsController extends AbstractController ], ]); } + + /** + * @return Response + */ + #[Route('/api_token/create', name: 'user_api_token_create')] + public function addApiToken(Request $request, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('@api.manage_tokens'); + //When user change its settings, he should be logged in fully. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $token = new ApiToken(); + if (!$this->getUser() instanceof User) { + throw new RuntimeException('This controller only works only for Part-DB User objects!'); + } + $token->setUser($this->getUser()); + + $secret = null; + + $form = $this->createFormBuilder($token) + ->add('name', TextType::class, [ + 'label' => 'api_tokens.name', + ]) + ->add('level', EnumType::class, [ + 'class' => ApiTokenLevel::class, + 'label' => 'api_tokens.access_level', + 'help' => 'api_tokens.access_level.help', + 'choice_label' => fn (ApiTokenLevel $level) => $level->getTranslationKey(), + ]) + ->add('valid_until', DateTimeType::class, [ + 'label' => 'api_tokens.expiration_date', + 'widget' => 'single_text', + 'help' => 'api_tokens.expiration_date.help', + 'required' => false, + 'html5' => true + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'save', + ]) + ->getForm(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($token); + $entityManager->flush(); + + $secret = $token->getToken(); + } + + return $this->render('users/api_token_create.html.twig', [ + 'token' => $token, + 'form' => $form, + 'secret' => $secret, + ]); + } + + #[Route(path: '/api_token/delete', name: 'user_api_tokens_delete', methods: ['DELETE'])] + public function apiTokenRemove(Request $request, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('@api.manage_tokens'); + //When user change its settings, he should be logged in fully. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $user = $this->getUser(); + if (!$user instanceof User) { + throw new RuntimeException('This controller only works only for Part-DB User objects!'); + } + + if (!$this->isCsrfTokenValid('delete'.$user->getID(), $request->request->get('_token'))) { + $this->addFlash('error', 'csfr_invalid'); + return $this->redirectToRoute('user_settings'); + } + + //Extract the token id from the request + $token_id = $request->request->getInt('token_id'); + + $token = $entityManager->find(ApiToken::class, $token_id); + if ($token === null) { + $this->addFlash('error', 'tfa_u2f.u2f_delete.not_existing'); + return $this->redirectToRoute('user_settings'); + } + //User can only delete its own API tokens + if ($token->getUser() !== $user) { + $this->addFlash('error', 'tfa_u2f.u2f_delete.access_denied'); + return $this->redirectToRoute('user_settings'); + } + + //Do the actual deletion + $entityManager->remove($token); + $entityManager->flush(); + + $this->addFlash('success', 'api_tokens.deleted'); + return $this->redirectToRoute('user_settings'); + } } diff --git a/src/Controller/WebauthnKeyRegistrationController.php b/src/Controller/WebauthnKeyRegistrationController.php index 8a26346a..b2c4f344 100644 --- a/src/Controller/WebauthnKeyRegistrationController.php +++ b/src/Controller/WebauthnKeyRegistrationController.php @@ -1,4 +1,7 @@ . */ - namespace App\Controller; use App\Entity\UserSystem\User; @@ -27,23 +29,19 @@ use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper; use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; use function Symfony\Component\Translation\t; class WebauthnKeyRegistrationController extends AbstractController { - private bool $demo_mode; - - public function __construct(bool $demo_mode) + public function __construct(private readonly bool $demo_mode) { - $this->demo_mode = $demo_mode; } - /** - * @Route("/webauthn/register", name="webauthn_register") - */ - public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em) + #[Route(path: '/webauthn/register', name: 'webauthn_register')] + public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em): Response { //When user change its settings, he should be logged in fully. $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); @@ -75,14 +73,19 @@ class WebauthnKeyRegistrationController extends AbstractController //Check the response try { $new_key = $registrationHelper->checkRegistrationResponse($webauthnResponse); - } catch (\Exception $exception) { + } catch (\Exception) { $this->addFlash('error', t('tfa_u2f.add_key.registration_error')); return $this->redirectToRoute('webauthn_register'); } + $user = $this->getUser(); + if (!$user instanceof User) { + throw new RuntimeException('This controller only works only for Part-DB User objects!'); + } + $keyEntity = WebauthnKey::fromRegistration($new_key); $keyEntity->setName($keyName); - $keyEntity->setUser($this->getUser()); + $keyEntity->setUser($user); $em->persist($keyEntity); $em->flush(); @@ -100,4 +103,4 @@ class WebauthnKeyRegistrationController extends AbstractController ] ); } -} \ No newline at end of file +} diff --git a/src/DataFixtures/APITokenFixtures.php b/src/DataFixtures/APITokenFixtures.php new file mode 100644 index 00000000..0ab0b7bb --- /dev/null +++ b/src/DataFixtures/APITokenFixtures.php @@ -0,0 +1,97 @@ +. + */ + +declare(strict_types=1); + + +namespace App\DataFixtures; + +use App\Entity\UserSystem\ApiToken; +use App\Entity\UserSystem\ApiTokenLevel; +use App\Entity\UserSystem\User; +use Doctrine\Bundle\FixturesBundle\Fixture; +use Doctrine\Common\DataFixtures\DependentFixtureInterface; +use Doctrine\Persistence\ObjectManager; + +class APITokenFixtures extends Fixture implements DependentFixtureInterface +{ + public const TOKEN_READONLY = 'tcp_readonly'; + public const TOKEN_EDIT = 'tcp_edit'; + public const TOKEN_ADMIN = 'tcp_admin'; + public const TOKEN_FULL = 'tcp_full'; + public const TOKEN_EXPIRED = 'tcp_expired'; + + public function load(ObjectManager $manager): void + { + /** @var User $admin_user */ + $admin_user = $this->getReference(UserFixtures::ADMIN, User::class); + + $read_only_token = new ApiToken(); + $read_only_token->setUser($admin_user); + $read_only_token->setLevel(ApiTokenLevel::READ_ONLY); + $read_only_token->setName('read-only'); + $this->setTokenSecret($read_only_token, self::TOKEN_READONLY); + $manager->persist($read_only_token); + + $editor_token = new ApiToken(); + $editor_token->setUser($admin_user); + $editor_token->setLevel(ApiTokenLevel::EDIT); + $editor_token->setName('edit'); + $this->setTokenSecret($editor_token, self::TOKEN_EDIT); + $manager->persist($editor_token); + + $admin_token = new ApiToken(); + $admin_token->setUser($admin_user); + $admin_token->setLevel(ApiTokenLevel::ADMIN); + $admin_token->setName('admin'); + $this->setTokenSecret($admin_token, self::TOKEN_ADMIN); + $manager->persist($admin_token); + + $full_token = new ApiToken(); + $full_token->setUser($admin_user); + $full_token->setLevel(ApiTokenLevel::FULL); + $full_token->setName('full'); + $this->setTokenSecret($full_token, self::TOKEN_FULL); + $manager->persist($full_token); + + $expired_token = new ApiToken(); + $expired_token->setUser($admin_user); + $expired_token->setLevel(ApiTokenLevel::FULL); + $expired_token->setName('expired'); + $expired_token->setValidUntil(new \DateTimeImmutable('-1 day')); + $this->setTokenSecret($expired_token, self::TOKEN_EXPIRED); + $manager->persist($expired_token); + + $manager->flush(); + } + + private function setTokenSecret(ApiToken $token, string $secret): void + { + //Access private property + $reflection = new \ReflectionClass($token); + $property = $reflection->getProperty('token'); + $property->setValue($token, $secret); + } + + public function getDependencies(): array + { + return [UserFixtures::class]; + } +} \ No newline at end of file diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php deleted file mode 100644 index dff739ae..00000000 --- a/src/DataFixtures/AppFixtures.php +++ /dev/null @@ -1,37 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\DataFixtures; - -use Doctrine\Bundle\FixturesBundle\Fixture; -use Doctrine\Persistence\ObjectManager; - -class AppFixtures extends Fixture -{ - public function load(ObjectManager $manager): void - { - // $product = new Product(); - // $manager->persist($product); - - $manager->flush(); - } -} diff --git a/src/DataFixtures/DataStructureFixtures.php b/src/DataFixtures/DataStructureFixtures.php index c7416abe..fc713d4d 100644 --- a/src/DataFixtures/DataStructureFixtures.php +++ b/src/DataFixtures/DataStructureFixtures.php @@ -29,20 +29,18 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use Doctrine\Bundle\FixturesBundle\Fixture; +use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ObjectManager; use InvalidArgumentException; -class DataStructureFixtures extends Fixture +class DataStructureFixtures extends Fixture implements DependentFixtureInterface { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $entityManager) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $entityManager; } /** @@ -52,7 +50,7 @@ class DataStructureFixtures extends Fixture { //Reset autoincrement $types = [AttachmentType::class, Project::class, Category::class, Footprint::class, Manufacturer::class, - MeasurementUnit::class, Storelocation::class, Supplier::class, ]; + MeasurementUnit::class, StorageLocation::class, Supplier::class,]; foreach ($types as $type) { $this->createNodesForClass($type, $manager); @@ -76,30 +74,37 @@ class DataStructureFixtures extends Fixture /** @var AbstractStructuralDBElement $node1 */ $node1 = new $class(); $node1->setName('Node 1'); + $this->addReference($class . '_1', $node1); /** @var AbstractStructuralDBElement $node2 */ $node2 = new $class(); $node2->setName('Node 2'); + $this->addReference($class . '_2', $node2); /** @var AbstractStructuralDBElement $node3 */ $node3 = new $class(); $node3->setName('Node 3'); + $this->addReference($class . '_3', $node3); $node1_1 = new $class(); $node1_1->setName('Node 1.1'); $node1_1->setParent($node1); + $this->addReference($class . '_4', $node1_1); $node1_2 = new $class(); $node1_2->setName('Node 1.2'); $node1_2->setParent($node1); + $this->addReference($class . '_5', $node1_2); $node2_1 = new $class(); $node2_1->setName('Node 2.1'); $node2_1->setParent($node2); + $this->addReference($class . '_6', $node2_1); $node1_1_1 = new $class(); $node1_1_1->setName('Node 1.1.1'); $node1_1_1->setParent($node1_1); + $this->addReference($class . '_7', $node1_1_1); $manager->persist($node1); $manager->persist($node2); @@ -109,4 +114,11 @@ class DataStructureFixtures extends Fixture $manager->persist($node2_1); $manager->persist($node1_1_1); } + + public function getDependencies(): array + { + return [ + UserFixtures::class + ]; + } } diff --git a/src/DataFixtures/EDADataFixtures.php b/src/DataFixtures/EDADataFixtures.php new file mode 100644 index 00000000..1484f03e --- /dev/null +++ b/src/DataFixtures/EDADataFixtures.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + + +namespace App\DataFixtures; + +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Part; +use Doctrine\Bundle\FixturesBundle\Fixture; +use Doctrine\Common\DataFixtures\DependentFixtureInterface; +use Doctrine\Persistence\ObjectManager; + +class EDADataFixtures extends Fixture implements DependentFixtureInterface +{ + + public function getDependencies(): array + { + return [PartFixtures::class]; + } + + public function load(ObjectManager $manager): void + { + //Load elements from DB + $category1 = $manager->find(Category::class, 1); + $footprint1 = $manager->find(Footprint::class, 1); + + $part1 = $manager->find(Part::class, 1); + + //Put some data into category1 and foorprint1 + $category1?->getEdaInfo() + ->setExcludeFromBoard(true) + ->setKicadSymbol('Category:1') + ->setReferencePrefix('C') + ; + + $footprint1?->getEdaInfo() + ->setKicadFootprint('Footprint:1') + ; + + //Put some data into part1 (which overrides the data from category1 and footprint1 on part1) + $part1?->getEdaInfo() + ->setExcludeFromSim(false) + ->setKicadSymbol('Part:1') + ->setKicadFootprint('Part:1') + ->setReferencePrefix('P') + ; + + //Flush the changes + $manager->flush(); + } +} \ No newline at end of file diff --git a/src/DataFixtures/GroupFixtures.php b/src/DataFixtures/GroupFixtures.php index 93e93b79..d8e54b9f 100644 --- a/src/DataFixtures/GroupFixtures.php +++ b/src/DataFixtures/GroupFixtures.php @@ -30,18 +30,12 @@ use Doctrine\Persistence\ObjectManager; class GroupFixtures extends Fixture { - public const ADMINS = 'group-admin'; - public const USERS = 'group-users'; - public const READONLY = 'group-readonly'; + final public const ADMINS = 'group-admin'; + final public const USERS = 'group-users'; + final public const READONLY = 'group-readonly'; - - private PermissionPresetsHelper $permission_presets; - private PermissionManager $permissionManager; - - public function __construct(PermissionPresetsHelper $permissionPresetsHelper, PermissionManager $permissionManager) + public function __construct(private readonly PermissionPresetsHelper $permission_presets, private readonly PermissionManager $permissionManager) { - $this->permission_presets = $permissionPresetsHelper; - $this->permissionManager = $permissionManager; } public function load(ObjectManager $manager): void diff --git a/src/DataFixtures/LabelProfileFixtures.php b/src/DataFixtures/LabelProfileFixtures.php index d2c23fad..c0eb85cd 100644 --- a/src/DataFixtures/LabelProfileFixtures.php +++ b/src/DataFixtures/LabelProfileFixtures.php @@ -41,19 +41,19 @@ declare(strict_types=1); namespace App\DataFixtures; +use App\Entity\LabelSystem\BarcodeType; use App\Entity\LabelSystem\LabelOptions; +use App\Entity\LabelSystem\LabelProcessMode; use App\Entity\LabelSystem\LabelProfile; +use App\Entity\LabelSystem\LabelSupportedElement; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ObjectManager; class LabelProfileFixtures extends Fixture { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $entityManager) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $entityManager; } public function load(ObjectManager $manager): void @@ -65,8 +65,8 @@ class LabelProfileFixtures extends Fixture $option1 = new LabelOptions(); $option1->setLines("[[NAME]]\n[[DESCRIPION]]"); - $option1->setBarcodeType('none'); - $option1->setSupportedElement('part'); + $option1->setBarcodeType(BarcodeType::NONE); + $option1->setSupportedElement(LabelSupportedElement::PART); $profile1->setOptions($option1); $manager->persist($profile1); @@ -77,8 +77,8 @@ class LabelProfileFixtures extends Fixture $option2 = new LabelOptions(); $option2->setLines("[[NAME]]\n[[DESCRIPION]]"); - $option2->setBarcodeType('qr'); - $option2->setSupportedElement('part'); + $option2->setBarcodeType(BarcodeType::QR); + $option2->setSupportedElement(LabelSupportedElement::PART); $profile2->setOptions($option2); $manager->persist($profile2); @@ -89,8 +89,8 @@ class LabelProfileFixtures extends Fixture $option3 = new LabelOptions(); $option3->setLines("[[NAME]]\n[[DESCRIPION]]"); - $option3->setBarcodeType('code128'); - $option3->setSupportedElement('part_lot'); + $option3->setBarcodeType(BarcodeType::CODE128); + $option3->setSupportedElement(LabelSupportedElement::PART_LOT); $profile3->setOptions($option3); $manager->persist($profile3); @@ -101,13 +101,20 @@ class LabelProfileFixtures extends Fixture $option4 = new LabelOptions(); $option4->setLines('{{ element.name }}'); - $option4->setBarcodeType('code39'); - $option4->setSupportedElement('part'); - $option4->setLinesMode('twig'); + $option4->setBarcodeType(BarcodeType::CODE39); + $option4->setSupportedElement(LabelSupportedElement::PART); + $option4->setProcessMode(LabelProcessMode::TWIG); $profile4->setOptions($option4); $manager->persist($profile4); $manager->flush(); } + + public function getDependencies(): array + { + return [ + PartFixtures::class, + ]; + } } diff --git a/src/DataFixtures/LogEntryFixtures.php b/src/DataFixtures/LogEntryFixtures.php new file mode 100644 index 00000000..eb9cf731 --- /dev/null +++ b/src/DataFixtures/LogEntryFixtures.php @@ -0,0 +1,106 @@ +. + */ + +declare(strict_types=1); + + +namespace App\DataFixtures; + +use App\Entity\LogSystem\ElementCreatedLogEntry; +use App\Entity\LogSystem\ElementDeletedLogEntry; +use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Entity\Parts\Category; +use App\Entity\UserSystem\User; +use Doctrine\Bundle\FixturesBundle\Fixture; +use Doctrine\Common\DataFixtures\DependentFixtureInterface; +use Doctrine\Persistence\ObjectManager; + +class LogEntryFixtures extends Fixture implements DependentFixtureInterface +{ + + public function load(ObjectManager $manager): void + { + $this->createCategoryEntries($manager); + $this->createDeletedCategory($manager); + } + + public function createCategoryEntries(ObjectManager $manager): void + { + $category = $this->getReference(Category::class . '_1', Category::class); + + $logEntry = new ElementCreatedLogEntry($category); + $logEntry->setTimestamp(new \DateTimeImmutable("+1 second")); + $logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class)); + $logEntry->setComment('Test'); + $manager->persist($logEntry); + + $logEntry = new ElementEditedLogEntry($category); + $logEntry->setTimestamp(new \DateTimeImmutable("+2 second")); + $logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class)); + $logEntry->setComment('Test'); + + $logEntry->setOldData(['name' => 'Test']); + $logEntry->setNewData(['name' => 'Node 1.1']); + + $manager->persist($logEntry); + $manager->flush(); + } + + public function createDeletedCategory(ObjectManager $manager): void + { + //We create a fictive category to test the deletion + $category = new Category(); + $category->setName('Node 100'); + + //Assume a category with id 100 was deleted + $reflClass = new \ReflectionClass($category); + $reflClass->getProperty('id')->setValue($category, 100); + + //The whole lifecycle from creation to deletion + $logEntry = new ElementCreatedLogEntry($category); + $logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class)); + $logEntry->setComment('Creation'); + $manager->persist($logEntry); + + $logEntry = new ElementEditedLogEntry($category); + $logEntry->setTimestamp(new \DateTimeImmutable("+1 second")); + $logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class)); + $logEntry->setComment('Edit'); + $logEntry->setOldData(['name' => 'Test']); + $logEntry->setNewData(['name' => 'Node 100']); + $manager->persist($logEntry); + + $logEntry = new ElementDeletedLogEntry($category); + $logEntry->setTimestamp(new \DateTimeImmutable("+2 second")); + $logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class)); + $logEntry->setOldData(['name' => 'Node 100', 'id' => 100, 'comment' => 'Test comment']); + $manager->persist($logEntry); + + $manager->flush(); + } + + public function getDependencies(): array + { + return [ + UserFixtures::class, + DataStructureFixtures::class + ]; + } +} \ No newline at end of file diff --git a/src/DataFixtures/PartFixtures.php b/src/DataFixtures/PartFixtures.php index 45789dd9..a60d037d 100644 --- a/src/DataFixtures/PartFixtures.php +++ b/src/DataFixtures/PartFixtures.php @@ -46,25 +46,24 @@ use App\Entity\Attachments\PartAttachment; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use Brick\Math\BigDecimal; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; +use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ObjectManager; -class PartFixtures extends Fixture +class PartFixtures extends Fixture implements DependentFixtureInterface { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $entityManager) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $entityManager; } public function load(ObjectManager $manager): void @@ -74,6 +73,7 @@ class PartFixtures extends Fixture $part = new Part(); $part->setName('Part 1'); $part->setCategory($manager->find(Category::class, 1)); + $this->addReference(Part::class . '_1', $part); $manager->persist($part); /** More complex part */ @@ -84,8 +84,10 @@ class PartFixtures extends Fixture $part->setManufacturer($manager->find(Manufacturer::class, 1)); $part->setTags('test, Test, Part2'); $part->setMass(100.2); + $part->setIpn('IPN123'); $part->setNeedsReview(true); - $part->setManufacturingStatus('active'); + $part->setManufacturingStatus(ManufacturingStatus::ACTIVE); + $this->addReference(Part::class . '_2', $part); $manager->persist($part); /** Part with orderdetails, storelocations and Attachments */ @@ -95,14 +97,16 @@ class PartFixtures extends Fixture $part->setCategory($manager->find(Category::class, 1)); $partLot1 = new PartLot(); $partLot1->setAmount(1.0); - $partLot1->setStorageLocation($manager->find(Storelocation::class, 1)); + $partLot1->setStorageLocation($manager->find(StorageLocation::class, 1)); $part->addPartLot($partLot1); + $partLot2 = new PartLot(); - $partLot2->setExpirationDate(new DateTime()); + $partLot2->setExpirationDate(new \DateTimeImmutable()); $partLot2->setComment('Test'); $partLot2->setNeedsRefill(true); - $partLot2->setStorageLocation($manager->find(Storelocation::class, 3)); + $partLot2->setStorageLocation($manager->find(StorageLocation::class, 3)); + $partLot2->setUserBarcode('lot2_vendor_barcode'); $part->addPartLot($partLot2); $orderdetail = new Orderdetail(); @@ -127,12 +131,21 @@ class PartFixtures extends Fixture $attachment = new PartAttachment(); $attachment->setName('Test2'); - $attachment->setPath('invalid'); + $attachment->setInternalPath('invalid'); $attachment->setShowInTable(true); $attachment->setAttachmentType($manager->find(AttachmentType::class, 1)); $part->addAttachment($attachment); + $this->addReference(Part::class . '_3', $part); + $manager->persist($part); $manager->flush(); } + + public function getDependencies(): array + { + return [ + DataStructureFixtures::class + ]; + } } diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index f9e138b6..922d0b1e 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -22,28 +22,27 @@ declare(strict_types=1); namespace App\DataFixtures; +use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use Doctrine\Bundle\FixturesBundle\Fixture; +use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ObjectManager; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; -class UserFixtures extends Fixture +class UserFixtures extends Fixture implements DependentFixtureInterface { - protected UserPasswordHasherInterface $encoder; - protected EntityManagerInterface $em; + public const ADMIN = 'user-admin'; - public function __construct(UserPasswordHasherInterface $encoder, EntityManagerInterface $entityManager) + public function __construct(protected UserPasswordHasherInterface $encoder, protected EntityManagerInterface $em) { - $this->em = $entityManager; - $this->encoder = $encoder; } public function load(ObjectManager $manager): void { $anonymous = new User(); $anonymous->setName('anonymous'); - $anonymous->setGroup($this->getReference(GroupFixtures::READONLY)); + $anonymous->setGroup($this->getReference(GroupFixtures::READONLY, Group::class)); $anonymous->setNeedPwChange(false); $anonymous->setPassword($this->encoder->hashPassword($anonymous, 'test')); $manager->persist($anonymous); @@ -52,15 +51,17 @@ class UserFixtures extends Fixture $admin->setName('admin'); $admin->setPassword($this->encoder->hashPassword($admin, 'test')); $admin->setNeedPwChange(false); - $admin->setGroup($this->getReference(GroupFixtures::ADMINS)); + $admin->setGroup($this->getReference(GroupFixtures::ADMINS, Group::class)); $manager->persist($admin); + $this->addReference(self::ADMIN, $admin); $user = new User(); $user->setName('user'); $user->setNeedPwChange(false); + $user->setEmail('user@invalid.invalid'); $user->setFirstName('Test')->setLastName('User'); $user->setPassword($this->encoder->hashPassword($user, 'test')); - $user->setGroup($this->getReference(GroupFixtures::USERS)); + $user->setGroup($this->getReference(GroupFixtures::USERS, Group::class)); $manager->persist($user); $noread = new User(); @@ -70,5 +71,15 @@ class UserFixtures extends Fixture $manager->persist($noread); $manager->flush(); + + //Ensure that the anonymous user has the ID 0 + $manager->getRepository(User::class)->changeID($anonymous, User::ID_ANONYMOUS); + } + + public function getDependencies(): array + { + return [ + GroupFixtures::class, + ]; } } diff --git a/src/DataTables/Adapters/CustomFetchJoinORMAdapter.php b/src/DataTables/Adapters/CustomFetchJoinORMAdapter.php index 052e2e28..ff69a69e 100644 --- a/src/DataTables/Adapters/CustomFetchJoinORMAdapter.php +++ b/src/DataTables/Adapters/CustomFetchJoinORMAdapter.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Adapters; -use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; use Omines\DataTablesBundle\Adapter\Doctrine\FetchJoinORMAdapter; @@ -38,7 +39,7 @@ use Omines\DataTablesBundle\Adapter\Doctrine\FetchJoinORMAdapter; */ class CustomFetchJoinORMAdapter extends FetchJoinORMAdapter { - public function getCount(QueryBuilder $queryBuilder, $identifier) + public function getCount(QueryBuilder $queryBuilder, $identifier): int { $qb_without_group_by = clone $queryBuilder; @@ -51,4 +52,4 @@ class CustomFetchJoinORMAdapter extends FetchJoinORMAdapter return $paginator->count(); } -} \ No newline at end of file +} diff --git a/src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php b/src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php new file mode 100644 index 00000000..eda48038 --- /dev/null +++ b/src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + + +namespace App\DataTables\Adapters; + +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\AdapterQuery; +use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents; +use Omines\DataTablesBundle\Column\AbstractColumn; + +/** + * This class is very similar to the original ORMAdapter, but instead of getting each line one by one, it gets all the results at once, + * which can save some time in combination with fetch hints. + */ +class FetchResultsAtOnceORMAdapter extends ORMAdapter +{ + protected function getResults(AdapterQuery $query): \Traversable + { + /** @var QueryBuilder $builder */ + $builder = $query->get('qb'); + $state = $query->getState(); + + // Apply definitive view state for current 'page' of the table + foreach ($state->getOrderBy() as [$column, $direction]) { + /** @var AbstractColumn $column */ + if ($column->isOrderable()) { + $builder->addOrderBy($column->getOrderField(), $direction); + } + } + if (null !== $state->getLength()) { + $builder + ->setFirstResult($state->getStart()) + ->setMaxResults($state->getLength()) + ; + } + + $q = $builder->getQuery(); + $event = new ORMAdapterQueryEvent($q); + $state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY); + + foreach ($q->getResult() as $item) { + yield $item; + } + } +} \ No newline at end of file diff --git a/src/DataTables/Adapters/TwoStepORMAdapter.php b/src/DataTables/Adapters/TwoStepORMAdapter.php new file mode 100644 index 00000000..51315c32 --- /dev/null +++ b/src/DataTables/Adapters/TwoStepORMAdapter.php @@ -0,0 +1,208 @@ +. + */ + +declare(strict_types=1); + + +namespace App\DataTables\Adapters; + +use Doctrine\ORM\Query\Expr\From; +use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Tools\Pagination\Paginator; +use Doctrine\Persistence\ManagerRegistry; +use Omines\DataTablesBundle\Adapter\AdapterQuery; +use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents; +use Omines\DataTablesBundle\Column\AbstractColumn; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * This adapter fetches entities from the database in two steps: + * In the first step, it uses a simple query (filter_query option), which returns only the ids of the main entity, + * to get the count of results, doing filtering and pagination. + * + * In the next step the IDs are passed to the detail_query callback option, which then can do a more complex queries (like fetch join) + * to get the entities and related stuff in an efficient way. + * This way we save the overhead of the fetch join query for the count and counting, which can be very slow, cause + * no indexes can be used. + */ +class TwoStepORMAdapter extends ORMAdapter +{ + private \Closure $detailQueryCallable; + + private bool $use_simple_total = false; + + private \Closure|null $query_modifier = null; + + public function __construct(?ManagerRegistry $registry = null) + { + parent::__construct($registry); + $this->detailQueryCallable = static function (QueryBuilder $qb, array $ids): never { + throw new \RuntimeException('You need to set the detail_query option to use the TwoStepORMAdapter'); + }; + } + + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + + $resolver->setRequired('filter_query'); + $resolver->setDefault('query', fn(Options $options) => $options['filter_query']); + + $resolver->setRequired('detail_query'); + $resolver->setAllowedTypes('detail_query', \Closure::class); + + /* + * Add the possibility to replace the query for total entity count through a very simple one, to improve performance. + * You can only use this option, if you did not apply any criteria to your total count. + */ + $resolver->setDefault('simple_total_query', false); + + //Add the possibility of a closure to modify the query builder before the query is executed + $resolver->setDefault('query_modifier', null); + $resolver->setAllowedTypes('query_modifier', ['null', \Closure::class]); + + } + + protected function afterConfiguration(array $options): void + { + parent::afterConfiguration($options); + $this->detailQueryCallable = $options['detail_query']; + $this->use_simple_total = $options['simple_total_query']; + $this->query_modifier = $options['query_modifier']; + } + + protected function prepareQuery(AdapterQuery $query): void + { + //Like the parent class, but we add the possibility to use the simple total query + + $state = $query->getState(); + $query->set('qb', $builder = $this->createQueryBuilder($state)); + $query->set('rootAlias', $rootAlias = $builder->getDQLPart('from')[0]->getAlias()); + + // Provide default field mappings if needed + foreach ($state->getDataTable()->getColumns() as $column) { + if (null === $column->getField() && isset($this->metadata->fieldMappings[$name = $column->getName()])) { + $column->setOption('field', "{$rootAlias}.{$name}"); + } + } + + /** @var From $fromClause */ + $fromClause = $builder->getDQLPart('from')[0]; + $identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}"; + + // Use simpler (faster) total count query if the user wanted so... + if ($this->use_simple_total) { + $query->setTotalRows($this->getSimpleTotalCount($builder)); + } else { + $query->setTotalRows($this->getCount($builder, $identifier)); + } + + // Get record count after filtering + $this->buildCriteria($builder, $state); + $query->setFilteredRows($this->getCount($builder, $identifier)); + + // Perform mapping of all referred fields and implied fields + $aliases = $this->getAliases($query); + $query->set('aliases', $aliases); + $query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases)); + } + + protected function hasGroupByPart(string $identifier, array $gbList): bool + { + //Always return true, to fix the issue with the count query, when having mutliple group by parts + return true; + } + + protected function getCount(QueryBuilder $queryBuilder, $identifier): int + { + if ($this->query_modifier !== null) { + $queryBuilder = $this->query_modifier->__invoke(clone $queryBuilder); + } + + //Check if the queryBuilder is having a HAVING clause, which would make the count query invalid + if (empty($queryBuilder->getDQLPart('having'))) { + //If not, we can use the simple count query + return parent::getCount($queryBuilder, $identifier); + } + + //Otherwise Use the paginator, which uses a subquery to get the right count even with HAVING clauses + return (new Paginator($queryBuilder, false))->count(); + } + + protected function getResults(AdapterQuery $query): \Traversable + { + //Very similar to the parent class + /** @var QueryBuilder $builder */ + $builder = $query->get('qb'); + $state = $query->getState(); + + // Apply definitive view state for current 'page' of the table + foreach ($state->getOrderBy() as [$column, $direction]) { + /** @var AbstractColumn $column */ + if ($column->isOrderable()) { + $builder->addOrderBy($column->getOrderField(), $direction); + } + } + if (null !== $state->getLength()) { + $builder + ->setFirstResult($state->getStart()) + ->setMaxResults($state->getLength()) + ; + } + + //Apply the query modifier, if set + if ($this->query_modifier !== null) { + $builder = $this->query_modifier->__invoke($builder); + } + + $id_query = $builder->getQuery(); + $event = new ORMAdapterQueryEvent($id_query); + $state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY); + + //In the first step we only get the ids of the main entity... + $ids = $id_query->getArrayResult(); + + //Which is then passed to the detailQuery, which filters for the entities based on the IDs + $detail_qb = $this->manager->createQueryBuilder(); + $this->detailQueryCallable->__invoke($detail_qb, $ids); + + $detail_query = $detail_qb->getQuery(); + + //We pass the results of the detail query to the datatable for view rendering + foreach ($detail_query->getResult() as $item) { + yield $item; + } + } + + protected function getSimpleTotalCount(QueryBuilder $queryBuilder): int + { + /** The paginator count queries can be rather slow, so when query for total count (100ms or longer), + * just return the entity count. + */ + /** @var From $from_expr */ + $from_expr = $queryBuilder->getDQLPart('from')[0]; + + return $this->manager->getRepository($from_expr->getFrom())->count([]); + } +} \ No newline at end of file diff --git a/src/DataTables/AttachmentDataTable.php b/src/DataTables/AttachmentDataTable.php index dd82f5a8..16e6a7a7 100644 --- a/src/DataTables/AttachmentDataTable.php +++ b/src/DataTables/AttachmentDataTable.php @@ -27,7 +27,6 @@ use App\DataTables\Column\PrettyBoolColumn; use App\DataTables\Column\RowClassColumn; use App\DataTables\Filters\AttachmentFilter; use App\Entity\Attachments\Attachment; -use App\Entity\LogSystem\AbstractLogEntry; use App\Services\Attachments\AttachmentManager; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\ElementTypeNameGenerator; @@ -35,6 +34,7 @@ use App\Services\EntityURLGenerator; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +use Omines\DataTablesBundle\Column\NumberColumn; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; @@ -42,29 +42,16 @@ use Symfony\Contracts\Translation\TranslatorInterface; final class AttachmentDataTable implements DataTableTypeInterface { - private TranslatorInterface $translator; - private EntityURLGenerator $entityURLGenerator; - private AttachmentManager $attachmentHelper; - private ElementTypeNameGenerator $elementTypeNameGenerator; - private AttachmentURLGenerator $attachmentURLGenerator; - - public function __construct(TranslatorInterface $translator, EntityURLGenerator $entityURLGenerator, - AttachmentManager $attachmentHelper, AttachmentURLGenerator $attachmentURLGenerator, - ElementTypeNameGenerator $elementTypeNameGenerator) + public function __construct(private readonly TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator, private readonly AttachmentManager $attachmentHelper, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly ElementTypeNameGenerator $elementTypeNameGenerator) { - $this->translator = $translator; - $this->entityURLGenerator = $entityURLGenerator; - $this->attachmentHelper = $attachmentHelper; - $this->elementTypeNameGenerator = $elementTypeNameGenerator; - $this->attachmentURLGenerator = $attachmentURLGenerator; } public function configure(DataTable $dataTable, array $options): void { $dataTable->add('dont_matter', RowClassColumn::class, [ - 'render' => function ($value, Attachment $context) { - //Mark attachments with missing files yellow - if(!$this->attachmentHelper->isFileExisting($context)){ + 'render' => function ($value, Attachment $context): string { + //Mark attachments yellow which have an internal file linked that doesn't exist + if($context->hasInternal() && !$this->attachmentHelper->isInternalFileExisting($context)){ return 'table-warning'; } @@ -75,10 +62,10 @@ final class AttachmentDataTable implements DataTableTypeInterface $dataTable->add('picture', TextColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => function ($value, Attachment $context) { + 'render' => function ($value, Attachment $context): string { if ($context->isPicture() - && !$context->isExternal() - && $this->attachmentHelper->isFileExisting($context)) { + && $this->attachmentHelper->isInternalFileExisting($context)) { + $title = htmlspecialchars($context->getName()); if ($context->getFilename()) { $title .= ' ('.htmlspecialchars($context->getFilename()).')'; @@ -98,19 +85,43 @@ final class AttachmentDataTable implements DataTableTypeInterface }, ]); + $dataTable->add('id', NumberColumn::class, [ + 'label' => $this->translator->trans('part.table.id'), + 'visible' => false, + ]); + $dataTable->add('name', TextColumn::class, [ 'label' => 'attachment.edit.name', - 'render' => function ($value, Attachment $context) { - //Link to external source - if ($context->isExternal()) { - return sprintf( - '%s', - htmlspecialchars($context->getURL()), - htmlspecialchars($value) - ); - } + 'orderField' => 'NATSORT(attachment.name)', + ]); - if ($this->attachmentHelper->isFileExisting($context)) { + $dataTable->add('attachment_type', TextColumn::class, [ + 'label' => 'attachment.table.type', + 'field' => 'attachment_type.name', + 'orderField' => 'NATSORT(attachment_type.name)', + 'render' => fn($value, Attachment $context): string => sprintf( + '%s', + $this->entityURLGenerator->editURL($context->getAttachmentType()), + htmlspecialchars((string) $value) + ), + ]); + + $dataTable->add('element', TextColumn::class, [ + 'label' => 'attachment.table.element', + //'propertyPath' => 'element.name', + 'render' => fn($value, Attachment $context): string => sprintf( + '%s', + $this->entityURLGenerator->infoURL($context->getElement()), + $this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true) + ), + ]); + + $dataTable->add('internal_link', TextColumn::class, [ + 'label' => 'attachment.table.internal_file', + 'propertyPath' => 'filename', + 'orderField' => 'NATSORT(attachment.original_filename)', + 'render' => function ($value, Attachment $context) { + if ($this->attachmentHelper->isInternalFileExisting($context)) { return sprintf( '%s', $this->entityURLGenerator->viewURL($context), @@ -119,52 +130,46 @@ final class AttachmentDataTable implements DataTableTypeInterface } return $value; - }, + } ]); - $dataTable->add('attachment_type', TextColumn::class, [ - 'label' => 'attachment.table.type', - 'field' => 'attachment_type.name', + $dataTable->add('external_link', TextColumn::class, [ + 'label' => 'attachment.table.external_link', + 'propertyPath' => 'host', + 'orderField' => 'attachment.external_path', 'render' => function ($value, Attachment $context) { - return sprintf( - '%s', - $this->entityURLGenerator->editURL($context->getAttachmentType()), - htmlspecialchars($value) - ); - }, - ]); + if ($context->hasExternal()) { + return sprintf( + '%s', + htmlspecialchars((string) $context->getExternalPath()), + htmlspecialchars((string) $context->getExternalPath()), + htmlspecialchars($value), + ); + } - $dataTable->add('element', TextColumn::class, [ - 'label' => 'attachment.table.element', - //'propertyPath' => 'element.name', - 'render' => function ($value, Attachment $context) { - return sprintf( - '%s', - $this->entityURLGenerator->infoURL($context->getElement()), - $this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true) - ); - }, - ]); - - $dataTable->add('filename', TextColumn::class, [ - 'label' => $this->translator->trans('attachment.table.filename'), - 'propertyPath' => 'filename', + return $value; + } ]); $dataTable->add('filesize', TextColumn::class, [ 'label' => $this->translator->trans('attachment.table.filesize'), 'render' => function ($value, Attachment $context) { - if ($context->isExternal()) { + if (!$context->hasInternal()) { return sprintf( ' %s ', - $this->translator->trans('attachment.external') + $this->translator->trans('attachment.external_only') ); } - if ($this->attachmentHelper->isFileExisting($context)) { - return $this->attachmentHelper->getHumanFileSize($context); + if ($this->attachmentHelper->isInternalFileExisting($context)) { + return sprintf( + ' + %s + ', + $this->attachmentHelper->getHumanFileSize($context) + ); } return sprintf( @@ -238,7 +243,7 @@ final class AttachmentDataTable implements DataTableTypeInterface //We do the most stuff here in the filter class if (isset($options['filter'])) { if(!$options['filter'] instanceof AttachmentFilter) { - throw new \Exception('filter must be an instance of AttachmentFilter!'); + throw new \RuntimeException('filter must be an instance of AttachmentFilter!'); } $filter = $options['filter']; diff --git a/src/DataTables/Column/EntityColumn.php b/src/DataTables/Column/EntityColumn.php index 4ccc2ce4..54ae3fb3 100644 --- a/src/DataTables/Column/EntityColumn.php +++ b/src/DataTables/Column/EntityColumn.php @@ -22,9 +22,8 @@ declare(strict_types=1); namespace App\DataTables\Column; -use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; -use App\Entity\Parts\Part; +use App\Entity\Base\AbstractStructuralDBElement; use App\Services\EntityURLGenerator; use Omines\DataTablesBundle\Column\AbstractColumn; use Symfony\Component\OptionsResolver\Options; @@ -33,13 +32,8 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class EntityColumn extends AbstractColumn { - protected EntityURLGenerator $urlGenerator; - protected PropertyAccessorInterface $accessor; - - public function __construct(EntityURLGenerator $URLGenerator, PropertyAccessorInterface $accessor) + public function __construct(protected EntityURLGenerator $urlGenerator, protected PropertyAccessorInterface $accessor) { - $this->urlGenerator = $URLGenerator; - $this->accessor = $accessor; } /** @@ -48,46 +42,46 @@ class EntityColumn extends AbstractColumn * @param mixed $value The single value of the column * @return mixed */ - public function normalize($value) + public function normalize(mixed $value): mixed { /** @var AbstractNamedDBElement $value */ return $value; } - public function configureOptions(OptionsResolver $resolver): self + /** + * @return $this + */ + public function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); $resolver->setRequired('property'); - $resolver->setDefault('field', static function (Options $option) { - return $option['property'].'.name'; - }); + $resolver->setDefault('field', static fn(Options $option): string => $option['property'].'.name'); - $resolver->setDefault('render', function (Options $options) { - return function ($value, $context) use ($options) { - if ($this->accessor->isReadable($context, $options['property'])) { - $entity = $this->accessor->getValue($context, $options['property']); - } else { - $entity = null; + $resolver->setDefault('render', fn(Options $options) => function ($value, $context) use ($options): string { + if ($this->accessor->isReadable($context, $options['property'])) { + $entity = $this->accessor->getValue($context, $options['property']); + } else { + $entity = null; + } + + /** @var AbstractNamedDBElement|null $entity */ + + if ($entity instanceof AbstractNamedDBElement) { + if (null !== $entity->getID()) { + return sprintf( + '%s', + $this->urlGenerator->listPartsURL($entity), + $entity instanceof AbstractStructuralDBElement ? htmlspecialchars($entity->getFullPath()) : htmlspecialchars($entity->getName()), + htmlspecialchars($entity->getName()) + ); } - /** @var AbstractNamedDBElement|null $entity */ + return sprintf('%s', $value); + } - if (null !== $entity) { - if (null !== $entity->getID()) { - return sprintf( - '%s', - $this->urlGenerator->listPartsURL($entity), - htmlspecialchars($entity->getName()) - ); - } - - return sprintf('%s', $value); - } - - return ''; - }; + return ''; }); return $this; diff --git a/src/DataTables/Column/EnumColumn.php b/src/DataTables/Column/EnumColumn.php new file mode 100644 index 00000000..5a5d998d --- /dev/null +++ b/src/DataTables/Column/EnumColumn.php @@ -0,0 +1,70 @@ +. + */ +namespace App\DataTables\Column; + +use Omines\DataTablesBundle\Column\AbstractColumn; +use Symfony\Component\OptionsResolver\OptionsResolver; +use UnitEnum; + +/** + * @template T of UnitEnum + */ +class EnumColumn extends AbstractColumn +{ + + /** + * @phpstan-return T|null + */ + public function normalize($value): ?UnitEnum + { + if ($value === null) { + return null; + } + + if (is_a($value, $this->getEnumClass())) { + return $value; + } + + //@phpstan-ignore-next-line + return ($this->getEnumClass())::from($value); + } + + protected function configureOptions(OptionsResolver $resolver): static + { + parent::configureOptions($resolver); + + $resolver->setRequired('class'); + $resolver->setAllowedTypes('class', 'string'); + $resolver->addAllowedValues('class', enum_exists(...)); + + return $this; + } + + /** + * @return class-string + */ + public function getEnumClass(): string + { + return $this->options['class']; + } +} diff --git a/src/DataTables/Column/IconLinkColumn.php b/src/DataTables/Column/IconLinkColumn.php index d1034e56..6704cb4a 100644 --- a/src/DataTables/Column/IconLinkColumn.php +++ b/src/DataTables/Column/IconLinkColumn.php @@ -50,12 +50,15 @@ class IconLinkColumn extends AbstractColumn * @param $value * @return mixed */ - public function normalize($value) + public function normalize($value): mixed { return $value; } - public function configureOptions(OptionsResolver $resolver): self + /** + * @return $this + */ + public function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); $resolver->setDefaults([ diff --git a/src/DataTables/Column/LocaleDateTimeColumn.php b/src/DataTables/Column/LocaleDateTimeColumn.php index 3904a4cd..e60b2867 100644 --- a/src/DataTables/Column/LocaleDateTimeColumn.php +++ b/src/DataTables/Column/LocaleDateTimeColumn.php @@ -31,22 +31,23 @@ use Omines\DataTablesBundle\Column\AbstractColumn; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Similar to the built in DateTimeColumn, but the datetime is formatted using a IntlDateFormatter, + * Similar to the built-in DateTimeColumn, but the datetime is formatted using a IntlDateFormatter, * to get prettier locale based formatting. */ class LocaleDateTimeColumn extends AbstractColumn { /** * @param $value - * @return string * @throws Exception */ public function normalize($value): string { if (null === $value) { return $this->options['nullValue']; - } elseif (!$value instanceof DateTimeInterface) { - $value = new DateTime((string) $value); + } + + if (!$value instanceof DateTimeInterface) { + $value = new \DateTimeImmutable((string) $value); } $formatValues = [ @@ -78,7 +79,7 @@ class LocaleDateTimeColumn extends AbstractColumn ); } - protected function configureOptions(OptionsResolver $resolver): self + protected function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); diff --git a/src/DataTables/Column/LogEntryExtraColumn.php b/src/DataTables/Column/LogEntryExtraColumn.php index da6b4865..e0281e42 100644 --- a/src/DataTables/Column/LogEntryExtraColumn.php +++ b/src/DataTables/Column/LogEntryExtraColumn.php @@ -28,20 +28,15 @@ use Symfony\Contracts\Translation\TranslatorInterface; class LogEntryExtraColumn extends AbstractColumn { - protected TranslatorInterface $translator; - protected LogEntryExtraFormatter $formatter; - - public function __construct(TranslatorInterface $translator, LogEntryExtraFormatter $formatter) + public function __construct(protected TranslatorInterface $translator, protected LogEntryExtraFormatter $formatter) { - $this->translator = $translator; - $this->formatter = $formatter; } /** * @param $value * @return mixed */ - public function normalize($value) + public function normalize($value): mixed { return $value; } diff --git a/src/DataTables/Column/LogEntryTargetColumn.php b/src/DataTables/Column/LogEntryTargetColumn.php index 4aaeb069..7a329cd7 100644 --- a/src/DataTables/Column/LogEntryTargetColumn.php +++ b/src/DataTables/Column/LogEntryTargetColumn.php @@ -22,54 +22,26 @@ declare(strict_types=1); namespace App\DataTables\Column; -use App\Entity\Attachments\Attachment; -use App\Entity\Base\AbstractDBElement; -use App\Entity\Contracts\NamedElementInterface; -use App\Entity\LogSystem\AbstractLogEntry; -use App\Entity\LogSystem\UserNotAllowedLogEntry; -use App\Entity\Parameters\AbstractParameter; -use App\Entity\Parts\PartLot; -use App\Entity\PriceInformations\Orderdetail; -use App\Entity\PriceInformations\Pricedetail; -use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Exceptions\EntityNotSupportedException; -use App\Repository\LogEntryRepository; -use App\Services\ElementTypeNameGenerator; -use App\Services\EntityURLGenerator; -use Doctrine\ORM\EntityManagerInterface; +use App\Services\LogSystem\LogTargetHelper; use Omines\DataTablesBundle\Column\AbstractColumn; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Contracts\Translation\TranslatorInterface; class LogEntryTargetColumn extends AbstractColumn { - protected EntityManagerInterface $em; - protected LogEntryRepository $entryRepository; - protected EntityURLGenerator $entityURLGenerator; - protected ElementTypeNameGenerator $elementTypeNameGenerator; - protected TranslatorInterface $translator; - - public function __construct(EntityManagerInterface $entityManager, EntityURLGenerator $entityURLGenerator, - ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator) + public function __construct(private readonly LogTargetHelper $logTargetHelper) { - $this->em = $entityManager; - $this->entryRepository = $entityManager->getRepository(AbstractLogEntry::class); - - $this->entityURLGenerator = $entityURLGenerator; - $this->elementTypeNameGenerator = $elementTypeNameGenerator; - $this->translator = $translator; } /** * @param $value * @return mixed */ - public function normalize($value) + public function normalize($value): mixed { return $value; } - public function configureOptions(OptionsResolver $resolver): self + public function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); $resolver->setDefault('show_associated', true); @@ -80,71 +52,9 @@ class LogEntryTargetColumn extends AbstractColumn public function render($value, $context): string { - if ($context instanceof UserNotAllowedLogEntry && $this->options['showAccessDeniedPath']) { - return htmlspecialchars($context->getPath()); - } - - /** @var AbstractLogEntry $context */ - $target = $this->entryRepository->getTargetElement($context); - - $tmp = ''; - - //The element is existing - if ($target instanceof NamedElementInterface && !empty($target->getName())) { - try { - $tmp = sprintf( - '%s', - $this->entityURLGenerator->infoURL($target), - $this->elementTypeNameGenerator->getTypeNameCombination($target, true) - ); - } catch (EntityNotSupportedException $exception) { - $tmp = $this->elementTypeNameGenerator->getTypeNameCombination($target, true); - } - } elseif ($target instanceof AbstractDBElement) { //Target does not have a name - $tmp = sprintf( - '%s: %s', - $this->elementTypeNameGenerator->getLocalizedTypeLabel($target), - $target->getID() - ); - } elseif (null === $target && $context->hasTarget()) { //Element was deleted - $tmp = sprintf( - '%s: %s [%s]', - $this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getTargetClass()), - $context->getTargetID(), - $this->translator->trans('log.target_deleted') - ); - } - - //Add a hint to the associated element if possible - if (null !== $target && $this->options['show_associated']) { - if ($target instanceof Attachment && null !== $target->getElement()) { - $on = $target->getElement(); - } elseif ($target instanceof AbstractParameter && null !== $target->getElement()) { - $on = $target->getElement(); - } elseif ($target instanceof PartLot && null !== $target->getPart()) { - $on = $target->getPart(); - } elseif ($target instanceof Orderdetail && null !== $target->getPart()) { - $on = $target->getPart(); - } elseif ($target instanceof Pricedetail && null !== $target->getOrderdetail() && null !== $target->getOrderdetail()->getPart()) { - $on = $target->getOrderdetail()->getPart(); - } elseif ($target instanceof ProjectBOMEntry && null !== $target->getProject()) { - $on = $target->getProject(); - } - - if (isset($on) && is_object($on)) { - try { - $tmp .= sprintf( - ' (%s)', - $this->entityURLGenerator->infoURL($on), - $this->elementTypeNameGenerator->getTypeNameCombination($on, true) - ); - } catch (EntityNotSupportedException $exception) { - $tmp .= ' ('.$this->elementTypeNameGenerator->getTypeNameCombination($target, true).')'; - } - } - } - - //Log is not associated with an element - return $tmp; + return $this->logTargetHelper->formatTarget($context, [ + 'showAccessDeniedPath' => $this->options['showAccessDeniedPath'], + 'show_associated' => $this->options['show_associated'], + ]); } } diff --git a/src/DataTables/Column/MarkdownColumn.php b/src/DataTables/Column/MarkdownColumn.php index 4e3dc9ff..41f62649 100644 --- a/src/DataTables/Column/MarkdownColumn.php +++ b/src/DataTables/Column/MarkdownColumn.php @@ -27,20 +27,17 @@ use Omines\DataTablesBundle\Column\AbstractColumn; class MarkdownColumn extends AbstractColumn { - protected MarkdownParser $markdown; - - public function __construct(MarkdownParser $markdown) + public function __construct(protected MarkdownParser $markdown) { - $this->markdown = $markdown; } /** * The normalize function is responsible for converting parsed and processed data to a datatables-appropriate type. * * @param mixed $value The single value of the column - * @return mixed + * @return string */ - public function normalize($value) + public function normalize(mixed $value): string { return $this->markdown->markForRendering($value, true); } diff --git a/src/DataTables/Column/PartAttachmentsColumn.php b/src/DataTables/Column/PartAttachmentsColumn.php index 48ab3201..7b209028 100644 --- a/src/DataTables/Column/PartAttachmentsColumn.php +++ b/src/DataTables/Column/PartAttachmentsColumn.php @@ -33,15 +33,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class PartAttachmentsColumn extends AbstractColumn { - protected FAIconGenerator $FAIconGenerator; - protected EntityURLGenerator $urlGenerator; - protected AttachmentManager $attachmentManager; - - public function __construct(FAIconGenerator $FAIconGenerator, EntityURLGenerator $urlGenerator, AttachmentManager $attachmentManager) + public function __construct(protected FAIconGenerator $FAIconGenerator, protected EntityURLGenerator $urlGenerator, protected AttachmentManager $attachmentManager) { - $this->FAIconGenerator = $FAIconGenerator; - $this->urlGenerator = $urlGenerator; - $this->attachmentManager = $attachmentManager; } /** @@ -50,7 +43,7 @@ class PartAttachmentsColumn extends AbstractColumn * @param mixed $value The single value of the column * @return mixed */ - public function normalize($value) + public function normalize(mixed $value): mixed { return $value; } @@ -61,9 +54,7 @@ class PartAttachmentsColumn extends AbstractColumn throw new RuntimeException('$context must be a Part object!'); } $tmp = ''; - $attachments = $context->getAttachments()->filter(function (Attachment $attachment) { - return $attachment->getShowInTable() && $this->attachmentManager->isFileExisting($attachment); - }); + $attachments = $context->getAttachments()->filter(fn(Attachment $attachment) => $attachment->getShowInTable() && $this->attachmentManager->isFileExisting($attachment)); $count = 5; foreach ($attachments as $attachment) { @@ -88,7 +79,7 @@ class PartAttachmentsColumn extends AbstractColumn return $tmp; } - public function configureOptions(OptionsResolver $resolver): self + public function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); diff --git a/src/DataTables/Column/PrettyBoolColumn.php b/src/DataTables/Column/PrettyBoolColumn.php index 3a74db6e..912a9122 100644 --- a/src/DataTables/Column/PrettyBoolColumn.php +++ b/src/DataTables/Column/PrettyBoolColumn.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Column; use Omines\DataTablesBundle\Column\AbstractColumn; @@ -25,11 +27,8 @@ use Symfony\Contracts\Translation\TranslatorInterface; class PrettyBoolColumn extends AbstractColumn { - protected TranslatorInterface $translator; - - public function __construct(TranslatorInterface $translator) + public function __construct(protected TranslatorInterface $translator) { - $this->translator = $translator; } public function normalize($value): ?bool @@ -41,7 +40,7 @@ class PrettyBoolColumn extends AbstractColumn return (bool) $value; } - public function render($value, $context) + public function render($value, $context): string { if ($value === true) { return ' ' @@ -63,4 +62,4 @@ class PrettyBoolColumn extends AbstractColumn throw new \RuntimeException('Unexpected value!'); } -} \ No newline at end of file +} diff --git a/src/DataTables/Column/RevertLogColumn.php b/src/DataTables/Column/RevertLogColumn.php index 2b71d8f0..6ad365c1 100644 --- a/src/DataTables/Column/RevertLogColumn.php +++ b/src/DataTables/Column/RevertLogColumn.php @@ -41,30 +41,25 @@ declare(strict_types=1); namespace App\DataTables\Column; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; use Omines\DataTablesBundle\Column\AbstractColumn; -use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; class RevertLogColumn extends AbstractColumn { - protected TranslatorInterface $translator; - protected Security $security; - - public function __construct(TranslatorInterface $translator, Security $security) + public function __construct(protected TranslatorInterface $translator, protected Security $security) { - $this->translator = $translator; - $this->security = $security; } /** * @param $value * @return mixed */ - public function normalize($value) + public function normalize($value): mixed { return $value; } @@ -73,13 +68,13 @@ class RevertLogColumn extends AbstractColumn { if ( $context instanceof CollectionElementDeleted - || ($context instanceof ElementDeletedLogEntry && $context->hasOldDataInformations()) + || ($context instanceof ElementDeletedLogEntry && $context->hasOldDataInformation()) ) { $icon = 'fa-trash-restore'; $title = $this->translator->trans('log.undo.undelete'); } elseif ( $context instanceof ElementCreatedLogEntry - || ($context instanceof ElementEditedLogEntry && $context->hasOldDataInformations()) + || ($context instanceof ElementEditedLogEntry && $context->hasOldDataInformation()) ) { $icon = 'fa-undo'; $title = $this->translator->trans('log.undo.undo'); @@ -105,8 +100,6 @@ class RevertLogColumn extends AbstractColumn $this->translator->trans('log.undo.revert') ); - $tmp .= '
'; - - return $tmp; + return $tmp . '
'; } } diff --git a/src/DataTables/Column/RowClassColumn.php b/src/DataTables/Column/RowClassColumn.php index 4ac61c02..15bf8bf2 100644 --- a/src/DataTables/Column/RowClassColumn.php +++ b/src/DataTables/Column/RowClassColumn.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Column; use Omines\DataTablesBundle\Column\AbstractColumn; @@ -26,8 +28,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class RowClassColumn extends AbstractColumn { - - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); @@ -42,14 +43,17 @@ class RowClassColumn extends AbstractColumn return $this; } - public function initialize(string $name, int $index, array $options, DataTable $dataTable) + public function initialize(string $name, int $index, array $options, DataTable $dataTable): void { //The field name is always "$$rowClass" as this is the name the frontend controller expects parent::initialize('$$rowClass', $index, $options, $dataTable); // TODO: Change the autogenerated stub } - public function normalize($value) + /** + * @return mixed + */ + public function normalize($value): mixed { return $value; } -} \ No newline at end of file +} diff --git a/src/DataTables/Column/SIUnitNumberColumn.php b/src/DataTables/Column/SIUnitNumberColumn.php index bc9d866f..b64152be 100644 --- a/src/DataTables/Column/SIUnitNumberColumn.php +++ b/src/DataTables/Column/SIUnitNumberColumn.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Column; use App\Services\Formatters\SIFormatter; @@ -26,14 +28,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class SIUnitNumberColumn extends AbstractColumn { - protected SIFormatter $formatter; - - public function __construct(SIFormatter $formatter) + public function __construct(protected SIFormatter $formatter) { - $this->formatter = $formatter; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); @@ -43,7 +42,7 @@ class SIUnitNumberColumn extends AbstractColumn return $this; } - public function normalize($value) + public function normalize($value): string { //Ignore null values if ($value === null) { @@ -52,4 +51,4 @@ class SIUnitNumberColumn extends AbstractColumn return htmlspecialchars($this->formatter->format((float) $value, $this->options['unit'], $this->options['precision'])); } -} \ No newline at end of file +} diff --git a/src/DataTables/Column/SelectColumn.php b/src/DataTables/Column/SelectColumn.php index 218f022d..39445ac8 100644 --- a/src/DataTables/Column/SelectColumn.php +++ b/src/DataTables/Column/SelectColumn.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Column; use Omines\DataTablesBundle\Column\AbstractColumn; @@ -28,7 +30,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; */ class SelectColumn extends AbstractColumn { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): static { parent::configureOptions($resolver); @@ -36,21 +38,24 @@ class SelectColumn extends AbstractColumn 'label' => '', 'orderable' => false, 'searchable' => false, - 'className' => 'select-checkbox no-colvis', + 'className' => 'dt-select no-colvis', 'visible' => true, ]); return $this; } - public function normalize($value) + /** + * @return mixed + */ + public function normalize($value): mixed { return $value; } - public function render($value, $context) + public function render($value, $context): string { //Return empty string, as it this column is filled by datatables on client side return ''; } -} \ No newline at end of file +} diff --git a/src/DataTables/Column/TagsColumn.php b/src/DataTables/Column/TagsColumn.php index 4d0dee0a..f98a3900 100644 --- a/src/DataTables/Column/TagsColumn.php +++ b/src/DataTables/Column/TagsColumn.php @@ -27,11 +27,8 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class TagsColumn extends AbstractColumn { - protected UrlGeneratorInterface $urlGenerator; - - public function __construct(UrlGeneratorInterface $urlGenerator) + public function __construct(protected UrlGeneratorInterface $urlGenerator) { - $this->urlGenerator = $urlGenerator; } /** @@ -40,17 +37,21 @@ class TagsColumn extends AbstractColumn * @param mixed $value The single value of the column * @return mixed */ - public function normalize($value) + public function normalize(mixed $value): mixed { if (empty($value)) { return []; } - return explode(',', $value); + return explode(',', (string) $value); } public function render($tags, $context): string { + if (!is_iterable($tags)) { + throw new \LogicException('TagsColumn::render() expects an iterable'); + } + $html = ''; $count = 10; foreach ($tags as $tag) { @@ -61,7 +62,7 @@ class TagsColumn extends AbstractColumn $html .= sprintf( '%s', $this->urlGenerator->generate('part_list_tags', ['tag' => $tag]), - htmlspecialchars($tag) + htmlspecialchars((string) $tag) ); } diff --git a/src/DataTables/ErrorDataTable.php b/src/DataTables/ErrorDataTable.php new file mode 100644 index 00000000..833ea934 --- /dev/null +++ b/src/DataTables/ErrorDataTable.php @@ -0,0 +1,87 @@ +. + */ +namespace App\DataTables; + +use App\DataTables\Column\RowClassColumn; +use Omines\DataTablesBundle\Adapter\ArrayAdapter; +use Omines\DataTablesBundle\Column\TextColumn; +use Omines\DataTablesBundle\DataTable; +use Omines\DataTablesBundle\DataTableFactory; +use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ErrorDataTable implements DataTableTypeInterface +{ + public function configureOptions(OptionsResolver $optionsResolver): void + { + $optionsResolver->setRequired('errors'); + $optionsResolver->setAllowedTypes('errors', ['array', 'string']); + $optionsResolver->setNormalizer('errors', function (OptionsResolver $optionsResolver, $errors) { + if (is_string($errors)) { + $errors = [$errors]; + } + + return $errors; + }); + } + + public function configure(DataTable $dataTable, array $options): void + { + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $options = $optionsResolver->resolve($options); + + $dataTable + ->add('dont_matter_we_only_set_color', RowClassColumn::class, [ + 'render' => fn($value, $context): string => 'table-warning', + ]) + + ->add('error', TextColumn::class, [ + 'label' => 'error_table.error', + 'render' => fn($value, $context): string => ' ' . $value, + ]) + ; + + //Build the array containing data + $data = []; + $n = 0; + foreach ($options['errors'] as $error) { + $data['error_' . $n] = ['error' => $error]; + $n++; + } + + $dataTable->createAdapter(ArrayAdapter::class, $data); + } + + /** + * @param string[]|string $errors + */ + public static function errorTable(DataTableFactory $dataTableFactory, Request $request, array|string $errors): Response + { + $error_table = $dataTableFactory->createFromType(self::class, ['errors' => $errors]); + $error_table->handleRequest($request); + return $error_table->getResponse(); + } +} diff --git a/src/DataTables/Filters/AttachmentFilter.php b/src/DataTables/Filters/AttachmentFilter.php index 9325bd60..d41bbe39 100644 --- a/src/DataTables/Filters/AttachmentFilter.php +++ b/src/DataTables/Filters/AttachmentFilter.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters; use App\DataTables\Filters\Constraints\BooleanConstraint; @@ -35,13 +37,16 @@ class AttachmentFilter implements FilterInterface { use CompoundFilterTrait; - protected NumberConstraint $dbId; - protected InstanceOfConstraint $targetType; - protected TextConstraint $name; - protected EntityConstraint $attachmentType; - protected BooleanConstraint $showInTable; - protected DateTimeConstraint $lastModified; - protected DateTimeConstraint $addedDate; + public readonly NumberConstraint $dbId; + public readonly InstanceOfConstraint $targetType; + public readonly TextConstraint $name; + public readonly EntityConstraint $attachmentType; + public readonly BooleanConstraint $showInTable; + public readonly DateTimeConstraint $lastModified; + public readonly DateTimeConstraint $addedDate; + + public readonly TextConstraint $originalFileName; + public readonly TextConstraint $externalLink; public function __construct(NodesListBuilder $nodesListBuilder) @@ -53,74 +58,13 @@ class AttachmentFilter implements FilterInterface $this->lastModified = new DateTimeConstraint('attachment.lastModified'); $this->addedDate = new DateTimeConstraint('attachment.addedDate'); $this->showInTable = new BooleanConstraint('attachment.show_in_table'); + $this->originalFileName = new TextConstraint('attachment.original_filename'); + $this->externalLink = new TextConstraint('attachment.external_path'); + } public function apply(QueryBuilder $queryBuilder): void { $this->applyAllChildFilters($queryBuilder); } - - /** - * @return NumberConstraint - */ - public function getDbId(): NumberConstraint - { - return $this->dbId; - } - - /** - * @return TextConstraint - */ - public function getName(): TextConstraint - { - return $this->name; - } - - /** - * @return DateTimeConstraint - */ - public function getLastModified(): DateTimeConstraint - { - return $this->lastModified; - } - - /** - * @return DateTimeConstraint - */ - public function getAddedDate(): DateTimeConstraint - { - return $this->addedDate; - } - - - /** - * @return BooleanConstraint - */ - public function getShowInTable(): BooleanConstraint - { - return $this->showInTable; - } - - - /** - * @return EntityConstraint - */ - public function getAttachmentType(): EntityConstraint - { - return $this->attachmentType; - } - - /** - * @return InstanceOfConstraint - */ - public function getTargetType(): InstanceOfConstraint - { - return $this->targetType; - } - - - - - - -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/CompoundFilterTrait.php b/src/DataTables/Filters/CompoundFilterTrait.php index ba778aac..5e722841 100644 --- a/src/DataTables/Filters/CompoundFilterTrait.php +++ b/src/DataTables/Filters/CompoundFilterTrait.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters; use Doctrine\Common\Collections\Collection; @@ -37,9 +39,6 @@ trait CompoundFilterTrait $reflection = new \ReflectionClass($this); foreach ($reflection->getProperties() as $property) { - //Set property to accessible (otherwise we run into problems on PHP < 8.1) - $property->setAccessible(true); - $value = $property->getValue($this); //We only want filters (objects implementing FilterInterface) if($value instanceof FilterInterface) { @@ -60,8 +59,6 @@ trait CompoundFilterTrait /** * Applies all children filters that are declared as property of this filter using reflection. - * @param QueryBuilder $queryBuilder - * @return void */ protected function applyAllChildFilters(QueryBuilder $queryBuilder): void { @@ -72,4 +69,4 @@ trait CompoundFilterTrait $filter->apply($queryBuilder); } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/AbstractConstraint.php b/src/DataTables/Filters/Constraints/AbstractConstraint.php index 0a888214..7f16511e 100644 --- a/src/DataTables/Filters/Constraints/AbstractConstraint.php +++ b/src/DataTables/Filters/Constraints/AbstractConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; use App\DataTables\Filters\FilterInterface; -use Doctrine\ORM\QueryBuilder; abstract class AbstractConstraint implements FilterInterface { use FilterTrait; - /** - * @var string The property where this BooleanConstraint should apply to - */ - protected string $property; - /** * @var string */ @@ -44,9 +40,13 @@ abstract class AbstractConstraint implements FilterInterface */ abstract public function isEnabled(): bool; - public function __construct(string $property, string $identifier = null) + public function __construct( + /** + * @var string The property where this BooleanConstraint should apply to + */ + protected string $property, + ?string $identifier = null) { - $this->property = $property; $this->identifier = $identifier ?? $this->generateParameterIdentifier($property); } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/BooleanConstraint.php b/src/DataTables/Filters/Constraints/BooleanConstraint.php index b180f6aa..8eb4f042 100644 --- a/src/DataTables/Filters/Constraints/BooleanConstraint.php +++ b/src/DataTables/Filters/Constraints/BooleanConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; -use App\DataTables\Filters\FilterInterface; use Doctrine\ORM\QueryBuilder; class BooleanConstraint extends AbstractConstraint { - /** @var bool|null The value of our constraint */ - protected ?bool $value; - - - public function __construct(string $property, string $identifier = null, ?bool $default_value = null) + public function __construct( + string $property, + ?string $identifier = null, + /** @var bool|null The value of our constraint */ + protected ?bool $value = null + ) { parent::__construct($property, $identifier); - $this->value = $default_value; } /** * Gets the value of this constraint. Null means "don't filter", true means "filter for true", false means "filter for false". - * @return bool|null */ public function getValue(): ?bool { @@ -46,7 +46,6 @@ class BooleanConstraint extends AbstractConstraint /** * Sets the value of this constraint. Null means "don't filter", true means "filter for true", false means "filter for false". - * @param bool|null $value */ public function setValue(?bool $value): void { @@ -68,4 +67,4 @@ class BooleanConstraint extends AbstractConstraint $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, '=', $this->value); } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/ChoiceConstraint.php b/src/DataTables/Filters/Constraints/ChoiceConstraint.php index 52c0739f..cce7ce2c 100644 --- a/src/DataTables/Filters/Constraints/ChoiceConstraint.php +++ b/src/DataTables/Filters/Constraints/ChoiceConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; use Doctrine\ORM\QueryBuilder; class ChoiceConstraint extends AbstractConstraint { - public const ALLOWED_OPERATOR_VALUES = ['ANY', 'NONE']; + final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'NONE']; /** * @var string[]|int[] The values to compare to */ - protected array $value; + protected array $value = []; /** * @var string The operator to use */ - protected string $operator; + protected string $operator = ""; /** * @return string[]|int[] @@ -46,7 +48,6 @@ class ChoiceConstraint extends AbstractConstraint /** * @param string[]|int[] $value - * @return ChoiceConstraint */ public function setValue(array $value): ChoiceConstraint { @@ -54,18 +55,11 @@ class ChoiceConstraint extends AbstractConstraint return $this; } - /** - * @return string - */ public function getOperator(): string { return $this->operator; } - /** - * @param string $operator - * @return ChoiceConstraint - */ public function setOperator(string $operator): ChoiceConstraint { $this->operator = $operator; @@ -76,7 +70,7 @@ class ChoiceConstraint extends AbstractConstraint public function isEnabled(): bool { - return !empty($this->operator); + return $this->operator !== '' && count($this->value) > 0; } public function apply(QueryBuilder $queryBuilder): void @@ -99,4 +93,4 @@ class ChoiceConstraint extends AbstractConstraint throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/DateTimeConstraint.php b/src/DataTables/Filters/Constraints/DateTimeConstraint.php index 39eaaa1d..a3043170 100644 --- a/src/DataTables/Filters/Constraints/DateTimeConstraint.php +++ b/src/DataTables/Filters/Constraints/DateTimeConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; +use Doctrine\ORM\QueryBuilder; +use RuntimeException; + /** - * An alias of NumberConstraint to use to filter on a DateTime + * Similar to NumberConstraint but for DateTime values */ -class DateTimeConstraint extends NumberConstraint +class DateTimeConstraint extends AbstractConstraint { -} \ No newline at end of file + protected const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN']; + + public function __construct( + string $property, + ?string $identifier = null, + /** + * The value1 used for comparison (this is the main one used for all mono-value comparisons) + */ + protected \DateTimeInterface|null $value1 = null, + protected ?string $operator = null, + /** + * The second value used when operator is RANGE; this is the upper bound of the range + */ + protected \DateTimeInterface|null $value2 = null) + { + parent::__construct($property, $identifier); + } + + public function getValue1(): ?\DateTimeInterface + { + return $this->value1; + } + + public function setValue1(\DateTimeInterface|null $value1): void + { + $this->value1 = $value1; + } + + public function getValue2(): ?\DateTimeInterface + { + return $this->value2; + } + + public function setValue2(?\DateTimeInterface $value2): void + { + $this->value2 = $value2; + } + + public function getOperator(): string|null + { + return $this->operator; + } + + /** + * @param string $operator + */ + public function setOperator(?string $operator): void + { + $this->operator = $operator; + } + + public function isEnabled(): bool + { + return $this->value1 !== null + && ($this->operator !== null && $this->operator !== ''); + } + + public function apply(QueryBuilder $queryBuilder): void + { + //If no value is provided then we do not apply a filter + if (!$this->isEnabled()) { + return; + } + + //Ensure we have an valid operator + if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); + } + + if ($this->operator !== 'BETWEEN') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value1); + } else { + if ($this->value2 === null) { + throw new RuntimeException("Cannot use operator BETWEEN without value2!"); + } + + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1); + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2); + } + } +} diff --git a/src/DataTables/Filters/Constraints/EntityConstraint.php b/src/DataTables/Filters/Constraints/EntityConstraint.php index facbbfea..c75da80d 100644 --- a/src/DataTables/Filters/Constraints/EntityConstraint.php +++ b/src/DataTables/Filters/Constraints/EntityConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; use App\Entity\Base\AbstractDBElement; @@ -33,46 +35,26 @@ class EntityConstraint extends AbstractConstraint private const ALLOWED_OPERATOR_VALUES_BASE = ['=', '!=']; private const ALLOWED_OPERATOR_VALUES_STRUCTURAL = ['INCLUDING_CHILDREN', 'EXCLUDING_CHILDREN']; - /** - * @var NodesListBuilder - */ - protected ?NodesListBuilder $nodesListBuilder; - - /** - * @var class-string The class to use for the comparison - */ - protected string $class; - - /** - * @var string|null The operator to use - */ - protected ?string $operator; - - /** - * @var T The value to compare to - */ - protected $value; - /** * @param NodesListBuilder|null $nodesListBuilder * @param class-string $class * @param string $property * @param string|null $identifier - * @param null $value - * @param string $operator + * @param T|null $value + * @param string|null $operator */ - public function __construct(?NodesListBuilder $nodesListBuilder, string $class, string $property, string $identifier = null, $value = null, string $operator = '') + public function __construct(protected ?NodesListBuilder $nodesListBuilder, + protected string $class, + string $property, + ?string $identifier = null, + protected ?AbstractDBElement $value = null, + protected ?string $operator = null) { - $this->nodesListBuilder = $nodesListBuilder; - $this->class = $class; - - if ($nodesListBuilder === null && $this->isStructural()) { + if (!$nodesListBuilder instanceof NodesListBuilder && $this->isStructural()) { throw new \InvalidArgumentException('NodesListBuilder must be provided for structural entities'); } parent::__construct($property, $identifier); - $this->value = $value; - $this->operator = $operator; } public function getClass(): string @@ -80,17 +62,11 @@ class EntityConstraint extends AbstractConstraint return $this->class; } - /** - * @return string|null - */ public function getOperator(): ?string { return $this->operator; } - /** - * @param string|null $operator - */ public function setOperator(?string $operator): self { $this->operator = $operator; @@ -106,9 +82,10 @@ class EntityConstraint extends AbstractConstraint } /** - * @param T|null $value + * @param AbstractDBElement|null $value + * @phpstan-param T|null $value */ - public function setValue(?AbstractDBElement $value): void + public function setValue(AbstractDBElement|null $value): void { if (!$value instanceof $this->class) { throw new \InvalidArgumentException('The value must be an instance of ' . $this->class); @@ -119,7 +96,7 @@ class EntityConstraint extends AbstractConstraint /** * Checks whether the constraints apply to a structural type or not - * @return bool + * @phpstan-assert-if-true AbstractStructuralDBElement $this->value */ public function isStructural(): bool { @@ -136,7 +113,7 @@ class EntityConstraint extends AbstractConstraint $tmp = self::ALLOWED_OPERATOR_VALUES_BASE; if ($this->isStructural()) { - $tmp = array_merge($tmp, self::ALLOWED_OPERATOR_VALUES_STRUCTURAL); + $tmp = [...$tmp, ...self::ALLOWED_OPERATOR_VALUES_STRUCTURAL]; } return $tmp; @@ -144,7 +121,7 @@ class EntityConstraint extends AbstractConstraint public function isEnabled(): bool { - return !empty($this->operator); + return $this->operator !== null && $this->operator !== ''; } public function apply(QueryBuilder $queryBuilder): void @@ -175,8 +152,9 @@ class EntityConstraint extends AbstractConstraint } if($this->operator === '=' || $this->operator === '!=') { - $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value); - return; + //Include null values on != operator, so that really all values are returned that are not equal to the given value + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value, $this->operator === '!='); + return; } //Otherwise retrieve the children list and apply the operator to it @@ -191,7 +169,8 @@ class EntityConstraint extends AbstractConstraint } if ($this->operator === 'EXCLUDING_CHILDREN') { - $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $list); + //Include null values in the result, so that all elements that are not in the list are returned + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $list, true); return; } } else { @@ -199,4 +178,4 @@ class EntityConstraint extends AbstractConstraint } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/FilterTrait.php b/src/DataTables/Filters/Constraints/FilterTrait.php index 988890ba..3260e4e3 100644 --- a/src/DataTables/Filters/Constraints/FilterTrait.php +++ b/src/DataTables/Filters/Constraints/FilterTrait.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; -use Doctrine\DBAL\ParameterType; use Doctrine\ORM\QueryBuilder; trait FilterTrait @@ -28,7 +29,7 @@ trait FilterTrait protected bool $useHaving = false; - public function useHaving($value = true): self + public function useHaving($value = true): static { $this->useHaving = $value; return $this; @@ -36,7 +37,6 @@ trait FilterTrait /** * Checks if the given input is an aggregateFunction like COUNT(part.partsLot) or so - * @return bool */ protected function isAggregateFunctionString(string $input): bool { @@ -45,26 +45,25 @@ trait FilterTrait /** * Generates a parameter identifier that can be used for the given property. It gives random results, to be unique, so you have to cache it. - * @param string $property - * @return string */ protected function generateParameterIdentifier(string $property): string { //Replace all special characters with underscores - $property = preg_replace('/[^a-zA-Z0-9_]/', '_', $property); + $property = preg_replace('/\W/', '_', $property); //Add a random number to the end of the property name for uniqueness return $property . '_' . uniqid("", false); } /** * Adds a simple constraint in the form of (property OPERATOR value) (e.g. "part.name = :name") to the given query builder. - * @param QueryBuilder $queryBuilder - * @param string $property - * @param string $comparison_operator - * @param mixed $value - * @return void + * @param QueryBuilder $queryBuilder The query builder to add the constraint to + * @param string $property The property to compare + * @param string $parameterIdentifier The identifier for the parameter + * @param string $comparison_operator The comparison operator to use + * @param mixed $value The value to compare to + * @param bool $include_null If true, the result of this constraint will also include null values of this property (useful for exclusion filters) */ - protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, $value): void + protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, mixed $value, bool $include_null = false): void { if ($comparison_operator === 'IN' || $comparison_operator === 'NOT IN') { $expression = sprintf("%s %s (:%s)", $property, $comparison_operator, $parameterIdentifier); @@ -72,6 +71,10 @@ trait FilterTrait $expression = sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier); } + if ($include_null) { + $expression = sprintf("(%s OR %s IS NULL)", $expression, $property); + } + if($this->useHaving || $this->isAggregateFunctionString($property)) { //If the property is an aggregate function, we have to use the "having" instead of the "where" $queryBuilder->andHaving($expression); } else { @@ -80,4 +83,4 @@ trait FilterTrait $queryBuilder->setParameter($parameterIdentifier, $value); } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/InstanceOfConstraint.php b/src/DataTables/Filters/Constraints/InstanceOfConstraint.php index 6efe8cce..7fc242d7 100644 --- a/src/DataTables/Filters/Constraints/InstanceOfConstraint.php +++ b/src/DataTables/Filters/Constraints/InstanceOfConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; use Doctrine\ORM\QueryBuilder; @@ -27,17 +29,17 @@ use Doctrine\ORM\QueryBuilder; */ class InstanceOfConstraint extends AbstractConstraint { - public const ALLOWED_OPERATOR_VALUES = ['ANY', 'NONE']; + final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'NONE']; /** * @var string[] The values to compare to (fully qualified class names) */ - protected array $value; + protected array $value = []; /** * @var string The operator to use */ - protected string $operator; + protected string $operator = ""; /** * @return string[] @@ -57,16 +59,12 @@ class InstanceOfConstraint extends AbstractConstraint return $this; } - /** - * @return string - */ public function getOperator(): string { return $this->operator; } /** - * @param string $operator * @return $this */ public function setOperator(string $operator): self @@ -79,7 +77,7 @@ class InstanceOfConstraint extends AbstractConstraint public function isEnabled(): bool { - return !empty($this->operator); + return $this->operator !== '' && count($this->value) > 0; } public function apply(QueryBuilder $queryBuilder): void @@ -96,9 +94,10 @@ class InstanceOfConstraint extends AbstractConstraint $expressions = []; + /** @phpstan-ignore-next-line */ if ($this->operator === 'ANY' || $this->operator === 'NONE') { foreach($this->value as $value) { - //We cannnot use an paramater here, as this is the only way to pass the FCQN to the query (via binded params, we would need to use ClassMetaData). See: https://github.com/doctrine/orm/issues/4462 + //We can not use a parameter here, as this is the only way to pass the FCQN to the query (via binded params, we would need to use ClassMetaData). See: https://github.com/doctrine/orm/issues/4462 $expressions[] = ($queryBuilder->expr()->isInstanceOf($this->property, $value)); } @@ -111,4 +110,4 @@ class InstanceOfConstraint extends AbstractConstraint throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES)); } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/IntConstraint.php b/src/DataTables/Filters/Constraints/IntConstraint.php index 601f6aa8..3fc5cce5 100644 --- a/src/DataTables/Filters/Constraints/IntConstraint.php +++ b/src/DataTables/Filters/Constraints/IntConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; use Doctrine\ORM\QueryBuilder; @@ -35,4 +37,4 @@ class IntConstraint extends NumberConstraint parent::apply($queryBuilder); } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/NumberConstraint.php b/src/DataTables/Filters/Constraints/NumberConstraint.php index e8382723..dc7cf733 100644 --- a/src/DataTables/Filters/Constraints/NumberConstraint.php +++ b/src/DataTables/Filters/Constraints/NumberConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; -use Doctrine\DBAL\ParameterType; use Doctrine\ORM\QueryBuilder; use RuntimeException; class NumberConstraint extends AbstractConstraint { - public const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN']; + protected const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN']; + public function __construct( + string $property, + ?string $identifier = null, + /** + * The value1 used for comparison (this is the main one used for all mono-value comparisons) + */ + protected float|int|null $value1 = null, + protected ?string $operator = null, + /** + * The second value used when operator is RANGE; this is the upper bound of the range + */ + protected float|int|null $value2 = null) + { + parent::__construct($property, $identifier); + } - /** - * The value1 used for comparison (this is the main one used for all mono-value comparisons) - * @var float|null|int|\DateTimeInterface - */ - protected $value1; - - /** - * The second value used when operator is RANGE; this is the upper bound of the range - * @var float|null|int|\DateTimeInterface - */ - protected $value2; - - /** - * @var string The operator to use - */ - protected ?string $operator; - - /** - * @return float|int|null|\DateTimeInterface - */ - public function getValue1() + public function getValue1(): float|int|null { return $this->value1; } - /** - * @param float|int|\DateTimeInterface|null $value1 - */ - public function setValue1($value1): void + public function setValue1(float|int|null $value1): void { $this->value1 = $value1; } - /** - * @return float|int|null - */ - public function getValue2() + public function getValue2(): float|int|null { return $this->value2; } - /** - * @param float|int|null $value2 - */ - public function setValue2($value2): void + public function setValue2(float|int|null $value2): void { $this->value2 = $value2; } - /** - * @return string - */ - public function getOperator(): string + public function getOperator(): string|null { return $this->operator; } @@ -95,18 +79,10 @@ class NumberConstraint extends AbstractConstraint } - public function __construct(string $property, string $identifier = null, $value1 = null, string $operator = null, $value2 = null) - { - parent::__construct($property, $identifier); - $this->value1 = $value1; - $this->value2 = $value2; - $this->operator = $operator; - } - public function isEnabled(): bool { return $this->value1 !== null - && !empty($this->operator); + && ($this->operator !== null && $this->operator !== ''); } public function apply(QueryBuilder $queryBuilder): void @@ -129,7 +105,13 @@ class NumberConstraint extends AbstractConstraint } $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1); - $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2); + + //Workaround for the amountSum which we need to add twice on postgres. Replace one of the __ with __2 to make it work + //Otherwise we get an error, that __partLot was already defined + + $property2 = str_replace('__', '__2', $this->property); + + $this->addSimpleAndConstraint($queryBuilder, $property2, $this->identifier . '2', '<=', $this->value2); } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php b/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php index 34d5c157..011824e5 100644 --- a/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php +++ b/src/DataTables/Filters/Constraints/Part/LessThanDesiredConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints\Part; use App\DataTables\Filters\Constraints\BooleanConstraint; +use App\Entity\Parts\PartLot; use Doctrine\ORM\QueryBuilder; class LessThanDesiredConstraint extends BooleanConstraint { - public function __construct(string $property = null, string $identifier = null, ?bool $default_value = null) + public function __construct(?string $property = null, ?string $identifier = null, ?bool $default_value = null) { - parent::__construct($property ?? 'amountSum', $identifier, $default_value); + parent::__construct($property ?? '( + SELECT COALESCE(SUM(ld_partLot.amount), 0.0) + FROM '.PartLot::class.' ld_partLot + WHERE ld_partLot.part = part.id + AND ld_partLot.instock_unknown = false + AND (ld_partLot.expiration_date IS NULL OR ld_partLot.expiration_date > CURRENT_DATE()) + )', $identifier ?? 'amountSumLessThanDesired', $default_value); } public function apply(QueryBuilder $queryBuilder): void @@ -39,9 +48,9 @@ class LessThanDesiredConstraint extends BooleanConstraint //If value is true, we want to filter for parts with stock < desired stock if ($this->value) { - $queryBuilder->andHaving('amountSum < part.minamount'); + $queryBuilder->andHaving( $this->property . ' < part.minamount'); } else { - $queryBuilder->andHaving('amountSum >= part.minamount'); + $queryBuilder->andHaving($this->property . ' >= part.minamount'); } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/Part/ParameterConstraint.php b/src/DataTables/Filters/Constraints/Part/ParameterConstraint.php index 76e39cdf..e68dd989 100644 --- a/src/DataTables/Filters/Constraints/Part/ParameterConstraint.php +++ b/src/DataTables/Filters/Constraints/Part/ParameterConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints\Part; use App\DataTables\Filters\Constraints\AbstractConstraint; use App\DataTables\Filters\Constraints\TextConstraint; use App\Entity\Parameters\PartParameter; use Doctrine\ORM\QueryBuilder; -use Svg\Tag\Text; class ParameterConstraint extends AbstractConstraint { - /** @var string */ - protected string $name; + protected string $name = ''; - /** @var string */ - protected string $symbol; + protected string $symbol = ''; - /** @var string */ - protected string $unit; + protected string $unit = ''; - /** @var TextConstraint */ protected TextConstraint $value_text; - - /** @var ParameterValueConstraint */ protected ParameterValueConstraint $value; /** @var string The alias to use for the subquery */ @@ -73,19 +68,19 @@ class ParameterConstraint extends AbstractConstraint ->from(PartParameter::class, $this->alias) ->where($this->alias . '.element = part'); - if (!empty($this->name)) { + if ($this->name !== '') { $paramName = $this->generateParameterIdentifier('params.name'); $subqb->andWhere($this->alias . '.name = :' . $paramName); $queryBuilder->setParameter($paramName, $this->name); } - if (!empty($this->symbol)) { + if ($this->symbol !== '') { $paramName = $this->generateParameterIdentifier('params.symbol'); $subqb->andWhere($this->alias . '.symbol = :' . $paramName); $queryBuilder->setParameter($paramName, $this->symbol); } - if (!empty($this->unit)) { + if ($this->unit !== '') { $paramName = $this->generateParameterIdentifier('params.unit'); $subqb->andWhere($this->alias . '.unit = :' . $paramName); $queryBuilder->setParameter($paramName, $this->unit); @@ -104,75 +99,48 @@ class ParameterConstraint extends AbstractConstraint $queryBuilder->andWhere('(' . $subqb->getDQL() . ') > 0'); } - /** - * @return string - */ public function getName(): string { return $this->name; } - /** - * @param string $name - * @return ParameterConstraint - */ public function setName(string $name): ParameterConstraint { $this->name = $name; return $this; } - /** - * @return string - */ public function getSymbol(): string { return $this->symbol; } - /** - * @param string $symbol - * @return ParameterConstraint - */ public function setSymbol(string $symbol): ParameterConstraint { $this->symbol = $symbol; return $this; } - /** - * @return string - */ public function getUnit(): string { return $this->unit; } - /** - * @param string $unit - * @return ParameterConstraint - */ public function setUnit(string $unit): ParameterConstraint { $this->unit = $unit; return $this; } - /** - * @return TextConstraint - */ public function getValueText(): TextConstraint { return $this->value_text; } - /** - * @return ParameterValueConstraint - */ public function getValue(): ParameterValueConstraint { return $this->value; } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/Part/ParameterValueConstraint.php b/src/DataTables/Filters/Constraints/Part/ParameterValueConstraint.php index 5da64098..469c18c6 100644 --- a/src/DataTables/Filters/Constraints/Part/ParameterValueConstraint.php +++ b/src/DataTables/Filters/Constraints/Part/ParameterValueConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints\Part; use App\DataTables\Filters\Constraints\NumberConstraint; @@ -25,18 +27,14 @@ use Doctrine\ORM\QueryBuilder; class ParameterValueConstraint extends NumberConstraint { - protected string $alias; - - public const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN', + protected const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN', //Additional operators 'IN_RANGE', 'NOT_IN_RANGE', 'GREATER_THAN_RANGE', 'GREATER_EQUAL_RANGE', 'LESS_THAN_RANGE', 'LESS_EQUAL_RANGE', 'RANGE_IN_RANGE', 'RANGE_INTERSECT_RANGE']; /** * @param string $alias The alias which is used in the sub query of ParameterConstraint */ - public function __construct(string $alias) { - $this->alias = $alias; - + public function __construct(protected string $alias) { parent::__construct($alias . '.value_typical'); } @@ -145,4 +143,4 @@ class ParameterValueConstraint extends NumberConstraint //For all other cases use the default implementation parent::apply($queryBuilder); } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/Part/TagsConstraint.php b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php index 92c78f6a..02eab7a1 100644 --- a/src/DataTables/Filters/Constraints/Part/TagsConstraint.php +++ b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints\Part; +use Doctrine\ORM\Query\Expr\Orx; use App\DataTables\Filters\Constraints\AbstractConstraint; -use Doctrine\ORM\Query\Expr; use Doctrine\ORM\QueryBuilder; class TagsConstraint extends AbstractConstraint { - public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE']; + final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE']; - /** - * @var string|null The operator to use - */ - protected ?string $operator; - - /** - * @var string The value to compare to - */ - protected $value; - - public function __construct(string $property, string $identifier = null, $value = null, string $operator = '') + public function __construct(string $property, ?string $identifier = null, + protected ?string $value = null, + protected ?string $operator = '') { parent::__construct($property, $identifier); - $this->value = $value; - $this->operator = $operator; } /** @@ -62,18 +54,12 @@ class TagsConstraint extends AbstractConstraint return $this; } - /** - * @return string - */ - public function getValue(): string + public function getValue(): ?string { return $this->value; } - /** - * @param string $value - */ - public function setValue(string $value): self + public function setValue(?string $value): self { $this->value = $value; return $this; @@ -82,7 +68,7 @@ class TagsConstraint extends AbstractConstraint public function isEnabled(): bool { return $this->value !== null - && !empty($this->operator); + && ($this->operator !== null && $this->operator !== ''); } /** @@ -96,21 +82,21 @@ class TagsConstraint extends AbstractConstraint /** * Builds an expression to query for a single tag - * @param QueryBuilder $queryBuilder - * @param string $tag - * @return Expr\Orx */ - protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Expr\Orx + protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Orx { + //Escape any %, _ or \ in the tag + $tag = addcslashes($tag, '%_\\'); + $tag_identifier_prefix = uniqid($this->identifier . '_', false); $expr = $queryBuilder->expr(); $tmp = $expr->orX( - $expr->like($this->property, ':' . $tag_identifier_prefix . '_1'), - $expr->like($this->property, ':' . $tag_identifier_prefix . '_2'), - $expr->like($this->property, ':' . $tag_identifier_prefix . '_3'), - $expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'), + 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_1) = TRUE', + 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_2) = TRUE', + 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_3) = TRUE', + 'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_4) = TRUE', ); //Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag) @@ -147,9 +133,10 @@ class TagsConstraint extends AbstractConstraint return; } + //@phpstan-ignore-next-line Keep this check to ensure that everything has the same structure even if we add a new operator if ($this->operator === 'NONE') { $queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions))); return; } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/Constraints/TextConstraint.php b/src/DataTables/Filters/Constraints/TextConstraint.php index 3cd53973..31b12a5e 100644 --- a/src/DataTables/Filters/Constraints/TextConstraint.php +++ b/src/DataTables/Filters/Constraints/TextConstraint.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters\Constraints; use Doctrine\ORM\QueryBuilder; @@ -25,23 +27,20 @@ use Doctrine\ORM\QueryBuilder; class TextConstraint extends AbstractConstraint { - public const ALLOWED_OPERATOR_VALUES = ['=', '!=', 'STARTS', 'ENDS', 'CONTAINS', 'LIKE', 'REGEX']; + final public const ALLOWED_OPERATOR_VALUES = ['=', '!=', 'STARTS', 'ENDS', 'CONTAINS', 'LIKE', 'REGEX']; /** + * @param string $value + */ + public function __construct(string $property, ?string $identifier = null, /** + * @var string|null The value to compare to + */ + protected ?string $value = null, /** * @var string|null The operator to use */ - protected ?string $operator; - - /** - * @var string The value to compare to - */ - protected $value; - - public function __construct(string $property, string $identifier = null, $value = null, string $operator = '') + protected ?string $operator = '') { parent::__construct($property, $identifier); - $this->value = $value; - $this->operator = $operator; } /** @@ -61,18 +60,12 @@ class TextConstraint extends AbstractConstraint return $this; } - /** - * @return string - */ - public function getValue(): string + public function getValue(): ?string { return $this->value; } - /** - * @param string $value - */ - public function setValue(string $value): self + public function setValue(?string $value): self { $this->value = $value; return $this; @@ -81,7 +74,7 @@ class TextConstraint extends AbstractConstraint public function isEnabled(): bool { return $this->value !== null - && !empty($this->operator); + && ($this->operator !== null && $this->operator !== ''); } public function apply(QueryBuilder $queryBuilder): void @@ -101,27 +94,28 @@ class TextConstraint extends AbstractConstraint return; } - //The CONTAINS, LIKE, STARTS and ENDS operators use the LIKE operator but we have to build the value string differently + //The CONTAINS, LIKE, STARTS and ENDS operators use the LIKE operator, but we have to build the value string differently $like_value = null; if ($this->operator === 'LIKE') { $like_value = $this->value; - } else if ($this->operator === 'STARTS') { + } elseif ($this->operator === 'STARTS') { $like_value = $this->value . '%'; - } else if ($this->operator === 'ENDS') { + } elseif ($this->operator === 'ENDS') { $like_value = '%' . $this->value; - } else if ($this->operator === 'CONTAINS') { + } elseif ($this->operator === 'CONTAINS') { $like_value = '%' . $this->value . '%'; } if ($like_value !== null) { - $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value); + $queryBuilder->andWhere(sprintf('ILIKE(%s, :%s) = TRUE', $this->property, $this->identifier)); + $queryBuilder->setParameter($this->identifier, $like_value); return; } //Regex is only supported on MySQL and needs a special function if ($this->operator === 'REGEX') { - $queryBuilder->andWhere(sprintf('REGEXP(%s, :%s) = 1', $this->property, $this->identifier)); + $queryBuilder->andWhere(sprintf('REGEXP(%s, :%s) = TRUE', $this->property, $this->identifier)); $queryBuilder->setParameter($this->identifier, $this->value); } } -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/FilterInterface.php b/src/DataTables/Filters/FilterInterface.php index 1abbdc30..bead3fda 100644 --- a/src/DataTables/Filters/FilterInterface.php +++ b/src/DataTables/Filters/FilterInterface.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters; use Doctrine\ORM\QueryBuilder; @@ -31,4 +33,4 @@ interface FilterInterface * @return void */ public function apply(QueryBuilder $queryBuilder): void; -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/LogFilter.php b/src/DataTables/Filters/LogFilter.php index 86a600b0..35d32e74 100644 --- a/src/DataTables/Filters/LogFilter.php +++ b/src/DataTables/Filters/LogFilter.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters; use App\DataTables\Filters\Constraints\ChoiceConstraint; @@ -25,7 +27,6 @@ use App\DataTables\Filters\Constraints\DateTimeConstraint; use App\DataTables\Filters\Constraints\EntityConstraint; use App\DataTables\Filters\Constraints\InstanceOfConstraint; use App\DataTables\Filters\Constraints\IntConstraint; -use App\DataTables\Filters\Constraints\NumberConstraint; use App\Entity\UserSystem\User; use Doctrine\ORM\QueryBuilder; @@ -33,13 +34,13 @@ class LogFilter implements FilterInterface { use CompoundFilterTrait; - protected DateTimeConstraint $timestamp; - protected IntConstraint $dbId; - protected ChoiceConstraint $level; - protected InstanceOfConstraint $eventType; - protected ChoiceConstraint $targetType; - protected IntConstraint $targetId; - protected EntityConstraint $user; + public readonly DateTimeConstraint $timestamp; + public readonly IntConstraint $dbId; + public readonly ChoiceConstraint $level; + public readonly InstanceOfConstraint $eventType; + public readonly ChoiceConstraint $targetType; + public readonly IntConstraint $targetId; + public readonly EntityConstraint $user; public function __construct() { @@ -57,59 +58,4 @@ class LogFilter implements FilterInterface { $this->applyAllChildFilters($queryBuilder); } - - /** - * @return DateTimeConstraint - */ - public function getTimestamp(): DateTimeConstraint - { - return $this->timestamp; - } - - /** - * @return IntConstraint|NumberConstraint - */ - public function getDbId() - { - return $this->dbId; - } - - /** - * @return ChoiceConstraint - */ - public function getLevel(): ChoiceConstraint - { - return $this->level; - } - - /** - * @return InstanceOfConstraint - */ - public function getEventType(): InstanceOfConstraint - { - return $this->eventType; - } - - /** - * @return ChoiceConstraint - */ - public function getTargetType(): ChoiceConstraint - { - return $this->targetType; - } - - /** - * @return IntConstraint - */ - public function getTargetId(): IntConstraint - { - return $this->targetId; - } - - public function getUser(): EntityConstraint - { - return $this->user; - } - - -} \ No newline at end of file +} diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index 14ab1a9c..ff98c76f 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters; use App\DataTables\Filters\Constraints\BooleanConstraint; @@ -35,59 +37,69 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; +use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\User; -use App\Form\Filters\Constraints\UserEntityConstraintType; use App\Services\Trees\NodesListBuilder; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\QueryBuilder; -use Svg\Tag\Text; class PartFilter implements FilterInterface { use CompoundFilterTrait; - protected IntConstraint $dbId; - protected TextConstraint $ipn; - protected TextConstraint $name; - protected TextConstraint $description; - protected TextConstraint $comment; - protected TagsConstraint $tags; - protected NumberConstraint $minAmount; - protected BooleanConstraint $favorite; - protected BooleanConstraint $needsReview; - protected NumberConstraint $mass; - protected DateTimeConstraint $lastModified; - protected DateTimeConstraint $addedDate; - protected EntityConstraint $category; - protected EntityConstraint $footprint; - protected EntityConstraint $manufacturer; - protected ChoiceConstraint $manufacturing_status; - protected EntityConstraint $supplier; - protected IntConstraint $orderdetailsCount; - protected BooleanConstraint $obsolete; - protected EntityConstraint $storelocation; - protected IntConstraint $lotCount; - protected IntConstraint $amountSum; - protected LessThanDesiredConstraint $lessThanDesired; + public readonly IntConstraint $dbId; + public readonly TextConstraint $ipn; + public readonly TextConstraint $name; + public readonly TextConstraint $description; + public readonly TextConstraint $comment; + public readonly TagsConstraint $tags; + public readonly NumberConstraint $minAmount; + public readonly BooleanConstraint $favorite; + public readonly BooleanConstraint $needsReview; + public readonly NumberConstraint $mass; + public readonly DateTimeConstraint $lastModified; + public readonly DateTimeConstraint $addedDate; + public readonly EntityConstraint $category; + public readonly EntityConstraint $footprint; + public readonly EntityConstraint $manufacturer; + public readonly ChoiceConstraint $manufacturing_status; + public readonly EntityConstraint $supplier; + public readonly IntConstraint $orderdetailsCount; + public readonly BooleanConstraint $obsolete; + public readonly EntityConstraint $storelocation; + public readonly IntConstraint $lotCount; + public readonly IntConstraint $amountSum; + public readonly LessThanDesiredConstraint $lessThanDesired; - protected BooleanConstraint $lotNeedsRefill; - protected TextConstraint $lotDescription; - protected BooleanConstraint $lotUnknownAmount; - protected DateTimeConstraint $lotExpirationDate; - protected EntityConstraint $lotOwner; + public readonly BooleanConstraint $lotNeedsRefill; + public readonly TextConstraint $lotDescription; + public readonly BooleanConstraint $lotUnknownAmount; + public readonly DateTimeConstraint $lotExpirationDate; + public readonly EntityConstraint $lotOwner; + + public readonly EntityConstraint $measurementUnit; + public readonly TextConstraint $manufacturer_product_url; + public readonly TextConstraint $manufacturer_product_number; + public readonly IntConstraint $attachmentsCount; + public readonly EntityConstraint $attachmentType; + public readonly TextConstraint $attachmentName; - protected EntityConstraint $measurementUnit; - protected TextConstraint $manufacturer_product_url; - protected TextConstraint $manufacturer_product_number; - protected IntConstraint $attachmentsCount; - protected EntityConstraint $attachmentType; - protected TextConstraint $attachmentName; /** @var ArrayCollection */ - protected ArrayCollection $parameters; - protected IntConstraint $parametersCount; + public readonly ArrayCollection $parameters; + public readonly IntConstraint $parametersCount; + + /************************************************* + * Project tab + *************************************************/ + + public readonly EntityConstraint $project; + public readonly NumberConstraint $bomQuantity; + public readonly TextConstraint $bomName; + public readonly TextConstraint $bomComment; public function __construct(NodesListBuilder $nodesListBuilder) { @@ -112,301 +124,48 @@ class PartFilter implements FilterInterface This seems to be related to the fact, that PDO does not have an float parameter type and using string type does not work in this situation (at least in SQLite) TODO: Find a better solution here */ - //We have to use Having here, as we use an alias column which is not supported on the where clause and would result in an error - $this->amountSum = (new IntConstraint('amountSum'))->useHaving(); - $this->lotCount = new IntConstraint('COUNT(partLots)'); + $this->amountSum = (new IntConstraint('( + SELECT COALESCE(SUM(__partLot.amount), 0.0) + FROM '.PartLot::class.' __partLot + WHERE __partLot.part = part.id + AND __partLot.instock_unknown = false + AND (__partLot.expiration_date IS NULL OR __partLot.expiration_date > CURRENT_DATE()) + )', identifier: "amountSumWhere")); + $this->lotCount = new IntConstraint('COUNT(_partLots)'); $this->lessThanDesired = new LessThanDesiredConstraint(); - $this->storelocation = new EntityConstraint($nodesListBuilder, Storelocation::class, 'partLots.storage_location'); - $this->lotNeedsRefill = new BooleanConstraint('partLots.needs_refill'); - $this->lotUnknownAmount = new BooleanConstraint('partLots.instock_unknown'); - $this->lotExpirationDate = new DateTimeConstraint('partLots.expiration_date'); - $this->lotDescription = new TextConstraint('partLots.description'); - $this->lotOwner = new EntityConstraint($nodesListBuilder, User::class, 'partLots.owner'); + $this->storelocation = new EntityConstraint($nodesListBuilder, StorageLocation::class, '_partLots.storage_location'); + $this->lotNeedsRefill = new BooleanConstraint('_partLots.needs_refill'); + $this->lotUnknownAmount = new BooleanConstraint('_partLots.instock_unknown'); + $this->lotExpirationDate = new DateTimeConstraint('_partLots.expiration_date'); + $this->lotDescription = new TextConstraint('_partLots.description'); + $this->lotOwner = new EntityConstraint($nodesListBuilder, User::class, '_partLots.owner'); $this->manufacturer = new EntityConstraint($nodesListBuilder, Manufacturer::class, 'part.manufacturer'); $this->manufacturer_product_number = new TextConstraint('part.manufacturer_product_number'); $this->manufacturer_product_url = new TextConstraint('part.manufacturer_product_url'); $this->manufacturing_status = new ChoiceConstraint('part.manufacturing_status'); - $this->attachmentsCount = new IntConstraint('COUNT(attachments)'); - $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, 'attachments.attachment_type'); - $this->attachmentName = new TextConstraint('attachments.name'); + $this->attachmentsCount = new IntConstraint('COUNT(_attachments)'); + $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, '_attachments.attachment_type'); + $this->attachmentName = new TextConstraint('_attachments.name'); - $this->supplier = new EntityConstraint($nodesListBuilder, Supplier::class, 'orderdetails.supplier'); - $this->orderdetailsCount = new IntConstraint('COUNT(orderdetails)'); - $this->obsolete = new BooleanConstraint('orderdetails.obsolete'); + $this->supplier = new EntityConstraint($nodesListBuilder, Supplier::class, '_orderdetails.supplier'); + $this->orderdetailsCount = new IntConstraint('COUNT(_orderdetails)'); + $this->obsolete = new BooleanConstraint('_orderdetails.obsolete'); $this->parameters = new ArrayCollection(); - $this->parametersCount = new IntConstraint('COUNT(parameters)'); + $this->parametersCount = new IntConstraint('COUNT(_parameters)'); + + $this->project = new EntityConstraint($nodesListBuilder, Project::class, '_projectBomEntries.project'); + $this->bomQuantity = new NumberConstraint('_projectBomEntries.quantity'); + $this->bomName = new TextConstraint('_projectBomEntries.name'); + $this->bomComment = new TextConstraint('_projectBomEntries.comment'); + } public function apply(QueryBuilder $queryBuilder): void { $this->applyAllChildFilters($queryBuilder); } - - - /** - * @return BooleanConstraint|false - */ - public function getFavorite() - { - return $this->favorite; - } - - /** - * @return BooleanConstraint - */ - public function getNeedsReview(): BooleanConstraint - { - return $this->needsReview; - } - - public function getMass(): NumberConstraint - { - return $this->mass; - } - - public function getName(): TextConstraint - { - return $this->name; - } - - public function getDescription(): TextConstraint - { - return $this->description; - } - - /** - * @return DateTimeConstraint - */ - public function getLastModified(): DateTimeConstraint - { - return $this->lastModified; - } - - /** - * @return DateTimeConstraint - */ - public function getAddedDate(): DateTimeConstraint - { - return $this->addedDate; - } - - public function getCategory(): EntityConstraint - { - return $this->category; - } - - /** - * @return EntityConstraint - */ - public function getFootprint(): EntityConstraint - { - return $this->footprint; - } - - /** - * @return EntityConstraint - */ - public function getManufacturer(): EntityConstraint - { - return $this->manufacturer; - } - - /** - * @return EntityConstraint - */ - public function getSupplier(): EntityConstraint - { - return $this->supplier; - } - - /** - * @return EntityConstraint - */ - public function getStorelocation(): EntityConstraint - { - return $this->storelocation; - } - - /** - * @return EntityConstraint - */ - public function getMeasurementUnit(): EntityConstraint - { - return $this->measurementUnit; - } - - /** - * @return NumberConstraint - */ - public function getDbId(): NumberConstraint - { - return $this->dbId; - } - - public function getIpn(): TextConstraint - { - return $this->ipn; - } - - /** - * @return TextConstraint - */ - public function getComment(): TextConstraint - { - return $this->comment; - } - - /** - * @return NumberConstraint - */ - public function getMinAmount(): NumberConstraint - { - return $this->minAmount; - } - - /** - * @return TextConstraint - */ - public function getManufacturerProductUrl(): TextConstraint - { - return $this->manufacturer_product_url; - } - - /** - * @return TextConstraint - */ - public function getManufacturerProductNumber(): TextConstraint - { - return $this->manufacturer_product_number; - } - - public function getLotCount(): NumberConstraint - { - return $this->lotCount; - } - - /** - * @return EntityConstraint - */ - public function getLotOwner(): EntityConstraint - { - return $this->lotOwner; - } - - /** - * @return TagsConstraint - */ - public function getTags(): TagsConstraint - { - return $this->tags; - } - - /** - * @return IntConstraint - */ - public function getOrderdetailsCount(): IntConstraint - { - return $this->orderdetailsCount; - } - - /** - * @return IntConstraint - */ - public function getAttachmentsCount(): IntConstraint - { - return $this->attachmentsCount; - } - - /** - * @return BooleanConstraint - */ - public function getLotNeedsRefill(): BooleanConstraint - { - return $this->lotNeedsRefill; - } - - /** - * @return BooleanConstraint - */ - public function getLotUnknownAmount(): BooleanConstraint - { - return $this->lotUnknownAmount; - } - - /** - * @return DateTimeConstraint - */ - public function getLotExpirationDate(): DateTimeConstraint - { - return $this->lotExpirationDate; - } - - /** - * @return EntityConstraint - */ - public function getAttachmentType(): EntityConstraint - { - return $this->attachmentType; - } - - /** - * @return TextConstraint - */ - public function getAttachmentName(): TextConstraint - { - return $this->attachmentName; - } - - public function getManufacturingStatus(): ChoiceConstraint - { - return $this->manufacturing_status; - } - - public function getAmountSum(): NumberConstraint - { - return $this->amountSum; - } - - /** - * @return ArrayCollection - */ - public function getParameters(): ArrayCollection - { - return $this->parameters; - } - - public function getParametersCount(): IntConstraint - { - return $this->parametersCount; - } - - /** - * @return TextConstraint - */ - public function getLotDescription(): TextConstraint - { - return $this->lotDescription; - } - - /** - * @return BooleanConstraint - */ - public function getObsolete(): BooleanConstraint - { - return $this->obsolete; - } - - /** - * @return LessThanDesiredConstraint - */ - public function getLessThanDesired(): LessThanDesiredConstraint - { - return $this->lessThanDesired; - } - - } diff --git a/src/DataTables/Filters/PartSearchFilter.php b/src/DataTables/Filters/PartSearchFilter.php index b3f1cafd..6e2e5894 100644 --- a/src/DataTables/Filters/PartSearchFilter.php +++ b/src/DataTables/Filters/PartSearchFilter.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables\Filters; - -use Doctrine\ORM\Query\Expr; use Doctrine\ORM\QueryBuilder; class PartSearchFilter implements FilterInterface { - /** @var string The string to query for */ - protected string $keyword; - /** @var boolean Whether to use regex for searching */ protected bool $regex = false; @@ -68,9 +65,11 @@ class PartSearchFilter implements FilterInterface /** @var bool Use Internal Part number for searching */ protected bool $ipn = true; - public function __construct(string $query) + public function __construct( + /** @var string The string to query for */ + protected string $keyword + ) { - $this->keyword = $query; } protected function getFieldsToSearch(): array @@ -81,31 +80,34 @@ class PartSearchFilter implements FilterInterface $fields_to_search[] = 'part.name'; } if($this->category) { - $fields_to_search[] = 'category.name'; + $fields_to_search[] = '_category.name'; } if($this->description) { $fields_to_search[] = 'part.description'; } + if ($this->comment) { + $fields_to_search[] = 'part.comment'; + } if($this->tags) { $fields_to_search[] = 'part.tags'; } if($this->storelocation) { - $fields_to_search[] = 'storelocations.name'; + $fields_to_search[] = '_storelocations.name'; } if($this->ordernr) { - $fields_to_search[] = 'orderdetails.supplierpartnr'; + $fields_to_search[] = '_orderdetails.supplierpartnr'; } if($this->mpn) { - $fields_to_search[] = 'part.manufacturer_product_url'; + $fields_to_search[] = 'part.manufacturer_product_number'; } if($this->supplier) { - $fields_to_search[] = 'suppliers.name'; + $fields_to_search[] = '_suppliers.name'; } if($this->manufacturer) { - $fields_to_search[] = 'manufacturer.name'; + $fields_to_search[] = '_manufacturer.name'; } if($this->footprint) { - $fields_to_search[] = 'footprint.name'; + $fields_to_search[] = '_footprint.name'; } if ($this->ipn) { $fields_to_search[] = 'part.ipn'; @@ -119,25 +121,25 @@ class PartSearchFilter implements FilterInterface $fields_to_search = $this->getFieldsToSearch(); //If we have nothing to search for, do nothing - if (empty($fields_to_search) || empty($this->keyword)) { + if ($fields_to_search === [] || $this->keyword === '') { return; } //Convert the fields to search to a list of expressions - $expressions = array_map(function (string $field) { + $expressions = array_map(function (string $field): string { if ($this->regex) { - return sprintf("REGEXP(%s, :search_query) = 1", $field); + return sprintf("REGEXP(%s, :search_query) = TRUE", $field); } - return sprintf("%s LIKE :search_query", $field); + return sprintf("ILIKE(%s, :search_query) = TRUE", $field); }, $fields_to_search); - //Add Or concatation of the expressions to our query + //Add Or concatenation of the expressions to our query $queryBuilder->andWhere( $queryBuilder->expr()->orX(...$expressions) ); - //For regex we pass the query as is, for like we add % to the start and end as wildcards + //For regex, we pass the query as is, for like we add % to the start and end as wildcards if ($this->regex) { $queryBuilder->setParameter('search_query', $this->keyword); } else { @@ -145,162 +147,99 @@ class PartSearchFilter implements FilterInterface } } - /** - * @return string - */ public function getKeyword(): string { return $this->keyword; } - /** - * @param string $keyword - * @return PartSearchFilter - */ public function setKeyword(string $keyword): PartSearchFilter { $this->keyword = $keyword; return $this; } - /** - * @return bool - */ public function isRegex(): bool { return $this->regex; } - /** - * @param bool $regex - * @return PartSearchFilter - */ public function setRegex(bool $regex): PartSearchFilter { $this->regex = $regex; return $this; } - /** - * @return bool - */ public function isName(): bool { return $this->name; } - /** - * @param bool $name - * @return PartSearchFilter - */ public function setName(bool $name): PartSearchFilter { $this->name = $name; return $this; } - /** - * @return bool - */ public function isCategory(): bool { return $this->category; } - /** - * @param bool $category - * @return PartSearchFilter - */ public function setCategory(bool $category): PartSearchFilter { $this->category = $category; return $this; } - /** - * @return bool - */ public function isDescription(): bool { return $this->description; } - /** - * @param bool $description - * @return PartSearchFilter - */ public function setDescription(bool $description): PartSearchFilter { $this->description = $description; return $this; } - /** - * @return bool - */ public function isTags(): bool { return $this->tags; } - /** - * @param bool $tags - * @return PartSearchFilter - */ public function setTags(bool $tags): PartSearchFilter { $this->tags = $tags; return $this; } - /** - * @return bool - */ public function isStorelocation(): bool { return $this->storelocation; } - /** - * @param bool $storelocation - * @return PartSearchFilter - */ public function setStorelocation(bool $storelocation): PartSearchFilter { $this->storelocation = $storelocation; return $this; } - /** - * @return bool - */ public function isOrdernr(): bool { return $this->ordernr; } - /** - * @param bool $ordernr - * @return PartSearchFilter - */ public function setOrdernr(bool $ordernr): PartSearchFilter { $this->ordernr = $ordernr; return $this; } - /** - * @return bool - */ public function isMpn(): bool { return $this->mpn; } - /** - * @param bool $mpn - * @return PartSearchFilter - */ public function setMpn(bool $mpn): PartSearchFilter { $this->mpn = $mpn; @@ -318,72 +257,44 @@ class PartSearchFilter implements FilterInterface return $this; } - /** - * @return bool - */ public function isSupplier(): bool { return $this->supplier; } - /** - * @param bool $supplier - * @return PartSearchFilter - */ public function setSupplier(bool $supplier): PartSearchFilter { $this->supplier = $supplier; return $this; } - /** - * @return bool - */ public function isManufacturer(): bool { return $this->manufacturer; } - /** - * @param bool $manufacturer - * @return PartSearchFilter - */ public function setManufacturer(bool $manufacturer): PartSearchFilter { $this->manufacturer = $manufacturer; return $this; } - /** - * @return bool - */ public function isFootprint(): bool { return $this->footprint; } - /** - * @param bool $footprint - * @return PartSearchFilter - */ public function setFootprint(bool $footprint): PartSearchFilter { $this->footprint = $footprint; return $this; } - /** - * @return bool - */ public function isComment(): bool { return $this->comment; } - /** - * @param bool $comment - * @return PartSearchFilter - */ public function setComment(bool $comment): PartSearchFilter { $this->comment = $comment; @@ -391,4 +302,4 @@ class PartSearchFilter implements FilterInterface } -} \ No newline at end of file +} diff --git a/src/DataTables/Helpers/ColumnSortHelper.php b/src/DataTables/Helpers/ColumnSortHelper.php new file mode 100644 index 00000000..05bd8182 --- /dev/null +++ b/src/DataTables/Helpers/ColumnSortHelper.php @@ -0,0 +1,130 @@ +. + */ + +declare(strict_types=1); + + +namespace App\DataTables\Helpers; + +use Omines\DataTablesBundle\DataTable; +use Psr\Log\LoggerInterface; + +class ColumnSortHelper +{ + private array $columns = []; + + public function __construct(private readonly LoggerInterface $logger) + { + } + + /** + * Add a new column which can be sorted and visibility controlled by the user. The basic syntax is similar to + * the DataTable add method, but with additional options. + * @param string $name + * @param string $type + * @param array $options + * @param string|null $alias If an alias is set here, the column will be available under this alias in the config + * string instead of the name. + * @param bool $visibility_configurable If set to false, this column can not be visibility controlled by the user + * @return $this + */ + public function add(string $name, string $type, array $options = [], ?string $alias = null, + bool $visibility_configurable = true): self + { + //Alias allows us to override the name of the column in the env variable + $this->columns[$alias ?? $name] = [ + 'name' => $name, + 'type' => $type, + 'options' => $options, + 'visibility_configurable' => $visibility_configurable + ]; + + return $this; + } + + /** + * Remove all columns saved inside this helper + * @return void + */ + public function reset(): void + { + $this->columns = []; + } + + /** + * Apply the visibility configuration to the given DataTable and configure the columns. + * @param DataTable $dataTable + * @param string|array $visible_columns Either a list or a comma separated string of column names, which should + * be visible by default. If a column is not listed here, it will be hidden by default. + * @return void + */ + public function applyVisibilityAndConfigureColumns(DataTable $dataTable, string|array $visible_columns, + string $config_var_name): void + { + //If the config is given as a string, convert it to an array first + if (!is_array($visible_columns)) { + $visible_columns = array_map(trim(...), explode(",", $visible_columns)); + } + + $processed_columns = []; + + //First add all columns which visibility is not configurable + foreach ($this->columns as $col_id => $col_data) { + if (!$col_data['visibility_configurable']) { + $this->addColumnEntry($dataTable, $this->columns[$col_id], null); + $processed_columns[] = $col_id; + } + } + + //Afterwards the columns, which should be visible by default + foreach ($visible_columns as $col_id) { + if (!isset($this->columns[$col_id]) || !$this->columns[$col_id]['visibility_configurable']) { + $this->logger->warning("Configuration option $config_var_name specify invalid column '$col_id'. Column is skipped."); + continue; + } + + if (in_array($col_id, $processed_columns, true)) { + $this->logger->warning("Configuration option $config_var_name specify column '$col_id' multiple time. Only first occurrence is used."); + continue; + } + $this->addColumnEntry($dataTable, $this->columns[$col_id], true); + $processed_columns[] = $col_id; + } + + //and the remaining non-visible columns + foreach (array_keys($this->columns) as $col_id) { + if (in_array($col_id, $processed_columns, true)) { + // column already processed + continue; + } + $this->addColumnEntry($dataTable, $this->columns[$col_id], false); + $processed_columns[] = $col_id; + } + } + + private function addColumnEntry(DataTable $dataTable, array $entry, ?bool $visible): void + { + $options = $entry['options'] ?? []; + if (!is_null($visible)) { + $options["visible"] = $visible; + } + $dataTable->add($entry['name'], $entry['type'], $options); + } +} \ No newline at end of file diff --git a/src/DataTables/Helpers/PartDataTableHelper.php b/src/DataTables/Helpers/PartDataTableHelper.php index b13ee813..c33c3a82 100644 --- a/src/DataTables/Helpers/PartDataTableHelper.php +++ b/src/DataTables/Helpers/PartDataTableHelper.php @@ -1,4 +1,7 @@ previewGenerator = $previewGenerator; - $this->attachmentURLGenerator = $attachmentURLGenerator; - $this->translator = $translator; - $this->entityURLGenerator = $entityURLGenerator; + public function __construct( + private readonly PartPreviewGenerator $previewGenerator, + private readonly AttachmentURLGenerator $attachmentURLGenerator, + private readonly EntityURLGenerator $entityURLGenerator, + private readonly TranslatorInterface $translator, + private readonly AmountFormatter $amountFormatter, + ) { } public function renderName(Part $context): string @@ -52,14 +53,16 @@ class PartDataTableHelper //Depending on the part status we show a different icon (the later conditions have higher priority) if ($context->isFavorite()) { - $icon = sprintf('', $this->translator->trans('part.favorite.badge')); + $icon = sprintf('', + $this->translator->trans('part.favorite.badge')); } if ($context->isNeedsReview()) { - $icon = sprintf('', $this->translator->trans('part.needs_review.badge')); + $icon = sprintf('', + $this->translator->trans('part.needs_review.badge')); } - if ($context->getBuiltProject() !== null) { + if ($context->getBuiltProject() instanceof Project) { $icon = sprintf('', - $this->translator->trans('part.info.projectBuildPart.hint') . ': ' . $context->getBuiltProject()->getName()); + $this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName()); } @@ -74,7 +77,7 @@ class PartDataTableHelper public function renderPicture(Part $context): string { $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context); - if (null === $preview_attachment) { + if (!$preview_attachment instanceof Attachment) { return ''; } @@ -92,4 +95,62 @@ class PartDataTableHelper $title ); } -} \ No newline at end of file + + public function renderStorageLocations(Part $context): string + { + $tmp = []; + foreach ($context->getPartLots() as $lot) { + //Ignore lots without storelocation + if (!$lot->getStorageLocation() instanceof StorageLocation) { + continue; + } + $tmp[] = sprintf( + '%s', + $this->entityURLGenerator->listPartsURL($lot->getStorageLocation()), + htmlspecialchars($lot->getStorageLocation()->getFullPath()), + htmlspecialchars($lot->getStorageLocation()->getName()) + ); + } + + return implode('
', $tmp); + } + + public function renderAmount(Part $context): string + { + $amount = $context->getAmountSum(); + $expiredAmount = $context->getExpiredAmountSum(); + + $ret = ''; + + if ($context->isAmountUnknown()) { + //When all amounts are unknown, we show a question mark + if ($amount === 0.0) { + $ret .= sprintf('?', + $this->translator->trans('part_lots.instock_unknown')); + } else { //Otherwise mark it with greater equal and the (known) amount + $ret .= sprintf('', + $this->translator->trans('part_lots.instock_unknown') + ); + $ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit())); + } + } else { + $ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit())); + } + + //If we have expired lots, we show them in parentheses behind + if ($expiredAmount > 0) { + $ret .= sprintf(' (+%s)', + $this->translator->trans('part_lots.is_expired'), + htmlspecialchars($this->amountFormatter->format($expiredAmount, $context->getPartUnit()))); + } + + //When the amount is below the minimum amount, we highlight the number red + if ($context->isNotEnoughInstock()) { + $ret = sprintf('%s', + $this->translator->trans('part.info.amount.less_than_desired'), + $ret); + } + + return $ret; + } +} diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 85ab3113..f6604279 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -22,13 +22,15 @@ declare(strict_types=1); namespace App\DataTables; +use App\DataTables\Column\EnumColumn; +use App\Entity\LogSystem\LogTargetType; +use Symfony\Bundle\SecurityBundle\Security; use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LogEntryExtraColumn; use App\DataTables\Column\LogEntryTargetColumn; use App\DataTables\Column\RevertLogColumn; use App\DataTables\Column\RowClassColumn; -use App\DataTables\Filters\AttachmentFilter; use App\DataTables\Filters\LogFilter; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\TimeTravelInterface; @@ -38,12 +40,12 @@ use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; use App\Entity\LogSystem\PartStockChangedLogEntry; -use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Exceptions\EntityNotSupportedException; use App\Repository\LogEntryRepository; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; +use App\Services\LogSystem\LogLevelHelper; use App\Services\UserSystem\UserAvatarHelper; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; @@ -52,36 +54,20 @@ use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; -use Psr\Log\LogLevel; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; -use function Symfony\Component\Translation\t; - class LogDataTable implements DataTableTypeInterface { - protected ElementTypeNameGenerator $elementTypeNameGenerator; - protected TranslatorInterface $translator; - protected UrlGeneratorInterface $urlGenerator; - protected EntityURLGenerator $entityURLGenerator; protected LogEntryRepository $logRepo; - protected Security $security; - protected UserAvatarHelper $userAvatarHelper; - public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator, - UrlGeneratorInterface $urlGenerator, EntityURLGenerator $entityURLGenerator, EntityManagerInterface $entityManager, - Security $security, UserAvatarHelper $userAvatarHelper) + public function __construct(protected ElementTypeNameGenerator $elementTypeNameGenerator, protected TranslatorInterface $translator, + protected UrlGeneratorInterface $urlGenerator, protected EntityURLGenerator $entityURLGenerator, EntityManagerInterface $entityManager, + protected Security $security, protected UserAvatarHelper $userAvatarHelper, protected LogLevelHelper $logLevelHelper) { - $this->elementTypeNameGenerator = $elementTypeNameGenerator; - $this->translator = $translator; - $this->urlGenerator = $urlGenerator; - $this->entityURLGenerator = $entityURLGenerator; $this->logRepo = $entityManager->getRepository(AbstractLogEntry::class); - $this->security = $security; - $this->userAvatarHelper = $userAvatarHelper; } public function configureOptions(OptionsResolver $optionsResolver): void @@ -115,72 +101,17 @@ class LogDataTable implements DataTableTypeInterface //This special $$rowClass column is used to set the row class depending on the log level. The class gets set by the frontend controller $dataTable->add('dont_matter', RowClassColumn::class, [ - 'render' => static function ($value, AbstractLogEntry $context) { - switch ($context->getLevel()) { - case AbstractLogEntry::LEVEL_EMERGENCY: - case AbstractLogEntry::LEVEL_ALERT: - case AbstractLogEntry::LEVEL_CRITICAL: - case AbstractLogEntry::LEVEL_ERROR: - return 'table-danger'; - case AbstractLogEntry::LEVEL_WARNING: - return 'table-warning'; - case AbstractLogEntry::LEVEL_NOTICE: - return 'table-info'; - default: - return ''; - } - }, + 'render' => fn($value, AbstractLogEntry $context) => $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()), ]); $dataTable->add('symbol', TextColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => static function ($value, AbstractLogEntry $context) { - switch ($context->getLevelString()) { - case LogLevel::DEBUG: - $symbol = 'fa-bug'; - - break; - case LogLevel::INFO: - $symbol = 'fa-info'; - - break; - case LogLevel::NOTICE: - $symbol = 'fa-flag'; - - break; - case LogLevel::WARNING: - $symbol = 'fa-exclamation-circle'; - - break; - case LogLevel::ERROR: - $symbol = 'fa-exclamation-triangle'; - - break; - case LogLevel::CRITICAL: - $symbol = 'fa-bolt'; - - break; - case LogLevel::ALERT: - $symbol = 'fa-radiation'; - - break; - case LogLevel::EMERGENCY: - $symbol = 'fa-skull-crossbones'; - - break; - default: - $symbol = 'fa-question-circle'; - - break; - } - - return sprintf( - '', - $symbol, - $context->getLevelString() - ); - }, + 'render' => fn($value, AbstractLogEntry $context): string => sprintf( + '', + $this->logLevelHelper->logLevelToIconClass($context->getLevelString()), + $context->getLevelString() + ), ]); $dataTable->add('id', TextColumn::class, [ @@ -191,6 +122,10 @@ class LogDataTable implements DataTableTypeInterface $dataTable->add('timestamp', LocaleDateTimeColumn::class, [ 'label' => 'log.timestamp', 'timeFormat' => 'medium', + 'render' => fn(string $value, AbstractLogEntry $context): string => sprintf('%s', + $this->urlGenerator->generate('log_details', ['id' => $context->getID()]), + $value + ) ]); $dataTable->add('type', TextColumn::class, [ @@ -202,7 +137,7 @@ class LogDataTable implements DataTableTypeInterface if ($context instanceof PartStockChangedLogEntry) { $text .= sprintf( ' (%s)', - $this->translator->trans('log.part_stock_changed.' . $context->getInstockChangeType()) + $this->translator->trans($context->getInstockChangeType()->toTranslationKey()) ); } @@ -214,21 +149,20 @@ class LogDataTable implements DataTableTypeInterface 'label' => 'log.level', 'visible' => 'system_log' === $options['mode'], 'propertyPath' => 'levelString', - 'render' => function (string $value, AbstractLogEntry $context) { - return $this->translator->trans('log.level.'.$value); - }, + 'render' => fn(string $value, AbstractLogEntry $context) => $this->translator->trans('log.level.'.$value), ]); $dataTable->add('user', TextColumn::class, [ 'label' => 'log.user', - 'render' => function ($value, AbstractLogEntry $context) { + 'orderField' => 'NATSORT(user.name)', + 'render' => function ($value, AbstractLogEntry $context): string { $user = $context->getUser(); //If user was deleted, show the info from the username field - if ($user === null) { + if (!$user instanceof User) { if ($context->isCLIEntry()) { return sprintf('%s [%s]', - htmlentities($context->getCLIUsername()), + htmlentities((string) $context->getCLIUsername()), $this->translator->trans('log.cli_user') ); } @@ -253,11 +187,12 @@ class LogDataTable implements DataTableTypeInterface }, ]); - $dataTable->add('target_type', TextColumn::class, [ + $dataTable->add('target_type', EnumColumn::class, [ 'label' => 'log.target_type', 'visible' => false, - 'render' => function ($value, AbstractLogEntry $context) { - $class = $context->getTargetClass(); + 'class' => LogTargetType::class, + 'render' => function (LogTargetType $value, AbstractLogEntry $context) { + $class = $value->toClass(); if (null !== $class) { return $this->elementTypeNameGenerator->getLocalizedTypeLabel($class); } @@ -281,24 +216,22 @@ class LogDataTable implements DataTableTypeInterface 'href' => function ($value, AbstractLogEntry $context) { if ( ($context instanceof TimeTravelInterface - && $context->hasOldDataInformations()) + && $context->hasOldDataInformation()) || $context instanceof CollectionElementDeleted ) { try { $target = $this->logRepo->getTargetElement($context); - if (null !== $target) { + if ($target instanceof AbstractDBElement) { return $this->entityURLGenerator->timeTravelURL($target, $context->getTimestamp()); } - } catch (EntityNotSupportedException $exception) { + } catch (EntityNotSupportedException) { return null; } } return null; }, - 'disabled' => function ($value, AbstractLogEntry $context) { - return !$this->security->isGranted('show_history', $context->getTargetClass()); - }, + 'disabled' => fn($value, AbstractLogEntry $context) => !$this->security->isGranted('show_history', $context->getTargetClass()), ]); $dataTable->add('actionRevert', RevertLogColumn::class, [ @@ -346,8 +279,8 @@ class LogDataTable implements DataTableTypeInterface ->andWhere('log.target_type NOT IN (:disallowed)'); $builder->setParameter('disallowed', [ - AbstractLogEntry::targetTypeClassToID(User::class), - AbstractLogEntry::targetTypeClassToID(Group::class), + LogTargetType::USER, + LogTargetType::GROUP, ]); } @@ -355,9 +288,16 @@ class LogDataTable implements DataTableTypeInterface foreach ($options['filter_elements'] as $element) { /** @var AbstractDBElement $element */ - $target_type = AbstractLogEntry::targetTypeClassToID(get_class($element)); + $target_type = LogTargetType::fromElementClass($element); $target_id = $element->getID(); - $builder->orWhere("log.target_type = ${target_type} AND log.target_id = ${target_id}"); + + //We have to create unique parameter names for each element + $target_type_var = 'filter_target_type_' . uniqid('', false); + $target_id_var = 'filter_target_id_' . uniqid('', false); + + $builder->orWhere("log.target_type = :$target_type_var AND log.target_id = :$target_id_var"); + $builder->setParameter($target_type_var, $target_type); + $builder->setParameter($target_id_var, $target_id); } } } diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index 011c4a8e..3163a38b 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -22,8 +22,9 @@ declare(strict_types=1); namespace App\DataTables; -use App\DataTables\Adapters\CustomFetchJoinORMAdapter; +use App\DataTables\Adapters\TwoStepORMAdapter; use App\DataTables\Column\EntityColumn; +use App\DataTables\Column\EnumColumn; use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; @@ -35,57 +36,38 @@ use App\DataTables\Column\SIUnitNumberColumn; use App\DataTables\Column\TagsColumn; use App\DataTables\Filters\PartFilter; use App\DataTables\Filters\PartSearchFilter; +use App\DataTables\Helpers\ColumnSortHelper; use App\DataTables\Helpers\PartDataTableHelper; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; -use App\Entity\Parts\Manufacturer; +use App\Doctrine\Helpers\FieldHelper; +use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; -use App\Entity\Parts\Supplier; -use App\Services\Formatters\AmountFormatter; -use App\Services\Attachments\AttachmentURLGenerator; -use App\Services\Attachments\PartPreviewGenerator; +use App\Entity\ProjectSystem\Project; use App\Services\EntityURLGenerator; -use App\Services\Trees\NodesListBuilder; +use App\Services\Formatters\AmountFormatter; +use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\QueryBuilder; -use Omines\DataTablesBundle\Adapter\Doctrine\FetchJoinORMAdapter; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; -use Omines\DataTablesBundle\Column\BoolColumn; -use Omines\DataTablesBundle\Column\MapColumn; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; final class PartsDataTable implements DataTableTypeInterface { - private TranslatorInterface $translator; - private NodesListBuilder $treeBuilder; - private AmountFormatter $amountFormatter; - private AttachmentURLGenerator $attachmentURLGenerator; - private Security $security; + const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]; - private PartDataTableHelper $partDataTableHelper; - - /** - * @var EntityURLGenerator - */ - private $urlGenerator; - - public function __construct(EntityURLGenerator $urlGenerator, TranslatorInterface $translator, - NodesListBuilder $treeBuilder, AmountFormatter $amountFormatter,PartDataTableHelper $partDataTableHelper, - AttachmentURLGenerator $attachmentURLGenerator, Security $security) - { - $this->urlGenerator = $urlGenerator; - $this->translator = $translator; - $this->treeBuilder = $treeBuilder; - $this->amountFormatter = $amountFormatter; - $this->attachmentURLGenerator = $attachmentURLGenerator; - $this->security = $security; - $this->partDataTableHelper = $partDataTableHelper; + public function __construct( + private readonly EntityURLGenerator $urlGenerator, + private readonly TranslatorInterface $translator, + private readonly AmountFormatter $amountFormatter, + private readonly PartDataTableHelper $partDataTableHelper, + private readonly Security $security, + private readonly string $visible_columns, + private readonly ColumnSortHelper $csh, + ) { } public function configureOptions(OptionsResolver $optionsResolver): void @@ -105,10 +87,10 @@ final class PartsDataTable implements DataTableTypeInterface $this->configureOptions($resolver); $options = $resolver->resolve($options); - $dataTable + $this->csh //Color the table rows depending on the review and favorite status - ->add('dont_matter', RowClassColumn::class, [ - 'render' => function ($value, Part $context) { + ->add('row_color', RowClassColumn::class, [ + 'render' => function ($value, Part $context): string { if ($context->isNeedsReview()) { return 'table-secondary'; } @@ -118,195 +100,208 @@ final class PartsDataTable implements DataTableTypeInterface return ''; //Default coloring otherwise }, - ]) - - ->add('select', SelectColumn::class) + ], visibility_configurable: false) + ->add('select', SelectColumn::class, visibility_configurable: false) ->add('picture', TextColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => function ($value, Part $context) { - return $this->partDataTableHelper->renderPicture($context); - }, - ]) + 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderPicture($context), + ], visibility_configurable: false) ->add('name', TextColumn::class, [ 'label' => $this->translator->trans('part.table.name'), - 'render' => function ($value, Part $context) { - return $this->partDataTableHelper->renderName($context); - }, + 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderName($context), + 'orderField' => 'NATSORT(part.name)' ]) ->add('id', TextColumn::class, [ 'label' => $this->translator->trans('part.table.id'), - 'visible' => false, ]) ->add('ipn', TextColumn::class, [ 'label' => $this->translator->trans('part.table.ipn'), - 'visible' => false, + 'orderField' => 'NATSORT(part.ipn)' ]) ->add('description', MarkdownColumn::class, [ 'label' => $this->translator->trans('part.table.description'), - ]); - - if ($this->security->isGranted('@categories.read')) { - $dataTable->add('category', EntityColumn::class, [ + ]) + ->add('category', EntityColumn::class, [ 'label' => $this->translator->trans('part.table.category'), 'property' => 'category', - ]); - } - - if ($this->security->isGranted('@footprints.read')) { - $dataTable->add('footprint', EntityColumn::class, [ + 'orderField' => 'NATSORT(_category.name)' + ]) + ->add('footprint', EntityColumn::class, [ 'property' => 'footprint', 'label' => $this->translator->trans('part.table.footprint'), - ]); - } - if ($this->security->isGranted('@manufacturers.read')) { - $dataTable->add('manufacturer', EntityColumn::class, [ + 'orderField' => 'NATSORT(_footprint.name)' + ]) + ->add('manufacturer', EntityColumn::class, [ 'property' => 'manufacturer', 'label' => $this->translator->trans('part.table.manufacturer'), - ]); - } - if ($this->security->isGranted('@storelocations.read')) { - $dataTable->add('storelocation', TextColumn::class, [ + 'orderField' => 'NATSORT(_manufacturer.name)' + ]) + ->add('storelocation', TextColumn::class, [ 'label' => $this->translator->trans('part.table.storeLocations'), - 'render' => function ($value, Part $context) { - $tmp = []; - foreach ($context->getPartLots() as $lot) { - //Ignore lots without storelocation - if (null === $lot->getStorageLocation()) { - continue; - } - $tmp[] = sprintf( - '%s', - $this->urlGenerator->listPartsURL($lot->getStorageLocation()), - htmlspecialchars($lot->getStorageLocation()->getName()) - ); - } + //We need to use a aggregate function to get the first store location, as we have a one-to-many relation + 'orderField' => 'NATSORT(MIN(_storelocations.name))', + 'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context), + ], alias: 'storage_location') - return implode('
', $tmp); - }, - ]); - } - - $dataTable->add('amount', TextColumn::class, [ - 'label' => $this->translator->trans('part.table.amount'), - 'render' => function ($value, Part $context) { - $amount = $context->getAmountSum(); - $expiredAmount = $context->getExpiredAmountSum(); - - $ret = htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit())); - - //If we have expired lots, we show them in parentheses behind - if ($expiredAmount > 0) { - $ret .= sprintf(' (+%s)', - $this->translator->trans('part_lots.is_expired'), - htmlspecialchars($this->amountFormatter->format($expiredAmount, $context->getPartUnit()))); - } - - //When the amount is below the minimum amount, we highlight the number red - if ($context->isNotEnoughInstock()) { - $ret = sprintf('%s', - $this->translator->trans('part.info.amount.less_than_desired'), - $ret); - } - - return $ret; - }, - 'orderField' => 'amountSum' - ]) + ->add('amount', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.amount'), + 'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderAmount($context), + 'orderField' => 'amountSum' + ]) ->add('minamount', TextColumn::class, [ 'label' => $this->translator->trans('part.table.minamount'), - 'visible' => false, - 'render' => function ($value, Part $context) { - return htmlspecialchars($this->amountFormatter->format($value, $context->getPartUnit())); - }, - ]); - - if ($this->security->isGranted('@footprints.read')) { - $dataTable->add('partUnit', TextColumn::class, [ - 'field' => 'partUnit.name', + 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format($value, + $context->getPartUnit())), + ]) + ->add('partUnit', TextColumn::class, [ 'label' => $this->translator->trans('part.table.partUnit'), - 'visible' => false, - ]); - } + 'orderField' => 'NATSORT(_partUnit.name)', + 'render' => function($value, Part $context): string { + $partUnit = $context->getPartUnit(); + if ($partUnit === null) { + return ''; + } - $dataTable->add('addedDate', LocaleDateTimeColumn::class, [ - 'label' => $this->translator->trans('part.table.addedDate'), - 'visible' => false, - ]) + $tmp = htmlspecialchars($partUnit->getName()); + + if ($partUnit->getUnit()) { + $tmp .= ' ('.htmlspecialchars($partUnit->getUnit()).')'; + } + return $tmp; + } + ]) + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.addedDate'), + ]) ->add('lastModified', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.lastModified'), - 'visible' => false, ]) ->add('needs_review', PrettyBoolColumn::class, [ 'label' => $this->translator->trans('part.table.needsReview'), - 'visible' => false, ]) ->add('favorite', PrettyBoolColumn::class, [ 'label' => $this->translator->trans('part.table.favorite'), - 'visible' => false, ]) - ->add('manufacturing_status', MapColumn::class, [ + ->add('manufacturing_status', EnumColumn::class, [ 'label' => $this->translator->trans('part.table.manufacturingStatus'), - 'visible' => false, - 'default' => $this->translator->trans('m_status.unknown'), - 'map' => [ - '' => $this->translator->trans('m_status.unknown'), - 'announced' => $this->translator->trans('m_status.announced'), - 'active' => $this->translator->trans('m_status.active'), - 'nrfnd' => $this->translator->trans('m_status.nrfnd'), - 'eol' => $this->translator->trans('m_status.eol'), - 'discontinued' => $this->translator->trans('m_status.discontinued'), - ], + 'class' => ManufacturingStatus::class, + 'render' => function (?ManufacturingStatus $status, Part $context): string { + if ($status === null) { + return ''; + } + + return $this->translator->trans($status->toTranslationKey()); + }, ]) ->add('manufacturer_product_number', TextColumn::class, [ 'label' => $this->translator->trans('part.table.mpn'), - 'visible' => false, + 'orderField' => 'NATSORT(part.manufacturer_product_number)' ]) ->add('mass', SIUnitNumberColumn::class, [ 'label' => $this->translator->trans('part.table.mass'), - 'visible' => false, 'unit' => 'g' ]) ->add('tags', TagsColumn::class, [ 'label' => $this->translator->trans('part.table.tags'), - 'visible' => false, ]) ->add('attachments', PartAttachmentsColumn::class, [ 'label' => $this->translator->trans('part.table.attachments'), - 'visible' => false, - ]) + ]); + + //Add a column to list the projects where the part is used, when the user has the permission to see the projects + if ($this->security->isGranted('read', Project::class)) { + $this->csh->add('projects', TextColumn::class, [ + 'label' => $this->translator->trans('project.labelp'), + 'render' => function ($value, Part $context): string { + //Only show the first 5 projects names + $projects = $context->getProjects(); + $tmp = ""; + + $max = 5; + + for ($i = 0; $i < min($max, count($projects)); $i++) { + $url = $this->urlGenerator->infoURL($projects[$i]); + $tmp .= sprintf('%s', $url, htmlspecialchars($projects[$i]->getName())); + if ($i < count($projects) - 1) { + $tmp .= ", "; + } + } + + if (count($projects) > $max) { + $tmp .= ", + ".(count($projects) - $max); + } + + return $tmp; + } + ]); + } + + $this->csh ->add('edit', IconLinkColumn::class, [ 'label' => $this->translator->trans('part.table.edit'), - 'visible' => false, - 'href' => function ($value, Part $context) { - return $this->urlGenerator->editURL($context); - }, - 'disabled' => function ($value, Part $context) { - return !$this->security->isGranted('edit', $context); - }, + 'href' => fn($value, Part $context) => $this->urlGenerator->editURL($context), + 'disabled' => fn($value, Part $context) => !$this->security->isGranted('edit', $context), 'title' => $this->translator->trans('part.table.edit.title'), - ]) + ]); - ->addOrderBy('name') - ->createAdapter(CustomFetchJoinORMAdapter::class, [ - 'simple_total_query' => true, - 'query' => function (QueryBuilder $builder): void { - $this->getQuery($builder); - }, + //Apply the user configured order and visibility and add the columns to the table + $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, + "TABLE_PARTS_DEFAULT_COLUMNS"); + + $dataTable->addOrderBy('name') + ->createAdapter(TwoStepORMAdapter::class, [ + 'filter_query' => $this->getFilterQuery(...), + 'detail_query' => $this->getDetailQuery(...), 'entity' => Part::class, + 'hydrate' => AbstractQuery::HYDRATE_OBJECT, + //Use the simple total query, as we just want to get the total number of parts without any conditions + //For this the normal query would be pretty slow + 'simple_total_query' => true, 'criteria' => [ function (QueryBuilder $builder) use ($options): void { $this->buildCriteria($builder, $options); }, new SearchCriteriaProvider(), ], + 'query_modifier' => $this->addJoins(...), ]); } - private function getQuery(QueryBuilder $builder): void + + private function getFilterQuery(QueryBuilder $builder): void { - //Distinct is very slow here, do not add this here (also I think this is not needed here, as the id column is always distinct) - $builder->select('part') + /* In the filter query we only select the IDs. The fetching of the full entities is done in the detail query. + * We only need to join the entities here, so we can filter by them. + * The filter conditions are added to this QB in the buildCriteria method. + * + * The amountSum field and the joins are dynmically added by the addJoins method, if the fields are used in the query. + * This improves the performance, as we do not need to join all tables, if we do not need them. + */ + $builder + ->select('part.id') + ->addSelect('part.minamount AS HIDDEN minamount') + ->from(Part::class, 'part') + + //The other group by fields, are dynamically added by the addJoins method + ->addGroupBy('part'); + } + + private function getDetailQuery(QueryBuilder $builder, array $filter_results): void + { + $ids = array_map(static fn($row) => $row['id'], $filter_results); + + /* + * In this query we take the IDs which were filtered, paginated and sorted in the filter query, and fetch the + * full entities. + * We can do complex fetch joins, as we do not need to filter or sort here (which would kill the performance). + * The only condition should be for the IDs. + * It is important that elements are ordered the same way, as the IDs are passed, or ordering will be wrong. + * + * We do not require the subqueries like amountSum here, as it is not used to render the table (and only for sorting) + */ + $builder + ->select('part') ->addSelect('category') ->addSelect('footprint') ->addSelect('manufacturer') @@ -317,16 +312,6 @@ final class PartsDataTable implements DataTableTypeInterface ->addSelect('orderdetails') ->addSelect('attachments') ->addSelect('storelocations') - //Calculate amount sum using a subquery, so we can filter and sort by it - ->addSelect( - '( - SELECT IFNULL(SUM(partLot.amount), 0.0) - FROM '. PartLot::class. ' partLot - WHERE partLot.part = part.id - AND partLot.instock_unknown = false - AND (partLot.expiration_date IS NULL OR partLot.expiration_date > CURRENT_DATE()) - ) AS HIDDEN amountSum' - ) ->from(Part::class, 'part') ->leftJoin('part.category', 'category') ->leftJoin('part.master_picture_attachment', 'master_picture_attachment') @@ -340,6 +325,8 @@ final class PartsDataTable implements DataTableTypeInterface ->leftJoin('part.attachments', 'attachments') ->leftJoin('part.partUnit', 'partUnit') ->leftJoin('part.parameters', 'parameters') + ->where('part.id IN (:ids)') + ->setParameter('ids', $ids) //We have to group by all elements, or only the first sub elements of an association is fetched! (caused issue #190) ->addGroupBy('part') @@ -354,8 +341,89 @@ final class PartsDataTable implements DataTableTypeInterface ->addGroupBy('suppliers') ->addGroupBy('attachments') ->addGroupBy('partUnit') - ->addGroupBy('parameters') - ; + ->addGroupBy('parameters'); + + //Get the results in the same order as the IDs were passed + FieldHelper::addOrderByFieldParam($builder, 'part.id', 'ids'); + } + + /** + * This function is called right before the filter query is executed. + * We use it to dynamically add joins to the query, if the fields are used in the query. + * @param QueryBuilder $builder + * @return QueryBuilder + */ + private function addJoins(QueryBuilder $builder): QueryBuilder + { + //Check if the query contains certain conditions, for which we need to add additional joins + //The join fields get prefixed with an underscore, so we can check if they are used in the query easy without confusing them for a part subfield + $dql = $builder->getDQL(); + + //Add the amountSum field, if it is used in the query + if (str_contains($dql, 'amountSum')) { + //Calculate amount sum using a subquery, so we can filter and sort by it + $builder->addSelect( + '( + SELECT COALESCE(SUM(partLot.amount), 0.0) + FROM '.PartLot::class.' partLot + WHERE partLot.part = part.id + AND partLot.instock_unknown = false + AND (partLot.expiration_date IS NULL OR partLot.expiration_date > CURRENT_DATE()) + ) AS HIDDEN amountSum' + ); + } + + if (str_contains($dql, '_category')) { + $builder->leftJoin('part.category', '_category'); + $builder->addGroupBy('_category'); + } + if (str_contains($dql, '_master_picture_attachment')) { + $builder->leftJoin('part.master_picture_attachment', '_master_picture_attachment'); + $builder->addGroupBy('_master_picture_attachment'); + } + if (str_contains($dql, '_partLots') || str_contains($dql, '_storelocations')) { + $builder->leftJoin('part.partLots', '_partLots'); + $builder->leftJoin('_partLots.storage_location', '_storelocations'); + //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 + //$builder->addGroupBy('_partLots'); + //$builder->addGroupBy('_storelocations'); + } + if (str_contains($dql, '_footprint')) { + $builder->leftJoin('part.footprint', '_footprint'); + $builder->addGroupBy('_footprint'); + } + if (str_contains($dql, '_manufacturer')) { + $builder->leftJoin('part.manufacturer', '_manufacturer'); + $builder->addGroupBy('_manufacturer'); + } + if (str_contains($dql, '_orderdetails') || str_contains($dql, '_suppliers')) { + $builder->leftJoin('part.orderdetails', '_orderdetails'); + $builder->leftJoin('_orderdetails.supplier', '_suppliers'); + //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 + //$builder->addGroupBy('_orderdetails'); + //$builder->addGroupBy('_suppliers'); + } + if (str_contains($dql, '_attachments')) { + $builder->leftJoin('part.attachments', '_attachments'); + //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 + //$builder->addGroupBy('_attachments'); + } + if (str_contains($dql, '_partUnit')) { + $builder->leftJoin('part.partUnit', '_partUnit'); + $builder->addGroupBy('_partUnit'); + } + if (str_contains($dql, '_parameters')) { + $builder->leftJoin('part.parameters', '_parameters'); + //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 + //$builder->addGroupBy('_parameters'); + } + if (str_contains($dql, '_projectBomEntries')) { + $builder->leftJoin('part.project_bom_entries', '_projectBomEntries'); + //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1 + //$builder->addGroupBy('_projectBomEntries'); + } + + return $builder; } private function buildCriteria(QueryBuilder $builder, array $options): void @@ -371,6 +439,5 @@ final class PartsDataTable implements DataTableTypeInterface $filter = $options['filter']; $filter->apply($builder); } - } } diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php index eac5ae5a..fcb06984 100644 --- a/src/DataTables/ProjectBomEntriesDataTable.php +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -1,4 +1,7 @@ . */ - namespace App\DataTables; use App\DataTables\Column\EntityColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\MarkdownColumn; -use App\DataTables\Column\SelectColumn; use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; @@ -40,22 +41,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class ProjectBomEntriesDataTable implements DataTableTypeInterface { - protected TranslatorInterface $translator; - protected PartDataTableHelper $partDataTableHelper; - protected EntityURLGenerator $entityURLGenerator; - protected AmountFormatter $amountFormatter; - - public function __construct(TranslatorInterface $translator, PartDataTableHelper $partDataTableHelper, - EntityURLGenerator $entityURLGenerator, AmountFormatter $amountFormatter) + public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter) { - $this->translator = $translator; - $this->partDataTableHelper = $partDataTableHelper; - $this->entityURLGenerator = $entityURLGenerator; - $this->amountFormatter = $amountFormatter; } - public function configure(DataTable $dataTable, array $options) + public function configure(DataTable $dataTable, array $options): void { $dataTable //->add('select', SelectColumn::class) @@ -63,7 +54,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => '', 'className' => 'no-colvis', 'render' => function ($value, ProjectBOMEntry $context) { - if($context->getPart() === null) { + if(!$context->getPart() instanceof Part) { return ''; } return $this->partDataTableHelper->renderPicture($context->getPart()); @@ -79,9 +70,9 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('project.bom.quantity'), 'className' => 'text-center', 'orderField' => 'bom_entry.quantity', - 'render' => function ($value, ProjectBOMEntry $context) { + 'render' => function ($value, ProjectBOMEntry $context): float|string { //If we have a non-part entry, only show the rounded quantity - if ($context->getPart() === null) { + if (!$context->getPart() instanceof Part) { return round($context->getQuantity()); } //Otherwise use the unit of the part to format the quantity @@ -91,26 +82,35 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface ->add('name', TextColumn::class, [ 'label' => $this->translator->trans('part.table.name'), - 'orderField' => 'part.name', + 'orderField' => 'NATSORT(part.name)', 'render' => function ($value, ProjectBOMEntry $context) { - if($context->getPart() === null) { - return htmlspecialchars($context->getName()); + if(!$context->getPart() instanceof Part) { + return htmlspecialchars((string) $context->getName()); } - if($context->getPart() !== null) { - $tmp = $this->partDataTableHelper->renderName($context->getPart()); - if(!empty($context->getName())) { - $tmp .= '
'.htmlspecialchars($context->getName()).''; - } - return $tmp; + + //Part exists if we reach this point + + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + if($context->getName() !== null && $context->getName() !== '') { + $tmp .= '
'.htmlspecialchars($context->getName()).''; } - throw new \Exception('This should never happen!'); + return $tmp; }, ]) - + ->add('ipn', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.ipn'), + 'orderField' => 'NATSORT(part.ipn)', + 'visible' => false, + 'render' => function ($value, ProjectBOMEntry $context) { + if($context->getPart() instanceof Part) { + return $context->getPart()->getIpn(); + } + } + ]) ->add('description', MarkdownColumn::class, [ 'label' => $this->translator->trans('part.table.description'), 'data' => function (ProjectBOMEntry $context) { - if($context->getPart() !== null) { + if($context->getPart() instanceof Part) { return $context->getPart()->getDescription(); } //For non-part BOM entries show the comment field @@ -122,18 +122,18 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface ->add('category', EntityColumn::class, [ 'label' => $this->translator->trans('part.table.category'), 'property' => 'part.category', - 'orderField' => 'category.name', + 'orderField' => 'NATSORT(category.name)', ]) ->add('footprint', EntityColumn::class, [ 'property' => 'part.footprint', 'label' => $this->translator->trans('part.table.footprint'), - 'orderField' => 'footprint.name', + 'orderField' => 'NATSORT(footprint.name)', ]) ->add('manufacturer', EntityColumn::class, [ 'property' => 'part.manufacturer', 'label' => $this->translator->trans('part.table.manufacturer'), - 'orderField' => 'manufacturer.name', + 'orderField' => 'NATSORT(manufacturer.name)', ]) ->add('mountnames', TextColumn::class, [ @@ -148,6 +148,28 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface }, ]) + ->add('instockAmount', TextColumn::class, [ + 'label' => 'project.bom.instockAmount', + 'visible' => false, + 'render' => function ($value, ProjectBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderAmount($context->getPart()); + } + + return ''; + } + ]) + ->add('storageLocations', TextColumn::class, [ + 'label' => 'part.table.storeLocations', + 'visible' => false, + 'render' => function ($value, ProjectBOMEntry $context) { + if ($context->getPart() !== null) { + return $this->partDataTableHelper->renderStorageLocations($context->getPart()); + } + + return ''; + } + ]) ->add('addedDate', LocaleDateTimeColumn::class, [ 'label' => $this->translator->trans('part.table.addedDate'), @@ -193,4 +215,4 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface { } -} \ No newline at end of file +} diff --git a/src/Doctrine/Functions/ArrayPosition.php b/src/Doctrine/Functions/ArrayPosition.php new file mode 100644 index 00000000..39276912 --- /dev/null +++ b/src/Doctrine/Functions/ArrayPosition.php @@ -0,0 +1,59 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Functions; + +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\AST\Node; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; + +class ArrayPosition extends FunctionNode +{ + private ?Node $array = null; + + private ?Node $field = null; + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->array = $parser->InParameter(); + + $parser->match(TokenType::T_COMMA); + + $this->field = $parser->ArithmeticPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $sqlWalker): string + { + return 'ARRAY_POSITION(' . + $this->array->dispatch($sqlWalker) . ', ' . + $this->field->dispatch($sqlWalker) . + ')'; + } +} \ No newline at end of file diff --git a/src/Doctrine/Functions/Field2.php b/src/Doctrine/Functions/Field2.php new file mode 100644 index 00000000..57f55653 --- /dev/null +++ b/src/Doctrine/Functions/Field2.php @@ -0,0 +1,82 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Functions; + +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\TokenType; + +/** + * Basically the same as the original Field function, but uses FIELD2 for the SQL query. + */ +class Field2 extends FunctionNode +{ + + private $field = null; + + private $values = []; + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + // Do the field. + $this->field = $parser->ArithmeticPrimary(); + + // Add the strings to the values array. FIELD must + // be used with at least 1 string not including the field. + + $lexer = $parser->getLexer(); + + while (count($this->values) < 1 || + $lexer->lookahead->type !== TokenType::T_CLOSE_PARENTHESIS) { + $parser->match(TokenType::T_COMMA); + $this->values[] = $parser->ArithmeticPrimary(); + } + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $sqlWalker): string + { + $query = 'FIELD2('; + + $query .= $this->field->dispatch($sqlWalker); + + $query .= ', '; + $counter = count($this->values); + + for ($i = 0; $i < $counter; $i++) { + if ($i > 0) { + $query .= ', '; + } + + $query .= $this->values[$i]->dispatch($sqlWalker); + } + + return $query . ')'; + } +} \ No newline at end of file diff --git a/src/Doctrine/Functions/ILike.php b/src/Doctrine/Functions/ILike.php new file mode 100644 index 00000000..5246220a --- /dev/null +++ b/src/Doctrine/Functions/ILike.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Functions; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; + +/** + * A platform invariant version of the case-insensitive LIKE operation. + * On MySQL and SQLite this is the normal LIKE, but on PostgreSQL it is the ILIKE operator. + */ +class ILike extends FunctionNode +{ + + public $value = null; + + public $expr = null; + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $this->value = $parser->StringPrimary(); + $parser->match(TokenType::T_COMMA); + $this->expr = $parser->StringExpression(); + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $sqlWalker): string + { + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + + // + if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) { + $operator = 'LIKE'; + } elseif ($platform instanceof PostgreSQLPlatform) { + //Use the case-insensitive operator, to have the same behavior as MySQL + $operator = 'ILIKE'; + } else { + throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.'); + } + + return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')'; + } +} \ No newline at end of file diff --git a/src/Doctrine/Functions/Natsort.php b/src/Doctrine/Functions/Natsort.php new file mode 100644 index 00000000..bd05e0d6 --- /dev/null +++ b/src/Doctrine/Functions/Natsort.php @@ -0,0 +1,151 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Functions; + +use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\MariaDBPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\AST\Node; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; +use Doctrine\ORM\Query\TokenType; + +class Natsort extends FunctionNode +{ + private ?Node $field = null; + + private static ?bool $supportsNaturalSort = null; + + private static bool $allowSlowNaturalSort = false; + + /** + * As we can not inject parameters into the function, we use an event listener, to call the value on the static function. + * This is the only way to inject the value into the function. + * @param bool $allow + * @return void + */ + public static function allowSlowNaturalSort(bool $allow = true): void + { + self::$allowSlowNaturalSort = $allow; + } + + /** + * Check if the slow natural sort is allowed + * @return bool + */ + public static function isSlowNaturalSortAllowed(): bool + { + return self::$allowSlowNaturalSort; + } + + /** + * Check if the MariaDB version which is connected to supports the natural sort (meaning it has a version of 10.7.0 or higher) + * The result is cached in memory. + * @param Connection $connection + * @return bool + * @throws Exception + */ + private function mariaDBSupportsNaturalSort(Connection $connection): bool + { + if (self::$supportsNaturalSort !== null) { + return self::$supportsNaturalSort; + } + + $version = $connection->getServerVersion(); + + //Get the effective MariaDB version number + $version = $this->getMariaDbMysqlVersionNumber($version); + + //We need at least MariaDB 10.7.0 to support the natural sort + self::$supportsNaturalSort = version_compare($version, '10.7.0', '>='); + return self::$supportsNaturalSort; + } + + /** + * Taken from Doctrine\DBAL\Driver\AbstractMySQLDriver + * + * Detect MariaDB server version, including hack for some mariadb distributions + * that starts with the prefix '5.5.5-' + * + * @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial' + */ + private function getMariaDbMysqlVersionNumber(string $versionString) : string + { + if ( ! preg_match( + '/^(?:5\.5\.5-)?(mariadb-)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', + $versionString, + $versionParts + )) { + throw new \RuntimeException('Could not detect MariaDB version from version string ' . $versionString); + } + + return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->field = $parser->ArithmeticExpression(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $sqlWalker): string + { + assert($this->field !== null, 'Field is not set'); + + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + + if ($platform instanceof PostgreSQLPlatform) { + return $this->field->dispatch($sqlWalker) . ' COLLATE numeric'; + } + + if ($platform instanceof MariaDBPlatform && $this->mariaDBSupportsNaturalSort($sqlWalker->getConnection())) { + return 'NATURAL_SORT_KEY(' . $this->field->dispatch($sqlWalker) . ')'; + } + + //Do the following operations only if we allow slow natural sort + if (self::$allowSlowNaturalSort) { + if ($platform instanceof SQLitePlatform) { + return $this->field->dispatch($sqlWalker).' COLLATE NATURAL_CMP'; + } + + if ($platform instanceof AbstractMySQLPlatform) { + return 'NatSortKey(' . $this->field->dispatch($sqlWalker) . ', 0)'; + } + } + + //For every other platform, return the field as is + return $this->field->dispatch($sqlWalker); + } + + +} \ No newline at end of file diff --git a/src/Doctrine/Functions/Regexp.php b/src/Doctrine/Functions/Regexp.php new file mode 100644 index 00000000..d7c6f1e7 --- /dev/null +++ b/src/Doctrine/Functions/Regexp.php @@ -0,0 +1,52 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Functions; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; +use Doctrine\ORM\Query\SqlWalker; + +/** + * Similar to the regexp function, but with support for multi platform. + */ +class Regexp extends \DoctrineExtensions\Query\Mysql\Regexp +{ + public function getSql(SqlWalker $sqlWalker): string + { + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + + // + if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) { + $operator = 'REGEXP'; + } elseif ($platform instanceof PostgreSQLPlatform) { + //Use the case-insensitive operator, to have the same behavior as MySQL + $operator = '~*'; + } else { + throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support regular expressions.'); + } + + return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->regexp->dispatch($sqlWalker) . ')'; + } +} \ No newline at end of file diff --git a/src/Doctrine/Helpers/FieldHelper.php b/src/Doctrine/Helpers/FieldHelper.php new file mode 100644 index 00000000..11300db3 --- /dev/null +++ b/src/Doctrine/Helpers/FieldHelper.php @@ -0,0 +1,124 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Helpers; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\ORM\QueryBuilder; + +/** + * The purpose of this class is to provide help with using the FIELD functions in Doctrine, which depends on the database platform. + */ +final class FieldHelper +{ + /** + * Add an ORDER BY FIELD expression to the query builder. The correct FIELD function is used depending on the database platform. + * In this function an already bound paramater is used. If you want to a not already bound value, use the addOrderByFieldValues function. + * @param QueryBuilder $qb The query builder to apply the order by to + * @param string $field_expr The expression to compare with the values + * @param string|int $bound_param The already bound parameter to use for the values + * @param string|null $order The order direction (ASC or DESC) + * @return QueryBuilder + */ + public static function addOrderByFieldParam(QueryBuilder $qb, string $field_expr, string|int $bound_param, ?string $order = null): QueryBuilder + { + $db_platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform(); + + //If we are on MySQL, we can just use the FIELD function + if ($db_platform instanceof AbstractMySQLPlatform ) { + $param = (is_numeric($bound_param) ? '?' : ":").(string)$bound_param; + $qb->orderBy("FIELD($field_expr, $param)", $order); + } else { //Use the sqlite/portable version or postgresql + //Retrieve the values from the bound parameter + $param = $qb->getParameter($bound_param); + if ($param === null) { + throw new \InvalidArgumentException("The bound parameter $bound_param does not exist."); + } + + //Generate a unique key from the field_expr + $key = 'field2_' . (string) $bound_param; + + if ($db_platform instanceof PostgreSQLPlatform) { + self::addPostgresOrderBy($qb, $field_expr, $key, $param->getValue(), $order); + } else { + self::addSqliteOrderBy($qb, $field_expr, $key, $param->getValue(), $order); + } + } + + return $qb; + } + + + private static function addPostgresOrderBy(QueryBuilder $qb, string $field_expr, string $key, array $values, ?string $order = null): void + { + //Use postgres native array_position function, to get the index of the value in the array + //In the end it gives a similar result as the FIELD function + $qb->orderBy("array_position(:$key, $field_expr)", $order); + + //Convert the values to a literal array, to overcome the problem of passing more than 100 parameters + $values = array_map(fn($value) => is_string($value) ? "'$value'" : $value, $values); + $literalArray = '{' . implode(',', $values) . '}'; + + $qb->setParameter($key, $literalArray); + } + + private static function addSqliteOrderBy(QueryBuilder $qb, string $field_expr, string $key, array $values, ?string $order = null): void + { + //Otherwise we emulate it using + $qb->orderBy("LOCATE(CONCAT(',', $field_expr, ','), :$key)", $order); + //The string must be padded with a comma on both sides, otherwise the search using INSTR will fail + $qb->setParameter($key, ',' .implode(',', $values) . ','); + } + + /** + * Add an ORDER BY FIELD expression to the query builder. The correct FIELD function is used depending on the database platform. + * In this function the values are passed as an array. If you want to reuse an existing bound parameter, use the addOrderByFieldParam function. + * @param QueryBuilder $qb The query builder to apply the order by to + * @param string $field_expr The expression to compare with the values + * @param array $values The values to compare with the expression as array + * @param string|null $order The order direction (ASC or DESC) + * @return QueryBuilder + */ + public static function addOrderByFieldValues(QueryBuilder $qb, string $field_expr, array $values, ?string $order = null): QueryBuilder + { + $db_platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform(); + + $key = 'field2_' . md5($field_expr); + + //If we are on MySQL, we can just use the FIELD function + if ($db_platform instanceof AbstractMySQLPlatform) { + $qb->orderBy("FIELD2($field_expr, :field_arr)", $order); + } elseif ($db_platform instanceof PostgreSQLPlatform) { + //Use the postgres native array_position function + self::addPostgresOrderBy($qb, $field_expr, $key, $values, $order); + } else { + //Otherwise use the portable version using string concatenation + self::addSqliteOrderBy($qb, $field_expr, $key, $values, $order); + } + + $qb->setParameter($key, $values); + + return $qb; + } +} \ No newline at end of file diff --git a/src/Doctrine/Middleware/MySQLSSLConnectionMiddlewareDriver.php b/src/Doctrine/Middleware/MySQLSSLConnectionMiddlewareDriver.php new file mode 100644 index 00000000..2a707e1f --- /dev/null +++ b/src/Doctrine/Middleware/MySQLSSLConnectionMiddlewareDriver.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Middleware; + +use Composer\CaBundle\CaBundle; +use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Driver\Connection; +use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; + +/** + * This middleware sets SSL options for MySQL connections + */ +class MySQLSSLConnectionMiddlewareDriver extends AbstractDriverMiddleware +{ + public function __construct(Driver $wrappedDriver, private readonly bool $enabled, private readonly bool $verify = true) + { + parent::__construct($wrappedDriver); + } + + public function connect(array $params): Connection + { + //Only set this on MySQL connections, as other databases don't support this parameter + if($this->enabled && $params['driver'] === 'pdo_mysql') { + $params['driverOptions'][\PDO::MYSQL_ATTR_SSL_CA] = CaBundle::getSystemCaRootBundlePath(); + $params['driverOptions'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->verify; + } + + return parent::connect($params); + } +} \ No newline at end of file diff --git a/src/Doctrine/Middleware/MySQLSSLConnectionMiddlewareWrapper.php b/src/Doctrine/Middleware/MySQLSSLConnectionMiddlewareWrapper.php new file mode 100644 index 00000000..8bd25971 --- /dev/null +++ b/src/Doctrine/Middleware/MySQLSSLConnectionMiddlewareWrapper.php @@ -0,0 +1,39 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Middleware; + +use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Driver\Middleware; + +class MySQLSSLConnectionMiddlewareWrapper implements Middleware +{ + public function __construct(private readonly bool $enabled, private readonly bool $verify = true) + { + } + + public function wrap(Driver $driver): Driver + { + return new MySQLSSLConnectionMiddlewareDriver($driver, $this->enabled, $this->verify); + } +} \ No newline at end of file diff --git a/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php new file mode 100644 index 00000000..ad572d4c --- /dev/null +++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareDriver.php @@ -0,0 +1,117 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Middleware; + +use App\Exceptions\InvalidRegexException; +use Doctrine\DBAL\Driver\Connection; +use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; + +/** + * This middleware is used to add the regexp operator to the SQLite platform. + * As a PHP callback is called for every entry to compare it is most likely much slower than using regex on MySQL. + * But as regex is not often used, this should be fine for most use cases, also it is almost impossible to implement a better solution. + */ +class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware +{ + public function connect(#[\SensitiveParameter] array $params): Connection + { + //Do connect process first + $connection = parent::connect($params); // TODO: Change the autogenerated stub + + //Then add the functions if we are on SQLite + if ($params['driver'] === 'pdo_sqlite') { + $native_connection = $connection->getNativeConnection(); + + //Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation + if($native_connection instanceof \PDO) { + $native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC); + $native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC); + $native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC); + + //Create a new collation for natural sorting + $native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...)); + } + } + + + return $connection; + } + + /** + * This function emulates the MySQL regexp function for SQLite + * @param string $pattern + * @param string $value + * @return int + */ + final public static function regexp(string $pattern, ?string $value): int + { + if ($value === null) { + return 0; + } + + try { + return (mb_ereg($pattern, $value)) ? 1 : 0; + + } catch (\ErrorException $e) { + throw InvalidRegexException::fromMBRegexError($e); + } + } + + /** + * Very similar to the field function, but takes the array values as a comma separated string. + * This is needed as SQLite has a pretty low argument count limit. + * @param string|int|null $value + * @param string $imploded_array + * @return int + */ + final public static function field2(string|int|null $value, string $imploded_array): int + { + $array = explode(',', $imploded_array); + return self::field($value, ...$array); + } + + /** + * This function emulates the MySQL field function for SQLite + * This function returns the index (position) of the first argument in the subsequent arguments. + * If the first argument is not found or is NULL, 0 is returned. + * @param string|int|null $value + * @return int + */ + final public static function field(string|int|null $value, mixed ...$array): int + { + if ($value === null) { + return 0; + } + + //We are loose with the types here + //@phpstan-ignore-next-line + $index = array_search($value, $array, false); + + if ($index === false) { + return 0; + } + + return $index + 1; + } +} \ No newline at end of file diff --git a/assets/css/app/darkmode.css b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareWrapper.php similarity index 63% rename from assets/css/app/darkmode.css rename to src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareWrapper.php index 8ef745f4..42aafaad 100644 --- a/assets/css/app/darkmode.css +++ b/src/Doctrine/Middleware/SQLiteRegexExtensionMiddlewareWrapper.php @@ -1,7 +1,8 @@ +. */ -.darkmode-layer { - z-index: 2020; -} +declare(strict_types=1); -/** If darkmode is enabled revert the blening for images and videos, as these should be shown not inverted */ -.darkmode--activated img, -.darkmode--activated video, -.darkmode--activated object { - mix-blend-mode: difference; -} -.darkmode--activated .hoverpic:hover { - background: black; -} +namespace App\Doctrine\Middleware; -.tools-ic-logos img { - mix-blend-mode: normal; +use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Driver\Middleware; + +class SQLiteRegexExtensionMiddlewareWrapper implements Middleware +{ + public function wrap(Driver $driver): Driver + { + return new SQLiteRegexExtensionMiddlewareDriver($driver); + } } \ No newline at end of file diff --git a/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareDriver.php b/src/Doctrine/Middleware/SetSQLModeMiddlewareDriver.php similarity index 79% rename from src/Doctrine/SetSQLMode/SetSQLModeMiddlewareDriver.php rename to src/Doctrine/Middleware/SetSQLModeMiddlewareDriver.php index 4b91ab57..d05b6b9c 100644 --- a/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareDriver.php +++ b/src/Doctrine/Middleware/SetSQLModeMiddlewareDriver.php @@ -1,4 +1,7 @@ . */ +namespace App\Doctrine\Middleware; -namespace App\Doctrine\SetSQLMode; - +use Doctrine\DBAL\Driver\Connection; use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; -use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; /** * This command sets the initial command parameter for MySQL connections, so we can set the SQL mode @@ -29,14 +31,14 @@ use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; */ class SetSQLModeMiddlewareDriver extends AbstractDriverMiddleware { - public function connect(array $params): \Doctrine\DBAL\Driver\Connection + public function connect(array $params): Connection { //Only set this on MySQL connections, as other databases don't support this parameter - if($this->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + if($params['driver'] === 'pdo_mysql') { //1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value - $params['driverOptions'][1002] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))'; + $params['driverOptions'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))'; } return parent::connect($params); } -} \ No newline at end of file +} diff --git a/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareWrapper.php b/src/Doctrine/Middleware/SetSQLModeMiddlewareWrapper.php similarity index 91% rename from src/Doctrine/SetSQLMode/SetSQLModeMiddlewareWrapper.php rename to src/Doctrine/Middleware/SetSQLModeMiddlewareWrapper.php index 0ff670ba..3307bc7f 100644 --- a/src/Doctrine/SetSQLMode/SetSQLModeMiddlewareWrapper.php +++ b/src/Doctrine/Middleware/SetSQLModeMiddlewareWrapper.php @@ -1,4 +1,7 @@ . */ - -namespace App\Doctrine\SetSQLMode; +namespace App\Doctrine\Middleware; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\Middleware; /** - * This class wraps the Doctrine DBAL driver and wraps it into an Midleware driver so we can change the SQL mode + * This class wraps the Doctrine DBAL driver and wraps it into a Midleware driver, so we can change the SQL mode */ class SetSQLModeMiddlewareWrapper implements Middleware { - public function wrap(Driver $driver): Driver { return new SetSQLModeMiddlewareDriver($driver); } -} \ No newline at end of file +} diff --git a/src/Doctrine/Purger/DoNotUsePurgerFactory.php b/src/Doctrine/Purger/DoNotUsePurgerFactory.php new file mode 100644 index 00000000..6d487573 --- /dev/null +++ b/src/Doctrine/Purger/DoNotUsePurgerFactory.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Purger; + +use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory; +use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface; +use Doctrine\Common\DataFixtures\Purger\PurgerInterface; +use Doctrine\ORM\EntityManagerInterface; + +class DoNotUsePurgerFactory implements PurgerFactory +{ + + public function createForEntityManager( + ?string $emName, + EntityManagerInterface $em, + array $excluded = [], + bool $purgeWithTruncate = false + ): PurgerInterface { + return new class() implements ORMPurgerInterface { + + public function purge(): void + { + throw new \LogicException('Do not use doctrine:fixtures:load directly. Use partdb:fixtures:load instead!'); + } + + public function setEntityManager(EntityManagerInterface $em): void + { + // TODO: Implement setEntityManager() method. + } + }; + } +} \ No newline at end of file diff --git a/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php b/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php index 811facf6..0fbf6cdb 100644 --- a/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php +++ b/src/Doctrine/Purger/ResetAutoIncrementORMPurger.php @@ -27,15 +27,11 @@ use Doctrine\Common\DataFixtures\Purger\PurgerInterface; use Doctrine\Common\DataFixtures\Sorter\TopologicalSorter; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Schema\Identifier; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\ClassMetadataInfo; - use function array_reverse; -use function array_search; use function assert; use function count; use function is_callable; @@ -49,25 +45,15 @@ use function preg_match; */ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface { - public const PURGE_MODE_DELETE = 1; - public const PURGE_MODE_TRUNCATE = 2; - - /** @var EntityManagerInterface|null */ - private $em; + final public const PURGE_MODE_DELETE = 1; + final public const PURGE_MODE_TRUNCATE = 2; /** * If the purge should be done through DELETE or TRUNCATE statements * * @var int */ - private $purgeMode = self::PURGE_MODE_DELETE; - - /** - * Table/view names to be excluded from purge - * - * @var string[] - */ - private $excluded; + private int $purgeMode = self::PURGE_MODE_DELETE; /** * Construct new purger instance. @@ -75,18 +61,20 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface * @param EntityManagerInterface|null $em EntityManagerInterface instance used for persistence. * @param string[] $excluded array of table/view names to be excluded from purge */ - public function __construct(?EntityManagerInterface $em = null, array $excluded = []) + public function __construct( + private ?EntityManagerInterface $em = null, + /** + * Table/view names to be excluded from purge + */ + private readonly array $excluded = [] + ) { - $this->em = $em; - $this->excluded = $excluded; } /** * Set the purge mode * - * @param int $mode * - * @return void */ public function setPurgeMode(int $mode): void { @@ -95,8 +83,6 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface /** * Get the purge mode - * - * @return int */ public function getPurgeMode(): int { @@ -125,7 +111,7 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface $classes = []; foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) { - if ($metadata->isMappedSuperclass || (isset($metadata->isEmbeddedClass) && $metadata->isEmbeddedClass)) { + if ($metadata->isMappedSuperclass || ($metadata->isEmbeddedClass !== null && $metadata->isEmbeddedClass)) { continue; } @@ -145,7 +131,7 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface $class = $commitOrder[$i]; if ( - (isset($class->isEmbeddedClass) && $class->isEmbeddedClass) || + ($class->isEmbeddedClass !== null && $class->isEmbeddedClass) || $class->isMappedSuperclass || ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName) ) { @@ -174,10 +160,15 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface foreach ($orderedTables as $tbl) { // If we have a filter expression, check it and skip if necessary - if (! $emptyFilterExpression && ! preg_match($filterExpr, $tbl)) { + if (! $emptyFilterExpression && ! preg_match($filterExpr, (string) $tbl)) { continue; } + // The table name might be quoted, we have to trim it + // See https://github.com/Part-DB/Part-DB-server/issues/299 + $tbl = trim((string) $tbl, '"'); + $tbl = trim($tbl, '`'); + // If the table is excluded, skip it as well if (in_array($tbl, $this->excluded, true)) { continue; @@ -195,8 +186,7 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface } //Reseting autoincrement is only supported on MySQL platforms - if ($platform instanceof AbstractMySQLPlatform) { - $connection->beginTransaction(); + if ($platform instanceof AbstractMySQLPlatform ) { //|| $platform instanceof SqlitePlatform) { $connection->executeQuery($this->getResetAutoIncrementSQL($tbl, $platform)); } } @@ -211,7 +201,16 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface { $tableIdentifier = new Identifier($tableName); - return 'ALTER TABLE '. $tableIdentifier->getQuotedName($platform) .' AUTO_INCREMENT = 1;'; + if ($platform instanceof AbstractMySQLPlatform) { + return 'ALTER TABLE '.$tableIdentifier->getQuotedName($platform).' AUTO_INCREMENT = 1;'; + } + + throw new \RuntimeException("Resetting autoincrement is not supported on this platform!"); + + //This seems to cause problems somehow + /*if ($platform instanceof SqlitePlatform) { + return 'DELETE FROM `sqlite_sequence` WHERE name = \''.$tableIdentifier->getQuotedName($platform).'\';'; + }*/ } /** @@ -273,18 +272,13 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface return array_reverse($sorter->sort()); } - /** - * @param array $classes - * - * @return array - */ private function getAssociationTables(array $classes, AbstractPlatform $platform): array { $associationTables = []; foreach ($classes as $class) { foreach ($class->associationMappings as $assoc) { - if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadataInfo::MANY_TO_MANY) { + if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadata::MANY_TO_MANY) { continue; } @@ -307,9 +301,6 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface return $this->em->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform); } - /** - * @param array $assoc - */ private function getJoinTableName( array $assoc, ClassMetadata $class, diff --git a/src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php b/src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php index 00152718..4d78e0f0 100644 --- a/src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php +++ b/src/Doctrine/Purger/ResetAutoIncrementPurgerFactory.php @@ -1,4 +1,7 @@ . */ - namespace App\Doctrine\Purger; use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory; @@ -35,4 +37,4 @@ class ResetAutoIncrementPurgerFactory implements PurgerFactory return $purger; } -} \ No newline at end of file +} diff --git a/src/Doctrine/SQLiteRegexExtension.php b/src/Doctrine/SQLiteRegexExtension.php deleted file mode 100644 index f1bca465..00000000 --- a/src/Doctrine/SQLiteRegexExtension.php +++ /dev/null @@ -1,58 +0,0 @@ -. - */ - -namespace App\Doctrine; - -use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface; -use Doctrine\DBAL\Event\ConnectionEventArgs; -use Doctrine\DBAL\Events; -use Doctrine\DBAL\Platforms\SqlitePlatform; - -/** - * This subscriber is used to add the regexp operator to the SQLite platform. - * As a PHP callback is called for every entry to compare it is most likely much slower than using regex on MySQL. - * But as regex is not often used, this should be fine for most use cases, also it is almost impossible to implement a better solution. - */ -class SQLiteRegexExtension implements EventSubscriberInterface -{ - public function postConnect(ConnectionEventArgs $eventArgs): void - { - $connection = $eventArgs->getConnection(); - - //We only execute this on SQLite databases - if ($connection->getDatabasePlatform() instanceof SqlitePlatform) { - $native_connection = $connection->getNativeConnection(); - - //Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation - if($native_connection instanceof \PDO && method_exists($native_connection, 'sqliteCreateFunction' )) { - $native_connection->sqliteCreateFunction('REGEXP', function ($pattern, $value) { - return (false !== mb_ereg($pattern, $value)) ? 1 : 0; - }); - } - } - } - - public function getSubscribedEvents() - { - return[ - Events::postConnect - ]; - } -} \ No newline at end of file diff --git a/src/Doctrine/Types/ArrayType.php b/src/Doctrine/Types/ArrayType.php new file mode 100644 index 00000000..daab9b75 --- /dev/null +++ b/src/Doctrine/Types/ArrayType.php @@ -0,0 +1,116 @@ +. + */ + +declare(strict_types=1); + +namespace App\Doctrine\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Exception\SerializationFailed; +use Doctrine\DBAL\Types\Type; +use Doctrine\Deprecations\Deprecation; + +use function is_resource; +use function restore_error_handler; +use function serialize; +use function set_error_handler; +use function stream_get_contents; +use function unserialize; + +use const E_DEPRECATED; +use const E_USER_DEPRECATED; + +/** + * This class is taken from doctrine ORM 3.8. https://github.com/doctrine/dbal/blob/3.8.x/src/Types/ArrayType.php + * + * It was removed in doctrine ORM 4.0. However, we require it for backward compatibility with WebauthnKey. + * Therefore, we manually added it here as a custom type as a forward compatibility layer. + */ +class ArrayType extends Type +{ + /** + * {@inheritDoc} + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + return $platform->getClobTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): string + { + return serialize($value); + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed + { + if ($value === null) { + return null; + } + + $value = is_resource($value) ? stream_get_contents($value) : $value; + + set_error_handler(function (int $code, string $message): bool { + if ($code === E_DEPRECATED || $code === E_USER_DEPRECATED) { + return false; + } + + //Change to original code. Use SerializationFailed instead of ConversionException. + throw new SerializationFailed("Serialization failed (Code $code): " . $message); + }); + + try { + //Change to original code. Use false for allowed_classes, to avoid unsafe unserialization of objects. + return unserialize($value, ['allowed_classes' => false]); + } finally { + restore_error_handler(); + } + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return "array"; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} \ No newline at end of file diff --git a/src/Doctrine/Types/BigDecimalType.php b/src/Doctrine/Types/BigDecimalType.php index f1522857..a9f796dd 100644 --- a/src/Doctrine/Types/BigDecimalType.php +++ b/src/Doctrine/Types/BigDecimalType.php @@ -1,4 +1,7 @@ . */ - namespace App\Doctrine\Types; use Brick\Math\BigDecimal; @@ -27,24 +29,21 @@ use Doctrine\DBAL\Types\Type; class BigDecimalType extends Type { - public const BIG_DECIMAL = 'big_decimal'; + final public const BIG_DECIMAL = 'big_decimal'; public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string { return $platform->getDecimalTypeDeclarationSQL($fieldDeclaration); } - /** - * @param string|null $value - * - * @return BigDecimal|BigNumber|mixed - */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?BigNumber { if (null === $value) { return null; } + + return BigDecimal::of($value); } @@ -53,7 +52,7 @@ class BigDecimalType extends Type */ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string { - if (null === $value) { + if (!$value instanceof BigDecimal) { return null; } diff --git a/src/Doctrine/Types/TinyIntType.php b/src/Doctrine/Types/TinyIntType.php index 817b87f1..c2daeeca 100644 --- a/src/Doctrine/Types/TinyIntType.php +++ b/src/Doctrine/Types/TinyIntType.php @@ -1,4 +1,7 @@ . */ - namespace App\Doctrine\Types; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\DBAL\Types\Type; /** @@ -29,19 +33,41 @@ use Doctrine\DBAL\Types\Type; class TinyIntType extends Type { - public function getSQLDeclaration(array $column, AbstractPlatform $platform) + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { - return 'TINYINT'; + //MySQL knows the TINYINT type directly + //We do not use the TINYINT for sqlite, as it will be resolved to a BOOL type and bring problems with migrations + if ($platform instanceof AbstractMySQLPlatform ) { + //Use TINYINT(1) to allow for proper migration diffs + return 'TINYINT(1)'; + } + + //For other platforms, we use the smallest integer type available + return $platform->getSmallIntTypeDeclarationSQL($column); } - public function getName() + public function getName(): string { return 'tinyint'; } - public function requiresSQLCommentHint(AbstractPlatform $platform) + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : int) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?int + { + return $value === null ? null : (int) $value; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform): bool { //We use the comment, so that doctrine migrations can properly detect, that nothing has changed and no migration is needed. return true; } -} \ No newline at end of file +} diff --git a/src/Doctrine/Types/UTCDateTimeImmutableType.php b/src/Doctrine/Types/UTCDateTimeImmutableType.php new file mode 100644 index 00000000..c0d6659d --- /dev/null +++ b/src/Doctrine/Types/UTCDateTimeImmutableType.php @@ -0,0 +1,97 @@ +. + */ + +declare(strict_types=1); + +namespace App\Doctrine\Types; + +use DateTime; +use DateTimeInterface; +use DateTimeZone; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\DateTimeImmutableType; +use Doctrine\DBAL\Types\DateTimeType; +use Doctrine\DBAL\Types\Exception\InvalidFormat; + +/** + * This DateTimeImmutableType all dates to UTC, so it can be later used with the timezones. + * Taken (and adapted) from here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/cookbook/working-with-datetime.html. + */ +class UTCDateTimeImmutableType extends DateTimeImmutableType +{ + private static ?DateTimeZone $utc_timezone = null; + + /** + * {@inheritdoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + if (!self::$utc_timezone instanceof \DateTimeZone) { + self::$utc_timezone = new DateTimeZone('UTC'); + } + + if ($value instanceof \DateTimeImmutable) { + $value = $value->setTimezone(self::$utc_timezone); + } + + return parent::convertToDatabaseValue($value, $platform); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform): ?\DateTimeImmutable + { + if (!self::$utc_timezone instanceof \DateTimeZone) { + self::$utc_timezone = new DateTimeZone('UTC'); + } + + if (null === $value || $value instanceof \DateTimeImmutable) { + return $value; + } + + $converted = \DateTimeImmutable::createFromFormat( + $platform->getDateTimeFormatString(), + $value, + self::$utc_timezone + ); + + if (!$converted) { + throw InvalidFormat::new( + $value, + static::class, + $platform->getDateTimeFormatString(), + ); + } + + return $converted; + } +} diff --git a/src/Doctrine/Types/UTCDateTimeType.php b/src/Doctrine/Types/UTCDateTimeType.php index c9fe215b..a6fda747 100644 --- a/src/Doctrine/Types/UTCDateTimeType.php +++ b/src/Doctrine/Types/UTCDateTimeType.php @@ -23,10 +23,12 @@ declare(strict_types=1); namespace App\Doctrine\Types; use DateTime; +use DateTimeInterface; use DateTimeZone; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\DateTimeType; +use Doctrine\DBAL\Types\Exception\InvalidFormat; /** * This DateTimeType all dates to UTC, so it can be later used with the timezones. @@ -47,7 +49,7 @@ class UTCDateTimeType extends DateTimeType */ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string { - if (!self::$utc_timezone) { + if (!self::$utc_timezone instanceof \DateTimeZone) { self::$utc_timezone = new DateTimeZone('UTC'); } @@ -58,9 +60,16 @@ class UTCDateTimeType extends DateTimeType return parent::convertToDatabaseValue($value, $platform); } + /** + * {@inheritDoc} + * + * @param T $value + * + * @template T + */ public function convertToPHPValue($value, AbstractPlatform $platform): ?DateTime { - if (!self::$utc_timezone) { + if (!self::$utc_timezone instanceof \DateTimeZone) { self::$utc_timezone = new DateTimeZone('UTC'); } @@ -75,7 +84,11 @@ class UTCDateTimeType extends DateTimeType ); if (!$converted) { - throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString()); + throw InvalidFormat::new( + $value, + static::class, + $platform->getDateTimeFormatString(), + ); } return $converted; diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 98565fe9..00cf581a 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -22,112 +22,191 @@ declare(strict_types=1); namespace App\Entity\Attachments; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use App\ApiPlatform\DocumentedAPIProperties\DocumentedAPIProperty; +use App\ApiPlatform\Filter\EntityFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\ApiPlatform\HandleAttachmentsUploadsProcessor; use App\Entity\Base\AbstractNamedDBElement; +use App\EntityListeners\AttachmentDeleteListener; +use App\Repository\AttachmentRepository; use App\Validator\Constraints\Selectable; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; -use Symfony\Component\Validator\Constraints as Assert; -use function in_array; use InvalidArgumentException; use LogicException; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; +use Symfony\Component\Validator\Constraints as Assert; + +use function in_array; /** * Class Attachment. - * - * @ORM\Entity(repositoryClass="App\Repository\AttachmentRepository") - * @ORM\Table(name="`attachments`", indexes={ - * @ORM\Index(name="attachments_idx_id_element_id_class_name", columns={"id", "element_id", "class_name"}), - * @ORM\Index(name="attachments_idx_class_name_id", columns={"class_name", "id"}), - * @ORM\Index(name="attachment_name_idx", columns={"name"}), - * @ORM\Index(name="attachment_element_idx", columns={"class_name", "element_id"}) - * }) - * @ORM\InheritanceType("SINGLE_TABLE") - * @ORM\DiscriminatorColumn(name="class_name", type="string") - * @ORM\DiscriminatorMap({ - * "PartDB\Part" = "PartAttachment", "Part" = "PartAttachment", - * "PartDB\Device" = "ProjectAttachment", "Device" = "ProjectAttachment", - * "AttachmentType" = "AttachmentTypeAttachment", "Category" = "CategoryAttachment", - * "Footprint" = "FootprintAttachment", "Manufacturer" = "ManufacturerAttachment", - * "Currency" = "CurrencyAttachment", "Group" = "GroupAttachment", - * "MeasurementUnit" = "MeasurementUnitAttachment", "Storelocation" = "StorelocationAttachment", - * "Supplier" = "SupplierAttachment", "User" = "UserAttachment", "LabelProfile" = "LabelAttachment", - * }) - * @ORM\EntityListeners({"App\EntityListeners\AttachmentDeleteListener"}) + * @see \App\Tests\Entity\Attachments\AttachmentTest + * @template-covariant T of AttachmentContainingDBElement */ +#[ORM\Entity(repositoryClass: AttachmentRepository::class)] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'class_name', type: 'string')] +#[ORM\DiscriminatorMap(self::ORM_DISCRIMINATOR_MAP)] +#[ORM\EntityListeners([AttachmentDeleteListener::class])] +#[ORM\Table(name: '`attachments`')] +#[ORM\Index(columns: ['id', 'element_id', 'class_name'], name: 'attachments_idx_id_element_id_class_name')] +#[ORM\Index(columns: ['class_name', 'id'], name: 'attachments_idx_class_name_id')] +#[ORM\Index(columns: ['name'], name: 'attachment_name_idx')] +#[ORM\Index(columns: ['class_name', 'element_id'], name: 'attachment_element_idx')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@attachments.list_attachments")'), + new Post(securityPostDenormalize: 'is_granted("create", object)', ), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['attachment:read', 'attachment:read:standalone', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['attachment:write', 'attachment:write:standalone', 'api:basic:write'], 'openapi_definition_name' => 'Write'], + processor: HandleAttachmentsUploadsProcessor::class, +)] +//This property is added by the denormalizer in order to resolve the placeholder +#[DocumentedAPIProperty( + schemaName: 'Attachment-Read', property: 'internal_path', type: 'string', nullable: false, + description: 'The URL to the internally saved copy of the file, if one exists', + example: '/media/part/2/bc547-6508afa5a79c8.pdf' +)] +#[DocumentedAPIProperty( + schemaName: 'Attachment-Read', property: 'thumbnail_url', type: 'string', nullable: true, + description: 'The URL to a thumbnail version of this file. This only exists for internal picture attachments.' +)] +#[ApiFilter(LikeFilter::class, properties: ["name"])] +#[ApiFilter(EntityFilter::class, properties: ["attachment_type"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +//This discriminator map is required for API platform to know which class to use for deserialization, when creating a new attachment. +#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { + private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class, + 'AttachmentType' => AttachmentTypeAttachment::class, + 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, + 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, + 'Storelocation' => StorageLocationAttachment::class, 'Supplier' => SupplierAttachment::class, + 'User' => UserAttachment::class, 'LabelProfile' => LabelAttachment::class]; + + /* + * The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field). + */ + private const API_DISCRIMINATOR_MAP = ["Part" => PartAttachment::class, "Project" => ProjectAttachment::class, "AttachmentType" => AttachmentTypeAttachment::class, + "Category" => CategoryAttachment::class, "Footprint" => FootprintAttachment::class, "Manufacturer" => ManufacturerAttachment::class, + "Currency" => CurrencyAttachment::class, "Group" => GroupAttachment::class, "MeasurementUnit" => MeasurementUnitAttachment::class, + "StorageLocation" => StorageLocationAttachment::class, "Supplier" => SupplierAttachment::class, "User" => UserAttachment::class, "LabelProfile" => LabelAttachment::class]; + /** * A list of file extensions, that browsers can show directly as image. * Based on: https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types - * It will be used to determine if a attachment is a picture and therefore will be shown to user as preview. + * It will be used to determine if an attachment is a picture and therefore will be shown to user as preview. */ - public const PICTURE_EXTS = ['apng', 'bmp', 'gif', 'ico', 'cur', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png', + final public const PICTURE_EXTS = ['apng', 'bmp', 'gif', 'ico', 'cur', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png', 'svg', 'webp', ]; /** * A list of extensions that will be treated as a 3D Model that can be shown to user directly in Part-DB. */ - public const MODEL_EXTS = ['x3d']; + final public const MODEL_EXTS = ['x3d']; - /** - * When the path begins with one of this placeholders. - */ - public const INTERNAL_PLACEHOLDER = ['%BASE%', '%MEDIA%', '%SECURE%']; /** * @var array placeholders for attachments which using built in files */ - public const BUILTIN_PLACEHOLDER = ['%FOOTPRINTS%', '%FOOTPRINTS3D%']; + final public const BUILTIN_PLACEHOLDER = ['%FOOTPRINTS%', '%FOOTPRINTS3D%']; /** * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses. + * @phpstan-var class-string */ - public const ALLOWED_ELEMENT_CLASS = ''; + protected const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class; + + /** + * @var AttachmentUpload|null The options used for uploading a file to this attachment or modify it. + * This value is not persisted in the database, but is just used to pass options to the upload manager. + * If it is null, no upload process is started. + */ + #[Groups(['attachment:write'])] + protected ?AttachmentUpload $upload = null; /** * @var string|null the original filename the file had, when the user uploaded it - * @ORM\Column(type="string", nullable=true) */ + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Groups(['attachment:read', 'import'])] + #[Assert\Length(max: 255)] protected ?string $original_filename = null; /** - * @var string The path to the file relative to a placeholder path like %MEDIA% - * @ORM\Column(type="string", name="path") + * @var string|null If a copy of the file is stored internally, the path to the file relative to a placeholder + * path like %MEDIA% */ - protected string $path = ''; + #[ORM\Column(type: Types::STRING, nullable: true)] + protected ?string $internal_path = null; + + + /** + * @var string|null The path to the external source if the file is stored externally or was downloaded from an + * external source. Null if there is no external source. + */ + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Groups(['attachment:read'])] + #[ApiProperty(example: 'http://example.com/image.jpg')] + protected ?string $external_path = null; /** * @var string the name of this element - * @ORM\Column(type="string") - * @Assert\NotBlank(message="validator.attachment.name_not_blank") - * @Groups({"simple", "extended", "full"}) */ + #[Assert\NotBlank(message: 'validator.attachment.name_not_blank')] + #[Groups(['simple', 'extended', 'full', 'attachment:read', 'attachment:write', 'import'])] protected string $name = ''; /** - * ORM mapping is done in sub classes (like PartAttachment). + * ORM mapping is done in subclasses (like PartAttachment). + * @phpstan-param T|null $element */ + #[Groups(['attachment:read:standalone', 'attachment:write:standalone'])] + #[ApiProperty(writableLink: false)] protected ?AttachmentContainingDBElement $element = null; - /** - * @var bool - * @ORM\Column(type="boolean") - */ + #[ORM\Column(type: Types::BOOLEAN)] + #[Groups(['attachment:read', 'attachment_write', 'full', 'import'])] protected bool $show_in_table = false; - /** - * @var AttachmentType - * @ORM\ManyToOne(targetEntity="AttachmentType", inversedBy="attachments_with_type") - * @ORM\JoinColumn(name="type_id", referencedColumnName="id", nullable=false) - * @Selectable() - * @Assert\NotNull(message="validator.attachment.must_not_be_null") - */ + #[Assert\NotNull(message: 'validator.attachment.must_not_be_null')] + #[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'attachments_with_type')] + #[ORM\JoinColumn(name: 'type_id', nullable: false)] + #[Selectable] + #[Groups(['attachment:read', 'attachment:write', 'import', 'full'])] + #[ApiProperty(readableLink: false)] protected ?AttachmentType $attachment_type = null; + #[Groups(['attachment:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['attachment:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + public function __construct() { //parent::__construct(); - if ('' === static::ALLOWED_ELEMENT_CLASS) { + if (AttachmentContainingDBElement::class === static::ALLOWED_ELEMENT_CLASS) { throw new LogicException('An *Attachment class must override the ALLOWED_ELEMENT_CLASS const!'); } } @@ -140,61 +219,106 @@ abstract class Attachment extends AbstractNamedDBElement } } + /** + * Gets the upload currently associated with this attachment. + * This is only temporary and not persisted directly in the database. + * @internal This function should only be used by the Attachment Submit handler service + * @return AttachmentUpload|null + */ + public function getUpload(): ?AttachmentUpload + { + return $this->upload; + } + + /** + * Sets the current upload for this attachment. + * It will be processed as the attachment is persisted/flushed. + * @param AttachmentUpload|null $upload + * @return $this + */ + public function setUpload(?AttachmentUpload $upload): Attachment + { + $this->upload = $upload; + return $this; + } + + + /*********************************************************** * Various function ***********************************************************/ /** * Check if this attachment is a picture (analyse the file's extension). - * If the link is external, it is assumed that this is true. + * If the link is only external and doesn't contain an extension, it is assumed that this is true. * * @return bool * true if the file extension is a picture extension * * otherwise false */ + #[Groups(['attachment:read'])] public function isPicture(): bool { - //We can not check if a external link is a picture, so just assume this is false - if ($this->isExternal()) { - return true; + if($this->hasInternal()){ + + $extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION); + + return in_array(strtolower($extension), static::PICTURE_EXTS, true); + } + if ($this->hasExternal()) { + //Check if we can extract a file extension from the URL + $extension = pathinfo(parse_url($this->getExternalPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION); - $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION); - - return in_array(strtolower($extension), static::PICTURE_EXTS, true); + //If no extension is found or it is known picture extension, we assume that this is a picture extension + return $extension === '' || in_array(strtolower($extension), static::PICTURE_EXTS, true); + } + //File doesn't have an internal, nor an external copy. This shouldn't happen, but it certainly isn't a picture... + return false; } /** * Check if this attachment is a 3D model and therefore can be directly shown to user. - * If the attachment is external, false is returned (3D Models must be internal). + * If no internal copy exists, false is returned (3D Models must be internal). */ + #[Groups(['attachment:read'])] + #[SerializedName('3d_model')] public function is3DModel(): bool { //We just assume that 3D Models are internally saved, otherwise we get problems loading them. - if ($this->isExternal()) { + if (!$this->hasInternal()) { return false; } - $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION); + $extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION); return in_array(strtolower($extension), static::MODEL_EXTS, true); } /** - * Checks if the attachment file is externally saved (the database saves an URL). + * Checks if this attachment has a path to an external file * - * @return bool true, if the file is saved externally + * @return bool true, if there is a path to an external file + * @phpstan-assert-if-true non-empty-string $this->external_path + * @phpstan-assert-if-true non-empty-string $this->getExternalPath()) */ - public function isExternal(): bool + #[Groups(['attachment:read'])] + public function hasExternal(): bool { - //When path is empty, this attachment can not be external - if (empty($this->path)) { - return false; - } + return $this->external_path !== null && $this->external_path !== ''; + } - //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode - $tmp = explode('/', $this->path); - - return !in_array($tmp[0], array_merge(static::INTERNAL_PLACEHOLDER, static::BUILTIN_PLACEHOLDER), true); + /** + * Checks if this attachment has a path to an internal file. + * Does not check if the file exists. + * + * @return bool true, if there is a path to an internal file + * @phpstan-assert-if-true non-empty-string $this->internal_path + * @phpstan-assert-if-true non-empty-string $this->getInternalPath()) + */ + #[Groups(['attachment:read'])] + public function hasInternal(): bool + { + return $this->internal_path !== null && $this->internal_path !== ''; } /** @@ -203,10 +327,16 @@ abstract class Attachment extends AbstractNamedDBElement * * @return bool true, if the file is secure */ + #[Groups(['attachment:read'])] + #[SerializedName('private')] public function isSecure(): bool { + if ($this->internal_path === null) { + return false; + } + //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode - $tmp = explode('/', $this->path); + $tmp = explode('/', $this->internal_path); return '%SECURE%' === $tmp[0]; } @@ -215,11 +345,16 @@ abstract class Attachment extends AbstractNamedDBElement * Checks if the attachment file is using a builtin file. (see BUILTIN_PLACEHOLDERS const for possible placeholders) * If a file is built in, the path is shown to user in url field (no sensitive infos are provided). * - * @return bool true if the attachment is using an builtin file + * @return bool true if the attachment is using a builtin file */ + #[Groups(['attachment:read'])] public function isBuiltIn(): bool { - return static::checkIfBuiltin($this->path); + if ($this->internal_path === null) { + return false; + } + + return static::checkIfBuiltin($this->internal_path); } /******************************************************************************** @@ -231,27 +366,28 @@ abstract class Attachment extends AbstractNamedDBElement /** * Returns the extension of the file referenced via the attachment. * For a path like %BASE/path/foo.bar, bar will be returned. - * If this attachment is external null is returned. + * If this attachment is only external null is returned. * * @return string|null the file extension in lower case */ public function getExtension(): ?string { - if ($this->isExternal()) { + if (!$this->hasInternal()) { return null; } - if (!empty($this->original_filename)) { + if ($this->original_filename !== null && $this->original_filename !== '') { return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION)); } - return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION)); + return strtolower(pathinfo($this->getInternalPath(), PATHINFO_EXTENSION)); } /** * Get the element, associated with this Attachment (for example a "Part" object). * - * @return AttachmentContainingDBElement the associated Element + * @return AttachmentContainingDBElement|null the associated Element + * @phpstan-return T|null */ public function getElement(): ?AttachmentContainingDBElement { @@ -259,59 +395,63 @@ abstract class Attachment extends AbstractNamedDBElement } /** - * The URL to the external file, or the path to the built in file. + * The URL to the external file, or the path to the built-in file, but not paths to uploaded files. * Returns null, if the file is not external (and not builtin). + * The output of this function is such, that no changes occur when it is fed back into setURL(). + * Required for the Attachment form field. */ public function getURL(): ?string { - if (!$this->isExternal() && !$this->isBuiltIn()) { - return null; + if($this->hasExternal()){ + return $this->getExternalPath(); } - - return $this->path; + if($this->isBuiltIn()){ + return $this->getInternalPath(); + } + return null; } /** * Returns the hostname where the external file is stored. - * Returns null, if the file is not external. + * Returns null, if there is no external path. */ public function getHost(): ?string { - if (!$this->isExternal()) { + if (!$this->hasExternal()) { return null; } - return parse_url($this->getURL(), PHP_URL_HOST); + return parse_url($this->getExternalPath(), PHP_URL_HOST); } - /** - * Get the filepath, relative to %BASE%. - * - * @return string A string like %BASE/path/foo.bar - */ - public function getPath(): string + public function getInternalPath(): ?string { - return $this->path; + return $this->internal_path; + } + + public function getExternalPath(): ?string + { + return $this->external_path; } /** * Returns the filename of the attachment. * For a path like %BASE/path/foo.bar, foo.bar will be returned. * - * If the path is a URL (can be checked via isExternal()), null will be returned. + * If there is no internal copy of the file, null will be returned. */ public function getFilename(): ?string { - if ($this->isExternal()) { + if (!$this->hasInternal()) { return null; } //If we have a stored original filename, then use it - if (!empty($this->original_filename)) { + if ($this->original_filename !== null && $this->original_filename !== '') { return $this->original_filename; } - return pathinfo($this->getPath(), PATHINFO_BASENAME); + return pathinfo($this->getInternalPath(), PATHINFO_BASENAME); } /** @@ -369,12 +509,12 @@ abstract class Attachment extends AbstractNamedDBElement /** * Sets the element that is associated with this attachment. - * * @return $this */ public function setElement(AttachmentContainingDBElement $element): self { - if (!is_a($element, static::ALLOWED_ELEMENT_CLASS)) { + //Do not allow Rector to replace this check with a instanceof. It will not work!! + if (!is_a($element, static::ALLOWED_ELEMENT_CLASS, true)) { throw new InvalidArgumentException(sprintf('The element associated with a %s must be a %s!', static::class, static::ALLOWED_ELEMENT_CLASS)); } @@ -384,15 +524,12 @@ abstract class Attachment extends AbstractNamedDBElement } /** - * Sets the filepath (with relative placeholder) for this attachment. - * - * @param string $path the new filepath of the attachment - * - * @return Attachment + * Sets the path to a file hosted internally. If you set this path to a file that was not downloaded from the + * external source in external_path, make sure to reset external_path. */ - public function setPath(string $path): self + public function setInternalPath(?string $internal_path): self { - $this->path = $path; + $this->internal_path = $internal_path; return $this; } @@ -408,24 +545,61 @@ abstract class Attachment extends AbstractNamedDBElement } /** - * Sets the url associated with this attachment. - * If the url is empty nothing is changed, to not override the file path. - * - * @return Attachment + * Sets up the paths using a user provided string which might contain an external path or a builtin path. Allows + * resetting the external path if an internal path exists. Resets any other paths if a (nonempty) new path is set. */ + #[Groups(['attachment:write'])] + #[SerializedName('url')] + #[ApiProperty(description: 'Set the path of the attachment here. + Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty + string if the attachment has an internal file associated and you\'d like to reset the external source. + If you set a new (nonempty) file path any associated internal file will be removed!')] public function setURL(?string $url): self { - //Only set if the URL is not empty - if (!empty($url)) { - if (false !== strpos($url, '%BASE%') || false !== strpos($url, '%MEDIA%')) { - throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!'); - } - - $this->path = $url; - //Reset internal filename - $this->original_filename = null; + //Don't allow the user to set an empty external path if the internal path is empty already + if (($url === null || $url === "") && !$this->hasInternal()) { + return $this; } + //The URL field can also contain the special builtin internal paths, so we need to distinguish here + if ($this::checkIfBuiltin($url)) { + $this->setInternalPath($url); + //make sure the external path isn't still pointing to something unrelated + $this->setExternalPath(null); + } else { + $this->setExternalPath($url); + } + return $this; + } + + + /** + * Sets the path to a file hosted on an external server. Setting the external path to a (nonempty) value different + * from the the old one _clears_ the internal path, so that the external path reflects where any associated internal + * file came from. + */ + public function setExternalPath(?string $external_path): self + { + //If we only clear the external path, don't reset the internal path, since that could be confusing + if($external_path === null || $external_path === '') { + $this->external_path = null; + return $this; + } + + $external_path = trim($external_path); + //Escape spaces in URL + $external_path = str_replace(' ', '%20', $external_path); + + if($this->external_path === $external_path) { + //Nothing changed, nothing to do + return $this; + } + + $this->external_path = $external_path; + $this->internal_path = null; + //Reset internal filename + $this->original_filename = null; + return $this; } @@ -436,17 +610,22 @@ abstract class Attachment extends AbstractNamedDBElement /** * Checks if the given path is a path to a builtin resource. * - * @param string $path The path that should be checked + * @param string|null $path The path that should be checked * * @return bool true if the path is pointing to a builtin resource */ - public static function checkIfBuiltin(string $path): bool + public static function checkIfBuiltin(?string $path): bool { + //An empty path can't be a builtin + if ($path === null) { + return false; + } + //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode $tmp = explode('/', $path); //Builtins must have a %PLACEHOLDER% construction - return in_array($tmp[0], static::BUILTIN_PLACEHOLDER, false); + return in_array($tmp[0], static::BUILTIN_PLACEHOLDER, true); } /** @@ -455,9 +634,9 @@ abstract class Attachment extends AbstractNamedDBElement * @param string $string The string which should be checked * @param bool $path_required If true, the string must contain a path to be valid. (e.g. foo.bar would be invalid, foo.bar/test.php would be valid). * @param bool $only_http Set this to true, if only HTTPS or HTTP schemata should be allowed. - * *Caution: When this is set to false, a attacker could use the file:// schema, to get internal server files, like /etc/passwd.* + * *Caution: When this is set to false, an attacker could use the file:// schema, to get internal server files, like /etc/passwd.* * - * @return bool True if the string is a valid URL. False, if the string is not an URL or invalid. + * @return bool True if the string is a valid URL. False, if the string is not a URL or invalid. */ public static function isValidURL(string $string, bool $path_required = true, bool $only_http = true): bool { @@ -473,4 +652,13 @@ abstract class Attachment extends AbstractNamedDBElement return (bool) filter_var($string, FILTER_VALIDATE_URL); } + + /** + * Returns the class of the element that is allowed to be associated with this attachment. + * @return string + */ + public function getElementClass(): string + { + return static::ALLOWED_ELEMENT_CLASS; + } } diff --git a/src/Entity/Attachments/AttachmentContainingDBElement.php b/src/Entity/Attachments/AttachmentContainingDBElement.php index b550394e..a78cb1f4 100644 --- a/src/Entity/Attachments/AttachmentContainingDBElement.php +++ b/src/Entity/Attachments/AttachmentContainingDBElement.php @@ -26,27 +26,27 @@ use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\MasterAttachmentTrait; use App\Entity\Contracts\HasAttachmentsInterface; use App\Entity\Contracts\HasMasterAttachmentInterface; +use App\Repository\AttachmentContainingDBElementRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; /** - * @ORM\MappedSuperclass() + * @template AT of Attachment */ +#[ORM\MappedSuperclass(repositoryClass: AttachmentContainingDBElementRepository::class)] abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface { use MasterAttachmentTrait; /** - * @var Attachment[]|Collection - * //TODO - * //@ORM\OneToMany(targetEntity="Attachment", mappedBy="element") - * - * Mapping is done in sub classes like part - * @Groups({"full"}) + * @var Collection + * @phpstan-var Collection + * ORM Mapping is done in subclasses (e.g. Part) */ - protected $attachments; + #[Groups(['full', 'import'])] + protected Collection $attachments; public function __construct() { @@ -79,9 +79,7 @@ abstract class AttachmentContainingDBElement extends AbstractNamedDBElement impl *********************************************************************************/ /** - * Gets all attachments associated with this element. - * - * @return Attachment[]|Collection + * Gets all attachments associated with this element. */ public function getAttachments(): Collection { diff --git a/src/Entity/Attachments/AttachmentType.php b/src/Entity/Attachments/AttachmentType.php index ad7a0e61..22333c16 100644 --- a/src/Entity/Attachments/AttachmentType.php +++ b/src/Entity/Attachments/AttachmentType.php @@ -22,66 +22,128 @@ declare(strict_types=1); namespace App\Entity\Attachments; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Repository\StructuralDBElementRepository; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\AttachmentTypeParameter; use App\Validator\Constraints\ValidFileFilter; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** * Class AttachmentType. - * - * @ORM\Entity(repositoryClass="App\Repository\StructuralDBElementRepository") - * @ORM\Table(name="`attachment_types`", indexes={ - * @ORM\Index(name="attachment_types_idx_name", columns={"name"}), - * @ORM\Index(name="attachment_types_idx_parent_name", columns={"parent_id", "name"}), - * }) + * @see \App\Tests\Entity\Attachments\AttachmentTypeTest + * @extends AbstractStructuralDBElement */ +#[ORM\Entity(repositoryClass: StructuralDBElementRepository::class)] +#[ORM\Table(name: '`attachment_types`')] +#[ORM\Index(columns: ['name'], name: 'attachment_types_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'attachment_types_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@attachment_types.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['attachment_type:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/attachment_types/{id}/children.{_format}', + operations: [ + new GetCollection(openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'), + security: 'is_granted("@attachment_types.read")') + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: AttachmentType::class) + ], + normalizationContext: ['groups' => ['attachment_type:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class AttachmentType extends AbstractStructuralDBElement { - /** - * @ORM\OneToMany(targetEntity="AttachmentType", mappedBy="parent", cascade={"persist"}) - * @ORM\OrderBy({"name" = "ASC"}) - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: AttachmentType::class, cascade: ['persist'])] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['attachment_type:read', 'attachment_type:write'])] + #[ApiProperty(readableLink: true, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; /** - * @ORM\ManyToOne(targetEntity="AttachmentType", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") - */ - protected $parent; - - /** - * @var string - * @ORM\Column(type="text") - * @ValidFileFilter + * @var string A comma separated list of file types, which are allowed for attachment files. + * Must be in the format of
accept attribute + * (See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers). */ + #[ORM\Column(type: Types::TEXT)] + #[ValidFileFilter] + #[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'extended'])] protected string $filetype_filter = ''; + /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\AttachmentTypeAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() */ - protected $attachments; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: AttachmentTypeAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'full'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: AttachmentTypeAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['attachment_type:read', 'attachment_type:write', 'full'])] + protected ?Attachment $master_picture_attachment = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\AttachmentTypeParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() */ - protected $parameters; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: AttachmentTypeParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'full'])] + protected Collection $parameters; /** - * @var Collection - * @ORM\OneToMany(targetEntity="Attachment", mappedBy="attachment_type") + * @var Collection */ - protected $attachments_with_type; + #[ORM\OneToMany(mappedBy: 'attachment_type', targetEntity: Attachment::class)] + protected Collection $attachments_with_type; + + #[Groups(['attachment_type:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['attachment_type:read'])] + protected ?\DateTimeImmutable $lastModified = null; + public function __construct() { + $this->children = new ArrayCollection(); + $this->parameters = new ArrayCollection(); parent::__construct(); $this->attachments = new ArrayCollection(); $this->attachments_with_type = new ArrayCollection(); @@ -90,8 +152,9 @@ class AttachmentType extends AbstractStructuralDBElement /** * Get all attachments ("Attachment" objects) with this type. * - * @return Collection|Attachment[] all attachments with this type, as a one-dimensional array of Attachments + * @return Collection all attachments with this type, as a one-dimensional array of Attachments * (sorted by their names) + * @phpstan-return Collection */ public function getAttachmentsForType(): Collection { @@ -99,7 +162,7 @@ class AttachmentType extends AbstractStructuralDBElement } /** - * Gets an filter, which file types are allowed for attachment files. + * Gets a filter, which file types are allowed for attachment files. * Must be in the format of accept attribute * (See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers). */ diff --git a/src/Entity/Attachments/AttachmentTypeAttachment.php b/src/Entity/Attachments/AttachmentTypeAttachment.php index 2a21405c..1e677ff0 100644 --- a/src/Entity/Attachments/AttachmentTypeAttachment.php +++ b/src/Entity/Attachments/AttachmentTypeAttachment.php @@ -22,22 +22,25 @@ declare(strict_types=1); namespace App\Entity\Attachments; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** * A attachment attached to an attachmentType element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class AttachmentTypeAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = AttachmentType::class; + final public const ALLOWED_ELEMENT_CLASS = AttachmentType::class; /** - * @var AttachmentType the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Attachments\AttachmentType", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var AttachmentContainingDBElement|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/AttachmentUpload.php b/src/Entity/Attachments/AttachmentUpload.php new file mode 100644 index 00000000..f2b042b7 --- /dev/null +++ b/src/Entity/Attachments/AttachmentUpload.php @@ -0,0 +1,77 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Attachments; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\Serializer\Attribute\Groups; + +/** + * This is a DTO representing a file upload for an attachment and which is used to pass data to the Attachment + * submit handler service. + */ +class AttachmentUpload +{ + public function __construct( + /** @var UploadedFile|null The file which was uploaded, or null if the file should not be changed */ + public readonly ?UploadedFile $file, + /** @var string|null The base64 encoded data of the file which should be uploaded. */ + #[Groups(['attachment:write'])] + public readonly ?string $data = null, + /** @vaar string|null The original filename of the file passed in data. */ + #[Groups(['attachment:write'])] + public readonly ?string $filename = null, + /** @var bool True, if the URL in the attachment should be downloaded by Part-DB */ + #[Groups(['attachment:write'])] + public readonly bool $downloadUrl = false, + /** @var bool If true the file will be moved to private attachment storage, + * if false it will be moved to public attachment storage. On null file is not moved + */ + #[Groups(['attachment:write'])] + public readonly ?bool $private = null, + /** @var bool If true and no preview image was set yet, the new uploaded file will become the preview image */ + #[Groups(['attachment:write'])] + public readonly ?bool $becomePreviewIfEmpty = true, + ) { + } + + /** + * Creates an AttachmentUpload object from an Attachment FormInterface + * @param FormInterface $form + * @return AttachmentUpload + */ + public static function fromAttachmentForm(FormInterface $form): AttachmentUpload + { + if (!$form->has('file')) { + throw new \InvalidArgumentException('The form does not have a file field. Is it an attachment form?'); + } + + return new self( + file: $form->get('file')->getData(), + downloadUrl: $form->get('downloadURL')->getData(), + private: $form->get('secureFile')->getData() + ); + + } +} \ No newline at end of file diff --git a/src/Entity/Attachments/CategoryAttachment.php b/src/Entity/Attachments/CategoryAttachment.php index e4d38137..3bea265e 100644 --- a/src/Entity/Attachments/CategoryAttachment.php +++ b/src/Entity/Attachments/CategoryAttachment.php @@ -23,22 +23,25 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\Parts\Category; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a category element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * An attachment attached to a category element. + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class CategoryAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Category::class; + final public const ALLOWED_ELEMENT_CLASS = Category::class; /** - * @var Category the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Category", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var AttachmentContainingDBElement|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/CurrencyAttachment.php b/src/Entity/Attachments/CurrencyAttachment.php index 73ad1145..a5d6e061 100644 --- a/src/Entity/Attachments/CurrencyAttachment.php +++ b/src/Entity/Attachments/CurrencyAttachment.php @@ -23,22 +23,26 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\PriceInformations\Currency; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** * An attachment attached to a currency element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class CurrencyAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Currency::class; + final public const ALLOWED_ELEMENT_CLASS = Currency::class; + /** - * @var Currency the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\PriceInformations\Currency", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var Currency|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: Currency::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/FootprintAttachment.php b/src/Entity/Attachments/FootprintAttachment.php index 84ba3fb7..4a9b866c 100644 --- a/src/Entity/Attachments/FootprintAttachment.php +++ b/src/Entity/Attachments/FootprintAttachment.php @@ -23,22 +23,26 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\Parts\Footprint; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a footprint element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * An attachment attached to a footprint element. + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class FootprintAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Footprint::class; + final public const ALLOWED_ELEMENT_CLASS = Footprint::class; + /** - * @var Footprint the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Footprint", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var Footprint|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: Footprint::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/GroupAttachment.php b/src/Entity/Attachments/GroupAttachment.php index e64c5745..e9bf947f 100644 --- a/src/Entity/Attachments/GroupAttachment.php +++ b/src/Entity/Attachments/GroupAttachment.php @@ -23,22 +23,26 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\UserSystem\Group; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a Group element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * An attachment attached to a Group element. + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class GroupAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Group::class; + final public const ALLOWED_ELEMENT_CLASS = Group::class; + /** - * @var Group the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\Group", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var Group|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: Group::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/LabelAttachment.php b/src/Entity/Attachments/LabelAttachment.php index f0f70b79..b8891ced 100644 --- a/src/Entity/Attachments/LabelAttachment.php +++ b/src/Entity/Attachments/LabelAttachment.php @@ -42,23 +42,26 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\LabelSystem\LabelProfile; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** * A attachment attached to a user element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class LabelAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = LabelProfile::class; + final public const ALLOWED_ELEMENT_CLASS = LabelProfile::class; /** * @var LabelProfile the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\LabelSystem\LabelProfile", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ + #[ORM\ManyToOne(targetEntity: LabelProfile::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/ManufacturerAttachment.php b/src/Entity/Attachments/ManufacturerAttachment.php index 0d113977..1b8891e5 100644 --- a/src/Entity/Attachments/ManufacturerAttachment.php +++ b/src/Entity/Attachments/ManufacturerAttachment.php @@ -23,22 +23,26 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\Parts\Manufacturer; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a manufacturer element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * An attachment attached to a manufacturer element. + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class ManufacturerAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Manufacturer::class; + final public const ALLOWED_ELEMENT_CLASS = Manufacturer::class; + /** - * @var Manufacturer the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Manufacturer", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var Manufacturer|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: Manufacturer::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/MeasurementUnitAttachment.php b/src/Entity/Attachments/MeasurementUnitAttachment.php index 0c24f649..dbc8829e 100644 --- a/src/Entity/Attachments/MeasurementUnitAttachment.php +++ b/src/Entity/Attachments/MeasurementUnitAttachment.php @@ -22,24 +22,25 @@ declare(strict_types=1); namespace App\Entity\Attachments; -use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a measurement unit element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * An attachment attached to a measurement unit element. + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class MeasurementUnitAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = MeasurementUnit::class; - /** - * @var Manufacturer the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\MeasurementUnit", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). - */ + final public const ALLOWED_ELEMENT_CLASS = MeasurementUnit::class; + + + #[ORM\ManyToOne(targetEntity: MeasurementUnit::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/PartAttachment.php b/src/Entity/Attachments/PartAttachment.php index 3aa2d05c..0873b3c5 100644 --- a/src/Entity/Attachments/PartAttachment.php +++ b/src/Entity/Attachments/PartAttachment.php @@ -23,22 +23,25 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\Parts\Part; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** * A attachment attached to a part element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class PartAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Part::class; + final public const ALLOWED_ELEMENT_CLASS = Part::class; /** * @var Part the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/ProjectAttachment.php b/src/Entity/Attachments/ProjectAttachment.php index 1fab6bc8..f3e96292 100644 --- a/src/Entity/Attachments/ProjectAttachment.php +++ b/src/Entity/Attachments/ProjectAttachment.php @@ -23,22 +23,25 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\ProjectSystem\Project; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** * A attachment attached to a device element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class ProjectAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Project::class; + final public const ALLOWED_ELEMENT_CLASS = Project::class; /** - * @var Project the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\ProjectSystem\Project", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var Project|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/StorelocationAttachment.php b/src/Entity/Attachments/StorageLocationAttachment.php similarity index 57% rename from src/Entity/Attachments/StorelocationAttachment.php rename to src/Entity/Attachments/StorageLocationAttachment.php index e2b9025a..3cd82d0c 100644 --- a/src/Entity/Attachments/StorelocationAttachment.php +++ b/src/Entity/Attachments/StorageLocationAttachment.php @@ -22,23 +22,27 @@ declare(strict_types=1); namespace App\Entity\Attachments; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a measurement unit element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * An attachment attached to a measurement unit element. + * @extends Attachment */ -class StorelocationAttachment extends Attachment +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] +class StorageLocationAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Storelocation::class; + final public const ALLOWED_ELEMENT_CLASS = StorageLocation::class; + /** - * @var Storelocation the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Storelocation", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var StorageLocation|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: StorageLocation::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/SupplierAttachment.php b/src/Entity/Attachments/SupplierAttachment.php index 59893b74..c3adc438 100644 --- a/src/Entity/Attachments/SupplierAttachment.php +++ b/src/Entity/Attachments/SupplierAttachment.php @@ -23,22 +23,26 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\Parts\Supplier; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** * A attachment attached to a supplier element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class SupplierAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Supplier::class; + final public const ALLOWED_ELEMENT_CLASS = Supplier::class; + /** - * @var Supplier the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Supplier", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var Supplier|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: Supplier::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Attachments/UserAttachment.php b/src/Entity/Attachments/UserAttachment.php index 84a04c17..b031d419 100644 --- a/src/Entity/Attachments/UserAttachment.php +++ b/src/Entity/Attachments/UserAttachment.php @@ -23,22 +23,26 @@ declare(strict_types=1); namespace App\Entity\Attachments; use App\Entity\UserSystem\User; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a user element. - * - * @ORM\Entity() - * @UniqueEntity({"name", "attachment_type", "element"}) + * An attachment attached to a user element. + * @extends Attachment */ +#[UniqueEntity(['name', 'attachment_type', 'element'])] +#[ORM\Entity] class UserAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = User::class; + final public const ALLOWED_ELEMENT_CLASS = User::class; + /** - * @var User the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User", inversedBy="attachments") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var User|null the element this attachment is associated with */ + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] protected ?AttachmentContainingDBElement $element = null; } diff --git a/src/Entity/Base/AbstractCompany.php b/src/Entity/Base/AbstractCompany.php index af77106f..947d1339 100644 --- a/src/Entity/Base/AbstractCompany.php +++ b/src/Entity/Base/AbstractCompany.php @@ -22,6 +22,9 @@ declare(strict_types=1); namespace App\Entity\Base; +use App\Entity\Attachments\Attachment; +use App\Entity\Parameters\AbstractParameter; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use function is_string; @@ -30,51 +33,69 @@ use Symfony\Component\Validator\Constraints as Assert; /** * This abstract class is used for companies like suppliers or manufacturers. * - * @ORM\MappedSuperclass() + * @template AT of Attachment + * @template PT of AbstractParameter + * @extends AbstractPartsContainingDBElement */ +#[ORM\MappedSuperclass] abstract class AbstractCompany extends AbstractPartsContainingDBElement { + #[Groups(['company:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['company:read'])] + protected ?\DateTimeImmutable $lastModified = null; + /** * @var string The address of the company - * @ORM\Column(type="string") - * @Groups({"full"}) */ + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $address = ''; /** * @var string The phone number of the company - * @ORM\Column(type="string") - * @Groups({"full"}) */ + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $phone_number = ''; /** * @var string The fax number of the company - * @ORM\Column(type="string") - * @Groups({"full"}) */ + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $fax_number = ''; /** * @var string The email address of the company - * @ORM\Column(type="string") - * @Assert\Email() - * @Groups({"full"}) */ + #[Assert\Email] + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $email_address = ''; /** * @var string The website of the company - * @ORM\Column(type="string") - * @Assert\Url() - * @Groups({"full"}) */ + #[Assert\Url] + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $website = ''; + #[Groups(['company:read', 'company:write', 'import', 'full', 'extended'])] + protected string $comment = ''; + /** - * @var string - * @ORM\Column(type="string") + * @var string The link to the website of an article. Use %PARTNUMBER% as placeholder for the part number. */ + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] + #[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])] protected string $auto_product_url = ''; /******************************************************************************** @@ -141,7 +162,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement * * @return string the link to the article */ - public function getAutoProductUrl(string $partnr = null): string + public function getAutoProductUrl(?string $partnr = null): string { if (is_string($partnr)) { return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url); diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index 97b77d53..871a22d0 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -22,6 +22,38 @@ declare(strict_types=1); namespace App\Entity\Base; +use App\Entity\Attachments\AttachmentType; +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentTypeAttachment; +use App\Entity\Attachments\CategoryAttachment; +use App\Entity\Attachments\CurrencyAttachment; +use App\Entity\Attachments\FootprintAttachment; +use App\Entity\Attachments\GroupAttachment; +use App\Entity\Attachments\LabelAttachment; +use App\Entity\Attachments\ManufacturerAttachment; +use App\Entity\Attachments\MeasurementUnitAttachment; +use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\ProjectAttachment; +use App\Entity\Attachments\StorageLocationAttachment; +use App\Entity\Attachments\SupplierAttachment; +use App\Entity\Attachments\UserAttachment; +use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parts\Category; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Entity\Parts\Footprint; +use App\Entity\UserSystem\Group; +use App\Entity\Parts\Manufacturer; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\Parts\Part; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\PartLot; +use App\Entity\PriceInformations\Currency; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Supplier; +use App\Entity\UserSystem\User; +use App\Repository\DBElementRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use JsonSerializable; use Symfony\Component\Serializer\Annotation\DiscriminatorMap; @@ -34,52 +66,18 @@ use Symfony\Component\Serializer\Annotation\Groups; * (except special tables like "internal"...) * Every database table which are managed with this class (or a subclass of it) * must have the table row "id"!! The ID is the unique key to identify the elements. - * - * @ORM\MappedSuperclass(repositoryClass="App\Repository\DBElementRepository") - * - * @DiscriminatorMap(typeProperty="type", mapping={ - * "attachment_type" = "App\Entity\Attachments\AttachmentType", - * "attachment" = "App\Entity\Attachments\Attachment", - * "attachment_type_attachment" = "App\Entity\Attachments\AttachmentTypeAttachment", - * "category_attachment" = "App\Entity\Attachments\CategoryAttachment", - * "currency_attachment" = "App\Entity\Attachments\CurrencyAttachment", - * "footprint_attachment" = "App\Entity\Attachments\FootprintAttachment", - * "group_attachment" = "App\Entity\Attachments\GroupAttachment", - * "label_attachment" = "App\Entity\Attachments\LabelAttachment", - * "manufacturer_attachment" = "App\Entity\Attachments\ManufacturerAttachment", - * "measurement_unit_attachment" = "App\Entity\Attachments\MeasurementUnitAttachment", - * "part_attachment" = "App\Entity\Attachments\PartAttachment", - * "project_attachment" = "App\Entity\Attachments\ProjectAttachment", - * "storelocation_attachment" = "App\Entity\Attachments\StorelocationAttachment", - * "supplier_attachment" = "App\Entity\Attachments\SupplierAttachment", - * "user_attachment" = "App\Entity\Attachments\UserAttachment", - * "category" = "App\Entity\Parts\Category", - * "project" = "App\Entity\ProjectSystem\Project", - * "project_bom_entry" = "App\Entity\ProjectSystem\ProjectBOMEntry", - * "footprint" = "App\Entity\Parts\Footprint", - * "group" = "App\Entity\UserSystem\Group", - * "manufacturer" = "App\Entity\Parts\Manufacturer", - * "orderdetail" = "App\Entity\PriceInformations\Orderdetail", - * "part" = "App\Entity\Parts\Part", - * "pricedetail" = "App\Entity\PriceInformation\Pricedetail", - * "storelocation" = "App\Entity\Parts\Storelocation", - * "part_lot" = "App\Entity\Parts\PartLot", - * "currency" = "App\Entity\PriceInformations\Currency", - * "measurement_unit" = "App\Entity\Parts\MeasurementUnit", - * "parameter" = "App\Entity\Parts\AbstractParameter", - * "supplier" = "App\Entity\Parts\Supplier", - * "user" = "App\Entity\UserSystem\User" - * }) */ +#[DiscriminatorMap(typeProperty: 'type', mapping: ['attachment_type' => AttachmentType::class, 'attachment' => Attachment::class, 'attachment_type_attachment' => AttachmentTypeAttachment::class, 'category_attachment' => CategoryAttachment::class, 'currency_attachment' => CurrencyAttachment::class, 'footprint_attachment' => FootprintAttachment::class, 'group_attachment' => GroupAttachment::class, 'label_attachment' => LabelAttachment::class, 'manufacturer_attachment' => ManufacturerAttachment::class, 'measurement_unit_attachment' => MeasurementUnitAttachment::class, 'part_attachment' => PartAttachment::class, 'project_attachment' => ProjectAttachment::class, 'storelocation_attachment' => StorageLocationAttachment::class, 'supplier_attachment' => SupplierAttachment::class, 'user_attachment' => UserAttachment::class, 'category' => Category::class, 'project' => Project::class, 'project_bom_entry' => ProjectBOMEntry::class, 'footprint' => Footprint::class, 'group' => Group::class, 'manufacturer' => Manufacturer::class, 'orderdetail' => Orderdetail::class, 'part' => Part::class, 'pricedetail' => 'App\Entity\PriceInformation\Pricedetail', 'storelocation' => StorageLocation::class, 'part_lot' => PartLot::class, 'currency' => Currency::class, 'measurement_unit' => MeasurementUnit::class, 'parameter' => AbstractParameter::class, 'supplier' => Supplier::class, 'user' => User::class])] +#[ORM\MappedSuperclass(repositoryClass: DBElementRepository::class)] abstract class AbstractDBElement implements JsonSerializable { /** @var int|null The Identification number for this part. This value is unique for the element in this table. * Null if the element is not saved to DB yet. - * @ORM\Column(type="integer") - * @ORM\Id() - * @ORM\GeneratedValue() - * @Groups({"full"}) */ + #[Groups(['full', 'api:basic:read'])] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\Id] + #[ORM\GeneratedValue] protected ?int $id = null; public function __clone() diff --git a/src/Entity/Base/AbstractNamedDBElement.php b/src/Entity/Base/AbstractNamedDBElement.php index ddb758c0..f7939589 100644 --- a/src/Entity/Base/AbstractNamedDBElement.php +++ b/src/Entity/Base/AbstractNamedDBElement.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\Entity\Base; +use App\Repository\NamedDBElementRepository; +use Doctrine\DBAL\Types\Types; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Contracts\TimeStampableInterface; use Doctrine\ORM\Mapping as ORM; @@ -30,20 +32,20 @@ use Symfony\Component\Validator\Constraints as Assert; /** * All subclasses of this class have an attribute "name". - * - * @ORM\MappedSuperclass(repositoryClass="App\Repository\NamedDBElement") - * @ORM\HasLifecycleCallbacks() */ -abstract class AbstractNamedDBElement extends AbstractDBElement implements NamedElementInterface, TimeStampableInterface +#[ORM\MappedSuperclass(repositoryClass: NamedDBElementRepository::class)] +#[ORM\HasLifecycleCallbacks] +abstract class AbstractNamedDBElement extends AbstractDBElement implements NamedElementInterface, TimeStampableInterface, \Stringable { use TimestampTrait; /** - * @var string the name of this element - * @ORM\Column(type="string") - * @Assert\NotBlank() - * @Groups({"simple", "extended", "full", "import"}) + * @var string The name of this element */ + #[Assert\NotBlank] + #[Groups(['simple', 'extended', 'full', 'import', 'api:basic:read', 'api:basic:write'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $name = ''; /****************************************************************************** @@ -52,7 +54,7 @@ abstract class AbstractNamedDBElement extends AbstractDBElement implements Named * ******************************************************************************/ - public function __toString() + public function __toString(): string { return $this->getName(); } diff --git a/src/Entity/Base/AbstractPartsContainingDBElement.php b/src/Entity/Base/AbstractPartsContainingDBElement.php index be0b230a..70d88fa9 100644 --- a/src/Entity/Base/AbstractPartsContainingDBElement.php +++ b/src/Entity/Base/AbstractPartsContainingDBElement.php @@ -22,16 +22,28 @@ declare(strict_types=1); namespace App\Entity\Base; +use App\Entity\Attachments\Attachment; +use App\Entity\Parameters\AbstractParameter; +use App\Repository\AbstractPartsContainingRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; /** - * Class PartsContainingDBElement. - * - * @ORM\MappedSuperclass(repositoryClass="App\Repository\AbstractPartsContainingRepository") + * @template AT of Attachment + * @template PT of AbstractParameter + * @extends AbstractStructuralDBElement */ +#[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)] abstract class AbstractPartsContainingDBElement extends AbstractStructuralDBElement { - /** @Groups({"full"}) */ - protected $parameters; + #[Groups(['full', 'import'])] + protected Collection $parameters; + + public function __construct() + { + parent::__construct(); + $this->parameters = new ArrayCollection(); + } } diff --git a/src/Entity/Base/AbstractStructuralDBElement.php b/src/Entity/Base/AbstractStructuralDBElement.php index 629669e9..660710db 100644 --- a/src/Entity/Base/AbstractStructuralDBElement.php +++ b/src/Entity/Base/AbstractStructuralDBElement.php @@ -22,14 +22,21 @@ declare(strict_types=1); namespace App\Entity\Base; +use App\Entity\Attachments\Attachment; +use App\Entity\Parameters\AbstractParameter; +use App\Repository\StructuralDBElementRepository; +use App\EntityListeners\TreeCacheInvalidationListener; +use App\Validator\Constraints\UniqueObjectCollection; +use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Parameters\ParametersTrait; use App\Validator\Constraints\NoneOfItsChildren; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Validator\Constraints as Assert; use function count; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use function get_class; use InvalidArgumentException; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; @@ -43,36 +50,40 @@ use Symfony\Component\Serializer\Annotation\Groups; * It's allowed to have instances of root elements, but if you try to change * an attribute of a root element, you will get an exception! * - * @ORM\MappedSuperclass(repositoryClass="App\Repository\StructuralDBElementRepository") * - * @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"}) + * @see \App\Tests\Entity\Base\AbstractStructuralDBElementTest * - * @UniqueEntity(fields={"name", "parent"}, ignoreNull=false, message="structural.entity.unique_name") + * @template AT of Attachment + * @template PT of AbstractParameter + * @template-use ParametersTrait + * @extends AttachmentContainingDBElement + * @uses ParametersTrait */ +#[UniqueEntity(fields: ['name', 'parent'], message: 'structural.entity.unique_name', ignoreNull: false)] +#[ORM\MappedSuperclass(repositoryClass: StructuralDBElementRepository::class)] +#[ORM\EntityListeners([TreeCacheInvalidationListener::class])] abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement { use ParametersTrait; - public const ID_ROOT_ELEMENT = 0; - /** * This is a not standard character, so build a const, so a dev can easily use it. */ - public const PATH_DELIMITER_ARROW = ' → '; + final public const PATH_DELIMITER_ARROW = ' → '; /** - * @var string The comment info for this element - * @ORM\Column(type="text") - * @Groups({"full", "import"}) + * @var string The comment info for this element as markdown */ + #[Groups(['full', 'import'])] + #[ORM\Column(type: Types::TEXT)] protected string $comment = ''; /** * @var bool If this property is set, this element can not be selected for part properties. * Useful if this element should be used only for grouping, sorting. - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) */ + #[Groups(['full', 'import'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $not_selectable = false; /** @@ -81,26 +92,44 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement protected int $level = 0; /** - * We can not define the mapping here or we will get an exception. Unfortunately we have to do the mapping in the + * We can not define the mapping here, or we will get an exception. Unfortunately we have to do the mapping in the * subclasses. * - * @var AbstractStructuralDBElement[]|Collection - * @Groups({"include_children"}) + * @var Collection + * @phpstan-var Collection */ - protected $children; + #[Groups(['include_children'])] + protected Collection $children; /** - * @var AbstractStructuralDBElement - * @NoneOfItsChildren() - * @Groups({"include_parents", "import"}) + * @var AbstractStructuralDBElement|null + * @phpstan-var static|null */ - protected $parent = null; + #[Groups(['include_parents', 'import'])] + #[NoneOfItsChildren] + protected ?AbstractStructuralDBElement $parent = null; - /** @var string[] all names of all parent elements as a array of strings, + /** + * Mapping done in subclasses. + * + * @var Collection + * @phpstan-var Collection + */ + #[Assert\Valid] + #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] + protected Collection $parameters; + + /** @var string[] all names of all parent elements as an array of strings, * the last array element is the name of the element itself */ private array $full_path_strings = []; + /** + * Alternative names (semicolon-separated) for this element, which can be used for searching (especially for info provider system) + */ + #[ORM\Column(type: Types::TEXT, nullable: true, options: ['default' => null])] + private ?string $alternative_names = ""; + public function __construct() { parent::__construct(); @@ -141,11 +170,11 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement //Check if both elements compared, are from the same type // (we have to check inheritance, or we get exceptions when using doctrine entities (they have a proxy type): - if (!is_a($another_element, $class_name) && !is_a($this, get_class($another_element))) { + if (!$another_element instanceof $class_name && !is_a($this, $another_element::class)) { throw new InvalidArgumentException('isChildOf() only works for objects of the same type!'); } - if (null === $this->getParent()) { // this is the root node + if (!$this->getParent() instanceof self) { // this is the root node return false; } @@ -155,10 +184,8 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement if ($this->getParent() === $another_element) { return true; } - } else { //If the IDs are defined, we can compare the IDs - if ($this->getParent()->getID() === $another_element->getID()) { - return true; - } + } elseif ($this->getParent()->getID() === $another_element->getID()) { + return true; } //Otherwise, check recursively @@ -168,11 +195,11 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement /** * Checks if this element is an root element (has no parent). * - * @return bool true if the this element is an root element + * @return bool true if this element is a root element */ public function isRoot(): bool { - return null === $this->parent; + return $this->parent === null; } /****************************************************************************** @@ -184,7 +211,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement /** * Get the parent of this element. * - * @return AbstractStructuralDBElement|null The parent element. Null if this element, does not have a parent. + * @return static|null The parent element. Null if this element, does not have a parent. */ public function getParent(): ?self { @@ -192,7 +219,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement } /** - * Get the comment of the element. + * Get the comment of the element as markdown encoded string. * * @return string the comment @@ -215,9 +242,9 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement /* * Only check for nodes that have a parent. In the other cases zero is correct. */ - if (0 === $this->level && null !== $this->parent) { + if (0 === $this->level && $this->parent instanceof self) { $element = $this->parent; - while (null !== $element) { + while ($element instanceof self) { /** @var AbstractStructuralDBElement $element */ $element = $element->parent; ++$this->level; @@ -234,16 +261,18 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement * * @return string the full path (incl. the name of this element), delimited by $delimiter */ + #[Groups(['api:basic:read'])] + #[SerializedName('full_path')] public function getFullPath(string $delimiter = self::PATH_DELIMITER_ARROW): string { - if (empty($this->full_path_strings)) { + if ($this->full_path_strings === []) { $this->full_path_strings = []; $this->full_path_strings[] = $this->getName(); $element = $this; $overflow = 20; //We only allow 20 levels depth - while (null !== $element->parent && $overflow >= 0) { + while ($element->parent instanceof self && $overflow >= 0) { $element = $element->parent; $this->full_path_strings[] = $element->getName(); //Decrement to prevent mem overflow. @@ -289,6 +318,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement return new ArrayCollection(); } + //@phpstan-ignore-next-line return $this->children ?? new ArrayCollection(); } @@ -316,9 +346,8 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement /** * Sets the new parent object. * - * @param AbstractStructuralDBElement|null $new_parent The new parent object - * - * @return AbstractStructuralDBElement + * @param static|null $new_parent The new parent object + * @return $this */ public function setParent(?self $new_parent): self { @@ -330,7 +359,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement $this->parent = $new_parent; //Add this element as child to the new parent - if (null !== $new_parent) { + if ($new_parent instanceof self) { $new_parent->getChildren()->add($this); } @@ -342,7 +371,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement * * @param string $new_comment the new comment * - * @return AbstractStructuralDBElement + * @return $this */ public function setComment(string $new_comment): self { @@ -393,4 +422,34 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement return $this; } + + /** + * Returns a comma separated list of alternative names. + * @return string|null + */ + public function getAlternativeNames(): ?string + { + if ($this->alternative_names === null) { + return null; + } + + //Remove trailing comma + return rtrim($this->alternative_names, ','); + } + + /** + * Sets a comma separated list of alternative names. + * @return $this + */ + public function setAlternativeNames(?string $new_value): self + { + //Add a trailing comma, if not already there (makes it easier to find in the database) + if (is_string($new_value) && !str_ends_with($new_value, ',')) { + $new_value .= ','; + } + + $this->alternative_names = $new_value; + + return $this; + } } diff --git a/src/Entity/Base/MasterAttachmentTrait.php b/src/Entity/Base/MasterAttachmentTrait.php index f9dd26aa..723bab07 100644 --- a/src/Entity/Base/MasterAttachmentTrait.php +++ b/src/Entity/Base/MasterAttachmentTrait.php @@ -23,20 +23,21 @@ declare(strict_types=1); namespace App\Entity\Base; use App\Entity\Attachments\Attachment; -use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /** - * A entity with this class has a master attachment, which is used as a preview image for this object. + * An entity with this class has a master attachment, which is used as a preview image for this object. */ trait MasterAttachmentTrait { /** - * @var Attachment - * @ORM\ManyToOne(targetEntity="App\Entity\Attachments\Attachment") - * @ORM\JoinColumn(name="id_preview_attachment", referencedColumnName="id", onDelete="SET NULL", nullable=true) - * @Assert\Expression("value == null or value.isPicture()", message="part.master_attachment.must_be_picture") + * @var Attachment|null + * Mapping is done in the subclasses (e.g. Part), like with the attachments. + * If this is done here (which is possible in theory), the attachment is not lazy loaded anymore, which causes unnecessary overhead. + * + * !!! If you change this name, you have to change it in the fetchHint in the AttachmentContainingDBElementRepository (getElementsAndPreviewAttachmentByIDs()) too !!! */ + #[Assert\Expression('value == null or value.isPicture()', message: 'part.master_attachment.must_be_picture')] protected ?Attachment $master_picture_attachment = null; /** diff --git a/src/Entity/Base/PartsContainingRepositoryInterface.php b/src/Entity/Base/PartsContainingRepositoryInterface.php index 16932677..89e7e5f6 100644 --- a/src/Entity/Base/PartsContainingRepositoryInterface.php +++ b/src/Entity/Base/PartsContainingRepositoryInterface.php @@ -1,4 +1,7 @@ . */ - namespace App\Entity\Base; use App\Entity\Parts\Part; @@ -28,11 +30,11 @@ interface PartsContainingRepositoryInterface * Returns all parts associated with this element. * * @param object $element the element for which the parts should be determined - * @param array $order_by The order of the parts. Format ['name' => 'ASC'] + * @param string $nameOrderDirection the direction in which the parts should be ordered by name, either ASC or DESC * * @return Part[] */ - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array; + public function getParts(object $element, string $nameOrderDirection = "ASC"): array; /** * Gets the count of the parts associated with this element. diff --git a/src/Entity/Base/TimestampTrait.php b/src/Entity/Base/TimestampTrait.php index df3779eb..77506b18 100644 --- a/src/Entity/Base/TimestampTrait.php +++ b/src/Entity/Base/TimestampTrait.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\Entity\Base; +use ApiPlatform\Metadata\ApiProperty; +use Doctrine\DBAL\Types\Types; use DateTime; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -32,26 +34,28 @@ use Symfony\Component\Serializer\Annotation\Groups; trait TimestampTrait { /** - * @var DateTime|null the date when this element was modified the last time - * @ORM\Column(type="datetime", name="last_modified", options={"default":"CURRENT_TIMESTAMP"}) - * @Groups({"extended", "full"}) + * @var \DateTimeImmutable|null the date when this element was modified the last time */ - protected ?DateTime $lastModified = null; + #[Groups(['extended', 'full'])] + #[ApiProperty(writable: false)] + #[ORM\Column(name: 'last_modified', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected ?\DateTimeImmutable $lastModified = null; /** - * @var DateTime|null the date when this element was created - * @ORM\Column(type="datetime", name="datetime_added", options={"default":"CURRENT_TIMESTAMP"}) - * @Groups({"extended", "full"}) + * @var \DateTimeImmutable|null the date when this element was created */ - protected ?DateTime $addedDate = null; + #[Groups(['extended', 'full'])] + #[ApiProperty(writable: false)] + #[ORM\Column(name: 'datetime_added', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected ?\DateTimeImmutable $addedDate = null; /** * Returns the last time when the element was modified. * Returns null if the element was not yet saved to DB yet. * - * @return DateTime|null the time of the last edit + * @return \DateTimeImmutable|null the time of the last edit */ - public function getLastModified(): ?DateTime + public function getLastModified(): ?\DateTimeImmutable { return $this->lastModified; } @@ -60,24 +64,23 @@ trait TimestampTrait * Returns the date/time when the element was created. * Returns null if the element was not yet saved to DB yet. * - * @return DateTime|null the creation time of the part + * @return \DateTimeImmutable|null the creation time of the part */ - public function getAddedDate(): ?DateTime + public function getAddedDate(): ?\DateTimeImmutable { return $this->addedDate; } /** * Helper for updating the timestamp. It is automatically called by doctrine before persisting. - * - * @ORM\PrePersist - * @ORM\PreUpdate */ + #[ORM\PrePersist] + #[ORM\PreUpdate] public function updateTimestamps(): void { - $this->lastModified = new DateTime('now'); + $this->lastModified = new \DateTimeImmutable('now'); if (null === $this->addedDate) { - $this->addedDate = new DateTime('now'); + $this->addedDate = new \DateTimeImmutable('now'); } } } diff --git a/src/Entity/Contracts/LogWithEventUndoInterface.php b/src/Entity/Contracts/LogWithEventUndoInterface.php index fecc6eaa..1e8db0a1 100644 --- a/src/Entity/Contracts/LogWithEventUndoInterface.php +++ b/src/Entity/Contracts/LogWithEventUndoInterface.php @@ -42,6 +42,7 @@ declare(strict_types=1); namespace App\Entity\Contracts; use App\Entity\LogSystem\AbstractLogEntry; +use App\Services\LogSystem\EventUndoMode; interface LogWithEventUndoInterface { @@ -60,12 +61,12 @@ interface LogWithEventUndoInterface * * @return $this */ - public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): self; + public function setUndoneEvent(AbstractLogEntry $event, EventUndoMode $mode = EventUndoMode::UNDO): self; /** * Returns the mode how the event was undone: * "undo" = Only a single event was applied to element * "revert" = Element was reverted to the state it was to the timestamp of the log. */ - public function getUndoMode(): string; + public function getUndoMode(): EventUndoMode; } diff --git a/src/Entity/Contracts/LogWithNewDataInterface.php b/src/Entity/Contracts/LogWithNewDataInterface.php new file mode 100644 index 00000000..c4128cb7 --- /dev/null +++ b/src/Entity/Contracts/LogWithNewDataInterface.php @@ -0,0 +1,43 @@ +. + */ +namespace App\Entity\Contracts; + +interface LogWithNewDataInterface +{ + /** + * Checks if this entry has information about the new data. + * @return bool + */ + public function hasNewDataInformation(): bool; + + /** + * Returns the new data for this entry. + */ + public function getNewData(): array; + + /** + * Sets the new data for this entry. + * @return $this + */ + public function setNewData(array $new_data): self; +} diff --git a/src/Entity/Contracts/TimeStampableInterface.php b/src/Entity/Contracts/TimeStampableInterface.php index e198ae7c..c99c0a1c 100644 --- a/src/Entity/Contracts/TimeStampableInterface.php +++ b/src/Entity/Contracts/TimeStampableInterface.php @@ -22,23 +22,21 @@ declare(strict_types=1); namespace App\Entity\Contracts; -use DateTime; - interface TimeStampableInterface { /** * Returns the last time when the element was modified. * Returns null if the element was not yet saved to DB yet. * - * @return DateTime|null the time of the last edit + * @return \DateTimeInterface|null the time of the last edit */ - public function getLastModified(): ?DateTime; + public function getLastModified(): ?\DateTimeInterface; /** * Returns the date/time when the element was created. * Returns null if the element was not yet saved to DB yet. * - * @return DateTime|null the creation time of the part + * @return \DateTimeInterface|null the creation time of the part */ - public function getAddedDate(): ?DateTime; + public function getAddedDate(): ?\DateTimeInterface; } diff --git a/src/Entity/Contracts/TimeTravelInterface.php b/src/Entity/Contracts/TimeTravelInterface.php index 6324d75f..2b0f4571 100644 --- a/src/Entity/Contracts/TimeTravelInterface.php +++ b/src/Entity/Contracts/TimeTravelInterface.php @@ -22,16 +22,14 @@ declare(strict_types=1); namespace App\Entity\Contracts; -use DateTime; - interface TimeTravelInterface { /** - * Checks if this entry has informations which data has changed. + * Checks if this entry has information which data has changed. * - * @return bool true if this entry has informations about the changed data + * @return bool true if this entry has information about the changed data */ - public function hasOldDataInformations(): bool; + public function hasOldDataInformation(): bool; /** * Returns the data the entity had before this log entry. @@ -39,7 +37,7 @@ interface TimeTravelInterface public function getOldData(): array; /** - * Returns the the timestamp associated with this change. + * Returns the timestamp associated with this change. */ - public function getTimestamp(): DateTime; + public function getTimestamp(): \DateTimeInterface; } diff --git a/src/Entity/EDA/EDACategoryInfo.php b/src/Entity/EDA/EDACategoryInfo.php new file mode 100644 index 00000000..0163dfb3 --- /dev/null +++ b/src/Entity/EDA/EDACategoryInfo.php @@ -0,0 +1,135 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\EDA; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embeddable; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints\Length; + +#[Embeddable] +class EDACategoryInfo +{ + /** + * @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors. + */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['full', 'category:read', 'category:write', 'import'])] + #[Length(max: 255)] + private ?string $reference_prefix = null; + + /** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */ + #[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility + #[Groups(['full', 'category:read', 'category:write', 'import'])] + private ?bool $visibility = null; + + /** @var bool|null If this is set to true, then this part will be excluded from the BOM */ + #[Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['full', 'category:read', 'category:write', 'import'])] + private ?bool $exclude_from_bom = null; + + /** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */ + #[Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['full', 'category:read', 'category:write', 'import'])] + private ?bool $exclude_from_board = null; + + /** @var bool|null If this is set to true, then this part will be excluded in the simulation */ + #[Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['full', 'category:read', 'category:write', 'import'])] + private ?bool $exclude_from_sim = true; + + /** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['full', 'category:read', 'category:write', 'import'])] + #[Length(max: 255)] + private ?string $kicad_symbol = null; + + public function getReferencePrefix(): ?string + { + return $this->reference_prefix; + } + + public function setReferencePrefix(?string $reference_prefix): EDACategoryInfo + { + $this->reference_prefix = $reference_prefix; + return $this; + } + + public function getVisibility(): ?bool + { + return $this->visibility; + } + + public function setVisibility(?bool $visibility): EDACategoryInfo + { + $this->visibility = $visibility; + return $this; + } + + public function getExcludeFromBom(): ?bool + { + return $this->exclude_from_bom; + } + + public function setExcludeFromBom(?bool $exclude_from_bom): EDACategoryInfo + { + $this->exclude_from_bom = $exclude_from_bom; + return $this; + } + + public function getExcludeFromBoard(): ?bool + { + return $this->exclude_from_board; + } + + public function setExcludeFromBoard(?bool $exclude_from_board): EDACategoryInfo + { + $this->exclude_from_board = $exclude_from_board; + return $this; + } + + public function getExcludeFromSim(): ?bool + { + return $this->exclude_from_sim; + } + + public function setExcludeFromSim(?bool $exclude_from_sim): EDACategoryInfo + { + $this->exclude_from_sim = $exclude_from_sim; + return $this; + } + + public function getKicadSymbol(): ?string + { + return $this->kicad_symbol; + } + + public function setKicadSymbol(?string $kicad_symbol): EDACategoryInfo + { + $this->kicad_symbol = $kicad_symbol; + return $this; + } + +} \ No newline at end of file diff --git a/src/Entity/EDA/EDAFootprintInfo.php b/src/Entity/EDA/EDAFootprintInfo.php new file mode 100644 index 00000000..9c5ef1c1 --- /dev/null +++ b/src/Entity/EDA/EDAFootprintInfo.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\EDA; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embeddable; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints\Length; + +#[Embeddable] +class EDAFootprintInfo +{ + /** @var string|null The KiCAD footprint, which should be used (the path to the library) */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['full', 'footprint:read', 'footprint:write', 'import'])] + #[Length(max: 255)] + private ?string $kicad_footprint = null; + + public function getKicadFootprint(): ?string + { + return $this->kicad_footprint; + } + + public function setKicadFootprint(?string $kicad_footprint): EDAFootprintInfo + { + $this->kicad_footprint = $kicad_footprint; + return $this; + } +} \ No newline at end of file diff --git a/src/Entity/EDA/EDAPartInfo.php b/src/Entity/EDA/EDAPartInfo.php new file mode 100644 index 00000000..b4fc3588 --- /dev/null +++ b/src/Entity/EDA/EDAPartInfo.php @@ -0,0 +1,175 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\EDA; + +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embeddable; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints\Length; + +#[Embeddable] +class EDAPartInfo +{ + /** + * @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors. + */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + #[Length(max: 255)] + private ?string $reference_prefix = null; + + /** @var string|null The value, which should be shown together with the part (e.g. 470 for a 470 Ohm resistor) */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + #[Length(max: 255)] + private ?string $value = null; + + /** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */ + #[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + private ?bool $visibility = null; + + /** @var bool|null If this is set to true, then this part will be excluded from the BOM */ + #[Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + private ?bool $exclude_from_bom = null; + + /** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */ + #[Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + private ?bool $exclude_from_board = null; + + /** @var bool|null If this is set to true, then this part will be excluded in the simulation */ + #[Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + private ?bool $exclude_from_sim = null; + + /** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + #[Length(max: 255)] + private ?string $kicad_symbol = null; + + /** @var string|null The KiCAD footprint, which should be used (the path to the library) */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])] + #[Length(max: 255)] + private ?string $kicad_footprint = null; + + public function __construct() + { + + } + + public function getReferencePrefix(): ?string + { + return $this->reference_prefix; + } + + public function setReferencePrefix(?string $reference_prefix): EDAPartInfo + { + $this->reference_prefix = $reference_prefix; + return $this; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(?string $value): EDAPartInfo + { + $this->value = $value; + return $this; + } + + public function getVisibility(): ?bool + { + return $this->visibility; + } + + public function setVisibility(?bool $visibility): EDAPartInfo + { + $this->visibility = $visibility; + return $this; + } + + public function getExcludeFromBom(): ?bool + { + return $this->exclude_from_bom; + } + + public function setExcludeFromBom(?bool $exclude_from_bom): EDAPartInfo + { + $this->exclude_from_bom = $exclude_from_bom; + return $this; + } + + public function getExcludeFromBoard(): ?bool + { + return $this->exclude_from_board; + } + + public function setExcludeFromBoard(?bool $exclude_from_board): EDAPartInfo + { + $this->exclude_from_board = $exclude_from_board; + return $this; + } + + public function getExcludeFromSim(): ?bool + { + return $this->exclude_from_sim; + } + + public function setExcludeFromSim(?bool $exclude_from_sim): EDAPartInfo + { + $this->exclude_from_sim = $exclude_from_sim; + return $this; + } + + public function getKicadSymbol(): ?string + { + return $this->kicad_symbol; + } + + public function setKicadSymbol(?string $kicad_symbol): EDAPartInfo + { + $this->kicad_symbol = $kicad_symbol; + return $this; + } + + public function getKicadFootprint(): ?string + { + return $this->kicad_footprint; + } + + public function setKicadFootprint(?string $kicad_footprint): EDAPartInfo + { + $this->kicad_footprint = $kicad_footprint; + return $this; + } + + +} \ No newline at end of file diff --git a/src/Entity/LabelSystem/BarcodeType.php b/src/Entity/LabelSystem/BarcodeType.php new file mode 100644 index 00000000..daf7d401 --- /dev/null +++ b/src/Entity/LabelSystem/BarcodeType.php @@ -0,0 +1,66 @@ +. + */ +namespace App\Entity\LabelSystem; + +enum BarcodeType: string +{ + case NONE = 'none'; + case QR = 'qr'; + case CODE39 = 'code39'; + case DATAMATRIX = 'datamatrix'; + case CODE93 = 'code93'; + case CODE128 = 'code128'; + + /** + * Returns true if the barcode is none. (Useful for twig templates) + * @return bool + */ + public function isNone(): bool + { + return $this === self::NONE; + } + + /** + * Returns true if the barcode is a 1D barcode (Code39, etc.). + * @return bool + */ + public function is1D(): bool + { + return match ($this) { + self::CODE39, self::CODE93, self::CODE128 => true, + default => false, + }; + } + + /** + * Returns true if the barcode is a 2D barcode (QR code, datamatrix). + * @return bool + */ + public function is2D(): bool + { + return match ($this) { + self::QR, self::DATAMATRIX => true, + default => false, + }; + } +} diff --git a/src/Entity/LabelSystem/LabelOptions.php b/src/Entity/LabelSystem/LabelOptions.php index f3f448ad..ee1a5414 100644 --- a/src/Entity/LabelSystem/LabelOptions.php +++ b/src/Entity/LabelSystem/LabelOptions.php @@ -41,71 +41,66 @@ declare(strict_types=1); namespace App\Entity\LabelSystem; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Validator\Constraints as Assert; -/** - * @ORM\Embeddable() - */ +#[ORM\Embeddable] class LabelOptions { - public const BARCODE_TYPES = ['none', /*'ean8',*/ 'qr', 'code39', 'datamatrix', 'code93', 'code128']; - public const SUPPORTED_ELEMENTS = ['part', 'part_lot', 'storelocation']; - public const PICTURE_TYPES = ['none', 'element_picture', 'main_attachment']; - - public const LINES_MODES = ['html', 'twig']; - /** * @var float The page size of the label in mm - * @Assert\Positive() - * @ORM\Column(type="float") */ + #[Assert\Positive] + #[ORM\Column(type: Types::FLOAT)] + #[Groups(["extended", "full", "import"])] protected float $width = 50.0; /** * @var float The page size of the label in mm - * @Assert\Positive() - * @ORM\Column(type="float") */ + #[Assert\Positive] + #[ORM\Column(type: Types::FLOAT)] + #[Groups(["extended", "full", "import"])] protected float $height = 30.0; /** - * @var string The type of the barcode that should be used in the label (e.g. 'qr') - * @Assert\Choice(choices=LabelOptions::BARCODE_TYPES) - * @ORM\Column(type="string") + * @var BarcodeType The type of the barcode that should be used in the label (e.g. 'qr') */ - protected string $barcode_type = 'none'; + #[ORM\Column(type: Types::STRING, enumType: BarcodeType::class)] + #[Groups(["extended", "full", "import"])] + protected BarcodeType $barcode_type = BarcodeType::NONE; /** - * @var string What image should be shown along the - * @Assert\Choice(choices=LabelOptions::PICTURE_TYPES) - * @ORM\Column(type="string") + * @var LabelPictureType What image should be shown along the label */ - protected string $picture_type = 'none'; + #[ORM\Column(type: Types::STRING, enumType: LabelPictureType::class)] + #[Groups(["extended", "full", "import"])] + protected LabelPictureType $picture_type = LabelPictureType::NONE; - /** - * @var string - * @Assert\Choice(choices=LabelOptions::SUPPORTED_ELEMENTS) - * @ORM\Column(type="string") - */ - protected string $supported_element = 'part'; + #[ORM\Column(type: Types::STRING, enumType: LabelSupportedElement::class)] + #[Groups(["extended", "full", "import"])] + protected LabelSupportedElement $supported_element = LabelSupportedElement::PART; /** * @var string any additional CSS for the label - * @ORM\Column(type="text") */ + #[ORM\Column(type: Types::TEXT)] + #[Groups([ "full", "import"])] protected string $additional_css = ''; - /** @var string The mode that will be used to interpret the lines - * @Assert\Choice(choices=LabelOptions::LINES_MODES) - * @ORM\Column(type="string") + /** @var LabelProcessMode The mode that will be used to interpret the lines */ - protected string $lines_mode = 'html'; + #[ORM\Column(name: 'lines_mode', type: Types::STRING, enumType: LabelProcessMode::class)] + #[Groups(["extended", "full", "import"])] + protected LabelProcessMode $process_mode = LabelProcessMode::PLACEHOLDER; /** * @var string - * @ORM\Column(type="text") */ + #[ORM\Column(type: Types::TEXT)] + #[Groups(["extended", "full", "import"])] protected string $lines = ''; public function getWidth(): float @@ -113,9 +108,6 @@ class LabelOptions return $this->width; } - /** - * @return LabelOptions - */ public function setWidth(float $width): self { $this->width = $width; @@ -128,9 +120,6 @@ class LabelOptions return $this->height; } - /** - * @return LabelOptions - */ public function setHeight(float $height): self { $this->height = $height; @@ -138,45 +127,36 @@ class LabelOptions return $this; } - public function getBarcodeType(): string + public function getBarcodeType(): BarcodeType { return $this->barcode_type; } - /** - * @return LabelOptions - */ - public function setBarcodeType(string $barcode_type): self + public function setBarcodeType(BarcodeType $barcode_type): self { $this->barcode_type = $barcode_type; return $this; } - public function getPictureType(): string + public function getPictureType(): LabelPictureType { return $this->picture_type; } - /** - * @return LabelOptions - */ - public function setPictureType(string $picture_type): self + public function setPictureType(LabelPictureType $picture_type): self { $this->picture_type = $picture_type; return $this; } - public function getSupportedElement(): string + public function getSupportedElement(): LabelSupportedElement { return $this->supported_element; } - /** - * @return LabelOptions - */ - public function setSupportedElement(string $supported_element): self + public function setSupportedElement(LabelSupportedElement $supported_element): self { $this->supported_element = $supported_element; @@ -188,9 +168,6 @@ class LabelOptions return $this->lines; } - /** - * @return LabelOptions - */ public function setLines(string $lines): self { $this->lines = $lines; @@ -206,9 +183,6 @@ class LabelOptions return $this->additional_css; } - /** - * @return LabelOptions - */ public function setAdditionalCss(string $additional_css): self { $this->additional_css = $additional_css; @@ -216,17 +190,14 @@ class LabelOptions return $this; } - public function getLinesMode(): string + public function getProcessMode(): LabelProcessMode { - return $this->lines_mode; + return $this->process_mode; } - /** - * @return LabelOptions - */ - public function setLinesMode(string $lines_mode): self + public function setProcessMode(LabelProcessMode $process_mode): self { - $this->lines_mode = $lines_mode; + $this->process_mode = $process_mode; return $this; } diff --git a/src/Entity/LabelSystem/LabelPictureType.php b/src/Entity/LabelSystem/LabelPictureType.php new file mode 100644 index 00000000..c1f90fe2 --- /dev/null +++ b/src/Entity/LabelSystem/LabelPictureType.php @@ -0,0 +1,39 @@ +. + */ +namespace App\Entity\LabelSystem; + +enum LabelPictureType: string +{ + /** + * Show no picture on the label + */ + case NONE = 'none'; + /** + * Show the preview picture of the element on the label + */ + case ELEMENT_PICTURE = 'element_picture'; + /** + * Show the main attachment of the element on the label + */ + case MAIN_ATTACHMENT = 'main_attachment'; +} diff --git a/src/Entity/LabelSystem/LabelProcessMode.php b/src/Entity/LabelSystem/LabelProcessMode.php new file mode 100644 index 00000000..d5967b49 --- /dev/null +++ b/src/Entity/LabelSystem/LabelProcessMode.php @@ -0,0 +1,31 @@ +. + */ +namespace App\Entity\LabelSystem; + +enum LabelProcessMode: string +{ + /** Use placeholders like [[PLACEHOLDER]] which gets replaced with content */ + case PLACEHOLDER = 'html'; + /** Interpret the given lines as twig template */ + case TWIG = 'twig'; +} diff --git a/src/Entity/LabelSystem/LabelProfile.php b/src/Entity/LabelSystem/LabelProfile.php index 46c478ee..d3616c34 100644 --- a/src/Entity/LabelSystem/LabelProfile.php +++ b/src/Entity/LabelSystem/LabelProfile.php @@ -41,49 +41,64 @@ declare(strict_types=1); namespace App\Entity\LabelSystem; +use Doctrine\Common\Collections\Criteria; +use App\Entity\Attachments\Attachment; +use App\Repository\LabelProfileRepository; +use App\EntityListeners\TreeCacheInvalidationListener; +use Doctrine\DBAL\Types\Types; +use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\LabelAttachment; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Validator\Constraints as Assert; /** - * @ORM\Entity(repositoryClass="App\Repository\LabelProfileRepository") - * @ORM\Table(name="label_profiles") - * @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"}) - * @UniqueEntity({"name", "options.supported_element"}) + * @extends AttachmentContainingDBElement */ +#[UniqueEntity(['name', 'options.supported_element'])] +#[ORM\Entity(repositoryClass: LabelProfileRepository::class)] +#[ORM\EntityListeners([TreeCacheInvalidationListener::class])] +#[ORM\Table(name: 'label_profiles')] class LabelProfile extends AttachmentContainingDBElement { /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\LabelAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) */ - protected $attachments; + #[ORM\OneToMany(mappedBy: 'element', targetEntity: LabelAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: LabelAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + protected ?Attachment $master_picture_attachment = null; /** * @var LabelOptions - * @ORM\Embedded(class="LabelOptions") - * @Assert\Valid() */ + #[Assert\Valid] + #[ORM\Embedded(class: 'LabelOptions')] + #[Groups(["extended", "full", "import"])] protected LabelOptions $options; /** * @var string The comment info for this element - * @ORM\Column(type="text") */ + #[ORM\Column(type: Types::TEXT)] protected string $comment = ''; /** * @var bool determines, if this label profile should be shown in the dropdown quick menu - * @ORM\Column(type="boolean") */ + #[ORM\Column(type: Types::BOOLEAN)] + #[Groups(["extended", "full", "import"])] protected bool $show_in_dropdown = true; public function __construct() { + $this->attachments = new ArrayCollection(); parent::__construct(); $this->options = new LabelOptions(); } @@ -127,8 +142,6 @@ class LabelProfile extends AttachmentContainingDBElement /** * Sets the show in dropdown menu. - * - * @return LabelProfile */ public function setShowInDropdown(bool $show_in_dropdown): self { diff --git a/src/Entity/LabelSystem/LabelSupportedElement.php b/src/Entity/LabelSystem/LabelSupportedElement.php new file mode 100644 index 00000000..7649e586 --- /dev/null +++ b/src/Entity/LabelSystem/LabelSupportedElement.php @@ -0,0 +1,49 @@ +. + */ +namespace App\Entity\LabelSystem; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractNamedDBElement; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\StorageLocation; + +enum LabelSupportedElement: string +{ + case PART = 'part'; + case PART_LOT = 'part_lot'; + case STORELOCATION = 'storelocation'; + + /** + * Returns the entity class for the given element type + * @return class-string + */ + public function getEntityClass(): string + { + return match ($this) { + self::PART => Part::class, + self::PART_LOT => PartLot::class, + self::STORELOCATION => StorageLocation::class, + }; + } +} diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php index 13906da3..aa795613 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -22,157 +22,60 @@ declare(strict_types=1); namespace App\Entity\LogSystem; -use App\Entity\Attachments\Attachment; -use App\Entity\Attachments\AttachmentType; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; -use App\Entity\ProjectSystem\Project; -use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Entity\LabelSystem\LabelProfile; -use App\Entity\Parameters\AbstractParameter; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; -use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Part; -use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; -use App\Entity\Parts\Supplier; -use App\Entity\PriceInformations\Currency; -use App\Entity\PriceInformations\Orderdetail; -use App\Entity\PriceInformations\Pricedetail; -use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; -use DateTime; + use Doctrine\ORM\Mapping as ORM; -use InvalidArgumentException; -use Psr\Log\LogLevel; +use App\Repository\LogEntryRepository; /** - * This entity describes a entry in the event log. - * - * @ORM\Entity(repositoryClass="App\Repository\LogEntryRepository") - * @ORM\Table("log", indexes={ - * @ORM\Index(name="log_idx_type", columns={"type"}), - * @ORM\Index(name="log_idx_type_target", columns={"type", "target_type", "target_id"}), - * @ORM\Index(name="log_idx_datetime", columns={"datetime"}), - * }) - * @ORM\InheritanceType("SINGLE_TABLE") - * @ORM\DiscriminatorColumn(name="type", type="smallint") - * @ORM\DiscriminatorMap({ - * 1 = "UserLoginLogEntry", - * 2 = "UserLogoutLogEntry", - * 3 = "UserNotAllowedLogEntry", - * 4 = "ExceptionLogEntry", - * 5 = "ElementDeletedLogEntry", - * 6 = "ElementCreatedLogEntry", - * 7 = "ElementEditedLogEntry", - * 8 = "ConfigChangedLogEntry", - * 9 = "LegacyInstockChangedLogEntry", - * 10 = "DatabaseUpdatedLogEntry", - * 11 = "CollectionElementDeleted", - * 12 = "SecurityEventLogEntry", - * 13 = "PartStockChangedLogEntry", - * }) + * This entity describes an entry in the event log. + * @see \App\Tests\Entity\LogSystem\AbstractLogEntryTest */ +#[ORM\Entity(repositoryClass: LogEntryRepository::class)] +#[ORM\Table('log')] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'type', type: 'smallint')] +#[ORM\DiscriminatorMap([1 => 'UserLoginLogEntry', 2 => 'UserLogoutLogEntry', 3 => 'UserNotAllowedLogEntry', 4 => 'ExceptionLogEntry', 5 => 'ElementDeletedLogEntry', 6 => 'ElementCreatedLogEntry', 7 => 'ElementEditedLogEntry', 8 => 'ConfigChangedLogEntry', 9 => 'LegacyInstockChangedLogEntry', 10 => 'DatabaseUpdatedLogEntry', 11 => 'CollectionElementDeleted', 12 => 'SecurityEventLogEntry', 13 => 'PartStockChangedLogEntry'])] +#[ORM\Index(columns: ['type'], name: 'log_idx_type')] +#[ORM\Index(columns: ['type', 'target_type', 'target_id'], name: 'log_idx_type_target')] +#[ORM\Index(columns: ['datetime'], name: 'log_idx_datetime')] abstract class AbstractLogEntry extends AbstractDBElement { - public const LEVEL_EMERGENCY = 0; - public const LEVEL_ALERT = 1; - public const LEVEL_CRITICAL = 2; - public const LEVEL_ERROR = 3; - public const LEVEL_WARNING = 4; - public const LEVEL_NOTICE = 5; - public const LEVEL_INFO = 6; - public const LEVEL_DEBUG = 7; - - protected const TARGET_TYPE_NONE = 0; - protected const TARGET_TYPE_USER = 1; - protected const TARGET_TYPE_ATTACHEMENT = 2; - protected const TARGET_TYPE_ATTACHEMENTTYPE = 3; - protected const TARGET_TYPE_CATEGORY = 4; - protected const TARGET_TYPE_DEVICE = 5; - protected const TARGET_TYPE_DEVICEPART = 6; - protected const TARGET_TYPE_FOOTPRINT = 7; - protected const TARGET_TYPE_GROUP = 8; - protected const TARGET_TYPE_MANUFACTURER = 9; - protected const TARGET_TYPE_PART = 10; - protected const TARGET_TYPE_STORELOCATION = 11; - protected const TARGET_TYPE_SUPPLIER = 12; - protected const TARGET_TYPE_PARTLOT = 13; - protected const TARGET_TYPE_CURRENCY = 14; - protected const TARGET_TYPE_ORDERDETAIL = 15; - protected const TARGET_TYPE_PRICEDETAIL = 16; - protected const TARGET_TYPE_MEASUREMENTUNIT = 17; - protected const TARGET_TYPE_PARAMETER = 18; - protected const TARGET_TYPE_LABEL_PROFILE = 19; - - /** - * @var array This const is used to convert the numeric level to a PSR-3 compatible log level - */ - protected const LEVEL_ID_TO_STRING = [ - self::LEVEL_EMERGENCY => LogLevel::EMERGENCY, - self::LEVEL_ALERT => LogLevel::ALERT, - self::LEVEL_CRITICAL => LogLevel::CRITICAL, - self::LEVEL_ERROR => LogLevel::ERROR, - self::LEVEL_WARNING => LogLevel::WARNING, - self::LEVEL_NOTICE => LogLevel::NOTICE, - self::LEVEL_INFO => LogLevel::INFO, - self::LEVEL_DEBUG => LogLevel::DEBUG, - ]; - - protected const TARGET_CLASS_MAPPING = [ - self::TARGET_TYPE_USER => User::class, - self::TARGET_TYPE_ATTACHEMENT => Attachment::class, - self::TARGET_TYPE_ATTACHEMENTTYPE => AttachmentType::class, - self::TARGET_TYPE_CATEGORY => Category::class, - self::TARGET_TYPE_DEVICE => Project::class, - self::TARGET_TYPE_DEVICEPART => ProjectBOMEntry::class, - self::TARGET_TYPE_FOOTPRINT => Footprint::class, - self::TARGET_TYPE_GROUP => Group::class, - self::TARGET_TYPE_MANUFACTURER => Manufacturer::class, - self::TARGET_TYPE_PART => Part::class, - self::TARGET_TYPE_STORELOCATION => Storelocation::class, - self::TARGET_TYPE_SUPPLIER => Supplier::class, - self::TARGET_TYPE_PARTLOT => PartLot::class, - self::TARGET_TYPE_CURRENCY => Currency::class, - self::TARGET_TYPE_ORDERDETAIL => Orderdetail::class, - self::TARGET_TYPE_PRICEDETAIL => Pricedetail::class, - self::TARGET_TYPE_MEASUREMENTUNIT => MeasurementUnit::class, - self::TARGET_TYPE_PARAMETER => AbstractParameter::class, - self::TARGET_TYPE_LABEL_PROFILE => LabelProfile::class, - ]; - - /** @var User The user which has caused this log entry - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User", fetch="EAGER") - * @ORM\JoinColumn(name="id_user", nullable=true, onDelete="SET NULL") + /** @var User|null The user which has caused this log entry */ + #[ORM\ManyToOne(targetEntity: User::class, fetch: 'EAGER')] + #[ORM\JoinColumn(name: 'id_user', onDelete: 'SET NULL')] protected ?User $user = null; /** * @var string The username of the user which has caused this log entry (shown if the user is deleted) - * @ORM\Column(type="string", nullable=false) */ + #[ORM\Column(type: Types::STRING)] protected string $username = ''; - /** @var DateTime The datetime the event associated with this log entry has occured - * @ORM\Column(type="datetime", name="datetime") + /** + * @var \DateTimeImmutable The datetime the event associated with this log entry has occured */ - protected ?DateTime $timestamp = null; + #[ORM\Column(name: 'datetime', type: Types::DATETIME_IMMUTABLE)] + protected \DateTimeImmutable $timestamp; - /** @var int The priority level of the associated level. 0 is highest, 7 lowest - * @ORM\Column(type="tinyint", name="level", nullable=false) + /** + * @var LogLevel The priority level of the associated level. 0 is highest, 7 lowest */ - protected int $level; + #[ORM\Column(name: 'level', type: 'tinyint', enumType: LogLevel::class)] + protected LogLevel $level = LogLevel::WARNING; /** @var int The ID of the element targeted by this event - * @ORM\Column(name="target_id", type="integer", nullable=false) */ + #[ORM\Column(name: 'target_id', type: Types::INTEGER)] protected int $target_id = 0; - /** @var int The Type of the targeted element - * @ORM\Column(name="target_type", type="smallint", nullable=false) + /** @var LogTargetType The Type of the targeted element */ - protected int $target_type = 0; + #[ORM\Column(name: 'target_type', type: Types::SMALLINT, enumType: LogTargetType::class)] + protected LogTargetType $target_type = LogTargetType::NONE; /** @var string The type of this log entry, aka the description what has happened. * The mapping between the log entry class and the discriminator column is done by doctrine. @@ -181,14 +84,13 @@ abstract class AbstractLogEntry extends AbstractDBElement protected string $typeString = 'unknown'; /** @var array The extra data in raw (short form) saved in the DB - * @ORM\Column(name="extra", type="json") */ - protected $extra = []; + #[ORM\Column(name: 'extra', type: Types::JSON)] + protected array $extra = []; public function __construct() { - $this->timestamp = new DateTime(); - $this->level = self::LEVEL_WARNING; + $this->timestamp = new \DateTimeImmutable(); } /** @@ -222,14 +124,13 @@ abstract class AbstractLogEntry extends AbstractDBElement */ public function isCLIEntry(): bool { - return strpos($this->username, '!!!CLI ') === 0; + return str_starts_with($this->username, '!!!CLI '); } /** * Marks this log entry as a CLI entry, and set the username of the CLI user. * This removes the association to a user object in database, as CLI users are not really related to logged in * Part-DB users. - * @param string $cli_username * @return $this */ public function setCLIUsername(string $cli_username): self @@ -262,9 +163,9 @@ abstract class AbstractLogEntry extends AbstractDBElement } /** - * Returns the timestamp when the event that caused this log entry happened. + * Returns the timestamp when the event that caused this log entry happened. */ - public function getTimestamp(): DateTime + public function getTimestamp(): \DateTimeImmutable { return $this->timestamp; } @@ -274,7 +175,7 @@ abstract class AbstractLogEntry extends AbstractDBElement * * @return $this */ - public function setTimestamp(DateTime $timestamp): self + public function setTimestamp(\DateTimeImmutable $timestamp): self { $this->timestamp = $timestamp; @@ -282,16 +183,10 @@ abstract class AbstractLogEntry extends AbstractDBElement } /** - * Get the priority level of this log entry. 0 is highest and 7 lowest level. - * See LEVEL_* consts in this class for more info. + * Get the priority level of this log entry. */ - public function getLevel(): int + public function getLevel(): LogLevel { - //It is always alerting when a wrong int is saved in DB... - if ($this->level < 0 || $this->level > 7) { - return self::LEVEL_ALERT; - } - return $this->level; } @@ -300,13 +195,9 @@ abstract class AbstractLogEntry extends AbstractDBElement * * @return $this */ - public function setLevel(int $level): self + public function setLevel(LogLevel $level): self { - if ($level < 0 || $this->level > 7) { - throw new InvalidArgumentException(sprintf('$level must be between 0 and 7! %d given!', $level)); - } $this->level = $level; - return $this; } @@ -315,7 +206,7 @@ abstract class AbstractLogEntry extends AbstractDBElement */ public function getLevelString(): string { - return self::levelIntToString($this->getLevel()); + return $this->level->toPSR3LevelString(); } /** @@ -325,8 +216,7 @@ abstract class AbstractLogEntry extends AbstractDBElement */ public function setLevelString(string $level): self { - $this->setLevel(self::levelStringToInt($level)); - + LogLevel::fromPSR3LevelString($level); return $this; } @@ -340,22 +230,27 @@ abstract class AbstractLogEntry extends AbstractDBElement /** * Returns the class name of the target element associated with this log entry. - * Returns null, if this log entry is not associated with an log entry. + * Returns null, if this log entry is not associated with a log entry. * * @return string|null the class name of the target class */ public function getTargetClass(): ?string { - if (self::TARGET_TYPE_NONE === $this->target_type) { - return null; - } + return $this->target_type->toClass(); + } - return self::targetTypeIdToClass($this->target_type); + /** + * Returns the type of the target element associated with this log entry. + * @return LogTargetType + */ + public function getTargetType(): LogTargetType + { + return $this->target_type; } /** * Returns the ID of the target element associated with this log entry. - * Returns null, if this log entry is not associated with an log entry. + * Returns null, if this log entry is not associated with a log entry. * * @return int|null the ID of the associated element */ @@ -387,14 +282,14 @@ abstract class AbstractLogEntry extends AbstractDBElement */ public function setTargetElement(?AbstractDBElement $element): self { - if (null === $element) { + if ($element === null) { $this->target_id = 0; - $this->target_type = self::TARGET_TYPE_NONE; + $this->target_type = LogTargetType::NONE; return $this; } - $this->target_type = static::targetTypeClassToID(get_class($element)); + $this->target_type = LogTargetType::fromElementClass($element); $this->target_id = $element->getID(); return $this; @@ -417,75 +312,4 @@ abstract class AbstractLogEntry extends AbstractDBElement return $this->extra; } - /** - * This function converts the internal numeric log level into an PSR3 compatible level string. - * - * @param int $level The numerical log level - * - * @return string The PSR3 compatible level string - */ - final public static function levelIntToString(int $level): string - { - if (!isset(self::LEVEL_ID_TO_STRING[$level])) { - throw new InvalidArgumentException('No level with this int is existing!'); - } - - return self::LEVEL_ID_TO_STRING[$level]; - } - - /** - * This function converts a PSR3 compatible string to the internal numeric level string. - * - * @param string $level the PSR3 compatible string that should be converted - * - * @return int the internal int representation - */ - final public static function levelStringToInt(string $level): int - { - $tmp = array_flip(self::LEVEL_ID_TO_STRING); - if (!isset($tmp[$level])) { - throw new InvalidArgumentException('No level with this string is existing!'); - } - - return $tmp[$level]; - } - - /** - * Converts an target type id to an full qualified class name. - * - * @param int $type_id The target type ID - */ - final public static function targetTypeIdToClass(int $type_id): string - { - if (!isset(self::TARGET_CLASS_MAPPING[$type_id])) { - throw new InvalidArgumentException('No target type with this ID is existing!'); - } - - return self::TARGET_CLASS_MAPPING[$type_id]; - } - - /** - * Convert a class name to a target type ID. - * - * @param string $class The name of the class (FQN) that should be converted to id - * - * @return int the ID of the associated target type ID - */ - final public static function targetTypeClassToID(string $class): int - { - $tmp = array_flip(self::TARGET_CLASS_MAPPING); - //Check if we can use a key directly - if (isset($tmp[$class])) { - return $tmp[$class]; - } - - //Otherwise we have to iterate over everything and check for inheritance - foreach ($tmp as $compare_class => $class_id) { - if (is_a($class, $compare_class, true)) { - return $class_id; - } - } - - throw new InvalidArgumentException('No target ID for this class is existing! (Class: '.$class.')'); - } } diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index 5b12119a..16bf33f5 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -52,7 +52,7 @@ use App\Entity\Attachments\GroupAttachment; use App\Entity\Attachments\ManufacturerAttachment; use App\Entity\Attachments\MeasurementUnitAttachment; use App\Entity\Attachments\PartAttachment; -use App\Entity\Attachments\StorelocationAttachment; +use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; use App\Entity\Base\AbstractDBElement; @@ -69,40 +69,36 @@ use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; use App\Entity\Parameters\MeasurementUnitParameter; use App\Entity\Parameters\PartParameter; -use App\Entity\Parameters\StorelocationParameter; +use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; -use App\Repository\Parts\ManufacturerRepository; use Doctrine\ORM\Mapping as ORM; -use InvalidArgumentException; -/** - * @ORM\Entity() - * This log entry is created when an element is deleted, that is used in a collection of an other entity. - * This is needed to signal time travel, that it has to undelete the deleted entity. - */ +#[ORM\Entity] class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventUndoInterface { + use LogWithEventUndoTrait; + protected string $typeString = 'collection_element_deleted'; - protected int $level = self::LEVEL_INFO; public function __construct(AbstractDBElement $changed_element, string $collection_name, AbstractDBElement $deletedElement) { parent::__construct(); - $this->level = self::LEVEL_INFO; + $this->level = LogLevel::INFO; + $this->setTargetElement($changed_element); $this->extra['n'] = $collection_name; - $this->extra['c'] = self::targetTypeClassToID(get_class($deletedElement)); + $this->extra['c'] = LogTargetType::fromElementClass($deletedElement)->value; $this->extra['i'] = $deletedElement->getID(); if ($deletedElement instanceof NamedElementInterface) { $this->extra['o'] = $deletedElement->getName(); @@ -132,7 +128,7 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU public function getDeletedElementClass(): string { //The class name of our target element - $tmp = self::targetTypeIdToClass($this->extra['c']); + $tmp = LogTargetType::from($this->extra['c'])->toClass(); $reflection_class = new \ReflectionClass($tmp); //If the class is abstract, we have to map it to an instantiable class @@ -146,71 +142,42 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU /** * This functions maps an abstract class name derived from the extra c element to an instantiable class name (based on the target element of this log entry). * For example if the target element is a part and the extra c element is "App\Entity\Attachments\Attachment", this function will return "App\Entity\Attachments\PartAttachment". - * @param string $abstract_class - * @return string */ private function resolveAbstractClassToInstantiableClass(string $abstract_class): string { if (is_a($abstract_class, AbstractParameter::class, true)) { - switch ($this->getTargetClass()) { - case AttachmentType::class: - return AttachmentTypeParameter::class; - case Category::class: - return CategoryParameter::class; - case Currency::class: - return CurrencyParameter::class; - case Project::class: - return ProjectParameter::class; - case Footprint::class: - return FootprintParameter::class; - case Group::class: - return GroupParameter::class; - case Manufacturer::class: - return ManufacturerParameter::class; - case MeasurementUnit::class: - return MeasurementUnitParameter::class; - case Part::class: - return PartParameter::class; - case Storelocation::class: - return StorelocationParameter::class; - case Supplier::class: - return SupplierParameter::class; - - default: - throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()); - } + return match ($this->getTargetClass()) { + AttachmentType::class => AttachmentTypeParameter::class, + Category::class => CategoryParameter::class, + Currency::class => CurrencyParameter::class, + Project::class => ProjectParameter::class, + Footprint::class => FootprintParameter::class, + Group::class => GroupParameter::class, + Manufacturer::class => ManufacturerParameter::class, + MeasurementUnit::class => MeasurementUnitParameter::class, + Part::class => PartParameter::class, + StorageLocation::class => StorageLocationParameter::class, + Supplier::class => SupplierParameter::class, + default => throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()), + }; } if (is_a($abstract_class, Attachment::class, true)) { - switch ($this->getTargetClass()) { - case AttachmentType::class: - return AttachmentTypeAttachment::class; - case Category::class: - return CategoryAttachment::class; - case Currency::class: - return CurrencyAttachment::class; - case Project::class: - return ProjectAttachment::class; - case Footprint::class: - return FootprintAttachment::class; - case Group::class: - return GroupAttachment::class; - case Manufacturer::class: - return ManufacturerAttachment::class; - case MeasurementUnit::class: - return MeasurementUnitAttachment::class; - case Part::class: - return PartAttachment::class; - case Storelocation::class: - return StorelocationAttachment::class; - case Supplier::class: - return SupplierAttachment::class; - case User::class: - return UserAttachment::class; - - default: - throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()); - } + return match ($this->getTargetClass()) { + AttachmentType::class => AttachmentTypeAttachment::class, + Category::class => CategoryAttachment::class, + Currency::class => CurrencyAttachment::class, + Project::class => ProjectAttachment::class, + Footprint::class => FootprintAttachment::class, + Group::class => GroupAttachment::class, + Manufacturer::class => ManufacturerAttachment::class, + MeasurementUnit::class => MeasurementUnitAttachment::class, + Part::class => PartAttachment::class, + StorageLocation::class => StorageLocationAttachment::class, + Supplier::class => SupplierAttachment::class, + User::class => UserAttachment::class, + default => throw new \RuntimeException('Unknown target class for parameter: '.$this->getTargetClass()), + }; } throw new \RuntimeException('The class '.$abstract_class.' is abstract and no explicit resolving to an concrete type is defined!'); @@ -223,39 +190,4 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU { return $this->extra['i']; } - - public function isUndoEvent(): bool - { - return isset($this->extra['u']); - } - - public function getUndoEventID(): ?int - { - return $this->extra['u'] ?? null; - } - - public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface - { - $this->extra['u'] = $event->getID(); - - if ('undo' === $mode) { - $this->extra['um'] = 1; - } elseif ('revert' === $mode) { - $this->extra['um'] = 2; - } else { - throw new InvalidArgumentException('Passed invalid $mode!'); - } - - return $this; - } - - public function getUndoMode(): string - { - $mode_int = $this->extra['um'] ?? 1; - if (1 === $mode_int) { - return 'undo'; - } - - return 'revert'; - } } diff --git a/src/Entity/LogSystem/ConfigChangedLogEntry.php b/src/Entity/LogSystem/ConfigChangedLogEntry.php index 543886bf..68f8edf3 100644 --- a/src/Entity/LogSystem/ConfigChangedLogEntry.php +++ b/src/Entity/LogSystem/ConfigChangedLogEntry.php @@ -25,9 +25,7 @@ namespace App\Entity\LogSystem; use App\Exceptions\LogEntryObsoleteException; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class ConfigChangedLogEntry extends AbstractLogEntry { protected string $typeString = 'config_changed'; diff --git a/src/Entity/LogSystem/DatabaseUpdatedLogEntry.php b/src/Entity/LogSystem/DatabaseUpdatedLogEntry.php index 9c600365..6e137373 100644 --- a/src/Entity/LogSystem/DatabaseUpdatedLogEntry.php +++ b/src/Entity/LogSystem/DatabaseUpdatedLogEntry.php @@ -24,9 +24,7 @@ namespace App\Entity\LogSystem; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class DatabaseUpdatedLogEntry extends AbstractLogEntry { protected string $typeString = 'database_updated'; @@ -43,7 +41,7 @@ class DatabaseUpdatedLogEntry extends AbstractLogEntry */ public function isSuccessful(): bool { - //We dont save unsuccessful updates now, so just assume it to save space. + //We don't save unsuccessful updates now, so just assume it to save space. return $this->extra['s'] ?? true; } diff --git a/src/Entity/LogSystem/ElementCreatedLogEntry.php b/src/Entity/LogSystem/ElementCreatedLogEntry.php index c78e6df0..8364974c 100644 --- a/src/Entity/LogSystem/ElementCreatedLogEntry.php +++ b/src/Entity/LogSystem/ElementCreatedLogEntry.php @@ -28,24 +28,23 @@ use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use Doctrine\ORM\Mapping as ORM; -use InvalidArgumentException; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class ElementCreatedLogEntry extends AbstractLogEntry implements LogWithCommentInterface, LogWithEventUndoInterface { + use LogWithEventUndoTrait; + protected string $typeString = 'element_created'; public function __construct(AbstractDBElement $new_element) { parent::__construct(); - $this->level = self::LEVEL_INFO; + $this->level = LogLevel::INFO; $this->setTargetElement($new_element); //Creation of new users is maybe more interesting... if ($new_element instanceof User || $new_element instanceof Group) { - $this->level = self::LEVEL_NOTICE; + $this->level = LogLevel::NOTICE; } } @@ -81,39 +80,4 @@ class ElementCreatedLogEntry extends AbstractLogEntry implements LogWithCommentI return $this; } - - public function isUndoEvent(): bool - { - return isset($this->extra['u']); - } - - public function getUndoEventID(): ?int - { - return $this->extra['u'] ?? null; - } - - public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface - { - $this->extra['u'] = $event->getID(); - - if ('undo' === $mode) { - $this->extra['um'] = 1; - } elseif ('revert' === $mode) { - $this->extra['um'] = 2; - } else { - throw new InvalidArgumentException('Passed invalid $mode!'); - } - - return $this; - } - - public function getUndoMode(): string - { - $mode_int = $this->extra['um'] ?? 1; - if (1 === $mode_int) { - return 'undo'; - } - - return 'revert'; - } } diff --git a/src/Entity/LogSystem/ElementDeletedLogEntry.php b/src/Entity/LogSystem/ElementDeletedLogEntry.php index a25f9bf7..e3dd2ac7 100644 --- a/src/Entity/LogSystem/ElementDeletedLogEntry.php +++ b/src/Entity/LogSystem/ElementDeletedLogEntry.php @@ -30,24 +30,23 @@ use App\Entity\Contracts\TimeTravelInterface; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use Doctrine\ORM\Mapping as ORM; -use InvalidArgumentException; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface, LogWithEventUndoInterface { protected string $typeString = 'element_deleted'; + use LogWithEventUndoTrait; + public function __construct(AbstractDBElement $deleted_element) { parent::__construct(); - $this->level = self::LEVEL_INFO; + $this->level = LogLevel::INFO; $this->setTargetElement($deleted_element); //Deletion of a user is maybe more interesting... if ($deleted_element instanceof User || $deleted_element instanceof Group) { - $this->level = self::LEVEL_NOTICE; + $this->level = LogLevel::NOTICE; } } @@ -88,7 +87,7 @@ class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInter return $this; } - public function hasOldDataInformations(): bool + public function hasOldDataInformation(): bool { return !empty($this->extra['o']); } @@ -114,39 +113,4 @@ class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInter return $this; } - - public function isUndoEvent(): bool - { - return isset($this->extra['u']); - } - - public function getUndoEventID(): ?int - { - return $this->extra['u'] ?? null; - } - - public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface - { - $this->extra['u'] = $event->getID(); - - if ('undo' === $mode) { - $this->extra['um'] = 1; - } elseif ('revert' === $mode) { - $this->extra['um'] = 2; - } else { - throw new InvalidArgumentException('Passed invalid $mode!'); - } - - return $this; - } - - public function getUndoMode(): string - { - $mode_int = $this->extra['um'] ?? 1; - if (1 === $mode_int) { - return 'undo'; - } - - return 'revert'; - } } diff --git a/src/Entity/LogSystem/ElementEditedLogEntry.php b/src/Entity/LogSystem/ElementEditedLogEntry.php index ec5b9f80..8d4b7b9d 100644 --- a/src/Entity/LogSystem/ElementEditedLogEntry.php +++ b/src/Entity/LogSystem/ElementEditedLogEntry.php @@ -25,21 +25,21 @@ namespace App\Entity\LogSystem; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithCommentInterface; use App\Entity\Contracts\LogWithEventUndoInterface; +use App\Entity\Contracts\LogWithNewDataInterface; use App\Entity\Contracts\TimeTravelInterface; use Doctrine\ORM\Mapping as ORM; -use InvalidArgumentException; -/** - * @ORM\Entity() - */ -class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface, LogWithEventUndoInterface +#[ORM\Entity] +class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface, LogWithEventUndoInterface, LogWithNewDataInterface { + use LogWithEventUndoTrait; + protected string $typeString = 'element_edited'; public function __construct(AbstractDBElement $changed_element) { parent::__construct(); - $this->level = self::LEVEL_INFO; + $this->level = LogLevel::INFO; $this->setTargetElement($changed_element); } @@ -49,7 +49,7 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf */ public function hasChangedFieldsInfo(): bool { - return isset($this->extra['f']) || $this->hasOldDataInformations(); + return isset($this->extra['f']) || $this->hasOldDataInformation(); } /** @@ -59,7 +59,7 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf */ public function getChangedFields(): array { - if ($this->hasOldDataInformations()) { + if ($this->hasOldDataInformation()) { return array_keys($this->getOldData()); } @@ -92,7 +92,29 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf return $this; } - public function hasOldDataInformations(): bool + public function hasNewDataInformation(): bool + { + return !empty($this->extra['n']); + } + + public function getNewData(): array + { + return $this->extra['n'] ?? []; + } + + /** + * Sets the old data for this entry. + * + * @return $this + */ + public function setNewData(array $new_data): self + { + $this->extra['n'] = $new_data; + + return $this; + } + + public function hasOldDataInformation(): bool { return !empty($this->extra['d']); } @@ -118,39 +140,4 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf return $this; } - - public function isUndoEvent(): bool - { - return isset($this->extra['u']); - } - - public function getUndoEventID(): ?int - { - return $this->extra['u'] ?? null; - } - - public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface - { - $this->extra['u'] = $event->getID(); - - if ('undo' === $mode) { - $this->extra['um'] = 1; - } elseif ('revert' === $mode) { - $this->extra['um'] = 2; - } else { - throw new InvalidArgumentException('Passed invalid $mode!'); - } - - return $this; - } - - public function getUndoMode(): string - { - $mode_int = $this->extra['um'] ?? 1; - if (1 === $mode_int) { - return 'undo'; - } - - return 'revert'; - } } diff --git a/src/Entity/LogSystem/ExceptionLogEntry.php b/src/Entity/LogSystem/ExceptionLogEntry.php index dc9a7f32..e8fb06f9 100644 --- a/src/Entity/LogSystem/ExceptionLogEntry.php +++ b/src/Entity/LogSystem/ExceptionLogEntry.php @@ -25,9 +25,7 @@ namespace App\Entity\LogSystem; use App\Exceptions\LogEntryObsoleteException; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class ExceptionLogEntry extends AbstractLogEntry { protected string $typeString = 'exception'; diff --git a/src/Entity/LogSystem/LegacyInstockChangedLogEntry.php b/src/Entity/LogSystem/LegacyInstockChangedLogEntry.php index 35d58592..27f7afe4 100644 --- a/src/Entity/LogSystem/LegacyInstockChangedLogEntry.php +++ b/src/Entity/LogSystem/LegacyInstockChangedLogEntry.php @@ -24,9 +24,7 @@ namespace App\Entity\LogSystem; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class LegacyInstockChangedLogEntry extends AbstractLogEntry { protected string $typeString = 'instock_changed'; @@ -56,7 +54,7 @@ class LegacyInstockChangedLogEntry extends AbstractLogEntry } /** - * Returns the price that has to be payed for the change (in the base currency). + * Returns the price that has to be paid for the change (in the base currency). * * @param bool $absolute Set this to true, if you want only get the absolute value of the price (without minus) */ @@ -92,9 +90,9 @@ class LegacyInstockChangedLogEntry extends AbstractLogEntry } /** - * Checks if the Change was an withdrawal of parts. + * Checks if the Change was a withdrawal of parts. * - * @return bool true if the change was an withdrawal, false if not + * @return bool true if the change was a withdrawal, false if not */ public function isWithdrawal(): bool { diff --git a/src/Entity/LogSystem/LogLevel.php b/src/Entity/LogSystem/LogLevel.php new file mode 100644 index 00000000..435c5468 --- /dev/null +++ b/src/Entity/LogSystem/LogLevel.php @@ -0,0 +1,119 @@ +. + */ +namespace App\Entity\LogSystem; + +use Psr\Log\LogLevel as PSRLogLevel; + +enum LogLevel: int +{ + case EMERGENCY = 0; + case ALERT = 1; + case CRITICAL = 2; + case ERROR = 3; + case WARNING = 4; + case NOTICE = 5; + case INFO = 6; + case DEBUG = 7; + + /** + * Converts the current log level to a PSR-3 log level string. + * @return string + */ + public function toPSR3LevelString(): string + { + return match ($this) { + self::EMERGENCY => PSRLogLevel::EMERGENCY, + self::ALERT => PSRLogLevel::ALERT, + self::CRITICAL => PSRLogLevel::CRITICAL, + self::ERROR => PSRLogLevel::ERROR, + self::WARNING => PSRLogLevel::WARNING, + self::NOTICE => PSRLogLevel::NOTICE, + self::INFO => PSRLogLevel::INFO, + self::DEBUG => PSRLogLevel::DEBUG, + }; + } + + /** + * Creates a log level (enum) from a PSR-3 log level string. + * @param string $level + * @return self + */ + public static function fromPSR3LevelString(string $level): self + { + return match ($level) { + PSRLogLevel::EMERGENCY => self::EMERGENCY, + PSRLogLevel::ALERT => self::ALERT, + PSRLogLevel::CRITICAL => self::CRITICAL, + PSRLogLevel::ERROR => self::ERROR, + PSRLogLevel::WARNING => self::WARNING, + PSRLogLevel::NOTICE => self::NOTICE, + PSRLogLevel::INFO => self::INFO, + PSRLogLevel::DEBUG => self::DEBUG, + default => throw new \InvalidArgumentException("Invalid log level: $level"), + }; + } + + /** + * Checks if the current log level is more important than the given one. + * @param LogLevel $other + * @return bool + */ + public function moreImportThan(self $other): bool + { + //Smaller values are more important + return $this->value < $other->value; + } + + /** + * Checks if the current log level is more important or equal than the given one. + * @param LogLevel $other + * @return bool + */ + public function moreImportOrEqualThan(self $other): bool + { + //Smaller values are more important + return $this->value <= $other->value; + } + + /** + * Checks if the current log level is less important than the given one. + * @param LogLevel $other + * @return bool + */ + public function lessImportThan(self $other): bool + { + //Bigger values are less important + return $this->value > $other->value; + } + + /** + * Checks if the current log level is less important or equal than the given one. + * @param LogLevel $other + * @return bool + */ + public function lessImportOrEqualThan(self $other): bool + { + //Bigger values are less important + return $this->value >= $other->value; + } +} diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php new file mode 100644 index 00000000..1c6e4f8c --- /dev/null +++ b/src/Entity/LogSystem/LogTargetType.php @@ -0,0 +1,129 @@ +. + */ +namespace App\Entity\LogSystem; + +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentType; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Entity\UserSystem\Group; +use App\Entity\UserSystem\User; + +enum LogTargetType: int +{ + case NONE = 0; + case USER = 1; + case ATTACHMENT = 2; + case ATTACHMENT_TYPE = 3; + case CATEGORY = 4; + case PROJECT = 5; + case BOM_ENTRY = 6; + case FOOTPRINT = 7; + case GROUP = 8; + case MANUFACTURER = 9; + case PART = 10; + case STORELOCATION = 11; + case SUPPLIER = 12; + case PART_LOT = 13; + case CURRENCY = 14; + case ORDERDETAIL = 15; + case PRICEDETAIL = 16; + case MEASUREMENT_UNIT = 17; + case PARAMETER = 18; + case LABEL_PROFILE = 19; + + case PART_ASSOCIATION = 20; + + /** + * Returns the class name of the target type or null if the target type is NONE. + * @return string|null + */ + public function toClass(): ?string + { + return match ($this) { + self::NONE => null, + self::USER => User::class, + self::ATTACHMENT => Attachment::class, + self::ATTACHMENT_TYPE => AttachmentType::class, + self::CATEGORY => Category::class, + self::PROJECT => Project::class, + self::BOM_ENTRY => ProjectBOMEntry::class, + self::FOOTPRINT => Footprint::class, + self::GROUP => Group::class, + self::MANUFACTURER => Manufacturer::class, + self::PART => Part::class, + self::STORELOCATION => StorageLocation::class, + self::SUPPLIER => Supplier::class, + self::PART_LOT => PartLot::class, + self::CURRENCY => Currency::class, + self::ORDERDETAIL => Orderdetail::class, + self::PRICEDETAIL => Pricedetail::class, + self::MEASUREMENT_UNIT => MeasurementUnit::class, + self::PARAMETER => AbstractParameter::class, + self::LABEL_PROFILE => LabelProfile::class, + self::PART_ASSOCIATION => PartAssociation::class, + }; + } + + /** + * Determines the target type from the given class name or object. + * @param object|string $element + * @phpstan-param object|class-string $element + * @return self + */ + public static function fromElementClass(object|string $element): self + { + //Iterate over all possible types + foreach (self::cases() as $case) { + $class = $case->toClass(); + + //Skip NONE + if ($class === null) { + continue; + } + + //Check if the given element is a instance of the class + if (is_a($element, $class, true)) { + return $case; + } + } + + $elementClass = is_object($element) ? $element::class : $element; + //If no matching type was found, throw an exception + throw new \InvalidArgumentException("The given class $elementClass is not a valid log target type."); + } +} diff --git a/src/Entity/LogSystem/LogWithEventUndoTrait.php b/src/Entity/LogSystem/LogWithEventUndoTrait.php new file mode 100644 index 00000000..ed8629dc --- /dev/null +++ b/src/Entity/LogSystem/LogWithEventUndoTrait.php @@ -0,0 +1,53 @@ +. + */ +namespace App\Entity\LogSystem; + +use App\Entity\Contracts\LogWithEventUndoInterface; +use App\Services\LogSystem\EventUndoMode; + +trait LogWithEventUndoTrait +{ + public function isUndoEvent(): bool + { + return isset($this->extra['u']); + } + + public function getUndoEventID(): ?int + { + return $this->extra['u'] ?? null; + } + + public function setUndoneEvent(AbstractLogEntry $event, EventUndoMode $mode = EventUndoMode::UNDO): LogWithEventUndoInterface + { + $this->extra['u'] = $event->getID(); + $this->extra['um'] = $mode->toExtraInt(); + + return $this; + } + + public function getUndoMode(): EventUndoMode + { + $mode_int = $this->extra['um'] ?? 1; + return EventUndoMode::fromExtraInt($mode_int); + } +} diff --git a/src/Entity/LogSystem/PartStockChangeType.php b/src/Entity/LogSystem/PartStockChangeType.php new file mode 100644 index 00000000..f69fe95f --- /dev/null +++ b/src/Entity/LogSystem/PartStockChangeType.php @@ -0,0 +1,58 @@ +. + */ +namespace App\Entity\LogSystem; + +enum PartStockChangeType: string +{ + case ADD = "add"; + case WITHDRAW = "withdraw"; + case MOVE = "move"; + + /** + * Converts the type to a short representation usable in the extra field of the log entry. + * @return string + */ + public function toExtraShortType(): string + { + return match ($this) { + self::ADD => 'a', + self::WITHDRAW => 'w', + self::MOVE => 'm', + }; + } + + public function toTranslationKey(): string + { + return 'log.part_stock_changed.' . $this->value; + } + + public static function fromExtraShortType(string $value): self + { + return match ($value) { + 'a' => self::ADD, + 'w' => self::WITHDRAW, + 'm' => self::MOVE, + default => throw new \InvalidArgumentException("Invalid short type: $value"), + }; + } +} diff --git a/src/Entity/LogSystem/PartStockChangedLogEntry.php b/src/Entity/LogSystem/PartStockChangedLogEntry.php index 44852076..1bac9e9f 100644 --- a/src/Entity/LogSystem/PartStockChangedLogEntry.php +++ b/src/Entity/LogSystem/PartStockChangedLogEntry.php @@ -1,4 +1,7 @@ . */ - namespace App\Entity\LogSystem; use App\Entity\Parts\PartLot; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class PartStockChangedLogEntry extends AbstractLogEntry { - public const TYPE_ADD = "add"; - public const TYPE_WITHDRAW = "withdraw"; - public const TYPE_MOVE = "move"; - protected string $typeString = 'part_stock_changed'; protected const COMMENT_MAX_LENGTH = 300; /** * Creates a new part stock changed log entry. - * @param string $type The type of the log entry. One of the TYPE_* constants. + * @param PartStockChangeType $type The type of the log entry. * @param PartLot $lot The part lot which has been changed. * @param float $old_stock The old stock of the lot. * @param float $new_stock The new stock of the lot. * @param float $new_total_part_instock The new total instock of the part. * @param string $comment The comment associated with the change. * @param PartLot|null $move_to_target The target lot if the type is TYPE_MOVE. + * @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards. */ - protected function __construct(string $type, PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?PartLot $move_to_target = null) + protected function __construct(PartStockChangeType $type, PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?PartLot $move_to_target = null, + ?\DateTimeInterface $action_timestamp = null) { parent::__construct(); - if (!in_array($type, [self::TYPE_ADD, self::TYPE_WITHDRAW, self::TYPE_MOVE], true)) { - throw new \InvalidArgumentException('Invalid type for PartStockChangedLogEntry!'); - } - //Same as every other element change log entry - $this->level = self::LEVEL_INFO; + $this->level = LogLevel::INFO; $this->setTargetElement($lot); - - $this->typeString = 'part_stock_changed'; $this->extra = array_merge($this->extra, [ - 't' => $this->typeToShortType($type), + 't' => $type->toExtraShortType(), 'o' => $old_stock, 'n' => $new_stock, 'p' => $new_total_part_instock, ]); - if (!empty($comment)) { + if ($comment !== '') { $this->extra['c'] = mb_strimwidth($comment, 0, self::COMMENT_MAX_LENGTH, '...'); } - if ($move_to_target) { - if ($type !== self::TYPE_MOVE) { + if ($action_timestamp instanceof \DateTimeInterface) { + //The action timestamp is saved as an ISO 8601 string + $this->extra['a'] = $action_timestamp->format(\DateTimeInterface::ATOM); + } + + if ($move_to_target instanceof PartLot) { + if ($type !== PartStockChangeType::MOVE) { throw new \InvalidArgumentException('The move_to_target parameter can only be set if the type is "move"!'); } @@ -86,11 +83,12 @@ class PartStockChangedLogEntry extends AbstractLogEntry * @param float $new_stock The new stock of the lot. * @param float $new_total_part_instock The new total instock of the part. * @param string $comment The comment associated with the change. - * @return static + * @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards. + * @return self */ - public static function add(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment): self + public static function add(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?\DateTimeInterface $action_timestamp = null): self { - return new self(self::TYPE_ADD, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment); + return new self(PartStockChangeType::ADD, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, action_timestamp: $action_timestamp); } /** @@ -100,11 +98,12 @@ class PartStockChangedLogEntry extends AbstractLogEntry * @param float $new_stock The new stock of the lot. * @param float $new_total_part_instock The new total instock of the part. * @param string $comment The comment associated with the change. - * @return static + * @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards. + * @return self */ - public static function withdraw(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment): self + public static function withdraw(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?\DateTimeInterface $action_timestamp = null): self { - return new self(self::TYPE_WITHDRAW, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment); + return new self(PartStockChangeType::WITHDRAW, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, action_timestamp: $action_timestamp); } /** @@ -115,24 +114,25 @@ class PartStockChangedLogEntry extends AbstractLogEntry * @param float $new_total_part_instock The new total instock of the part. * @param string $comment The comment associated with the change. * @param PartLot $move_to_target The target lot. + * @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards. + * @return self */ - public static function move(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, PartLot $move_to_target): self + public static function move(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, PartLot $move_to_target, ?\DateTimeInterface $action_timestamp = null): self { - return new self(self::TYPE_MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target); + return new self(PartStockChangeType::MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target, action_timestamp: $action_timestamp); } /** * Returns the instock change type of this entry - * @return string One of the TYPE_* constants. + * @return PartStockChangeType */ - public function getInstockChangeType(): string + public function getInstockChangeType(): PartStockChangeType { - return $this->shortTypeToType($this->extra['t']); + return PartStockChangeType::fromExtraShortType($this->extra['t']); } /** * Returns the old stock of the lot. - * @return float */ public function getOldStock(): float { @@ -141,7 +141,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry /** * Returns the new stock of the lot. - * @return float */ public function getNewStock(): float { @@ -150,7 +149,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry /** * Returns the new total instock of the part. - * @return float */ public function getNewTotalPartInstock(): float { @@ -159,7 +157,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry /** * Returns the comment associated with the change. - * @return string */ public function getComment(): string { @@ -168,7 +165,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry /** * Gets the difference between the old and the new stock value of the lot as a positive number. - * @return float */ public function getChangeAmount(): float { @@ -177,7 +173,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry /** * Returns the target lot ID (where the instock was moved to) if the type is TYPE_MOVE. - * @return int|null */ public function getMoveToTargetID(): ?int { @@ -185,40 +180,16 @@ class PartStockChangedLogEntry extends AbstractLogEntry } /** - * Converts the human-readable type (TYPE_* consts) to the version stored in DB - * @param string $type - * @return string + * Returns the timestamp when this action was performed and not when the log entry was created. + * This is useful if the action happened in the past, and the log entry is created afterwards. + * If the timestamp is not set, null is returned. + * @return \DateTimeInterface|null */ - protected function typeToShortType(string $type): string + public function getActionTimestamp(): ?\DateTimeInterface { - switch ($type) { - case self::TYPE_ADD: - return 'a'; - case self::TYPE_WITHDRAW: - return 'w'; - case self::TYPE_MOVE: - return 'm'; - default: - throw new \InvalidArgumentException('Invalid type: '.$type); + if (!empty($this->extra['a'])) { + return \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $this->extra['a']); } + return null; } - - /** - * Converts the short type stored in DB to the human-readable type (TYPE_* consts). - * @param string $short_type - * @return string - */ - protected function shortTypeToType(string $short_type): string - { - switch ($short_type) { - case 'a': - return self::TYPE_ADD; - case 'w': - return self::TYPE_WITHDRAW; - case 'm': - return self::TYPE_MOVE; - default: - throw new \InvalidArgumentException('Invalid short type: '.$short_type); - } - } -} \ No newline at end of file +} diff --git a/src/Entity/LogSystem/SecurityEventLogEntry.php b/src/Entity/LogSystem/SecurityEventLogEntry.php index 095996c2..12e8e65e 100644 --- a/src/Entity/LogSystem/SecurityEventLogEntry.php +++ b/src/Entity/LogSystem/SecurityEventLogEntry.php @@ -44,18 +44,17 @@ namespace App\Entity\LogSystem; use App\Entity\Base\AbstractDBElement; use App\Entity\UserSystem\User; use App\Events\SecurityEvents; +use App\Helpers\IPAnonymizer; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; -use Symfony\Component\HttpFoundation\IpUtils; /** * This log entry is created when something security related to a user happens. - * - * @ORM\Entity() */ +#[ORM\Entity] class SecurityEventLogEntry extends AbstractLogEntry { - public const SECURITY_TYPE_MAPPING = [ + final public const SECURITY_TYPE_MAPPING = [ 0 => SecurityEvents::PASSWORD_CHANGED, 1 => SecurityEvents::PASSWORD_RESET, 2 => SecurityEvents::BACKUP_KEYS_RESET, @@ -65,15 +64,15 @@ class SecurityEventLogEntry extends AbstractLogEntry 6 => SecurityEvents::GOOGLE_DISABLED, 7 => SecurityEvents::TRUSTED_DEVICE_RESET, 8 => SecurityEvents::TFA_ADMIN_RESET, + 9 => SecurityEvents::USER_IMPERSONATED, ]; public function __construct(string $type, string $ip_address, bool $anonymize = true) { parent::__construct(); - $this->level = self::LEVEL_INFO; $this->setIPAddress($ip_address, $anonymize); $this->setEventType($type); - $this->level = self::LEVEL_NOTICE; + $this->level = LogLevel::NOTICE; } public function setTargetElement(?AbstractDBElement $element): AbstractLogEntry @@ -113,11 +112,11 @@ class SecurityEventLogEntry extends AbstractLogEntry { $key = $this->extra['e']; - return static::SECURITY_TYPE_MAPPING[$key] ?? 'unkown'; + return static::SECURITY_TYPE_MAPPING[$key] ?? 'unknown'; } /** - * Return the (anonymized) IP address used to login the user. + * Return the (anonymized) IP address used to log in the user. */ public function getIPAddress(): string { @@ -125,17 +124,17 @@ class SecurityEventLogEntry extends AbstractLogEntry } /** - * Sets the IP address used to login the user. + * Sets the IP address used to log in the user. * - * @param string $ip the IP address used to login the user - * @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant + * @param string $ip the IP address used to log in the user + * @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant * * @return $this */ public function setIPAddress(string $ip, bool $anonymize = true): self { if ($anonymize) { - $ip = IpUtils::anonymize($ip); + $ip = IPAnonymizer::anonymize($ip); } $this->extra['i'] = $ip; diff --git a/src/Entity/LogSystem/UserLoginLogEntry.php b/src/Entity/LogSystem/UserLoginLogEntry.php index 5d1733ac..0719a740 100644 --- a/src/Entity/LogSystem/UserLoginLogEntry.php +++ b/src/Entity/LogSystem/UserLoginLogEntry.php @@ -22,14 +22,14 @@ declare(strict_types=1); namespace App\Entity\LogSystem; +use App\Helpers\IPAnonymizer; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\HttpFoundation\IpUtils; + /** * This log entry is created when a user logs in. - * - * @ORM\Entity() */ +#[ORM\Entity] class UserLoginLogEntry extends AbstractLogEntry { protected string $typeString = 'user_login'; @@ -37,12 +37,12 @@ class UserLoginLogEntry extends AbstractLogEntry public function __construct(string $ip_address, bool $anonymize = true) { parent::__construct(); - $this->level = self::LEVEL_INFO; + $this->level = LogLevel::INFO; $this->setIPAddress($ip_address, $anonymize); } /** - * Return the (anonymized) IP address used to login the user. + * Return the (anonymized) IP address used to log in the user. */ public function getIPAddress(): string { @@ -50,17 +50,17 @@ class UserLoginLogEntry extends AbstractLogEntry } /** - * Sets the IP address used to login the user. + * Sets the IP address used to log in the user. * - * @param string $ip the IP address used to login the user - * @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant + * @param string $ip the IP address used to log in the user + * @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant * * @return $this */ public function setIPAddress(string $ip, bool $anonymize = true): self { if ($anonymize) { - $ip = IpUtils::anonymize($ip); + $ip = IPAnonymizer::anonymize($ip); } $this->extra['i'] = $ip; diff --git a/src/Entity/LogSystem/UserLogoutLogEntry.php b/src/Entity/LogSystem/UserLogoutLogEntry.php index d3abb931..f9f9a3dc 100644 --- a/src/Entity/LogSystem/UserLogoutLogEntry.php +++ b/src/Entity/LogSystem/UserLogoutLogEntry.php @@ -22,12 +22,10 @@ declare(strict_types=1); namespace App\Entity\LogSystem; +use App\Helpers\IPAnonymizer; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\HttpFoundation\IpUtils; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class UserLogoutLogEntry extends AbstractLogEntry { protected string $typeString = 'user_logout'; @@ -35,12 +33,12 @@ class UserLogoutLogEntry extends AbstractLogEntry public function __construct(string $ip_address, bool $anonymize = true) { parent::__construct(); - $this->level = self::LEVEL_INFO; + $this->level = LogLevel::INFO; $this->setIPAddress($ip_address, $anonymize); } /** - * Return the (anonymized) IP address used to login the user. + * Return the (anonymized) IP address used to log in the user. */ public function getIPAddress(): string { @@ -48,17 +46,17 @@ class UserLogoutLogEntry extends AbstractLogEntry } /** - * Sets the IP address used to login the user. + * Sets the IP address used to log in the user. * - * @param string $ip the IP address used to login the user - * @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant + * @param string $ip the IP address used to log in the user + * @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant * * @return $this */ public function setIPAddress(string $ip, bool $anonymize = true): self { if ($anonymize) { - $ip = IpUtils::anonymize($ip); + $ip = IPAnonymizer::anonymize($ip); } $this->extra['i'] = $ip; diff --git a/src/Entity/LogSystem/UserNotAllowedLogEntry.php b/src/Entity/LogSystem/UserNotAllowedLogEntry.php index 5d4e3acd..c570a012 100644 --- a/src/Entity/LogSystem/UserNotAllowedLogEntry.php +++ b/src/Entity/LogSystem/UserNotAllowedLogEntry.php @@ -24,9 +24,7 @@ namespace App\Entity\LogSystem; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class UserNotAllowedLogEntry extends AbstractLogEntry { protected string $typeString = 'user_not_allowed'; @@ -34,7 +32,7 @@ class UserNotAllowedLogEntry extends AbstractLogEntry public function __construct(string $path) { parent::__construct(); - $this->level = static::LEVEL_WARNING; + $this->level = LogLevel::WARNING; $this->extra['a'] = $path; } diff --git a/src/Entity/OAuthToken.php b/src/Entity/OAuthToken.php new file mode 100644 index 00000000..bc692369 --- /dev/null +++ b/src/Entity/OAuthToken.php @@ -0,0 +1,151 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity; + +use App\Entity\Base\AbstractNamedDBElement; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use League\OAuth2\Client\Token\AccessTokenInterface; + +/** + * This entity represents a OAuth token pair (access and refresh token), for an application + */ +#[ORM\Entity] +#[ORM\Table(name: 'oauth_tokens')] +#[ORM\UniqueConstraint(name: 'oauth_tokens_unique_name', columns: ['name'])] +#[ORM\Index(columns: ['name'], name: 'oauth_tokens_name_idx')] +class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface +{ + /** @var string|null The short-term usable OAuth2 token */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $token = null; + + /** @var \DateTimeImmutable|null The date when the token expires */ + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeImmutable $expires_at = null; + + /** @var string|null The refresh token for the OAuth2 auth */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $refresh_token = null; + + /** + * The default expiration time for a authorization token, if no expiration time is given + */ + private const DEFAULT_EXPIRATION_TIME = 3600; + + public function __construct(string $name, ?string $refresh_token, ?string $token = null, ?\DateTimeImmutable $expires_at = null) + { + //If token is given, you also have to give the expires_at date + if ($token !== null && $expires_at === null) { + throw new \InvalidArgumentException('If you give a token, you also have to give the expires_at date'); + } + + //If no refresh_token is given, the token is a client credentials grant token, which must have a token + if ($refresh_token === null && $token === null) { + throw new \InvalidArgumentException('If you give no refresh_token, you have to give a token!'); + } + + $this->name = $name; + $this->refresh_token = $refresh_token; + $this->expires_at = $expires_at; + $this->token = $token; + } + + public static function fromAccessToken(AccessTokenInterface $accessToken, string $name): self + { + return new self( + $name, + $accessToken->getRefreshToken(), + $accessToken->getToken(), + self::unixTimestampToDatetime($accessToken->getExpires() ?? time() + self::DEFAULT_EXPIRATION_TIME) + ); + } + + private static function unixTimestampToDatetime(int $timestamp): \DateTimeImmutable + { + return \DateTimeImmutable::createFromFormat('U', (string)$timestamp); + } + + public function getToken(): ?string + { + return $this->token; + } + + public function getExpirationDate(): ?\DateTimeImmutable + { + return $this->expires_at; + } + + public function getRefreshToken(): string + { + return $this->refresh_token; + } + + public function isExpired(): bool + { + //null token is always expired + if ($this->token === null) { + return true; + } + + if ($this->expires_at === null) { + return false; + } + + return $this->expires_at->getTimestamp() < time(); + } + + /** + * Returns true if this token is a client credentials grant token (meaning it has no refresh token), and + * needs to be refreshed via the client credentials grant. + * @return bool + */ + public function isClientCredentialsGrant(): bool + { + return $this->refresh_token === null; + } + + public function replaceWithNewToken(AccessTokenInterface $accessToken): void + { + $this->token = $accessToken->getToken(); + $this->refresh_token = $accessToken->getRefreshToken(); + //If no expiration date is given, we set it to the default expiration time + $this->expires_at = self::unixTimestampToDatetime($accessToken->getExpires() ?? time() + self::DEFAULT_EXPIRATION_TIME); + } + + public function getExpires(): ?int + { + return $this->expires_at->getTimestamp(); + } + + public function hasExpired(): bool + { + return $this->isExpired(); + } + + public function getValues(): array + { + return []; + } +} \ No newline at end of file diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index 01aca08a..edcedc3e 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -41,109 +41,143 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use App\ApiPlatform\Filter\LikeFilter; +use App\Repository\ParameterRepository; +use App\Validator\UniqueValidatableInterface; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; use LogicException; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; use Symfony\Component\Validator\Constraints as Assert; use function sprintf; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @ORM\Table("parameters", indexes={ - * @ORM\Index(name="parameter_name_idx", columns={"name"}), - * @ORM\Index(name="parameter_group_idx", columns={"param_group"}), - * @ORM\Index(name="parameter_type_element_idx", columns={"type", "element_id"}) - * }) - * @ORM\InheritanceType("SINGLE_TABLE") - * @ORM\DiscriminatorColumn(name="type", type="smallint") - * @ORM\DiscriminatorMap({ - * 0 = "CategoryParameter", - * 1 = "CurrencyParameter", - * 2 = "ProjectParameter", - * 3 = "FootprintParameter", - * 4 = "GroupParameter", - * 5 = "ManufacturerParameter", - * 6 = "MeasurementUnitParameter", - * 7 = "PartParameter", - * 8 = "StorelocationParameter", - * 9 = "SupplierParameter", - * 10 = "AttachmentTypeParameter" - * }) - */ -abstract class AbstractParameter extends AbstractNamedDBElement +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'type', type: 'smallint')] +#[ORM\DiscriminatorMap([0 => CategoryParameter::class, 1 => CurrencyParameter::class, 2 => ProjectParameter::class, + 3 => FootprintParameter::class, 4 => GroupParameter::class, 5 => ManufacturerParameter::class, + 6 => MeasurementUnitParameter::class, 7 => PartParameter::class, 8 => StorageLocationParameter::class, + 9 => SupplierParameter::class, 10 => AttachmentTypeParameter::class])] +#[ORM\Table('parameters')] +#[ORM\Index(columns: ['name'], name: 'parameter_name_idx')] +#[ORM\Index(columns: ['param_group'], name: 'parameter_group_idx')] +#[ORM\Index(columns: ['type', 'element_id'], name: 'parameter_type_element_idx')] +#[ApiResource( + shortName: 'Parameter', + operations: [ + new Get(security: 'is_granted("read", object)'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['parameter:read', 'parameter:read:standalone', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['parameter:write', 'parameter:write:standalone', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(LikeFilter::class, properties: ["name", "symbol", "unit", "group", "value_text"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(RangeFilter::class, properties: ["value_min", "value_typical", "value_max"])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +//This discriminator map is required for API platform to know which class to use for deserialization, when creating a new parameter. +#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] +abstract class AbstractParameter extends AbstractNamedDBElement implements UniqueValidatableInterface { + + /* + * The discriminator map used for API platform. The key should be the same as the api platform short type (the @type JSONLD field). + */ + private const API_DISCRIMINATOR_MAP = ["Part" => PartParameter::class, + "AttachmentType" => AttachmentTypeParameter::class, "Category" => CategoryParameter::class, "Currency" => CurrencyParameter::class, + "Project" => ProjectParameter::class, "Footprint" => FootprintParameter::class, "Group" => GroupParameter::class, + "Manufacturer" => ManufacturerParameter::class, "MeasurementUnit" => MeasurementUnitParameter::class, + "StorageLocation" => StorageLocationParameter::class, "Supplier" => SupplierParameter::class]; + /** * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses. */ - public const ALLOWED_ELEMENT_CLASS = ''; + protected const ALLOWED_ELEMENT_CLASS = ''; /** * @var string The mathematical symbol for this specification. Can be rendered pretty later. Should be short - * @Assert\Length(max=20) - * @ORM\Column(type="string", nullable=false) - * @Groups({"full"}) */ + #[Assert\Length(max: 20)] + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(type: Types::STRING)] protected string $symbol = ''; /** * @var float|null the guaranteed minimum value of this property - * @Assert\Type({"float","null"}) - * @Assert\LessThanOrEqual(propertyPath="value_typical", message="parameters.validator.min_lesser_typical") - * @Assert\LessThan(propertyPath="value_max", message="parameters.validator.min_lesser_max") - * @ORM\Column(type="float", nullable=true) - * @Groups({"full"}) */ + #[Assert\Type(['float', null])] + #[Assert\LessThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.min_lesser_typical')] + #[Assert\LessThan(propertyPath: 'value_max', message: 'parameters.validator.min_lesser_max')] + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(type: Types::FLOAT, nullable: true)] protected ?float $value_min = null; /** * @var float|null the typical value of this property - * @Assert\Type({"null", "float"}) - * @ORM\Column(type="float", nullable=true) - * @Groups({"full"}) */ + #[Assert\Type([null, 'float'])] + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(type: Types::FLOAT, nullable: true)] protected ?float $value_typical = null; /** * @var float|null the maximum value of this property - * @Assert\Type({"float", "null"}) - * @Assert\GreaterThanOrEqual(propertyPath="value_typical", message="parameters.validator.max_greater_typical") - * @ORM\Column(type="float", nullable=true) - * @Groups({"full"}) */ + #[Assert\Type(['float', null])] + #[Assert\GreaterThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.max_greater_typical')] + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(type: Types::FLOAT, nullable: true)] protected ?float $value_max = null; /** * @var string The unit in which the value values are given (e.g. V) - * @ORM\Column(type="string", nullable=false) - * @Groups({"full"}) */ + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 50)] protected string $unit = ''; /** * @var string a text value for the given property - * @ORM\Column(type="string", nullable=false) - * @Groups({"full"}) */ + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $value_text = ''; /** * @var string the group this parameter belongs to - * @ORM\Column(type="string", nullable=false, name="param_group") - * @Groups({"full"}) - * @Groups({"full"}) */ + #[Groups(['full', 'parameter:read', 'parameter:write', 'import'])] + #[ORM\Column(name: 'param_group', type: Types::STRING)] + #[Assert\Length(max: 255)] protected string $group = ''; /** - * Mapping is done in sub classes. + * Mapping is done in subclasses. * * @var AbstractDBElement|null the element to which this parameter belongs to */ - protected $element; + #[Groups(['parameter:read:standalone', 'parameter:write:standalone'])] + protected ?AbstractDBElement $element = null; public function __construct() { @@ -172,7 +206,9 @@ abstract class AbstractParameter extends AbstractNamedDBElement * Return a formatted string version of the values of the string. * Based on the set values it can return something like this: 34 V (12 V ... 50 V) [Text]. */ - public function getFormattedValue(): string + #[Groups(['parameter:read', 'full'])] + #[SerializedName('formatted')] + public function getFormattedValue(bool $latex_formatted = false): string { //If we just only have text value, return early if (null === $this->value_typical && null === $this->value_min && null === $this->value_max) { @@ -182,7 +218,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement $str = ''; $bracket_opened = false; if ($this->value_typical) { - $str .= $this->getValueTypicalWithUnit(); + $str .= $this->getValueTypicalWithUnit($latex_formatted); if ($this->value_min || $this->value_max) { $bracket_opened = true; $str .= ' ('; @@ -190,11 +226,11 @@ abstract class AbstractParameter extends AbstractNamedDBElement } if ($this->value_max && $this->value_min) { - $str .= $this->getValueMinWithUnit().' ... '.$this->getValueMaxWithUnit(); + $str .= $this->getValueMinWithUnit($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted); } elseif ($this->value_max) { - $str .= 'max. '.$this->getValueMaxWithUnit(); + $str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted); } elseif ($this->value_min) { - $str .= 'min. '.$this->getValueMinWithUnit(); + $str .= 'min. '.$this->getValueMinWithUnit($latex_formatted); } //Add closing bracket @@ -202,7 +238,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement $str .= ')'; } - if ($this->value_text) { + if ($this->value_text !== '' && $this->value_text !== '0') { $str .= ' ['.$this->value_text.']'; } @@ -308,31 +344,30 @@ abstract class AbstractParameter extends AbstractNamedDBElement /** * Return a formatted version with the minimum value with the unit of this parameter. */ - public function getValueTypicalWithUnit(): string + public function getValueTypicalWithUnit(bool $with_latex = false): string { - return $this->formatWithUnit($this->value_typical); + return $this->formatWithUnit($this->value_typical, with_latex: $with_latex); } /** * Return a formatted version with the maximum value with the unit of this parameter. */ - public function getValueMaxWithUnit(): string + public function getValueMaxWithUnit(bool $with_latex = false): string { - return $this->formatWithUnit($this->value_max); + return $this->formatWithUnit($this->value_max, with_latex: $with_latex); } /** * Return a formatted version with the typical value with the unit of this parameter. */ - public function getValueMinWithUnit(): string + public function getValueMinWithUnit(bool $with_latex = false): string { - return $this->formatWithUnit($this->value_min); + return $this->formatWithUnit($this->value_min, with_latex: $with_latex); } /** * Sets the typical value of this property. * - * @param float|null $value_typical * * @return $this */ @@ -406,13 +441,34 @@ abstract class AbstractParameter extends AbstractNamedDBElement /** * Return a string representation and (if possible) with its unit. */ - protected function formatWithUnit(float $value, string $format = '%g'): string + protected function formatWithUnit(float $value, string $format = '%g', bool $with_latex = false): string { $str = sprintf($format, $value); - if (!empty($this->unit)) { - return $str.' '.$this->unit; + if ($this->unit !== '') { + + if (!$with_latex) { + $unit = $this->unit; + } else { + $unit = '$\mathrm{'.$this->unit.'}$'; + } + + return $str.' '.$unit; } return $str; } + + /** + * Returns the class of the element that is allowed to be associated with this attachment. + * @return string + */ + public function getElementClass(): string + { + return static::ALLOWED_ELEMENT_CLASS; + } + + public function getComparableFields(): array + { + return ['name' => $this->name, 'group' => $this->group, 'element' => $this->element?->getId()]; + } } diff --git a/src/Entity/Parameters/AttachmentTypeParameter.php b/src/Entity/Parameters/AttachmentTypeParameter.php index aa39a9a6..9a272a7d 100644 --- a/src/Entity/Parameters/AttachmentTypeParameter.php +++ b/src/Entity/Parameters/AttachmentTypeParameter.php @@ -42,20 +42,23 @@ declare(strict_types=1); namespace App\Entity\Parameters; use App\Entity\Attachments\AttachmentType; +use App\Entity\Base\AbstractDBElement; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class AttachmentTypeParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = AttachmentType::class; + final public const ALLOWED_ELEMENT_CLASS = AttachmentType::class; /** * @var AttachmentType the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Attachments\AttachmentType", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/CategoryParameter.php b/src/Entity/Parameters/CategoryParameter.php index f2ce3807..ecab1740 100644 --- a/src/Entity/Parameters/CategoryParameter.php +++ b/src/Entity/Parameters/CategoryParameter.php @@ -41,21 +41,24 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\Parts\Category; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class CategoryParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Category::class; + final public const ALLOWED_ELEMENT_CLASS = Category::class; /** * @var Category the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Category", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/CurrencyParameter.php b/src/Entity/Parameters/CurrencyParameter.php index c5fa1e2a..9ab09bed 100644 --- a/src/Entity/Parameters/CurrencyParameter.php +++ b/src/Entity/Parameters/CurrencyParameter.php @@ -41,24 +41,28 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\PriceInformations\Currency; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * A attachment attached to a category element. - * - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) + * An attachment attached to a category element. */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class CurrencyParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Currency::class; + final public const ALLOWED_ELEMENT_CLASS = Currency::class; /** * @var Currency the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\PriceInformations\Currency", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: Currency::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/FootprintParameter.php b/src/Entity/Parameters/FootprintParameter.php index 9c682720..578ddef3 100644 --- a/src/Entity/Parameters/FootprintParameter.php +++ b/src/Entity/Parameters/FootprintParameter.php @@ -41,22 +41,25 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\Parts\Footprint; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class FootprintParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Footprint::class; + final public const ALLOWED_ELEMENT_CLASS = Footprint::class; /** * @var Footprint the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Footprint", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: Footprint::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/GroupParameter.php b/src/Entity/Parameters/GroupParameter.php index aa15edb8..7fb5540f 100644 --- a/src/Entity/Parameters/GroupParameter.php +++ b/src/Entity/Parameters/GroupParameter.php @@ -41,22 +41,25 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\UserSystem\Group; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class GroupParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Group::class; + final public const ALLOWED_ELEMENT_CLASS = Group::class; /** * @var Group the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\Group", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: Group::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/ManufacturerParameter.php b/src/Entity/Parameters/ManufacturerParameter.php index 1f01ce4d..883a78f4 100644 --- a/src/Entity/Parameters/ManufacturerParameter.php +++ b/src/Entity/Parameters/ManufacturerParameter.php @@ -41,22 +41,25 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\Parts\Manufacturer; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class ManufacturerParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Manufacturer::class; + final public const ALLOWED_ELEMENT_CLASS = Manufacturer::class; /** * @var Manufacturer the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Manufacturer", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: Manufacturer::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/MeasurementUnitParameter.php b/src/Entity/Parameters/MeasurementUnitParameter.php index 7ca4b1c5..09ff81ec 100644 --- a/src/Entity/Parameters/MeasurementUnitParameter.php +++ b/src/Entity/Parameters/MeasurementUnitParameter.php @@ -41,22 +41,25 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\Parts\MeasurementUnit; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class MeasurementUnitParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = MeasurementUnit::class; + final public const ALLOWED_ELEMENT_CLASS = MeasurementUnit::class; /** * @var MeasurementUnit the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\MeasurementUnit", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: MeasurementUnit::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/ParametersTrait.php b/src/Entity/Parameters/ParametersTrait.php index c7ccbddf..2ccaa763 100644 --- a/src/Entity/Parameters/ParametersTrait.php +++ b/src/Entity/Parameters/ParametersTrait.php @@ -44,20 +44,25 @@ namespace App\Entity\Parameters; use Doctrine\Common\Collections\Collection; use Symfony\Component\Validator\Constraints as Assert; +/** + * @template-covariant T of AbstractParameter + */ trait ParametersTrait { /** * Mapping done in subclasses. * * @var Collection - * @Assert\Valid() + * @phpstan-var Collection */ - protected $parameters; + #[Assert\Valid] + protected Collection $parameters; /** * Return all associated specifications. + * @return Collection + * @phpstan-return Collection * - * @psalm-return Collection */ public function getParameters(): Collection { @@ -66,7 +71,7 @@ trait ParametersTrait /** * Add a new parameter information. - * + * @phpstan-param T $parameter * @return $this */ public function addParameter(AbstractParameter $parameter): self @@ -77,6 +82,9 @@ trait ParametersTrait return $this; } + /** + * @phpstan-param T $parameter + */ public function removeParameter(AbstractParameter $parameter): self { $this->parameters->removeElement($parameter); @@ -84,6 +92,10 @@ trait ParametersTrait return $this; } + /** + * @return array> + * @phpstan-return array> + */ public function getGroupedParameters(): array { $tmp = []; diff --git a/src/Entity/Parameters/PartParameter.php b/src/Entity/Parameters/PartParameter.php index 45c566d9..91b51c00 100644 --- a/src/Entity/Parameters/PartParameter.php +++ b/src/Entity/Parameters/PartParameter.php @@ -41,22 +41,28 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\Parts\Part; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; /** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) + * @see \App\Tests\Entity\Parameters\PartParameterTest */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class PartParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Part::class; + final public const ALLOWED_ELEMENT_CLASS = Part::class; /** * @var Part the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/ProjectParameter.php b/src/Entity/Parameters/ProjectParameter.php index 2961a843..7c3907cd 100644 --- a/src/Entity/Parameters/ProjectParameter.php +++ b/src/Entity/Parameters/ProjectParameter.php @@ -41,22 +41,25 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\ProjectSystem\Project; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class ProjectParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Project::class; + final public const ALLOWED_ELEMENT_CLASS = Project::class; /** * @var Project the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\ProjectSystem\Project", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ - protected $element; + #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/StorelocationParameter.php b/src/Entity/Parameters/StorageLocationParameter.php similarity index 68% rename from src/Entity/Parameters/StorelocationParameter.php rename to src/Entity/Parameters/StorageLocationParameter.php index 2a7aa202..f5cc6415 100644 --- a/src/Entity/Parameters/StorelocationParameter.php +++ b/src/Entity/Parameters/StorageLocationParameter.php @@ -41,22 +41,25 @@ declare(strict_types=1); namespace App\Entity\Parameters; -use App\Entity\Parts\Storelocation; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Parts\StorageLocation; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ -class StorelocationParameter extends AbstractParameter +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] +class StorageLocationParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Storelocation::class; + final public const ALLOWED_ELEMENT_CLASS = StorageLocation::class; /** - * @var Storelocation the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Storelocation", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var StorageLocation the element this para is associated with */ - protected $element; + #[ORM\ManyToOne(targetEntity: StorageLocation::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parameters/SupplierParameter.php b/src/Entity/Parameters/SupplierParameter.php index a40e3e92..6e42206f 100644 --- a/src/Entity/Parameters/SupplierParameter.php +++ b/src/Entity/Parameters/SupplierParameter.php @@ -41,22 +41,25 @@ declare(strict_types=1); namespace App\Entity\Parameters; +use App\Entity\Base\AbstractDBElement; use App\Entity\Parts\Supplier; +use App\Repository\ParameterRepository; +use App\Serializer\APIPlatform\OverrideClassDenormalizer; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Attribute\Context; -/** - * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") - * @UniqueEntity(fields={"name", "group", "element"}) - */ +#[UniqueEntity(fields: ['name', 'group', 'element'])] +#[ORM\Entity(repositoryClass: ParameterRepository::class)] class SupplierParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Supplier::class; + final public const ALLOWED_ELEMENT_CLASS = Supplier::class; /** - * @var Supplier the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Supplier", inversedBy="parameters") - * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). + * @var Supplier the element this parameter is associated with */ - protected $element; + #[ORM\ManyToOne(targetEntity: Supplier::class, inversedBy: 'parameters')] + #[ORM\JoinColumn(name: 'element_id', nullable: false, onDelete: 'CASCADE')] + #[Context(denormalizationContext: [OverrideClassDenormalizer::CONTEXT_KEY => self::ALLOWED_ELEMENT_CLASS])] + protected ?AbstractDBElement $element = null; } diff --git a/src/Entity/Parts/AssociationType.php b/src/Entity/Parts/AssociationType.php new file mode 100644 index 00000000..52a56af2 --- /dev/null +++ b/src/Entity/Parts/AssociationType.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Parts; + +/** + * The values of this enums are used to describe how two parts are associated with each other. + */ +enum AssociationType: int +{ + /** A user definable association type, which can be described in the comment field */ + case OTHER = 0; + /** The owning part is compatible with the other part */ + case COMPATIBLE = 1; + /** The owning part supersedes the other part (owner is newer version) */ + case SUPERSEDES = 2; + + /** + * Returns the translation key for this association type. + * @return string + */ + public function getTranslationKey(): string + { + return 'part_association.type.' . strtolower($this->name); + } +} diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 8bcf32d2..99ed3c6d 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -22,8 +22,30 @@ declare(strict_types=1); namespace App\Entity\Parts; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Entity\EDA\EDACategoryInfo; +use App\Repository\Parts\CategoryRepository; +use Doctrine\DBAL\Types\Types; +use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\CategoryParameter; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -31,109 +53,159 @@ use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** - * Class AttachmentType. + * This entity describes a category, a part can belong to, which is used to group parts by their function. * - * @ORM\Entity(repositoryClass="App\Repository\Parts\CategoryRepository") - * @ORM\Table(name="`categories`", indexes={ - * @ORM\Index(name="category_idx_name", columns={"name"}), - * @ORM\Index(name="category_idx_parent_name", columns={"parent_id", "name"}), - * }) + * @extends AbstractPartsContainingDBElement */ +#[ORM\Entity(repositoryClass: CategoryRepository::class)] +#[ORM\Table(name: '`categories`')] +#[ORM\Index(columns: ['name'], name: 'category_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'category_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@categories.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['category:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/categories/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a category.'), + security: 'is_granted("@categories.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Category::class) + ], + normalizationContext: ['groups' => ['category:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Category extends AbstractPartsContainingDBElement { - /** - * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['category:read', 'category:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + #[Groups(['category:read', 'category:write'])] + protected string $comment = ''; /** - * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") - */ - protected $parent; - - /** - * @var string - * @ORM\Column(type="text") - * @Groups({"full", "import"}) + * @var string The hint which is shown as hint under the partname field, when a part is created in this category. */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $partname_hint = ''; /** - * @var string - * @ORM\Column(type="text") - * @Groups({"full", "import"}) + * @var string The regular expression which is used to validate the partname of a part in this category. */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $partname_regex = ''; /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) + * @var bool Set to true, if the footprints should be disabled for parts this category (not implemented yet). */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $disable_footprints = false; /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) + * @var bool Set to true, if the manufacturers should be disabled for parts this category (not implemented yet). */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $disable_manufacturers = false; /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) + * @var bool Set to true, if the autodatasheets should be disabled for parts this category (not implemented yet). */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $disable_autodatasheets = false; /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) + * @var bool Set to true, if the properties should be disabled for parts this category (not implemented yet). */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $disable_properties = false; /** - * @var string - * @ORM\Column(type="text") - * @Groups({"full", "import"}) + * @var string The default description for parts in this category. */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $default_description = ''; /** - * @var string - * @ORM\Column(type="text") - * @Groups({"full", "import"}) + * @var string The default comment for parts in this category. */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $default_comment = ''; + /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\CategoryAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() - * @Groups({"full"}) */ - protected $attachments; + #[Assert\Valid] + #[Groups(['full', 'category:read', 'category:write'])] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: CategoryAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: CategoryAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['category:read', 'category:write'])] + protected ?Attachment $master_picture_attachment = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\CategoryParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() - * @Groups({"full"}) */ - protected $parameters; + #[Assert\Valid] + #[Groups(['full', 'category:read', 'category:write'])] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: CategoryParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + protected Collection $parameters; + + #[Groups(['category:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['category:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + #[Assert\Valid] + #[ORM\Embedded(class: EDACategoryInfo::class)] + #[Groups(['full', 'category:read', 'category:write'])] + protected EDACategoryInfo $eda_info; + + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + $this->eda_info = new EDACategoryInfo(); + } public function getPartnameHint(): string { return $this->partname_hint; } - /** - * @return Category - */ public function setPartnameHint(string $partname_hint): self { $this->partname_hint = $partname_hint; @@ -146,9 +218,6 @@ class Category extends AbstractPartsContainingDBElement return $this->partname_regex; } - /** - * @return Category - */ public function setPartnameRegex(string $partname_regex): self { $this->partname_regex = $partname_regex; @@ -161,9 +230,6 @@ class Category extends AbstractPartsContainingDBElement return $this->disable_footprints; } - /** - * @return Category - */ public function setDisableFootprints(bool $disable_footprints): self { $this->disable_footprints = $disable_footprints; @@ -176,9 +242,6 @@ class Category extends AbstractPartsContainingDBElement return $this->disable_manufacturers; } - /** - * @return Category - */ public function setDisableManufacturers(bool $disable_manufacturers): self { $this->disable_manufacturers = $disable_manufacturers; @@ -191,9 +254,6 @@ class Category extends AbstractPartsContainingDBElement return $this->disable_autodatasheets; } - /** - * @return Category - */ public function setDisableAutodatasheets(bool $disable_autodatasheets): self { $this->disable_autodatasheets = $disable_autodatasheets; @@ -206,9 +266,6 @@ class Category extends AbstractPartsContainingDBElement return $this->disable_properties; } - /** - * @return Category - */ public function setDisableProperties(bool $disable_properties): self { $this->disable_properties = $disable_properties; @@ -221,9 +278,6 @@ class Category extends AbstractPartsContainingDBElement return $this->default_description; } - /** - * @return Category - */ public function setDefaultDescription(string $default_description): self { $this->default_description = $default_description; @@ -236,13 +290,20 @@ class Category extends AbstractPartsContainingDBElement return $this->default_comment; } - /** - * @return Category - */ public function setDefaultComment(string $default_comment): self { $this->default_comment = $default_comment; + return $this; + } + public function getEdaInfo(): EDACategoryInfo + { + return $this->eda_info; + } + + public function setEdaInfo(EDACategoryInfo $eda_info): Category + { + $this->eda_info = $eda_info; return $this; } } diff --git a/src/Entity/Parts/Footprint.php b/src/Entity/Parts/Footprint.php index cac63fe2..6b043562 100644 --- a/src/Entity/Parts/Footprint.php +++ b/src/Entity/Parts/Footprint.php @@ -22,58 +22,135 @@ declare(strict_types=1); namespace App\Entity\Parts; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Entity\EDA\EDAFootprintInfo; +use App\Repository\Parts\FootprintRepository; +use App\Entity\Base\AbstractStructuralDBElement; +use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Base\AbstractPartsContainingDBElement; use App\Entity\Parameters\FootprintParameter; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** - * Class Footprint. + * This entity represents a footprint of a part (its physical dimensions and shape). * - * @ORM\Entity(repositoryClass="App\Repository\Parts\FootprintRepository") - * @ORM\Table("`footprints`", indexes={ - * @ORM\Index(name="footprint_idx_name", columns={"name"}), - * @ORM\Index(name="footprint_idx_parent_name", columns={"parent_id", "name"}), - * }) + * @extends AbstractPartsContainingDBElement */ +#[ORM\Entity(repositoryClass: FootprintRepository::class)] +#[ORM\Table('`footprints`')] +#[ORM\Index(columns: ['name'], name: 'footprint_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'footprint_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@footprints.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['footprint:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/footprints/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'), + security: 'is_granted("@footprints.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Footprint::class) + ], + normalizationContext: ['groups' => ['footprint:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Footprint extends AbstractPartsContainingDBElement { - /** - * @ORM\ManyToOne(targetEntity="Footprint", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") - */ - protected $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['footprint:read', 'footprint:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; - /** - * @ORM\OneToMany(targetEntity="Footprint", mappedBy="parent") - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[Groups(['footprint:read', 'footprint:write'])] + protected string $comment = ''; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\FootprintAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() */ - protected $attachments; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: FootprintAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['footprint:read', 'footprint:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: FootprintAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['footprint:read', 'footprint:write'])] + protected ?Attachment $master_picture_attachment = null; /** * @var FootprintAttachment|null - * @ORM\ManyToOne(targetEntity="App\Entity\Attachments\FootprintAttachment") - * @ORM\JoinColumn(name="id_footprint_3d", referencedColumnName="id") */ + #[ORM\ManyToOne(targetEntity: FootprintAttachment::class)] + #[ORM\JoinColumn(name: 'id_footprint_3d')] + #[Groups(['footprint:read', 'footprint:write'])] protected ?FootprintAttachment $footprint_3d = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\FootprintParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() */ - protected $parameters; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: FootprintParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['footprint:read', 'footprint:write'])] + protected Collection $parameters; + + #[Groups(['footprint:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['footprint:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + #[Assert\Valid] + #[ORM\Embedded(class: EDAFootprintInfo::class)] + #[Groups(['full', 'footprint:read', 'footprint:write'])] + protected EDAFootprintInfo $eda_info; + + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + $this->eda_info = new EDAFootprintInfo(); + } /**************************************** * Getters @@ -92,13 +169,10 @@ class Footprint extends AbstractPartsContainingDBElement * Setters * *********************************************************************************/ - /** * Sets the 3D Model associated with this footprint. * * @param FootprintAttachment|null $new_attachment The new 3D Model - * - * @return Footprint */ public function setFootprint3d(?FootprintAttachment $new_attachment): self { @@ -106,4 +180,15 @@ class Footprint extends AbstractPartsContainingDBElement return $this; } + + public function getEdaInfo(): EDAFootprintInfo + { + return $this->eda_info; + } + + public function setEdaInfo(EDAFootprintInfo $eda_info): Footprint + { + $this->eda_info = $eda_info; + return $this; + } } diff --git a/src/Entity/Parts/InfoProviderReference.php b/src/Entity/Parts/InfoProviderReference.php new file mode 100644 index 00000000..bfa62f32 --- /dev/null +++ b/src/Entity/Parts/InfoProviderReference.php @@ -0,0 +1,160 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Parts; + +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embeddable; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * This class represents a reference to a info provider inside a part. + * @see \App\Tests\Entity\Parts\InfoProviderReferenceTest + */ +#[Embeddable] +class InfoProviderReference +{ + + /** @var string|null The key referencing the provider used to get this part, or null if it was not provided by a data provider */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['provider_reference:read', 'full'])] + private ?string $provider_key = null; + + /** @var string|null The id of this part inside the provider system or null if the part was not provided by a data provider */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['provider_reference:read', 'full'])] + private ?string $provider_id = null; + + /** + * @var string|null The url of this part inside the provider system or null if this info is not existing + */ + #[Column(type: Types::STRING, nullable: true)] + #[Groups(['provider_reference:read', 'full'])] + private ?string $provider_url = null; + + #[Column(type: Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])] + #[Groups(['provider_reference:read', 'full'])] + private ?\DateTimeImmutable $last_updated = null; + + /** + * Constructing is forbidden from outside. + */ + private function __construct() + { + + } + + /** + * Returns the key usable to identify the provider, which provided this part. Returns null, if the part was not created by a provider. + * @return string|null + */ + public function getProviderKey(): ?string + { + return $this->provider_key; + } + + /** + * Returns the id of this part inside the provider system or null if the part was not provided by a data provider. + * @return string|null + */ + public function getProviderId(): ?string + { + return $this->provider_id; + } + + /** + * Returns the url of this part inside the provider system or null if this info is not existing. + * @return string|null + */ + public function getProviderUrl(): ?string + { + return $this->provider_url; + } + + /** + * Gets the time, when the part was last time updated by the provider. + */ + public function getLastUpdated(): ?\DateTimeImmutable + { + return $this->last_updated; + } + + /** + * Returns true, if this part was created based on infos from a provider. + * Or false, if this part was created by a user manually. + * @return bool + */ + public function isProviderCreated(): bool + { + return $this->provider_key !== null; + } + + /** + * Creates a new instance, without any provider information. + * Use this for parts, which are created by a user manually. + * @return InfoProviderReference + */ + public static function noProvider(): self + { + $ref = new InfoProviderReference(); + $ref->provider_key = null; + $ref->provider_id = null; + $ref->provider_url = null; + $ref->last_updated = null; + return $ref; + } + + /** + * Creates a reference to an info provider based on the given parameters. + * @param string $provider_key + * @param string $provider_id + * @param string|null $provider_url + * @return self + */ + public static function providerReference(string $provider_key, string $provider_id, ?string $provider_url = null): self + { + $ref = new InfoProviderReference(); + $ref->provider_key = $provider_key; + $ref->provider_id = $provider_id; + $ref->provider_url = $provider_url; + $ref->last_updated = new \DateTimeImmutable(); + return $ref; + } + + /** + * Creates a reference to an info provider based on the given Part DTO + * @param SearchResultDTO $dto + * @return self + */ + public static function fromPartDTO(SearchResultDTO $dto): self + { + $ref = new InfoProviderReference(); + $ref->provider_key = $dto->provider_key; + $ref->provider_id = $dto->provider_id; + $ref->provider_url = $dto->provider_url; + $ref->last_updated = new \DateTimeImmutable(); + return $ref; + } +} \ No newline at end of file diff --git a/src/Entity/Parts/Manufacturer.php b/src/Entity/Parts/Manufacturer.php index 45e4d140..0edf8232 100644 --- a/src/Entity/Parts/Manufacturer.php +++ b/src/Entity/Parts/Manufacturer.php @@ -22,49 +22,112 @@ declare(strict_types=1); namespace App\Entity\Parts; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Repository\Parts\ManufacturerRepository; +use App\Entity\Base\AbstractStructuralDBElement; +use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\ManufacturerAttachment; use App\Entity\Base\AbstractCompany; use App\Entity\Parameters\ManufacturerParameter; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** - * Class Manufacturer. + * This entity represents a manufacturer of a part (The company that produces the part). * - * @ORM\Entity(repositoryClass="App\Repository\Parts\ManufacturerRepository") - * @ORM\Table("`manufacturers`", indexes={ - * @ORM\Index(name="manufacturer_name", columns={"name"}), - * @ORM\Index(name="manufacturer_idx_parent_name", columns={"parent_id", "name"}), - * }) + * @extends AbstractCompany */ +#[ORM\Entity(repositoryClass: ManufacturerRepository::class)] +#[ORM\Table('`manufacturers`')] +#[ORM\Index(columns: ['name'], name: 'manufacturer_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'manufacturer_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@manufacturers.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['manufacturer:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/manufacturers/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'), + security: 'is_granted("@manufacturers.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class) + ], + normalizationContext: ['groups' => ['manufacturer:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Manufacturer extends AbstractCompany { - /** - * @ORM\ManyToOne(targetEntity="Manufacturer", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") - */ - protected $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['manufacturer:read', 'manufacturer:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; - /** - * @ORM\OneToMany(targetEntity="Manufacturer", mappedBy="parent") - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\ManufacturerAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() */ - protected $attachments; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: ManufacturerAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['manufacturer:read', 'manufacturer:write'])] + #[ApiProperty(readableLink: false, writableLink: true)] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: ManufacturerAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['manufacturer:read', 'manufacturer:write'])] + #[ApiProperty(readableLink: false, writableLink: true)] + protected ?Attachment $master_picture_attachment = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\ManufacturerParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() */ - protected $parameters; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: ManufacturerParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['manufacturer:read', 'manufacturer:write'])] + #[ApiProperty(readableLink: false, writableLink: true)] + protected Collection $parameters; + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + } } diff --git a/src/Entity/Parts/ManufacturingStatus.php b/src/Entity/Parts/ManufacturingStatus.php new file mode 100644 index 00000000..2b6de800 --- /dev/null +++ b/src/Entity/Parts/ManufacturingStatus.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Parts; + +enum ManufacturingStatus: string +{ + /** Part has been announced, but is not in production yet */ + case ANNOUNCED = 'announced'; + /** Part is in production and will be for the foreseeable future */ + case ACTIVE = 'active'; + /** Not recommended for new designs. */ + case NRFND = 'nrfnd'; + /** End of life: Part will become discontinued soon */ + case EOL = 'eol'; + /** Part is obsolete/discontinued by the manufacturer. */ + case DISCONTINUED = 'discontinued'; + + /** Status not set */ + case NOT_SET = ''; + + public function toTranslationKey(): string + { + return match ($this) { + self::ANNOUNCED => 'm_status.announced', + self::ACTIVE => 'm_status.active', + self::NRFND => 'm_status.nrfnd', + self::EOL => 'm_status.eol', + self::DISCONTINUED => 'm_status.discontinued', + self::NOT_SET => '', + }; + } +} \ No newline at end of file diff --git a/src/Entity/Parts/MeasurementUnit.php b/src/Entity/Parts/MeasurementUnit.php index 5ff9cef4..6dd0b9f2 100644 --- a/src/Entity/Parts/MeasurementUnit.php +++ b/src/Entity/Parts/MeasurementUnit.php @@ -22,6 +22,27 @@ declare(strict_types=1); namespace App\Entity\Parts; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Repository\Parts\MeasurementUnitRepository; +use Doctrine\DBAL\Types\Types; +use App\Entity\Base\AbstractStructuralDBElement; +use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\MeasurementUnitAttachment; use App\Entity\Base\AbstractPartsContainingDBElement; use App\Entity\Parameters\MeasurementUnitParameter; @@ -30,73 +51,115 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; /** * This unit represents the unit in which the amount of parts in stock are measured. * This could be something like N, grams, meters, etc... * - * @ORM\Entity(repositoryClass="App\Repository\Parts\MeasurementUnitRepository") - * @ORM\Table(name="`measurement_units`", indexes={ - * @ORM\Index(name="unit_idx_name", columns={"name"}), - * @ORM\Index(name="unit_idx_parent_name", columns={"parent_id", "name"}), - * }) - * @UniqueEntity("unit") + * @extends AbstractPartsContainingDBElement */ +#[UniqueEntity('unit')] +#[ORM\Entity(repositoryClass: MeasurementUnitRepository::class)] +#[ORM\Table(name: '`measurement_units`')] +#[ORM\Index(columns: ['name'], name: 'unit_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'unit_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@measurement_units.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['measurement_unit:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/measurement_units/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'), + security: 'is_granted("@measurement_units.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class) + ], + normalizationContext: ['groups' => ['measurement_unit:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "unit"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class MeasurementUnit extends AbstractPartsContainingDBElement { /** * @var string The unit symbol that should be used for the Unit. This could be something like "", g (for grams) * or m (for meters). - * @ORM\Column(type="string", name="unit", nullable=true) - * @Assert\Length(max=10) - * @Groups({"extended", "full", "import"}) */ + #[Assert\Length(max: 10)] + #[Groups(['simple', 'extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])] + #[ORM\Column(name: 'unit', type: Types::STRING, nullable: true)] protected ?string $unit = null; + #[Groups(['measurement_unit:read', 'measurement_unit:write'])] + protected string $comment = ''; + /** * @var bool Determines if the amount value associated with this unit should be treated as integer. * Set to false, to measure continuous sizes likes masses or lengths. - * @ORM\Column(type="boolean", name="is_integer") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])] + #[ORM\Column(name: 'is_integer', type: Types::BOOLEAN)] protected bool $is_integer = false; /** * @var bool Determines if the unit can be used with SI Prefixes (kilo, giga, milli, etc.). * Useful for sizes like meters. For this the unit must be set - * @ORM\Column(type="boolean", name="use_si_prefix") - * @Assert\Expression("this.isUseSIPrefix() == false or this.getUnit() != null", message="validator.measurement_unit.use_si_prefix_needs_unit") - * @Groups({"full", "import"}) */ + #[Assert\Expression('this.isUseSIPrefix() == false or this.getUnit() != null', message: 'validator.measurement_unit.use_si_prefix_needs_unit')] + #[Groups(['simple', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])] + #[ORM\Column(name: 'use_si_prefix', type: Types::BOOLEAN)] protected bool $use_si_prefix = false; - /** - * @ORM\OneToMany(targetEntity="MeasurementUnit", mappedBy="parent", cascade={"persist"}) - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; - /** - * @ORM\ManyToOne(targetEntity="MeasurementUnit", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") - */ - protected $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['measurement_unit:read', 'measurement_unit:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\MeasurementUnitAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() */ - protected $attachments; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: MeasurementUnitAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['measurement_unit:read', 'measurement_unit:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: MeasurementUnitAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['measurement_unit:read', 'measurement_unit:write'])] + protected ?Attachment $master_picture_attachment = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\MeasurementUnitParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() */ - protected $parameters; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: MeasurementUnitParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['measurement_unit:read', 'measurement_unit:write'])] + protected Collection $parameters; + + #[Groups(['measurement_unit:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['measurement_unit:read'])] + protected ?\DateTimeImmutable $lastModified = null; + /** * @return string @@ -106,11 +169,6 @@ class MeasurementUnit extends AbstractPartsContainingDBElement return $this->unit; } - /** - * @param string|null $unit - * - * @return MeasurementUnit - */ public function setUnit(?string $unit): self { $this->unit = $unit; @@ -123,9 +181,6 @@ class MeasurementUnit extends AbstractPartsContainingDBElement return $this->is_integer; } - /** - * @return MeasurementUnit - */ public function setIsInteger(bool $isInteger): self { $this->is_integer = $isInteger; @@ -138,13 +193,17 @@ class MeasurementUnit extends AbstractPartsContainingDBElement return $this->use_si_prefix; } - /** - * @return MeasurementUnit - */ public function setUseSIPrefix(bool $usesSIPrefixes): self { $this->use_si_prefix = $usesSIPrefixes; return $this; } + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + } } diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index c345302c..14a7903f 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -22,20 +22,41 @@ declare(strict_types=1); namespace App\Entity\Parts; +use App\ApiPlatform\Filter\TagFilter; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\EntityFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\ApiPlatform\Filter\PartStoragelocationFilter; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\PartAttachment; -use App\Entity\Parts\PartTraits\ProjectTrait; -use App\Entity\ProjectSystem\Project; +use App\Entity\EDA\EDAPartInfo; use App\Entity\Parameters\ParametersTrait; use App\Entity\Parameters\PartParameter; use App\Entity\Parts\PartTraits\AdvancedPropertyTrait; +use App\Entity\Parts\PartTraits\AssociationTrait; use App\Entity\Parts\PartTraits\BasicPropertyTrait; +use App\Entity\Parts\PartTraits\EDATrait; use App\Entity\Parts\PartTraits\InstockTrait; use App\Entity\Parts\PartTraits\ManufacturerTrait; use App\Entity\Parts\PartTraits\OrderTrait; -use App\Entity\ProjectSystem\ProjectBOMEntry; -use DateTime; +use App\Entity\Parts\PartTraits\ProjectTrait; +use App\EntityListeners\TreeCacheInvalidationListener; +use App\Repository\PartRepository; +use App\Validator\Constraints\UniqueObjectCollection; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -48,16 +69,41 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * Part class. * * The class properties are split over various traits in directory PartTraits. - * Otherwise this class would be too big, to be maintained. - * - * @ORM\Entity(repositoryClass="App\Repository\PartRepository") - * @ORM\Table("`parts`", indexes={ - * @ORM\Index(name="parts_idx_datet_name_last_id_needs", columns={"datetime_added", "name", "last_modified", "id", "needs_review"}), - * @ORM\Index(name="parts_idx_name", columns={"name"}), - * @ORM\Index(name="parts_idx_ipn", columns={"ipn"}), - * }) - * @UniqueEntity(fields={"ipn"}, message="part.ipn.must_be_unique") + * Otherwise, this class would be too big, to be maintained. + * @see \App\Tests\Entity\Parts\PartTest + * @extends AttachmentContainingDBElement + * @template-use ParametersTrait */ +#[UniqueEntity(fields: ['ipn'], message: 'part.ipn.must_be_unique')] +#[ORM\Entity(repositoryClass: PartRepository::class)] +#[ORM\EntityListeners([TreeCacheInvalidationListener::class])] +#[ORM\Table('`parts`')] +#[ORM\Index(columns: ['datetime_added', 'name', 'last_modified', 'id', 'needs_review'], name: 'parts_idx_datet_name_last_id_needs')] +#[ORM\Index(columns: ['name'], name: 'parts_idx_name')] +#[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')] +#[ApiResource( + operations: [ + new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read', + 'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read'], + 'openapi_definition_name' => 'Read', + ], security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@parts.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])] +#[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])] +#[ApiFilter(TagFilter::class, properties: ["tags"])] +#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])] +#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Part extends AttachmentContainingDBElement { use AdvancedPropertyTrait; @@ -68,19 +114,18 @@ class Part extends AttachmentContainingDBElement use OrderTrait; use ParametersTrait; use ProjectTrait; + use AssociationTrait; + use EDATrait; /** @var Collection - * @Assert\Valid() - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\PartParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Groups({"full"}) */ - protected $parameters; + #[Assert\Valid] + #[Groups(['full', 'part:read', 'part:write', 'import'])] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: PartParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[UniqueObjectCollection(fields: ['name', 'group', 'element'])] + protected Collection $parameters; - /** - * @ORM\Column(type="datetime", name="datetime_added", options={"default":"CURRENT_TIMESTAMP"}) - */ - protected ?DateTime $addedDate = null; /** ************************************************************* * Overridden properties @@ -89,40 +134,48 @@ class Part extends AttachmentContainingDBElement /** * @var string The name of this part - * @ORM\Column(type="string") */ protected string $name = ''; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\PartAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() - * @Groups({"full"}) */ - protected $attachments; + #[Assert\Valid] + #[Groups(['full', 'part:read', 'part:write'])] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: PartAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $attachments; /** - * @var DateTime the date when this element was modified the last time - * @ORM\Column(type="datetime", name="last_modified", options={"default":"CURRENT_TIMESTAMP"}) - */ - protected ?DateTime $lastModified = null; - - /** - * @var Attachment - * @ORM\ManyToOne(targetEntity="App\Entity\Attachments\Attachment") - * @ORM\JoinColumn(name="id_preview_attachment", referencedColumnName="id", onDelete="SET NULL", nullable=true) - * @Assert\Expression("value == null or value.isPicture()", message="part.master_attachment.must_be_picture") + * @var Attachment|null */ + #[Assert\Expression('value == null or value.isPicture()', message: 'part.master_attachment.must_be_picture')] + #[ORM\ManyToOne(targetEntity: PartAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['part:read', 'part:write'])] protected ?Attachment $master_picture_attachment = null; + #[Groups(['part:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['part:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + public function __construct() { + $this->attachments = new ArrayCollection(); parent::__construct(); $this->partLots = new ArrayCollection(); $this->orderdetails = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->project_bom_entries = new ArrayCollection(); + + $this->associated_parts_as_owner = new ArrayCollection(); + $this->associated_parts_as_other = new ArrayCollection(); + + //By default, the part has no provider + $this->providerReference = InfoProviderReference::noProvider(); + $this->eda_info = new EDAPartInfo(); } public function __clone() @@ -148,25 +201,32 @@ class Part extends AttachmentContainingDBElement foreach ($parameters as $parameter) { $this->addParameter(clone $parameter); } + + //Deep clone the owned part associations (the owned ones make not much sense without the owner) + $ownedAssociations = $this->associated_parts_as_owner; + $this->associated_parts_as_owner = new ArrayCollection(); + foreach ($ownedAssociations as $association) { + $this->addAssociatedPartsAsOwner(clone $association); + } + + //Deep clone info provider + $this->providerReference = clone $this->providerReference; + $this->eda_info = clone $this->eda_info; } parent::__clone(); } - /** - * @Assert\Callback - */ - public function validate(ExecutionContextInterface $context, $payload) + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void { //Ensure that the part name fullfills the regex of the category - if ($this->category) { + if ($this->category instanceof Category) { $regex = $this->category->getPartnameRegex(); - if (!empty($regex)) { - if (!preg_match($regex, $this->name)) { - $context->buildViolation('part.name.must_match_category_regex') - ->atPath('name') - ->setParameter('%regex%', $regex) - ->addViolation(); - } + if ($regex !== '' && !preg_match($regex, $this->name)) { + $context->buildViolation('part.name.must_match_category_regex') + ->atPath('name') + ->setParameter('%regex%', $regex) + ->addViolation(); } } } diff --git a/src/Entity/Parts/PartAssociation.php b/src/Entity/Parts/PartAssociation.php new file mode 100644 index 00000000..32017488 --- /dev/null +++ b/src/Entity/Parts/PartAssociation.php @@ -0,0 +1,235 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Parts; + +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Contracts\TimeStampableInterface; +use App\Repository\DBElementRepository; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\TimestampTrait; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; + +/** + * This entity describes a part association, which is a semantic connection between two parts. + * For example, a part association can be used to describe that a part is a replacement for another part. + * @see \App\Tests\Entity\Parts\PartAssociationTest + */ +#[ORM\Entity(repositoryClass: DBElementRepository::class)] +#[ORM\HasLifecycleCallbacks] +#[UniqueEntity(fields: ['other', 'owner', 'type'], message: 'validator.part_association.already_exists')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@parts.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['part_assoc:read', 'part_assoc:read:standalone', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['part_assoc:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["other_type", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['comment', 'addedDate', 'lastModified'])] +class PartAssociation extends AbstractDBElement implements TimeStampableInterface +{ + use TimestampTrait; + + /** + * @var AssociationType The type of this association (how the two parts are related) + */ + #[ORM\Column(type: Types::SMALLINT, enumType: AssociationType::class)] + #[Groups(['part_assoc:read', 'part_assoc:write'])] + protected AssociationType $type = AssociationType::OTHER; + + /** + * @var string|null A user definable association type, which can be described in the comment field, which + * is used if the type is OTHER + */ + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + #[Assert\Expression("this.getType().value !== 0 or this.getOtherType() !== null", + message: 'validator.part_association.must_set_an_value_if_type_is_other')] + #[Groups(['part_assoc:read', 'part_assoc:write'])] + #[Length(max: 255)] + protected ?string $other_type = null; + + /** + * @var string|null A comment describing this association further. + */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups(['part_assoc:read', 'part_assoc:write'])] + protected ?string $comment = null; + + /** + * @var Part|null The part which "owns" this association, e.g. the part which is a replacement for another part + */ + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'associated_parts_as_owner')] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + #[Assert\NotNull] + #[Groups(['part_assoc:read:standalone', 'part_assoc:write'])] + protected ?Part $owner = null; + + /** + * @var Part|null The part which is "owned" by this association, e.g. the part which is replaced by another part + */ + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'associated_parts_as_other')] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + #[Assert\NotNull] + #[Assert\Expression("this.getOwner() !== this.getOther()", + message: 'validator.part_association.part_cannot_be_associated_with_itself')] + #[Groups(['part_assoc:read', 'part_assoc:write'])] + protected ?Part $other = null; + + /** + * Returns the (semantic) relation type of this association as an AssociationType enum value. + * If the type is set to OTHER, then the other_type field value is used for the user defined type. + * @return AssociationType + */ + public function getType(): AssociationType + { + return $this->type; + } + + /** + * Sets the (semantic) relation type of this association as an AssociationType enum value. + * @param AssociationType $type + * @return $this + */ + public function setType(AssociationType $type): PartAssociation + { + $this->type = $type; + return $this; + } + + /** + * Returns a comment, which describes this association further. + * @return string|null + */ + public function getComment(): ?string + { + return $this->comment; + } + + /** + * Sets a comment, which describes this association further. + * @param string|null $comment + * @return $this + */ + public function setComment(?string $comment): PartAssociation + { + $this->comment = $comment; + return $this; + } + + /** + * Returns the part which "owns" this association, e.g. the part which is a replacement for another part. + * @return Part|null + */ + public function getOwner(): ?Part + { + return $this->owner; + } + + /** + * Sets the part which "owns" this association, e.g. the part which is a replacement for another part. + * @param Part|null $owner + * @return $this + */ + public function setOwner(?Part $owner): PartAssociation + { + $this->owner = $owner; + return $this; + } + + /** + * Returns the part which is "owned" by this association, e.g. the part which is replaced by another part. + * @return Part|null + */ + public function getOther(): ?Part + { + return $this->other; + } + + /** + * Sets the part which is "owned" by this association, e.g. the part which is replaced by another part. + * @param Part|null $other + * @return $this + */ + public function setOther(?Part $other): PartAssociation + { + $this->other = $other; + return $this; + } + + /** + * Returns the user defined association type, which is used if the type is set to OTHER. + * @return string|null + */ + public function getOtherType(): ?string + { + return $this->other_type; + } + + /** + * Sets the user defined association type, which is used if the type is set to OTHER. + * @param string|null $other_type + * @return $this + */ + public function setOtherType(?string $other_type): PartAssociation + { + $this->other_type = $other_type; + return $this; + } + + /** + * Returns the translation key for the type of this association. + * If the type is set to OTHER, then the other_type field value is used. + * @return string + */ + public function getTypeTranslationKey(): string + { + if ($this->type === AssociationType::OTHER) { + return $this->other_type ?? 'Unknown'; + } + return $this->type->getTranslationKey(); + } + +} \ No newline at end of file diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index 32db4828..d893e6de 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -22,6 +22,23 @@ declare(strict_types=1); namespace App\Entity\Parts; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Validator\Constraints\Year2038BugWorkaround; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Contracts\NamedElementInterface; @@ -32,94 +49,128 @@ use App\Validator\Constraints\ValidPartLot; use DateTime; use Doctrine\ORM\Mapping as ORM; use Exception; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * This entity describes a lot where parts can be stored. * It is the connection between a part and its store locations. * - * @ORM\Entity() - * @ORM\Table(name="part_lots", indexes={ - * @ORM\Index(name="part_lots_idx_instock_un_expiration_id_part", columns={"instock_unknown", "expiration_date", "id_part"}), - * @ORM\Index(name="part_lots_idx_needs_refill", columns={"needs_refill"}), - * }) - * @ORM\HasLifecycleCallbacks() - * @ValidPartLot() + * @see \App\Tests\Entity\Parts\PartLotTest */ +#[ORM\Entity] +#[ORM\HasLifecycleCallbacks] +#[ORM\Table(name: 'part_lots')] +#[ORM\Index(columns: ['instock_unknown', 'expiration_date', 'id_part'], name: 'part_lots_idx_instock_un_expiration_id_part')] +#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')] +#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')] +#[ValidPartLot] +#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@parts.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['part_lot:read', 'part_lot:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['part_lot:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["description", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(BooleanFilter::class, properties: ['instock_unknown', 'needs_refill'])] +#[ApiFilter(RangeFilter::class, properties: ['amount'])] +#[ApiFilter(OrderFilter::class, properties: ['description', 'comment', 'addedDate', 'lastModified'])] class PartLot extends AbstractDBElement implements TimeStampableInterface, NamedElementInterface { use TimestampTrait; /** * @var string A short description about this lot, shown in table - * @ORM\Column(type="text") - * @Groups({"simple", "extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $description = ''; /** * @var string a comment stored with this lot - * @ORM\Column(type="text") - * @Groups({"full", "import"}) */ + #[Groups(['full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $comment = ''; /** - * @var ?DateTime Set a time until when the lot must be used. + * @var \DateTimeImmutable|null Set a time until when the lot must be used. * Set to null, if the lot can be used indefinitely. - * @ORM\Column(type="datetime", name="expiration_date", nullable=true) - * @Groups({"extended", "full", "import"}) */ - protected ?DateTime $expiration_date = null; + #[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column(name: 'expiration_date', type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Year2038BugWorkaround] + protected ?\DateTimeImmutable $expiration_date = null; /** - * @var Storelocation|null The storelocation of this lot - * @ORM\ManyToOne(targetEntity="Storelocation") - * @ORM\JoinColumn(name="id_store_location", referencedColumnName="id", nullable=true) - * @Selectable() - * @Groups({"simple", "extended", "full", "import"}) + * @var StorageLocation|null The storelocation of this lot */ - protected ?Storelocation $storage_location = null; + #[Groups(['simple', 'extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\ManyToOne(targetEntity: StorageLocation::class, fetch: 'EAGER')] + #[ORM\JoinColumn(name: 'id_store_location')] + #[Selectable] + protected ?StorageLocation $storage_location = null; /** * @var bool If this is set to true, the instock amount is marked as not known - * @ORM\Column(type="boolean") - * @Groups({"simple", "extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $instock_unknown = false; /** - * @var float For continuous sizes (length, volume, etc.) the instock is saved here. - * @ORM\Column(type="float") - * @Assert\PositiveOrZero() - * @Groups({"simple", "extended", "full", "import"}) + * @var float The amount of parts in this lot. For integer-quantities this value is rounded to the next integer. */ + #[Assert\PositiveOrZero] + #[Groups(['simple', 'extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column(type: Types::FLOAT)] protected float $amount = 0.0; /** * @var bool determines if this lot was manually marked for refilling - * @ORM\Column(type="boolean") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $needs_refill = false; /** - * @var Part The part that is stored in this lot - * @ORM\ManyToOne(targetEntity="Part", inversedBy="partLots") - * @ORM\JoinColumn(name="id_part", referencedColumnName="id", nullable=false, onDelete="CASCADE") - * @Assert\NotNull() + * @var Part|null The part that is stored in this lot */ - protected Part $part; + #[Assert\NotNull] + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'partLots')] + #[ORM\JoinColumn(name: 'id_part', nullable: false, onDelete: 'CASCADE')] + #[Groups(['part_lot:read:standalone', 'part_lot:write'])] + #[ApiProperty(writableLink: false)] + protected ?Part $part = null; /** * @var User|null The owner of this part lot - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User") - * @ORM\JoinColumn(name="id_owner", referencedColumnName="id", nullable=true, onDelete="SET NULL") */ + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(name: 'id_owner', onDelete: 'SET NULL')] + #[Groups(['part_lot:read', 'part_lot:write'])] + #[ApiProperty(writableLink: false)] protected ?User $owner = null; + /** + * @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor) + */ + #[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)] + #[Groups(['part_lot:read', 'part_lot:write'])] + #[Length(max: 255)] + protected ?string $user_barcode = null; + public function __clone() { if ($this->id) { @@ -130,20 +181,19 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Check if the current part lot is expired. - * This is the case, if the expiration date is greater the the current date. + * This is the case, if the expiration date is greater the current date. * * @return bool|null True, if the part lot is expired. Returns null, if no expiration date was set. * - * @throws Exception If an error with the DateTime occurs */ public function isExpired(): ?bool { - if (null === $this->expiration_date) { + if (!$this->expiration_date instanceof \DateTimeInterface) { return null; } //Check if the expiration date is bigger then current time - return $this->expiration_date < new DateTime('now'); + return $this->expiration_date < new \DateTimeImmutable('now'); } /** @@ -156,8 +206,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Sets the description of the part lot. - * - * @return PartLot */ public function setDescription(string $description): self { @@ -176,8 +224,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Sets the comment for this part lot. - * - * @return PartLot */ public function setComment(string $comment): self { @@ -189,19 +235,17 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Gets the expiration date for the part lot. Returns null, if no expiration date was set. */ - public function getExpirationDate(): ?DateTime + public function getExpirationDate(): ?\DateTimeImmutable { return $this->expiration_date; } /** - * Sets the expiration date for the part lot. Set to null, if the part lot does not expires. + * Sets the expiration date for the part lot. Set to null, if the part lot does not expire. * - * @param DateTime|null $expiration_date * - * @return PartLot */ - public function setExpirationDate(?DateTime $expiration_date): self + public function setExpirationDate(?\DateTimeImmutable $expiration_date): self { $this->expiration_date = $expiration_date; @@ -211,19 +255,17 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Gets the storage location, where this part lot is stored. * - * @return Storelocation|null The store location where this part is stored + * @return StorageLocation|null The store location where this part is stored */ - public function getStorageLocation(): ?Storelocation + public function getStorageLocation(): ?StorageLocation { return $this->storage_location; } /** * Sets the storage location, where this part lot is stored. - * - * @return PartLot */ - public function setStorageLocation(?Storelocation $storage_location): self + public function setStorageLocation(?StorageLocation $storage_location): self { $this->storage_location = $storage_location; @@ -233,15 +275,13 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Return the part that is stored in this part lot. */ - public function getPart(): Part + public function getPart(): ?Part { return $this->part; } /** * Sets the part that is stored in this part lot. - * - * @return PartLot */ public function setPart(Part $part): self { @@ -260,8 +300,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Set the unknown instock status of this part lot. - * - * @return PartLot */ public function setInstockUnknown(bool $instock_unknown): self { @@ -303,9 +341,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named return $this->needs_refill; } - /** - * @return PartLot - */ public function setNeedsRefill(bool $needs_refill): self { $this->needs_refill = $needs_refill; @@ -315,7 +350,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Returns the owner of this part lot. - * @return User|null */ public function getOwner(): ?User { @@ -324,8 +358,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * Sets the owner of this part lot. - * @param User|null $owner - * @return PartLot */ public function setOwner(?User $owner): PartLot { @@ -339,9 +371,30 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named } /** - * @Assert\Callback + * The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor), or + * null if no barcode is set. + * @return string|null */ - public function validate(ExecutionContextInterface $context, $payload) + public function getUserBarcode(): ?string + { + return $this->user_barcode; + } + + /** + * Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor). + * @param string|null $user_barcode + * @return $this + */ + public function setUserBarcode(?string $user_barcode): PartLot + { + $this->user_barcode = $user_barcode; + return $this; + } + + + + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void { //Ensure that the owner is not the anonymous user if ($this->getOwner() && $this->getOwner()->isAnonymousUser()) { @@ -352,14 +405,12 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named //When the storage location sets the owner must match, the part lot owner must match the storage location owner if ($this->getStorageLocation() && $this->getStorageLocation()->isPartOwnerMustMatch() - && $this->getStorageLocation()->getOwner() && $this->getOwner()) { - if ($this->getOwner() !== $this->getStorageLocation()->getOwner() - && $this->owner->getID() !== $this->getStorageLocation()->getOwner()->getID()) { - $context->buildViolation('validator.part_lot.owner_must_match_storage_location_owner') - ->setParameter('%owner_name%', $this->getStorageLocation()->getOwner()->getFullName(true)) - ->atPath('owner') - ->addViolation(); - } + && $this->getStorageLocation()->getOwner() && $this->getOwner() && ($this->getOwner() !== $this->getStorageLocation()->getOwner() + && $this->owner->getID() !== $this->getStorageLocation()->getOwner()->getID())) { + $context->buildViolation('validator.part_lot.owner_must_match_storage_location_owner') + ->setParameter('%owner_name%', $this->getStorageLocation()->getOwner()->getFullName(true)) + ->atPath('owner') + ->addViolation(); } } } diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 482004b0..230ba7b7 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -22,10 +22,13 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; +use App\Entity\Parts\InfoProviderReference; +use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; /** * Advanced properties of a part, not related to a more specific group. @@ -34,34 +37,42 @@ trait AdvancedPropertyTrait { /** * @var bool Determines if this part entry needs review (for example, because it is work in progress) - * @ORM\Column(type="boolean") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $needs_review = false; /** - * @var string a comma separated list of tags, associated with the part - * @ORM\Column(type="text") - * @Groups({"extended", "full", "import"}) + * @var string A comma separated list of tags, associated with the part */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $tags = ''; /** - * @var float|null how much a single part unit weighs in grams - * @ORM\Column(type="float", nullable=true) - * @Assert\PositiveOrZero() - * @Groups({"extended", "full", "import"}) + * @var float|null How much a single part unit weighs in grams */ + #[Assert\PositiveOrZero] + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::FLOAT, nullable: true)] protected ?float $mass = null; /** - * @var string The internal part number of the part - * @ORM\Column(type="string", length=100, nullable=true, unique=true) - * @Assert\Length(max="100") - * @Groups({"extended", "full", "import"}) + * @var string|null The internal part number of the part */ + #[Assert\Length(max: 100)] + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[Length(max: 100)] protected ?string $ipn = null; + /** + * @var InfoProviderReference The reference to the info provider, that provided the information about this part + */ + #[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')] + #[Groups(['full', 'part:read'])] + protected InfoProviderReference $providerReference; + /** * Checks if this part is marked, for that it needs further review. */ @@ -142,7 +153,6 @@ trait AdvancedPropertyTrait /** * Sets the internal part number of the part * @param string $ipn The new IPN of the part - * @return Part */ public function setIpn(?string $ipn): Part { @@ -150,5 +160,27 @@ trait AdvancedPropertyTrait return $this; } + /** + * Returns the reference to the info provider, that provided the information about this part. + * @return InfoProviderReference + */ + public function getProviderReference(): InfoProviderReference + { + return $this->providerReference; + } + + /** + * Sets the reference to the info provider, that provided the information about this part. + * @param InfoProviderReference $providerReference + * @return Part + */ + public function setProviderReference(InfoProviderReference $providerReference): Part + { + $this->providerReference = $providerReference; + return $this; + } + + + } diff --git a/src/Entity/Parts/PartTraits/AssociationTrait.php b/src/Entity/Parts/PartTraits/AssociationTrait.php new file mode 100644 index 00000000..bb80fc5a --- /dev/null +++ b/src/Entity/Parts/PartTraits/AssociationTrait.php @@ -0,0 +1,110 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Parts\PartTraits; + +use App\Entity\Parts\PartAssociation; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints\Valid; +use Doctrine\ORM\Mapping as ORM; + +trait AssociationTrait +{ + /** + * @var Collection All associations where this part is the owner + */ + #[Valid] + #[ORM\OneToMany(mappedBy: 'owner', targetEntity: PartAssociation::class, + cascade: ['persist', 'remove'], orphanRemoval: true)] + #[Groups(['part:read', 'part:write', 'full'])] + protected Collection $associated_parts_as_owner; + + /** + * @var Collection All associations where this part is the owned/other part + */ + #[Valid] + #[ORM\OneToMany(mappedBy: 'other', targetEntity: PartAssociation::class, + cascade: ['persist', 'remove'], orphanRemoval: true)] + #[Groups(['part:read'])] + protected Collection $associated_parts_as_other; + + /** + * Returns all associations where this part is the owner. + * @return Collection + */ + public function getAssociatedPartsAsOwner(): Collection + { + return $this->associated_parts_as_owner; + } + + /** + * Add a new association where this part is the owner. + * @param PartAssociation $association + * @return $this + */ + public function addAssociatedPartsAsOwner(PartAssociation $association): self + { + //Ensure that the association is really owned by this part + $association->setOwner($this); + + $this->associated_parts_as_owner->add($association); + return $this; + } + + /** + * Remove an association where this part is the owner. + * @param PartAssociation $association + * @return $this + */ + public function removeAssociatedPartsAsOwner(PartAssociation $association): self + { + $this->associated_parts_as_owner->removeElement($association); + return $this; + } + + /** + * Returns all associations where this part is the owned/other part. + * If you want to modify the association, do it on the owning part + * @return Collection + */ + public function getAssociatedPartsAsOther(): Collection + { + return $this->associated_parts_as_other; + } + + /** + * Returns all associations where this part is the owned or other part. + * @return Collection + */ + public function getAssociatedPartsAll(): Collection + { + return new ArrayCollection( + array_merge( + $this->associated_parts_as_owner->toArray(), + $this->associated_parts_as_other->toArray() + ) + ); + } +} \ No newline at end of file diff --git a/src/Entity/Parts/PartTraits/BasicPropertyTrait.php b/src/Entity/Parts/PartTraits/BasicPropertyTrait.php index 0f88787f..7e483ed2 100644 --- a/src/Entity/Parts/PartTraits/BasicPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/BasicPropertyTrait.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; +use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Validator\Constraints\Selectable; @@ -33,49 +34,49 @@ trait BasicPropertyTrait { /** * @var string A text describing what this part does - * @ORM\Column(type="text") - * @Groups({"simple", "extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $description = ''; /** * @var string A comment/note related to this part - * @ORM\Column(type="text") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $comment = ''; /** - * @var bool Kept for compatibility (it is not used now, and I dont think it was used in old versions) - * @ORM\Column(type="boolean") + * @var bool Kept for compatibility (it is not used now, and I don't think it was used in old versions) */ + #[ORM\Column(type: Types::BOOLEAN)] protected bool $visible = true; /** * @var bool true, if the part is marked as favorite - * @ORM\Column(type="boolean") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $favorite = false; /** - * @var Category The category this part belongs too (e.g. Resistors). Use tags, for more complex grouping. + * @var Category|null The category this part belongs too (e.g. Resistors). Use tags, for more complex grouping. * Every part must have a category. - * @ORM\ManyToOne(targetEntity="Category") - * @ORM\JoinColumn(name="id_category", referencedColumnName="id", nullable=false) - * @Selectable() - * @Assert\NotNull(message="validator.select_valid_category") - * @Groups({"simple", "extended", "full", "import"}) */ + #[Assert\NotNull(message: 'validator.select_valid_category')] + #[Selectable] + #[Groups(['simple', 'extended', 'full', 'import', "part:read", "part:write"])] + #[ORM\ManyToOne(targetEntity: Category::class)] + #[ORM\JoinColumn(name: 'id_category', nullable: false)] protected ?Category $category = null; /** * @var Footprint|null The footprint of this part (e.g. DIP8) - * @ORM\ManyToOne(targetEntity="Footprint") - * @ORM\JoinColumn(name="id_footprint", referencedColumnName="id") - * @Selectable() - * @Groups({"simple", "extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\ManyToOne(targetEntity: Footprint::class)] + #[ORM\JoinColumn(name: 'id_footprint')] + #[Selectable] protected ?Footprint $footprint = null; /** @@ -136,7 +137,7 @@ trait BasicPropertyTrait /** * Gets the Footprint of this part (e.g. DIP8). * - * @return Footprint|null The footprint of this part. Null if this part should no have a footprint. + * @return Footprint|null The footprint of this part. Null if this part should not have a footprint. */ public function getFootprint(): ?Footprint { diff --git a/src/Entity/Parts/PartTraits/EDATrait.php b/src/Entity/Parts/PartTraits/EDATrait.php new file mode 100644 index 00000000..313552e7 --- /dev/null +++ b/src/Entity/Parts/PartTraits/EDATrait.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Parts\PartTraits; + +use App\Entity\EDA\EDAPartInfo; +use Doctrine\ORM\Mapping\Embedded; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints\Valid; + +trait EDATrait +{ + #[Valid] + #[Embedded(class: EDAPartInfo::class)] + #[Groups(['full', 'part:read', 'part:write', 'import'])] + protected EDAPartInfo $eda_info; + + public function getEdaInfo(): EDAPartInfo + { + return $this->eda_info; + } + + public function setEdaInfo(?EDAPartInfo $eda_info): self + { + if ($eda_info !== null) { + //Do a clone, to ensure that the property is updated in the database + $eda_info = clone $eda_info; + } + + $this->eda_info = $eda_info; + return $this; + } +} \ No newline at end of file diff --git a/src/Entity/Parts/PartTraits/InstockTrait.php b/src/Entity/Parts/PartTraits/InstockTrait.php index d7b59469..08b070f3 100644 --- a/src/Entity/Parts/PartTraits/InstockTrait.php +++ b/src/Entity/Parts/PartTraits/InstockTrait.php @@ -22,11 +22,14 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; +use Doctrine\Common\Collections\Criteria; +use Doctrine\DBAL\Types\Types; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\PartLot; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Attribute\SerializedName; use Symfony\Component\Validator\Constraints as Assert; /** @@ -35,35 +38,34 @@ use Symfony\Component\Validator\Constraints as Assert; trait InstockTrait { /** - * @var Collection|PartLot[] A list of part lots where this part is stored - * @ORM\OneToMany(targetEntity="PartLot", mappedBy="part", cascade={"persist", "remove"}, orphanRemoval=true) - * @Assert\Valid() - * @ORM\OrderBy({"amount" = "DESC"}) - * @Groups({"extended", "full", "import"}) + * @var Collection A list of part lots where this part is stored */ - protected $partLots; + #[Assert\Valid] + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\OneToMany(mappedBy: 'part', targetEntity: PartLot::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['amount' => Criteria::DESC])] + protected Collection $partLots; /** * @var float The minimum amount of the part that has to be instock, otherwise more is ordered. * Given in the partUnit. - * @ORM\Column(type="float") - * @Assert\PositiveOrZero() - * @Groups({"extended", "full", "import"}) */ + #[Assert\PositiveOrZero] + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::FLOAT)] protected float $minamount = 0; /** * @var ?MeasurementUnit the unit in which the part's amount is measured - * @ORM\ManyToOne(targetEntity="MeasurementUnit") - * @ORM\JoinColumn(name="id_part_unit", referencedColumnName="id", nullable=true) - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\ManyToOne(targetEntity: MeasurementUnit::class)] + #[ORM\JoinColumn(name: 'id_part_unit')] protected ?MeasurementUnit $partUnit = null; /** * Get all part lots where this part is stored. - * - * @return PartLot[]|Collection + * @phpstan-return Collection */ public function getPartLots(): Collection { @@ -122,7 +124,7 @@ trait InstockTrait /** * Get the count of parts which must be in stock at least. - * If a integer-based part unit is selected, the value will be rounded to integers. + * If an integer-based part unit is selected, the value will be rounded to integers. * * @return float count of parts which must be in stock at least */ @@ -153,26 +155,43 @@ trait InstockTrait /** * Returns true, if the total instock amount of this part is less than the minimum amount. - * @return bool */ public function isNotEnoughInstock(): bool { return $this->getAmountSum() < $this->getMinAmount(); } + /** + * Returns true, if at least one of the part lots has an unknown amount. + * It is possible that other part lots have a known amount, then getAmountSum() will return sum of all known amounts. + * @return bool True if at least one part lot has an unknown amount. + */ + public function isAmountUnknown(): bool + { + foreach ($this->getPartLots() as $lot) { + if ($lot->isInstockUnknown()) { + return true; + } + } + + return false; + } + /** * Returns the summed amount of this part (over all part lots) - * Part Lots that have unknown value or are expired, are not used for this value. + * Part Lots that have unknown value or are expired, are not used for this value (counted as 0). * * @return float The amount of parts given in partUnit */ + #[Groups(['simple', 'extended', 'full', 'part:read'])] + #[SerializedName('total_instock')] public function getAmountSum(): float { //TODO: Find a method to do this natively in SQL, the current method could be a bit slow $sum = 0; foreach ($this->getPartLots() as $lot) { - //Dont use the instock value, if it is unkown - if ($lot->isInstockUnknown() || $lot->isExpired() ?? false) { + //Don't use the in stock value, if it is unknown + if ($lot->isInstockUnknown() || ($lot->isExpired() ?? false)) { continue; } @@ -188,7 +207,6 @@ trait InstockTrait /** * Returns the summed amount of all part lots that are expired. If no part lots are expired 0 is returned. - * @return float */ public function getExpiredAmountSum(): float { diff --git a/src/Entity/Parts/PartTraits/ManufacturerTrait.php b/src/Entity/Parts/PartTraits/ManufacturerTrait.php index d87e11cc..5d7f8749 100644 --- a/src/Entity/Parts/PartTraits/ManufacturerTrait.php +++ b/src/Entity/Parts/PartTraits/ManufacturerTrait.php @@ -22,12 +22,15 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; +use App\Entity\Parts\ManufacturingStatus; +use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Validator\Constraints\Selectable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; /** * In this trait all manufacturer related properties of a part are collected (like MPN, manufacturer URL). @@ -36,35 +39,35 @@ trait ManufacturerTrait { /** * @var Manufacturer|null The manufacturer of this part - * @ORM\ManyToOne(targetEntity="Manufacturer") - * @ORM\JoinColumn(name="id_manufacturer", referencedColumnName="id") - * @Selectable() - * @Groups({"simple","extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\ManyToOne(targetEntity: Manufacturer::class)] + #[ORM\JoinColumn(name: 'id_manufacturer')] + #[Selectable] protected ?Manufacturer $manufacturer = null; /** - * @var string the url to the part on the manufacturer's homepage - * @ORM\Column(type="string") - * @Assert\Url() - * @Groups({"full", "import"}) + * @var string The url to the part on the manufacturer's homepage */ + #[Assert\Url] + #[Groups(['full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $manufacturer_product_url = ''; /** * @var string The product number used by the manufacturer. If this is set to "", the name field is used. - * @ORM\Column(type="string") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::STRING)] + #[Length(max: 255)] protected string $manufacturer_product_number = ''; /** - * @var string The production status of this part. Can be one of the specified ones. - * @ORM\Column(type="string", length=255, nullable=true) - * @Assert\Choice({"announced", "active", "nrfnd", "eol", "discontinued", ""}) - * @Groups({"extended", "full", "import"}) + * @var ManufacturingStatus|null The production status of this part. Can be one of the specified ones. */ - protected ?string $manufacturing_status = ''; + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: true, enumType: ManufacturingStatus::class)] + protected ?ManufacturingStatus $manufacturing_status = ManufacturingStatus::NOT_SET; /** * Get the link to the website of the article on the manufacturers website @@ -112,9 +115,9 @@ trait ManufacturerTrait * * "eol": Part will become discontinued soon * * "discontinued": Part is obsolete/discontinued by the manufacturer. * - * @return string + * @return ManufacturingStatus|null */ - public function getManufacturingStatus(): ?string + public function getManufacturingStatus(): ?ManufacturingStatus { return $this->manufacturing_status; } @@ -123,9 +126,9 @@ trait ManufacturerTrait * Sets the manufacturing status for this part * See getManufacturingStatus() for valid values. * - * @return Part + * @return $this */ - public function setManufacturingStatus(string $manufacturing_status): self + public function setManufacturingStatus(ManufacturingStatus $manufacturing_status): self { $this->manufacturing_status = $manufacturing_status; diff --git a/src/Entity/Parts/PartTraits/OrderTrait.php b/src/Entity/Parts/PartTraits/OrderTrait.php index 79721443..2c142016 100644 --- a/src/Entity/Parts/PartTraits/OrderTrait.php +++ b/src/Entity/Parts/PartTraits/OrderTrait.php @@ -22,8 +22,9 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; +use Doctrine\Common\Collections\Criteria; +use Doctrine\DBAL\Types\Types; use App\Entity\PriceInformations\Orderdetail; -use Doctrine\Common\Collections\ArrayCollection; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use function count; @@ -36,37 +37,37 @@ use Doctrine\ORM\Mapping as ORM; trait OrderTrait { /** - * @var Orderdetail[]|Collection the details about how and where you can order this part - * @ORM\OneToMany(targetEntity="App\Entity\PriceInformations\Orderdetail", mappedBy="part", cascade={"persist", "remove"}, orphanRemoval=true) - * @Assert\Valid() - * @ORM\OrderBy({"supplierpartnr" = "ASC"}) - * @Groups({"extended", "full", "import"}) + * @var Collection The details about how and where you can order this part */ - protected $orderdetails; + #[Assert\Valid] + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\OneToMany(mappedBy: 'part', targetEntity: Orderdetail::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['supplierpartnr' => Criteria::ASC])] + protected Collection $orderdetails; /** * @var int - * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] protected int $order_quantity = 0; /** * @var bool - * @ORM\Column(type="boolean") */ + #[ORM\Column(type: Types::BOOLEAN)] protected bool $manual_order = false; /** - * @var Orderdetail - * @ORM\OneToOne(targetEntity="App\Entity\PriceInformations\Orderdetail") - * @ORM\JoinColumn(name="order_orderdetails_id", referencedColumnName="id") + * @var Orderdetail|null */ + #[ORM\OneToOne(targetEntity: Orderdetail::class)] + #[ORM\JoinColumn(name: 'order_orderdetails_id')] protected ?Orderdetail $order_orderdetail = null; /** * Get the selected order orderdetails of this part. * - * @return Orderdetail the selected order orderdetails + * @return Orderdetail|null the selected order orderdetails */ public function getOrderOrderdetails(): ?Orderdetail { @@ -98,7 +99,7 @@ trait OrderTrait * * @param bool $hide_obsolete If true, obsolete orderdetails will NOT be returned * - * @return Collection|Orderdetail[] * all orderdetails as a one-dimensional array of Orderdetails objects + * @return Collection * all orderdetails as a one-dimensional array of Orderdetails objects * (empty array if there are no ones) * * the array is sorted by the suppliers names / minimum order quantity */ @@ -106,9 +107,7 @@ trait OrderTrait { //If needed hide the obsolete entries if ($hide_obsolete) { - return $this->orderdetails->filter(function (Orderdetail $orderdetail) { - return ! $orderdetail->getObsolete(); - }); + return $this->orderdetails->filter(fn(Orderdetail $orderdetail) => ! $orderdetail->getObsolete()); } return $this->orderdetails; diff --git a/src/Entity/Parts/PartTraits/ProjectTrait.php b/src/Entity/Parts/PartTraits/ProjectTrait.php index 58697427..45719377 100644 --- a/src/Entity/Parts/PartTraits/ProjectTrait.php +++ b/src/Entity/Parts/PartTraits/ProjectTrait.php @@ -1,30 +1,34 @@ $project_bom_entries - * @ORM\OneToMany(targetEntity="App\Entity\ProjectSystem\ProjectBOMEntry", mappedBy="part", cascade={"remove"}, orphanRemoval=true) + * @var Collection $project_bom_entries */ - protected $project_bom_entries = []; + #[ORM\OneToMany(mappedBy: 'part', targetEntity: ProjectBOMEntry::class, cascade: ['remove'], orphanRemoval: true)] + protected Collection $project_bom_entries; /** * @var Project|null If a project is set here, then this part is special and represents the builds of a project. - * @ORM\OneToOne(targetEntity="App\Entity\ProjectSystem\Project", inversedBy="build_part") - * @ORM\JoinColumn(nullable=true) */ + #[ORM\OneToOne(inversedBy: 'build_part', targetEntity: Project::class)] + #[ORM\JoinColumn] protected ?Project $built_project = null; /** - * Returns all ProjectBOMEntries that use this part. - * @return Collection|ProjectBOMEntry[] + * Returns all ProjectBOMEntries that use this part. + * + * @phpstan-return Collection */ public function getProjectBomEntries(): Collection { @@ -35,14 +39,14 @@ trait ProjectTrait * Checks whether this part represents the builds of a project * @return bool True if it represents the builds, false if not */ + #[Groups(['part:read'])] public function isProjectBuildPart(): bool { return $this->built_project !== null; } /** - * Returns the project that this part represents the builds of, or null if it doesnt - * @return Project|null + * Returns the project that this part represents the builds of, or null if it doesn't */ public function getBuiltProject(): ?Project { @@ -78,4 +82,4 @@ trait ProjectTrait return $projects; } -} \ No newline at end of file +} diff --git a/src/Entity/Parts/StorageLocation.php b/src/Entity/Parts/StorageLocation.php new file mode 100644 index 00000000..6c455ae5 --- /dev/null +++ b/src/Entity/Parts/StorageLocation.php @@ -0,0 +1,303 @@ +. + */ + +declare(strict_types=1); + +namespace App\Entity\Parts; + +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Repository\Parts\StorelocationRepository; +use Doctrine\DBAL\Types\Types; +use Doctrine\Common\Collections\ArrayCollection; +use App\Entity\Attachments\StorageLocationAttachment; +use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\StorageLocationParameter; +use App\Entity\UserSystem\User; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * This entity represents a storage location, where parts can be stored. + * @extends AbstractPartsContainingDBElement + */ +#[ORM\Entity(repositoryClass: StorelocationRepository::class)] +#[ORM\Table('`storelocations`')] +#[ORM\Index(columns: ['name'], name: 'location_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'location_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@storelocations.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['location:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/storage_locations/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'), + security: 'is_granted("@storelocations.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class) + ], + normalizationContext: ['groups' => ['location:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +class StorageLocation extends AbstractPartsContainingDBElement +{ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['location:read', 'location:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + #[Groups(['location:read', 'location:write'])] + protected string $comment = ''; + + /** + * @var MeasurementUnit|null The measurement unit, which parts can be stored in here + */ + #[ORM\ManyToOne(targetEntity: MeasurementUnit::class)] + #[ORM\JoinColumn(name: 'storage_type_id')] + #[Groups(['location:read', 'location:write'])] + protected ?MeasurementUnit $storage_type = null; + + /** @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: StorageLocationParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['location:read', 'location:write'])] + protected Collection $parameters; + + /** + * @var bool When this attribute is set, it is not possible to add additional parts or increase the instock of existing parts. + */ + #[Groups(['full', 'import', 'location:read', 'location:write'])] + #[ORM\Column(type: Types::BOOLEAN)] + protected bool $is_full = false; + + /** + * @var bool When this property is set, only one part (but many instock) is allowed to be stored in this store location. + */ + #[Groups(['full', 'import', 'location:read', 'location:write'])] + #[ORM\Column(type: Types::BOOLEAN)] + protected bool $only_single_part = false; + + /** + * @var bool When this property is set, it is only possible to increase the instock of parts, that are already stored here. + */ + #[Groups(['full', 'import', 'location:read', 'location:write'])] + #[ORM\Column(type: Types::BOOLEAN)] + protected bool $limit_to_existing_parts = false; + + /** + * @var User|null The owner of this storage location + */ + #[Assert\Expression('this.getOwner() == null or this.getOwner().isAnonymousUser() === false', message: 'validator.part_lot.owner_must_not_be_anonymous')] + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(name: 'id_owner', onDelete: 'SET NULL')] + #[Groups(['location:read', 'location:write'])] + protected ?User $owner = null; + + /** + * @var bool If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. + */ + #[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])] + #[Groups(['location:read', 'location:write'])] + protected bool $part_owner_must_match = false; + + /** + * @var Collection + */ + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: StorageLocationAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[Groups(['location:read', 'location:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: StorageLocationAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['location:read', 'location:write'])] + protected ?Attachment $master_picture_attachment = null; + + #[Groups(['location:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['location:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + + /******************************************************************************** + * + * Getters + * + *********************************************************************************/ + + /** + * Get the "is full" attribute. + * + * When this attribute is set, it is not possible to add additional parts or increase the instock of existing parts. + * + * @return bool * true if the store location is full + * * false if the store location isn't full + */ + public function isFull(): bool + { + return $this->is_full; + } + + /** + * When this property is set, only one part (but many instock) is allowed to be stored in this store location. + */ + public function isOnlySinglePart(): bool + { + return $this->only_single_part; + } + + public function setOnlySinglePart(bool $only_single_part): self + { + $this->only_single_part = $only_single_part; + + return $this; + } + + /** + * When this property is set, it is only possible to increase the instock of parts, that are already stored here. + */ + public function isLimitToExistingParts(): bool + { + return $this->limit_to_existing_parts; + } + + public function setLimitToExistingParts(bool $limit_to_existing_parts): self + { + $this->limit_to_existing_parts = $limit_to_existing_parts; + + return $this; + } + + public function getStorageType(): ?MeasurementUnit + { + return $this->storage_type; + } + + public function setStorageType(?MeasurementUnit $storage_type): self + { + $this->storage_type = $storage_type; + + return $this; + } + + /** + * Returns the owner of this storage location + */ + public function getOwner(): ?User + { + return $this->owner; + } + + /** + * Sets the owner of this storage location + */ + public function setOwner(?User $owner): StorageLocation + { + $this->owner = $owner; + return $this; + } + + /** + * If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. + */ + public function isPartOwnerMustMatch(): bool + { + return $this->part_owner_must_match; + } + + /** + * If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. + */ + public function setPartOwnerMustMatch(bool $part_owner_must_match): StorageLocation + { + $this->part_owner_must_match = $part_owner_must_match; + return $this; + } + + + + + /******************************************************************************** + * + * Setters + * + *********************************************************************************/ + /** + * Change the "is full" attribute of this store location. + * + * "is_full" = true means that there is no more space in this storelocation. + * This attribute is only for information, it has no effect. + * + * @param bool $new_is_full * true means that the storelocation is full + * * false means that the storelocation isn't full + */ + public function setIsFull(bool $new_is_full): self + { + $this->is_full = $new_is_full; + + return $this; + } + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + } +} diff --git a/src/Entity/Parts/Storelocation.php b/src/Entity/Parts/Storelocation.php deleted file mode 100644 index e7a7dfb7..00000000 --- a/src/Entity/Parts/Storelocation.php +++ /dev/null @@ -1,250 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Entity\Parts; - -use App\Entity\Attachments\StorelocationAttachment; -use App\Entity\Base\AbstractPartsContainingDBElement; -use App\Entity\Parameters\StorelocationParameter; -use App\Entity\UserSystem\User; -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; -use Symfony\Component\Validator\Constraints as Assert; - -/** - * Class Store location. - * - * @ORM\Entity(repositoryClass="App\Repository\Parts\StorelocationRepository") - * @ORM\Table("`storelocations`", indexes={ - * @ORM\Index(name="location_idx_name", columns={"name"}), - * @ORM\Index(name="location_idx_parent_name", columns={"parent_id", "name"}), - * }) - */ -class Storelocation extends AbstractPartsContainingDBElement -{ - /** - * @ORM\OneToMany(targetEntity="Storelocation", mappedBy="parent") - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; - - /** - * @ORM\ManyToOne(targetEntity="Storelocation", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") - */ - protected $parent; - - /** - * @var MeasurementUnit|null The measurement unit, which parts can be stored in here - * @ORM\ManyToOne(targetEntity="MeasurementUnit") - * @ORM\JoinColumn(name="storage_type_id", referencedColumnName="id") - */ - protected ?MeasurementUnit $storage_type = null; - - /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\StorelocationParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() - */ - protected $parameters; - - /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) - */ - protected bool $is_full = false; - - /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) - */ - protected bool $only_single_part = false; - - /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"full", "import"}) - */ - protected bool $limit_to_existing_parts = false; - - /** - * @var User|null The owner of this storage location - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User") - * @ORM\JoinColumn(name="id_owner", referencedColumnName="id", nullable=true, onDelete="SET NULL") - * @Assert\Expression("this.getOwner() == null or this.getOwner().isAnonymousUser() === false", message="validator.part_lot.owner_must_not_be_anonymous") - */ - protected ?User $owner = null; - - /** - * @var bool If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. - * @ORM\Column(type="boolean", options={"default":false}) - */ - protected bool $part_owner_must_match = false; - - /** - * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\StorelocationAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @Assert\Valid() - */ - protected $attachments; - - /******************************************************************************** - * - * Getters - * - *********************************************************************************/ - - /** - * Get the "is full" attribute. - * - * When this attribute is set, it is not possible to add additional parts or increase the instock of existing parts. - * - * @return bool * true if the store location is full - * * false if the store location isn't full - */ - public function isFull(): bool - { - return $this->is_full; - } - - /** - * When this property is set, only one part (but many instock) is allowed to be stored in this store location. - */ - public function isOnlySinglePart(): bool - { - return $this->only_single_part; - } - - /** - * @return Storelocation - */ - public function setOnlySinglePart(bool $only_single_part): self - { - $this->only_single_part = $only_single_part; - - return $this; - } - - /** - * When this property is set, it is only possible to increase the instock of parts, that are already stored here. - */ - public function isLimitToExistingParts(): bool - { - return $this->limit_to_existing_parts; - } - - /** - * @return Storelocation - */ - public function setLimitToExistingParts(bool $limit_to_existing_parts): self - { - $this->limit_to_existing_parts = $limit_to_existing_parts; - - return $this; - } - - public function getStorageType(): ?MeasurementUnit - { - return $this->storage_type; - } - - /** - * @return Storelocation - */ - public function setStorageType(?MeasurementUnit $storage_type): self - { - $this->storage_type = $storage_type; - - return $this; - } - - /** - * Returns the owner of this storage location - * @return User|null - */ - public function getOwner(): ?User - { - return $this->owner; - } - - /** - * Sets the owner of this storage location - * @param User|null $owner - * @return Storelocation - */ - public function setOwner(?User $owner): Storelocation - { - $this->owner = $owner; - return $this; - } - - /** - * If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. - * @return bool - */ - public function isPartOwnerMustMatch(): bool - { - return $this->part_owner_must_match; - } - - /** - * If this is set to true, only parts lots, which are owned by the same user as the store location are allowed to be stored here. - * @param bool $part_owner_must_match - * @return Storelocation - */ - public function setPartOwnerMustMatch(bool $part_owner_must_match): Storelocation - { - $this->part_owner_must_match = $part_owner_must_match; - return $this; - } - - - - - /******************************************************************************** - * - * Setters - * - *********************************************************************************/ - - /** - * Change the "is full" attribute of this store location. - * - * "is_full" = true means that there is no more space in this storelocation. - * This attribute is only for information, it has no effect. - * - * @param bool $new_is_full * true means that the storelocation is full - * * false means that the storelocation isn't full - * - * @return Storelocation - */ - public function setIsFull(bool $new_is_full): self - { - $this->is_full = $new_is_full; - - return $this; - } -} diff --git a/src/Entity/Parts/Supplier.php b/src/Entity/Parts/Supplier.php index ae1e4ca1..2c004e9e 100644 --- a/src/Entity/Parts/Supplier.php +++ b/src/Entity/Parts/Supplier.php @@ -22,8 +22,29 @@ declare(strict_types=1); namespace App\Entity\Parts; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Repository\Parts\SupplierRepository; +use App\Entity\PriceInformations\Orderdetail; +use Doctrine\Common\Collections\ArrayCollection; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Base\AbstractCompany; +use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\SupplierParameter; use App\Entity\PriceInformations\Currency; use App\Validator\Constraints\BigDecimal\BigDecimalPositiveOrZero; @@ -35,65 +56,99 @@ use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** - * Class Supplier. + * This entity represents a supplier of parts (the company that sells the parts). * - * @ORM\Entity(repositoryClass="App\Repository\Parts\SupplierRepository") - * @ORM\Table("`suppliers`", indexes={ - * @ORM\Index(name="supplier_idx_name", columns={"name"}), - * @ORM\Index(name="supplier_idx_parent_name", columns={"parent_id", "name"}), - * }) + * @extends AbstractCompany */ +#[ORM\Entity(repositoryClass: SupplierRepository::class)] +#[ORM\Table('`suppliers`')] +#[ORM\Index(columns: ['name'], name: 'supplier_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'supplier_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@suppliers.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['supplier:write', 'company:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/suppliers/{id}/children.{_format}', + operations: [new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'), + security: 'is_granted("@manufacturers.read")' + )], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Supplier::class) + ], + normalizationContext: ['groups' => ['supplier:read', 'company:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Supplier extends AbstractCompany { - /** - * @ORM\OneToMany(targetEntity="Supplier", mappedBy="parent") - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['supplier:read', 'supplier:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; /** - * @ORM\ManyToOne(targetEntity="Supplier", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") + * @var Collection */ - protected $parent; - - /** - * @ORM\OneToMany(targetEntity="App\Entity\PriceInformations\Orderdetail", mappedBy="supplier") - */ - protected $orderdetails; + #[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Orderdetail::class)] + protected Collection $orderdetails; /** * @var Currency|null The currency that should be used by default for order informations with this supplier. * Set to null, to use global base currency. - * @ORM\ManyToOne(targetEntity="App\Entity\PriceInformations\Currency") - * @ORM\JoinColumn(name="default_currency_id", referencedColumnName="id", nullable=true) - * @Selectable() */ + #[ORM\ManyToOne(targetEntity: Currency::class)] + #[ORM\JoinColumn(name: 'default_currency_id')] + #[Selectable] protected ?Currency $default_currency = null; /** - * @var BigDecimal|null the shipping costs that have to be paid, when ordering via this supplier - * @ORM\Column(name="shipping_costs", nullable=true, type="big_decimal", precision=11, scale=5) - * @Groups({"extended", "full", "import"}) - * @BigDecimalPositiveOrZero() + * @var BigDecimal|null The shipping costs that have to be paid, when ordering via this supplier */ + #[Groups(['extended', 'full', 'import'])] + #[ORM\Column(name: 'shipping_costs', type: 'big_decimal', precision: 11, scale: 5, nullable: true)] + #[BigDecimalPositiveOrZero] protected ?BigDecimal $shipping_costs = null; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\SupplierAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() */ - protected $attachments; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: SupplierAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['supplier:read', 'supplier:write'])] + #[ApiProperty(readableLink: false, writableLink: true)] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: SupplierAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['supplier:read', 'supplier:write'])] + #[ApiProperty(readableLink: false, writableLink: true)] + protected ?Attachment $master_picture_attachment = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\SupplierParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() */ - protected $parameters; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: SupplierParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['supplier:read', 'supplier:write'])] + #[ApiProperty(readableLink: false, writableLink: true)] + protected Collection $parameters; /** * Gets the currency that should be used by default, when creating a orderdetail with this supplier. @@ -105,8 +160,6 @@ class Supplier extends AbstractCompany /** * Sets the default currency. - * - * @return Supplier */ public function setDefaultCurrency(?Currency $default_currency): self { @@ -129,12 +182,10 @@ class Supplier extends AbstractCompany * Sets the shipping costs for an order with this supplier. * * @param BigDecimal|null $shipping_costs a BigDecimal with the shipping costs - * - * @return Supplier */ public function setShippingCosts(?BigDecimal $shipping_costs): self { - if (null === $shipping_costs) { + if (!$shipping_costs instanceof BigDecimal) { $this->shipping_costs = null; } @@ -145,4 +196,12 @@ class Supplier extends AbstractCompany return $this; } + public function __construct() + { + parent::__construct(); + $this->children = new ArrayCollection(); + $this->orderdetails = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); + } } diff --git a/src/Entity/PriceInformations/Currency.php b/src/Entity/PriceInformations/Currency.php index f2c44504..ce20caf8 100644 --- a/src/Entity/PriceInformations/Currency.php +++ b/src/Entity/PriceInformations/Currency.php @@ -22,6 +22,25 @@ declare(strict_types=1); namespace App\Entity\PriceInformations; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Repository\CurrencyRepository; +use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\CurrencyAttachment; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\CurrencyParameter; @@ -38,67 +57,115 @@ use Symfony\Component\Validator\Constraints as Assert; /** * This entity describes a currency that can be used for price information. * - * @UniqueEntity("iso_code") - * @ORM\Entity() - * @ORM\Table(name="currencies", indexes={ - * @ORM\Index(name="currency_idx_name", columns={"name"}), - * @ORM\Index(name="currency_idx_parent_name", columns={"parent_id", "name"}), - * }) + * @extends AbstractStructuralDBElement */ +#[UniqueEntity('iso_code')] +#[ORM\Entity(repositoryClass: CurrencyRepository::class)] +#[ORM\Table(name: 'currencies')] +#[ORM\Index(columns: ['name'], name: 'currency_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'currency_idx_parent_name')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@currencies.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['currency:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/currencies/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a currency.'), + security: 'is_granted("@currencies.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Currency::class) + ], + normalizationContext: ['groups' => ['currency:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "iso_code"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Currency extends AbstractStructuralDBElement { - public const PRICE_SCALE = 5; + final public const PRICE_SCALE = 5; /** * @var BigDecimal|null The exchange rate between this currency and the base currency * (how many base units the current currency is worth) - * @ORM\Column(type="big_decimal", precision=11, scale=5, nullable=true) - * @BigDecimalPositive() */ + #[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)] + #[BigDecimalPositive] + #[Groups(['currency:read', 'currency:write', 'simple', 'extended', 'full', 'import'])] + #[ApiProperty(readableLink: false, writableLink: false)] protected ?BigDecimal $exchange_rate = null; + #[Groups(['currency:read', 'currency:write'])] + protected string $comment = ""; + /** * @var string the 3-letter ISO code of the currency - * @ORM\Column(type="string") - * @Assert\Currency() - * @Groups({"extended", "full", "import"}) */ + #[Assert\Currency] + #[Assert\NotBlank] + #[Groups(['simple', 'extended', 'full', 'import', 'currency:read', 'currency:write'])] + #[ORM\Column(type: Types::STRING)] protected string $iso_code = ""; - /** - * @ORM\OneToMany(targetEntity="Currency", mappedBy="parent", cascade={"persist"}) - * @ORM\OrderBy({"name" = "ASC"}) - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; - /** - * @ORM\ManyToOne(targetEntity="Currency", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") - */ - protected $parent; + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['currency:read', 'currency:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\CurrencyAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() */ - protected $attachments; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: CurrencyAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['currency:read', 'currency:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: CurrencyAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['currency:read', 'currency:write'])] + protected ?Attachment $master_picture_attachment = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\CurrencyParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() */ - protected $parameters; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: CurrencyParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['currency:read', 'currency:write'])] + protected Collection $parameters; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\PriceInformations\Pricedetail", mappedBy="currency") */ - protected $pricedetails; + #[ORM\OneToMany(mappedBy: 'currency', targetEntity: Pricedetail::class)] + protected Collection $pricedetails; + + #[Groups(['currency:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['currency:read'])] + protected ?\DateTimeImmutable $lastModified = null; + public function __construct() { + $this->children = new ArrayCollection(); + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); $this->pricedetails = new ArrayCollection(); parent::__construct(); } @@ -113,17 +180,12 @@ class Currency extends AbstractStructuralDBElement * * @return string */ - public function getIsoCode(): ?string + public function getIsoCode(): string { return $this->iso_code; } - /** - * @param string|null $iso_code - * - * @return Currency - */ - public function setIsoCode(?string $iso_code): self + public function setIsoCode(string $iso_code): self { $this->iso_code = $iso_code; @@ -133,11 +195,12 @@ class Currency extends AbstractStructuralDBElement /** * Returns the inverse exchange rate (how many of the current currency the base unit is worth). */ + #[Groups(['currency:read'])] public function getInverseExchangeRate(): ?BigDecimal { $tmp = $this->getExchangeRate(); - if (null === $tmp || $tmp->isZero()) { + if (!$tmp instanceof BigDecimal || $tmp->isZero()) { return null; } @@ -158,17 +221,18 @@ class Currency extends AbstractStructuralDBElement * * @param BigDecimal|null $exchange_rate The new exchange rate of the currency. * Set to null, if the exchange rate is unknown. - * - * @return Currency */ public function setExchangeRate(?BigDecimal $exchange_rate): self { - if (null === $exchange_rate) { + //If the exchange rate is null, set it to null and return. + if ($exchange_rate === null) { $this->exchange_rate = null; + return $this; } - $tmp = $exchange_rate->toScale(self::PRICE_SCALE, RoundingMode::HALF_UP); + //Only change the object, if the value changes, so that doctrine does not detect it as changed. - if ((string) $tmp !== (string) $this->exchange_rate) { + //Or if the current exchange rate is currently null, as we can not compare it then + if ($this->exchange_rate === null || $exchange_rate->compareTo($this->exchange_rate) !== 0) { $this->exchange_rate = $exchange_rate; } diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index 82b59b1f..3709b37d 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -23,79 +23,128 @@ declare(strict_types=1); namespace App\Entity\PriceInformations; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Contracts\TimeStampableInterface; use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; -use DateTime; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\Length; /** * Class Orderdetail. - * - * @ORM\Table("`orderdetails`", indexes={ - * @ORM\Index(name="orderdetails_supplier_part_nr", columns={"supplierpartnr"}), - * }) - * @ORM\Entity() - * @ORM\HasLifecycleCallbacks() - * @UniqueEntity({"supplierpartnr", "supplier", "part"}) */ +#[UniqueEntity(['supplierpartnr', 'supplier', 'part'])] +#[ORM\Entity] +#[ORM\HasLifecycleCallbacks] +#[ORM\Table('`orderdetails`')] +#[ORM\Index(columns: ['supplierpartnr'], name: 'orderdetails_supplier_part_nr')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@parts.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['orderdetail:read', 'orderdetail:read:standalone', 'api:basic:read', 'pricedetail:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['orderdetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/parts/{id}/orderdetails.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'), + security: 'is_granted("@parts.read")' + ) + ], + uriVariables: [ + 'id' => new Link(toProperty: 'part', fromClass: Part::class) + ], + normalizationContext: ['groups' => ['orderdetail:read', 'pricedetail:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["supplierpartnr", "supplier_product_url"])] +#[ApiFilter(BooleanFilter::class, properties: ["obsolete"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['supplierpartnr', 'id', 'addedDate', 'lastModified'])] class Orderdetail extends AbstractDBElement implements TimeStampableInterface, NamedElementInterface { use TimestampTrait; /** - * @ORM\OneToMany(targetEntity="Pricedetail", mappedBy="orderdetail", cascade={"persist", "remove"}, orphanRemoval=true) - * @Assert\Valid() - * @ORM\OrderBy({"min_discount_quantity" = "ASC"}) - * @Groups({"extended", "full", "import"}) + * @var Collection */ - protected $pricedetails; + #[Assert\Valid] + #[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])] + #[ORM\OneToMany(mappedBy: 'orderdetail', targetEntity: Pricedetail::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['min_discount_quantity' => Criteria::ASC])] + protected Collection $pricedetails; /** - * @var string - * @ORM\Column(type="string") - * @Groups({"extended", "full", "import"}) + * @var string The order number of the part at the supplier */ + #[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])] + #[ORM\Column(type: Types::STRING)] + #[Length(max: 255)] protected string $supplierpartnr = ''; /** - * @var bool - * @ORM\Column(type="boolean") - * @Groups({"extended", "full", "import"}) + * @var bool True if this part is obsolete/not available anymore at the supplier */ + #[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $obsolete = false; /** - * @var string - * @ORM\Column(type="string") - * @Assert\Url() - * @Groups({"full", "import"}) + * @var string The URL to the product on the supplier's website */ + #[Assert\Url] + #[Groups(['full', 'import', 'orderdetail:read', 'orderdetail:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $supplier_product_url = ''; /** - * @var Part - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part", inversedBy="orderdetails") - * @ORM\JoinColumn(name="part_id", referencedColumnName="id", nullable=false, onDelete="CASCADE") - * @Assert\NotNull() + * @var Part|null The part with which this orderdetail is associated */ + #[Assert\NotNull] + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'orderdetails')] + #[Groups(['orderdetail:read:standalone', 'orderdetail:write'])] + #[ORM\JoinColumn(name: 'part_id', nullable: false, onDelete: 'CASCADE')] protected ?Part $part = null; /** - * @var Supplier - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Supplier", inversedBy="orderdetails") - * @ORM\JoinColumn(name="id_supplier", referencedColumnName="id") - * @Assert\NotNull(message="validator.orderdetail.supplier_must_not_be_null") - * @Groups({"extended", "full", "import"}) + * @var Supplier|null The supplier of this orderdetail */ + #[Assert\NotNull(message: 'validator.orderdetail.supplier_must_not_be_null')] + #[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])] + #[ORM\ManyToOne(targetEntity: Supplier::class, inversedBy: 'orderdetails')] + #[ORM\JoinColumn(name: 'id_supplier')] protected ?Supplier $supplier = null; public function __construct() @@ -119,15 +168,14 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N /** * Helper for updating the timestamp. It is automatically called by doctrine before persisting. - * - * @ORM\PrePersist - * @ORM\PreUpdate */ + #[ORM\PrePersist] + #[ORM\PreUpdate] public function updateTimestamps(): void { - $this->lastModified = new DateTime('now'); - if (null === $this->addedDate) { - $this->addedDate = new DateTime('now'); + $this->lastModified = new DateTimeImmutable('now'); + if (!$this->addedDate instanceof \DateTimeInterface) { + $this->addedDate = new DateTimeImmutable('now'); } if ($this->part instanceof Part) { @@ -185,6 +233,11 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N return $this->obsolete; } + public function isObsolete(): bool + { + return $this->getObsolete(); + } + /** * Get the link to the website of the article on the supplier's website. * @@ -199,7 +252,7 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N return $this->supplier_product_url; } - if (null === $this->getSupplier()) { + if (!$this->getSupplier() instanceof Supplier) { return ''; } @@ -209,8 +262,7 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N /** * Get all pricedetails. * - * @return Pricedetail[]|Collection all pricedetails as a one-dimensional array of Pricedetails objects, - * sorted by minimum discount quantity + * @return Collection */ public function getPricedetails(): Collection { @@ -221,8 +273,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N * Adds a price detail to this orderdetail. * * @param Pricedetail $pricedetail The pricedetail to add - * - * @return Orderdetail */ public function addPricedetail(Pricedetail $pricedetail): self { @@ -234,8 +284,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N /** * Removes a price detail from this orderdetail. - * - * @return Orderdetail */ public function removePricedetail(Pricedetail $pricedetail): self { @@ -278,11 +326,8 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N * Setters * *********************************************************************************/ - /** * Sets a new part with which this orderdetail is associated. - * - * @return Orderdetail */ public function setPart(Part $part): self { @@ -293,8 +338,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N /** * Sets the new supplier associated with this orderdetail. - * - * @return Orderdetail */ public function setSupplier(Supplier $new_supplier): self { @@ -307,9 +350,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N * Set the supplier part-nr. * * @param string $new_supplierpartnr the new supplier-part-nr - * - * @return Orderdetail - * @return Orderdetail */ public function setSupplierpartnr(string $new_supplierpartnr): self { @@ -322,9 +362,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N * Set if the part is obsolete at the supplier of that orderdetails. * * @param bool $new_obsolete true means that this part is obsolete - * - * @return Orderdetail - * @return Orderdetail */ public function setObsolete(bool $new_obsolete): self { @@ -338,8 +375,6 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N * Set this to "", if the function getSupplierProductURL should return the automatic generated URL. * * @param string $new_url The new URL for the supplier URL - * - * @return Orderdetail */ public function setSupplierProductUrl(string $new_url): self { diff --git a/src/Entity/PriceInformations/Pricedetail.php b/src/Entity/PriceInformations/Pricedetail.php index b1ac5155..86a7bcd5 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -22,6 +22,15 @@ declare(strict_types=1); namespace App\Entity\PriceInformations; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Contracts\TimeStampableInterface; @@ -29,75 +38,87 @@ use App\Validator\Constraints\BigDecimal\BigDecimalPositive; use App\Validator\Constraints\Selectable; use Brick\Math\BigDecimal; use Brick\Math\RoundingMode; -use DateTime; +use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Validator\Constraints as Assert; /** * Class Pricedetail. - * - * @ORM\Entity() - * @ORM\Table("`pricedetails`", indexes={ - * @ORM\Index(name="pricedetails_idx_min_discount", columns={"min_discount_quantity"}), - * @ORM\Index(name="pricedetails_idx_min_discount_price_qty", columns={"min_discount_quantity", "price_related_quantity"}), - * }) - * @ORM\HasLifecycleCallbacks() - * @UniqueEntity(fields={"min_discount_quantity", "orderdetail"}) */ +#[UniqueEntity(fields: ['min_discount_quantity', 'orderdetail'])] +#[ORM\Entity] +#[ORM\HasLifecycleCallbacks] +#[ORM\Table('`pricedetails`')] +#[ORM\Index(columns: ['min_discount_quantity'], name: 'pricedetails_idx_min_discount')] +#[ORM\Index(columns: ['min_discount_quantity', 'price_related_quantity'], name: 'pricedetails_idx_min_discount_price_qty')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@parts.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['pricedetail:read', 'pricedetail:read:standalone', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['pricedetail:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiFilter(PropertyFilter::class)] class Pricedetail extends AbstractDBElement implements TimeStampableInterface { use TimestampTrait; - public const PRICE_PRECISION = 5; + final public const PRICE_PRECISION = 5; /** * @var BigDecimal The price related to the detail. (Given in the selected currency) - * @ORM\Column(type="big_decimal", precision=11, scale=5) - * @BigDecimalPositive() - * @Groups({"extended", "full"}) */ + #[Groups(['extended', 'full', 'import', 'pricedetail:read', 'pricedetail:write'])] + #[ORM\Column(type: 'big_decimal', precision: 11, scale: 5)] + #[BigDecimalPositive] protected BigDecimal $price; /** * @var ?Currency The currency used for the current price information. - * If this is null, the global base unit is assumed. - * @ORM\ManyToOne(targetEntity="Currency", inversedBy="pricedetails") - * @ORM\JoinColumn(name="id_currency", referencedColumnName="id", nullable=true) - * @Selectable() - * @Groups({"extended", "full", "import"}) + * If this is null, the global base unit is assumed */ + #[Groups(['extended', 'full', 'import', 'pricedetail:read', 'pricedetail:write'])] + #[ORM\ManyToOne(targetEntity: Currency::class, inversedBy: 'pricedetails')] + #[ORM\JoinColumn(name: 'id_currency')] + #[Selectable] protected ?Currency $currency = null; /** - * @var float - * @ORM\Column(type="float") - * @Assert\Positive() - * @Groups({"extended", "full", "import"}) + * @var float The amount/quantity for which the price is for (in part unit) */ + #[Assert\Positive] + #[Groups(['extended', 'full', 'import', 'pricedetail:read', 'pricedetail:write'])] + #[ORM\Column(type: Types::FLOAT)] protected float $price_related_quantity = 1.0; /** - * @var float - * @ORM\Column(type="float") - * @Assert\Positive() - * @Groups({"extended", "full", "import"}) + * @var float The minimum amount/quantity, which is needed to get this discount (in part unit) */ + #[Assert\Positive] + #[Groups(['extended', 'full', 'import', 'pricedetail:read', 'pricedetail:write'])] + #[ORM\Column(type: Types::FLOAT)] protected float $min_discount_quantity = 1.0; /** * @var bool - * @ORM\Column(type="boolean") */ + #[ORM\Column(type: Types::BOOLEAN)] protected bool $manual_input = true; /** * @var Orderdetail|null - * @ORM\ManyToOne(targetEntity="Orderdetail", inversedBy="pricedetails") - * @ORM\JoinColumn(name="orderdetails_id", referencedColumnName="id", nullable=false, onDelete="CASCADE") - * @Assert\NotNull() */ + #[Assert\NotNull] + #[ORM\ManyToOne(targetEntity: Orderdetail::class, inversedBy: 'pricedetails')] + #[ORM\JoinColumn(name: 'orderdetails_id', nullable: false, onDelete: 'CASCADE')] + #[Groups(['pricedetail:read:standalone', 'pricedetail:write'])] protected ?Orderdetail $orderdetail = null; public function __construct() @@ -115,15 +136,14 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface /** * Helper for updating the timestamp. It is automatically called by doctrine before persisting. - * - * @ORM\PrePersist - * @ORM\PreUpdate */ + #[ORM\PrePersist] + #[ORM\PreUpdate] public function updateTimestamps(): void { - $this->lastModified = new DateTime('now'); - if (null === $this->addedDate) { - $this->addedDate = new DateTime('now'); + $this->lastModified = new DateTimeImmutable('now'); + if (!$this->addedDate instanceof \DateTimeInterface) { + $this->addedDate = new DateTimeImmutable('now'); } if ($this->orderdetail instanceof Orderdetail) { @@ -169,7 +189,9 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface * * @return BigDecimal the price as a bcmath string */ - public function getPricePerUnit($multiplier = 1.0): BigDecimal + #[Groups(['pricedetail:read'])] + #[SerializedName('price_per_unit')] + public function getPricePerUnit(float|string|BigDecimal $multiplier = 1.0): BigDecimal { $tmp = BigDecimal::of($multiplier); $tmp = $tmp->multipliedBy($this->price); @@ -230,6 +252,18 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface return $this->currency; } + /** + * Returns the ISO code of the currency associated with this price information, or null if no currency is selected. + * Then the global base currency should be assumed. + * @return string|null + */ + #[Groups(['pricedetail:read'])] + #[SerializedName('currency_iso_code')] + public function getCurrencyISOCode(): ?string + { + return $this->currency?->getIsoCode(); + } + /******************************************************************************** * * Setters @@ -251,8 +285,6 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface /** * Sets the currency associated with the price information. * Set to null, to use the global base currency. - * - * @return Pricedetail */ public function setCurrency(?Currency $currency): self { @@ -266,9 +298,9 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface * * @param BigDecimal $new_price the new price as a float number * - * * This is the price for "price_related_quantity" parts!! - * * Example: if "price_related_quantity" is '10', - * you have to set here the price for 10 parts! + * This is the price for "price_related_quantity" parts!! + * Example: if "price_related_quantity" is 10, + * you have to set here the price for 10 parts! * * @return $this */ diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index f6089687..a103d694 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -22,6 +22,24 @@ declare(strict_types=1); namespace App\Entity\ProjectSystem; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Repository\Parts\DeviceRepository; +use App\Validator\Constraints\UniqueObjectCollection; +use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\ProjectAttachment; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\ProjectParameter; @@ -35,76 +53,115 @@ use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** - * Class AttachmentType. + * This class represents a project in the database. * - * @ORM\Entity(repositoryClass="App\Repository\Parts\DeviceRepository") - * @ORM\Table(name="projects") + * @extends AbstractStructuralDBElement */ +#[ORM\Entity(repositoryClass: DeviceRepository::class)] +#[ORM\Table(name: 'projects')] +#[ApiResource( + operations: [ + new Get(security: 'is_granted("read", object)'), + new GetCollection(security: 'is_granted("@projects.read")'), + new Post(securityPostDenormalize: 'is_granted("create", object)'), + new Patch(security: 'is_granted("edit", object)'), + new Delete(security: 'is_granted("delete", object)'), + ], + normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['project:write', 'api:basic:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/projects/{id}/children.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the children elements of a project.'), + security: 'is_granted("@projects.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'children', fromClass: Project::class) + ], + normalizationContext: ['groups' => ['project:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment"])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] class Project extends AbstractStructuralDBElement { - /** - * @ORM\OneToMany(targetEntity="Project", mappedBy="parent") - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + #[Groups(['project:read', 'project:write'])] + #[ApiProperty(readableLink: false, writableLink: false)] + protected ?AbstractStructuralDBElement $parent = null; + + #[Groups(['project:read', 'project:write'])] + protected string $comment = ''; /** - * @ORM\ManyToOne(targetEntity="Project", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") + * @var Collection */ - protected $parent; + #[Assert\Valid] + #[Groups(['extended', 'full', 'import'])] + #[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part'])] + #[UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name'])] + protected Collection $bom_entries; - /** - * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="project", cascade={"persist", "remove"}, orphanRemoval=true) - * @Assert\Valid() - * @Groups({"extended", "full"}) - */ - protected $bom_entries; - - /** - * @ORM\Column(type="integer") - */ + #[ORM\Column(type: Types::INTEGER)] protected int $order_quantity = 0; /** - * @var string The current status of the project - * @ORM\Column(type="string", length=64, nullable=true) - * @Assert\Choice({"draft","planning","in_production","finished","archived"}) - * @Groups({"extended", "full"}) + * @var string|null The current status of the project */ + #[Assert\Choice(['draft', 'planning', 'in_production', 'finished', 'archived'])] + #[Groups(['extended', 'full', 'project:read', 'project:write', 'import'])] + #[ORM\Column(type: Types::STRING, length: 64, nullable: true)] protected ?string $status = null; /** * @var Part|null The (optional) part that represents the builds of this project in the stock - * @ORM\OneToOne(targetEntity="App\Entity\Parts\Part", mappedBy="built_project", cascade={"persist"}, orphanRemoval=true) */ + #[ORM\OneToOne(mappedBy: 'built_project', targetEntity: Part::class, cascade: ['persist'], orphanRemoval: true)] + #[Groups(['project:read', 'project:write'])] protected ?Part $build_part = null; - /** - * @ORM\Column(type="boolean") - */ + #[ORM\Column(type: Types::BOOLEAN)] protected bool $order_only_missing_parts = false; - /** - * @ORM\Column(type="text", nullable=false) - * @Groups({"simple", "extended", "full"}) - */ + #[Groups(['simple', 'extended', 'full', 'project:read', 'project:write'])] + #[ORM\Column(type: Types::TEXT)] protected string $description = ''; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\ProjectAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) */ - protected $attachments; + #[ORM\OneToMany(mappedBy: 'element', targetEntity: ProjectAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['project:read', 'project:write'])] + protected Collection $attachments; + + #[ORM\ManyToOne(targetEntity: ProjectAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['project:read', 'project:write'])] + protected ?Attachment $master_picture_attachment = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\ProjectParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) */ - protected $parameters; + #[ORM\OneToMany(mappedBy: 'element', targetEntity: ProjectParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + #[Groups(['project:read', 'project:write'])] + protected Collection $parameters; + + #[Groups(['project:read'])] + protected ?\DateTimeImmutable $addedDate = null; + #[Groups(['project:read'])] + protected ?\DateTimeImmutable $lastModified = null; + /******************************************************************************** * @@ -114,8 +171,11 @@ class Project extends AbstractStructuralDBElement public function __construct() { + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); parent::__construct(); $this->bom_entries = new ArrayCollection(); + $this->children = new ArrayCollection(); } public function __clone() @@ -127,7 +187,7 @@ class Project extends AbstractStructuralDBElement //Set master attachment is needed foreach ($bom_entries as $bom_entry) { $clone = clone $bom_entry; - $this->bom_entries->add($clone); + $this->addBomEntry($clone); } } @@ -182,8 +242,6 @@ class Project extends AbstractStructuralDBElement * Set the "order_only_missing_parts" attribute. * * @param bool $new_order_only_missing_parts the new "order_only_missing_parts" attribute - * - * @return Project */ public function setOrderOnlyMissingParts(bool $new_order_only_missing_parts): self { @@ -192,16 +250,12 @@ class Project extends AbstractStructuralDBElement return $this; } - /** - * @return Collection|ProjectBOMEntry[] - */ public function getBomEntries(): Collection { return $this->bom_entries; } /** - * @param ProjectBOMEntry $entry * @return $this */ public function addBomEntry(ProjectBOMEntry $entry): self @@ -212,7 +266,6 @@ class Project extends AbstractStructuralDBElement } /** - * @param ProjectBOMEntry $entry * @return $this */ public function removeBomEntry(ProjectBOMEntry $entry): self @@ -221,18 +274,11 @@ class Project extends AbstractStructuralDBElement return $this; } - /** - * @return string - */ public function getDescription(): string { return $this->description; } - /** - * @param string $description - * @return Project - */ public function setDescription(string $description): Project { $this->description = $description; @@ -257,16 +303,14 @@ class Project extends AbstractStructuralDBElement /** * Checks if this project has an associated part representing the builds of this project in the stock. - * @return bool */ public function hasBuildPart(): bool { - return $this->build_part !== null; + return $this->build_part instanceof Part; } /** * Gets the part representing the builds of this project in the stock, if it is existing - * @return Part|null */ public function getBuildPart(): ?Part { @@ -275,25 +319,21 @@ class Project extends AbstractStructuralDBElement /** * Sets the part representing the builds of this project in the stock. - * @param Part|null $build_part */ public function setBuildPart(?Part $build_part): void { $this->build_part = $build_part; - if ($build_part) { + if ($build_part instanceof Part) { $build_part->setBuiltProject($this); } } - /** - * @Assert\Callback - */ - public function validate(ExecutionContextInterface $context, $payload) + #[Assert\Callback] + public function validate(ExecutionContextInterface $context, $payload): void { //If this project has subprojects, and these have builds part, they must be included in the BOM foreach ($this->getChildren() as $child) { - /** @var $child Project */ - if ($child->getBuildPart() === null) { + if (!$child->getBuildPart() instanceof Part) { continue; } //We have to search all bom entries for the build part diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index b48b3f50..2a7862ec 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -22,6 +22,22 @@ declare(strict_types=1); namespace App\Entity\ProjectSystem; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Doctrine\Orm\Filter\RangeFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Contracts\TimeStampableInterface; +use App\Validator\UniqueValidatableInterface; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Parts\Part; @@ -30,120 +46,127 @@ use App\Validator\Constraints\BigDecimal\BigDecimalPositive; use App\Validator\Constraints\Selectable; use Brick\Math\BigDecimal; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * The ProjectBOMEntry class represents an entry in a project's BOM. - * - * @ORM\Table("project_bom_entries") - * @ORM\HasLifecycleCallbacks() - * @ORM\Entity() - * @UniqueEntity(fields={"part", "project"}, message="project.bom_entry.part_already_in_bom") - * @UniqueEntity(fields={"name", "project"}, message="project.bom_entry.name_already_in_bom", ignoreNull=true) */ -class ProjectBOMEntry extends AbstractDBElement +#[ORM\HasLifecycleCallbacks] +#[ORM\Entity] +#[ORM\Table('project_bom_entries')] +#[ApiResource( + operations: [ + new Get(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("read", object)',), + new GetCollection(uriTemplate: '/project_bom_entries.{_format}', security: 'is_granted("@projects.read")',), + new Post(uriTemplate: '/project_bom_entries.{_format}', securityPostDenormalize: 'is_granted("create", object)',), + new Patch(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("edit", object)',), + new Delete(uriTemplate: '/project_bom_entries/{id}.{_format}', security: 'is_granted("delete", object)',), + ], + normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + denormalizationContext: ['groups' => ['bom_entry:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], +)] +#[ApiResource( + uriTemplate: '/projects/{id}/bom.{_format}', + operations: [ + new GetCollection( + openapi: new Operation(summary: 'Retrieves the BOM entries of the given project.'), + security: 'is_granted("@projects.read")' + ) + ], + uriVariables: [ + 'id' => new Link(fromProperty: 'bom_entries', fromClass: Project::class) + ], + normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'] +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])] +#[ApiFilter(RangeFilter::class, properties: ['quantity'])] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified', 'quantity'])] +class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInterface, TimeStampableInterface { use TimestampTrait; - /** - * @var float - * @ORM\Column(type="float", name="quantity") - * @Assert\Positive() - */ - protected float $quantity; + #[Assert\Positive] + #[ORM\Column(name: 'quantity', type: Types::FLOAT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] + protected float $quantity = 1.0; /** * @var string A comma separated list of the names, where this parts should be placed - * @ORM\Column(type="text", name="mountnames") */ + #[ORM\Column(name: 'mountnames', type: Types::TEXT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected string $mountnames = ''; /** - * @var string An optional name describing this BOM entry (useful for non-part entries) - * @ORM\Column(type="string", nullable=true) - * @Assert\Expression( - * "this.getPart() !== null or this.getName() !== null", - * message="validator.project.bom_entry.name_or_part_needed" - * ) + * @var string|null An optional name describing this BOM entry (useful for non-part entries) */ + #[Assert\Expression('this.getPart() !== null or this.getName() !== null', message: 'validator.project.bom_entry.name_or_part_needed')] + #[ORM\Column(type: Types::STRING, nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])] protected ?string $name = null; /** * @var string An optional comment for this BOM entry - * @ORM\Column(type="text") */ - protected string $comment; + #[ORM\Column(type: Types::TEXT)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + protected string $comment = ''; /** - * @var Project - * @ORM\ManyToOne(targetEntity="Project", inversedBy="bom_entries") - * @ORM\JoinColumn(name="id_device", referencedColumnName="id") + * @var Project|null */ + #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'bom_entries')] + #[ORM\JoinColumn(name: 'id_device')] + #[Groups(['bom_entry:read', 'bom_entry:write', ])] protected ?Project $project = null; /** * @var Part|null The part associated with this - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part", inversedBy="project_bom_entries") - * @ORM\JoinColumn(name="id_part", referencedColumnName="id", nullable=true) */ + #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'project_bom_entries')] + #[ORM\JoinColumn(name: 'id_part')] + #[Groups(['bom_entry:read', 'bom_entry:write', 'full'])] protected ?Part $part = null; /** - * @var BigDecimal The price of this non-part BOM entry - * @ORM\Column(type="big_decimal", precision=11, scale=5, nullable=true) - * @Assert\AtLeastOneOf({ - * @BigDecimalPositive(), - * @Assert\IsNull() - * }) + * @var BigDecimal|null The price of this non-part BOM entry */ - protected ?BigDecimal $price; + #[Assert\AtLeastOneOf([new BigDecimalPositive(), new Assert\IsNull()])] + #[ORM\Column(type: 'big_decimal', precision: 11, scale: 5, nullable: true)] + #[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'extended', 'full'])] + protected ?BigDecimal $price = null; /** * @var ?Currency The currency for the price of this non-part BOM entry - * @ORM\ManyToOne(targetEntity="App\Entity\PriceInformations\Currency") - * @ORM\JoinColumn(nullable=true) - * @Selectable() */ + #[ORM\ManyToOne(targetEntity: Currency::class)] + #[ORM\JoinColumn] + #[Selectable] protected ?Currency $price_currency = null; public function __construct() { - //$this->price = BigDecimal::zero()->toScale(5); - $this->price = null; } - /** - * @return float - */ public function getQuantity(): float { return $this->quantity; } - /** - * @param float $quantity - * @return ProjectBOMEntry - */ public function setQuantity(float $quantity): ProjectBOMEntry { $this->quantity = $quantity; return $this; } - /** - * @return string - */ public function getMountnames(): string { return $this->mountnames; } - /** - * @param string $mountnames - * @return ProjectBOMEntry - */ public function setMountnames(string $mountnames): ProjectBOMEntry { $this->mountnames = $mountnames; @@ -160,7 +183,6 @@ class ProjectBOMEntry extends AbstractDBElement /** * @param string $name - * @return ProjectBOMEntry */ public function setName(?string $name): ProjectBOMEntry { @@ -168,36 +190,22 @@ class ProjectBOMEntry extends AbstractDBElement return $this; } - /** - * @return string - */ public function getComment(): string { return $this->comment; } - /** - * @param string $comment - * @return ProjectBOMEntry - */ public function setComment(string $comment): ProjectBOMEntry { $this->comment = $comment; return $this; } - /** - * @return Project|null - */ public function getProject(): ?Project { return $this->project; } - /** - * @param Project|null $project - * @return ProjectBOMEntry - */ public function setProject(?Project $project): ProjectBOMEntry { $this->project = $project; @@ -206,18 +214,11 @@ class ProjectBOMEntry extends AbstractDBElement - /** - * @return Part|null - */ public function getPart(): ?Part { return $this->part; } - /** - * @param Part|null $part - * @return ProjectBOMEntry - */ public function setPart(?Part $part): ProjectBOMEntry { $this->part = $part; @@ -227,7 +228,6 @@ class ProjectBOMEntry extends AbstractDBElement /** * Returns the price of this BOM entry, if existing. * Prices are only valid on non-Part BOM entries. - * @return BigDecimal|null */ public function getPrice(): ?BigDecimal { @@ -237,24 +237,17 @@ class ProjectBOMEntry extends AbstractDBElement /** * Sets the price of this BOM entry. * Prices are only valid on non-Part BOM entries. - * @param BigDecimal|null $price */ public function setPrice(?BigDecimal $price): void { $this->price = $price; } - /** - * @return Currency|null - */ public function getPriceCurrency(): ?Currency { return $this->price_currency; } - /** - * @param Currency|null $price_currency - */ public function setPriceCurrency(?Currency $price_currency): void { $this->price_currency = $price_currency; @@ -266,22 +259,18 @@ class ProjectBOMEntry extends AbstractDBElement */ public function isPartBomEntry(): bool { - return $this->part !== null; + return $this->part instanceof Part; } - /** - * @Assert\Callback - */ + #[Assert\Callback] public function validate(ExecutionContextInterface $context, $payload): void { //Round quantity to whole numbers, if the part is not a decimal part - if ($this->part) { - if (!$this->part->getPartUnit() || $this->part->getPartUnit()->isInteger()) { - $this->quantity = round($this->quantity); - } + if ($this->part instanceof Part && (!$this->part->getPartUnit() || $this->part->getPartUnit()->isInteger())) { + $this->quantity = round($this->quantity); } //Non-Part BOM entries are rounded - if ($this->part === null) { + if (!$this->part instanceof Part) { $this->quantity = round($this->quantity); } @@ -298,21 +287,21 @@ class ProjectBOMEntry extends AbstractDBElement } //Check that the number of mountnames is the same as the (rounded) quantity - if (!empty($this->mountnames) && count($uniq_mountnames) !== (int) round ($this->quantity)) { + if ($this->mountnames !== '' && count($uniq_mountnames) !== (int) round ($this->quantity)) { $context->buildViolation('project.bom_entry.mountnames_quantity_mismatch') ->atPath('mountnames') ->addViolation(); } //Prices are only allowed on non-part BOM entries - if ($this->part !== null && $this->price !== null) { + if ($this->part instanceof Part && $this->price instanceof BigDecimal) { $context->buildViolation('project.bom_entry.price_not_allowed_on_parts') ->atPath('price') ->addViolation(); } //Check that the part is not the build representation part of this device or one of its parents - if ($this->part && $this->part->getBuiltProject() !== null) { + if ($this->part && $this->part->getBuiltProject() instanceof Project) { //Get the associated project $associated_project = $this->part->getBuiltProject(); //Check that it is not the same as the current project neither one of its parents @@ -329,4 +318,11 @@ class ProjectBOMEntry extends AbstractDBElement } + public function getComparableFields(): array + { + return [ + 'name' => $this->getName(), + 'part' => $this->getPart()?->getID(), + ]; + } } diff --git a/src/Entity/UserSystem/ApiToken.php b/src/Entity/UserSystem/ApiToken.php new file mode 100644 index 00000000..f5cbf541 --- /dev/null +++ b/src/Entity/UserSystem/ApiToken.php @@ -0,0 +1,199 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\UserSystem; + +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\Entity\Base\TimestampTrait; +use App\Entity\Contracts\TimeStampableInterface; +use App\Repository\UserSystem\ApiTokenRepository; +use App\State\CurrentApiTokenProvider; +use App\Validator\Constraints\Year2038BugWorkaround; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; + +#[ORM\Entity(repositoryClass: ApiTokenRepository::class)] +#[ORM\Table(name: 'api_tokens')] +#[ORM\HasLifecycleCallbacks] +#[UniqueEntity(fields: ['name', 'user'])] + +#[ApiResource( + uriTemplate: '/tokens/current.{_format}', + description: 'A token used to authenticate API requests.', + operations: [new Get( + openapi: new Operation(summary: 'Get information about the API token that is currently used.'), + )], + normalizationContext: ['groups' => ['token:read', 'api:basic:read'], 'openapi_definition_name' => 'Read'], + provider: CurrentApiTokenProvider::class, +)] +#[ApiFilter(PropertyFilter::class)] +class ApiToken implements TimeStampableInterface +{ + + use TimestampTrait; + + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\GeneratedValue] + protected int $id; + + #[ORM\Column(type: Types::STRING)] + #[Length(max: 255)] + #[NotBlank] + #[Groups('token:read')] + protected string $name = ''; + + #[ORM\ManyToOne(inversedBy: 'api_tokens')] + #[Groups('token:read')] + private ?User $user = null; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Groups('token:read')] + #[Year2038BugWorkaround] + private ?\DateTimeImmutable $valid_until; + + #[ORM\Column(length: 68, unique: true)] + private string $token; + + #[ORM\Column(type: Types::SMALLINT, enumType: ApiTokenLevel::class)] + #[Groups('token:read')] + private ApiTokenLevel $level = ApiTokenLevel::READ_ONLY; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Groups('token:read')] + private ?\DateTimeImmutable $last_time_used = null; + + public function __construct(ApiTokenType $tokenType = ApiTokenType::PERSONAL_ACCESS_TOKEN) + { + // Generate a rondom token on creation. The tokenType is 3 characters long (plus underscore), so the token is 68 characters long. + $this->token = $tokenType->getTokenPrefix() . bin2hex(random_bytes(32)); + + //By default, tokens are valid for 1 year. + $this->valid_until = new \DateTimeImmutable('+1 year'); + } + + public function getTokenType(): ApiTokenType + { + return ApiTokenType::getTypeFromToken($this->token); + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): ApiToken + { + $this->user = $user; + return $this; + } + + public function getValidUntil(): ?\DateTimeImmutable + { + return $this->valid_until; + } + + /** + * Checks if the token is still valid. + * @return bool + */ + public function isValid(): bool + { + return $this->valid_until === null || $this->valid_until > new \DateTimeImmutable(); + } + + public function setValidUntil(?\DateTimeImmutable $valid_until): ApiToken + { + $this->valid_until = $valid_until; + return $this; + } + + public function getToken(): string + { + return $this->token; + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): ApiToken + { + $this->name = $name; + return $this; + } + + /** + * Gets the last time the token was used to authenticate or null if it was never used. + */ + public function getLastTimeUsed(): ?\DateTimeImmutable + { + return $this->last_time_used; + } + + /** + * Sets the last time the token was used to authenticate. + * @return ApiToken + */ + public function setLastTimeUsed(?\DateTimeImmutable $last_time_used): ApiToken + { + $this->last_time_used = $last_time_used; + return $this; + } + + public function getLevel(): ApiTokenLevel + { + return $this->level; + } + + public function setLevel(ApiTokenLevel $level): ApiToken + { + $this->level = $level; + return $this; + } + + /** + * Returns the last 4 characters of the token secret, which can be used to identify the token. + * @return string + */ + public function getLastTokenChars(): string + { + return substr($this->token, -4); + } + + +} \ No newline at end of file diff --git a/src/Entity/UserSystem/ApiTokenLevel.php b/src/Entity/UserSystem/ApiTokenLevel.php new file mode 100644 index 00000000..3f997300 --- /dev/null +++ b/src/Entity/UserSystem/ApiTokenLevel.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\UserSystem; + +enum ApiTokenLevel: int +{ + private const ROLE_READ_ONLY = 'ROLE_API_READ_ONLY'; + private const ROLE_EDIT = 'ROLE_API_EDIT'; + private const ROLE_ADMIN = 'ROLE_API_ADMIN'; + private const ROLE_FULL = 'ROLE_API_FULL'; + + /** + * The token can only read (non-sensitive) data. + */ + case READ_ONLY = 1; + /** + * The token can read and edit (non-sensitive) data. + */ + case EDIT = 2; + /** + * The token can do some administrative tasks (like viewing all log entries), but can not change passwords and create new tokens. + */ + case ADMIN = 3; + /** + * The token can do everything the user can do. + */ + case FULL = 4; + + /** + * Returns the additional roles that the authenticated user should have when using this token. + * @return string[] + */ + public function getAdditionalRoles(): array + { + //The higher roles should always include the lower ones + return match ($this) { + self::READ_ONLY => [self::ROLE_READ_ONLY], + self::EDIT => [self::ROLE_READ_ONLY, self::ROLE_EDIT], + self::ADMIN => [self::ROLE_READ_ONLY, self::ROLE_EDIT, self::ROLE_ADMIN], + self::FULL => [self::ROLE_READ_ONLY, self::ROLE_EDIT, self::ROLE_ADMIN, self::ROLE_FULL], + }; + } + + /** + * Returns the translation key for the name of this token level. + * @return string + */ + public function getTranslationKey(): string + { + return 'api_token.level.' . strtolower($this->name); + } +} \ No newline at end of file diff --git a/src/Entity/UserSystem/ApiTokenType.php b/src/Entity/UserSystem/ApiTokenType.php new file mode 100644 index 00000000..f8beb378 --- /dev/null +++ b/src/Entity/UserSystem/ApiTokenType.php @@ -0,0 +1,56 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\UserSystem; + +/** + * The type of ApiToken. + * The enum value is the prefix of the token. It must be 3 characters long. + */ +enum ApiTokenType: string +{ + case PERSONAL_ACCESS_TOKEN = 'tcp'; + + /** + * Get the prefix of the token including the underscore + * @return string + */ + public function getTokenPrefix(): string + { + return $this->value . '_'; + } + + /** + * Get the type from the token prefix + * @param string $api_token + * @return ApiTokenType + */ + public static function getTypeFromToken(string $api_token): ApiTokenType + { + $parts = explode('_', $api_token); + if (count($parts) !== 2) { + throw new \InvalidArgumentException('Invalid token format'); + } + return self::from($parts[0]); + } +} diff --git a/src/Entity/UserSystem/Group.php b/src/Entity/UserSystem/Group.php index 30bec19c..6da9d35f 100644 --- a/src/Entity/UserSystem/Group.php +++ b/src/Entity/UserSystem/Group.php @@ -22,6 +22,10 @@ declare(strict_types=1); namespace App\Entity\UserSystem; +use Doctrine\Common\Collections\Criteria; +use App\Entity\Attachments\Attachment; +use App\Validator\Constraints\NoLockout; +use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\GroupAttachment; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\GroupParameter; @@ -34,69 +38,71 @@ use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; /** - * This entity represents an user group. + * This entity represents a user group. * - * @ORM\Entity() - * @ORM\Table("`groups`", indexes={ - * @ORM\Index(name="group_idx_name", columns={"name"}), - * @ORM\Index(name="group_idx_parent_name", columns={"parent_id", "name"}), - * }) + * @extends AbstractStructuralDBElement */ +#[ORM\Entity] +#[ORM\Table('`groups`')] +#[ORM\Index(columns: ['name'], name: 'group_idx_name')] +#[ORM\Index(columns: ['parent_id', 'name'], name: 'group_idx_parent_name')] +#[NoLockout] class Group extends AbstractStructuralDBElement implements HasPermissionsInterface { - /** - * @ORM\OneToMany(targetEntity="Group", mappedBy="parent") - * @ORM\OrderBy({"name" = "ASC"}) - * @var Collection - */ - protected $children; + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $children; + + #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] + #[ORM\JoinColumn(name: 'parent_id')] + protected ?AbstractStructuralDBElement $parent = null; /** - * @ORM\ManyToOne(targetEntity="Group", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") + * @var Collection */ - protected $parent; + #[ORM\OneToMany(mappedBy: 'group', targetEntity: User::class)] + protected Collection $users; /** - * @ORM\OneToMany(targetEntity="User", mappedBy="group") - * @var Collection + * @var bool If true all users associated with this group must have enabled some kind of two-factor authentication */ - protected $users; + #[Groups(['extended', 'full', 'import'])] + #[ORM\Column(name: 'enforce_2fa', type: Types::BOOLEAN)] + protected bool $enforce2FA = false; - /** - * @var bool If true all users associated with this group must have enabled some kind of 2 factor authentication - * @ORM\Column(type="boolean", name="enforce_2fa") - * @Groups({"extended", "full", "import"}) - */ - protected $enforce2FA = false; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\GroupAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) - * @Assert\Valid() */ - protected $attachments; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: GroupAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + protected Collection $attachments; - /** - * @var PermissionData|null - * @ValidPermission() - * @ORM\Embedded(class="PermissionData", columnPrefix="permissions_") - * @Groups({"full"}) - */ + #[ORM\ManyToOne(targetEntity: GroupAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + protected ?Attachment $master_picture_attachment = null; + + #[Groups(['full'])] + #[ORM\Embedded(class: PermissionData::class, columnPrefix: 'permissions_')] + #[ValidPermission] protected ?PermissionData $permissions = null; - /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\GroupParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) - * @Assert\Valid() + /** + * @var Collection */ - protected $parameters; + #[Assert\Valid] + #[ORM\OneToMany(mappedBy: 'element', targetEntity: GroupParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])] + protected Collection $parameters; public function __construct() { + $this->attachments = new ArrayCollection(); + $this->parameters = new ArrayCollection(); parent::__construct(); $this->permissions = new PermissionData(); $this->users = new ArrayCollection(); + $this->children = new ArrayCollection(); } /** @@ -123,7 +129,7 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa public function getPermissions(): PermissionData { - if ($this->permissions === null) { + if (!$this->permissions instanceof PermissionData) { $this->permissions = new PermissionData(); } diff --git a/src/Entity/UserSystem/PermissionData.php b/src/Entity/UserSystem/PermissionData.php index 121e7f5f..9ebdc9c9 100644 --- a/src/Entity/UserSystem/PermissionData.php +++ b/src/Entity/UserSystem/PermissionData.php @@ -1,4 +1,7 @@ . */ - namespace App\Entity\UserSystem; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * This class is used to store the permissions of a user. * This has to be an embeddable or otherwise doctrine could not track the changes of the underlying data array (which is serialized to JSON in the database) - * - * @ORM\Embeddable() + * @see \App\Tests\Entity\UserSystem\PermissionDataTest */ +#[ORM\Embeddable] final class PermissionData implements \JsonSerializable { /** @@ -40,29 +43,24 @@ final class PermissionData implements \JsonSerializable /** * The current schema version of the permission data */ - public const CURRENT_SCHEMA_VERSION = 2; - - /** - * @var array This array contains the permission values for each permission - * This array contains the permission values for each permission, in the form of: - * permission => [ - * operation => value, - * ] - * @ORM\Column(type="json", name="data") - */ - protected ?array $data = [ - //$ prefixed entries are used for metadata - '$ver' => self::CURRENT_SCHEMA_VERSION, //The schema version of the permission data - ]; + public const CURRENT_SCHEMA_VERSION = 3; /** * Creates a new Permission Data Instance using the given data. - * By default, a empty array is used, meaning + * By default, an empty array is used, meaning */ - public function __construct(array $data = []) + public function __construct( + /** + * @var array This array contains the permission values for each permission + * This array contains the permission values for each permission, in the form of: + * permission => [ + * operation => value, + * ] + */ + #[ORM\Column(name: 'data', type: Types::JSON)] + protected array $data = [] + ) { - $this->data = $data; - //If the passed data did not contain a schema version, we set it to the current version if (!isset($this->data['$ver'])) { $this->data['$ver'] = self::CURRENT_SCHEMA_VERSION; @@ -71,8 +69,6 @@ final class PermissionData implements \JsonSerializable /** * Checks if any of the operations of the given permission is defined (meaning it is either ALLOW or DENY) - * @param string $permission - * @return bool */ public function isAnyOperationOfPermissionSet(string $permission): bool { @@ -81,7 +77,6 @@ final class PermissionData implements \JsonSerializable /** * Returns an associative array containing all defined (non-INHERIT) operations of the given permission. - * @param string $permission * @return array An array in the form ["operation" => value], returns an empty array if no operations are defined */ public function getAllDefinedOperationsOfPermission(string $permission): array @@ -96,8 +91,6 @@ final class PermissionData implements \JsonSerializable /** * Sets all operations of the given permission via the given array. * The data is an array in the form [$operation => $value], all existing values will be overwritten/deleted. - * @param string $permission - * @param array $data * @return $this */ public function setAllOperationsOfPermission(string $permission, array $data): self @@ -109,7 +102,6 @@ final class PermissionData implements \JsonSerializable /** * Removes a whole permission from the data including all operations (effectivly setting them to INHERIT) - * @param string $permission * @return $this */ public function removePermission(string $permission): self @@ -121,14 +113,12 @@ final class PermissionData implements \JsonSerializable /** * Check if a permission value is set for the given permission and operation (meaning there value is not inherit). - * @param string $permission - * @param string $operation * @return bool True if the permission value is set, false otherwise */ public function isPermissionSet(string $permission, string $operation): bool { //We cannot access metadata via normal permission data - if (strpos($permission, '$') !== false) { + if (str_contains($permission, '$')) { return false; } @@ -137,8 +127,6 @@ final class PermissionData implements \JsonSerializable /** * Returns the permission value for the given permission and operation. - * @param string $permission - * @param string $operation * @return bool|null True means allow, false means disallow, null means inherit */ public function getPermissionValue(string $permission, string $operation): ?bool @@ -153,15 +141,12 @@ final class PermissionData implements \JsonSerializable /** * Sets the permission value for the given permission and operation. - * @param string $permission - * @param string $operation - * @param bool|null $value * @return $this */ public function setPermissionValue(string $permission, string $operation, ?bool $value): self { - if ($value === null) { - //If the value is null, unset the permission value (meaning implicit inherit) + //If the value is null, unset the permission value, if it was set befoere (meaning implicit inherit) + if ($value === null && isset($this->data[$permission][$operation])) { unset($this->data[$permission][$operation]); } else { //Otherwise, set the pemission value @@ -186,8 +171,6 @@ final class PermissionData implements \JsonSerializable /** * Creates a new Permission Data Instance using the given JSON encoded data - * @param string $json - * @return static * @throws \JsonException */ public static function fromJSON(string $json): self @@ -203,9 +186,8 @@ final class PermissionData implements \JsonSerializable /** * Returns an JSON encodable representation of this object. - * @return array|mixed */ - public function jsonSerialize() + public function jsonSerialize(): array { $ret = []; @@ -216,9 +198,7 @@ final class PermissionData implements \JsonSerializable continue; } - $ret[$permission] = array_filter($operations, function ($value) { - return $value !== null; - }); + $ret[$permission] = array_filter($operations, static fn($value) => $value !== null); //If the permission has no operations, unset it if (empty($ret[$permission])) { @@ -240,7 +220,6 @@ final class PermissionData implements \JsonSerializable /** * Sets the schema version of this permission data - * @param int $new_version * @return $this */ public function setSchemaVersion(int $new_version): self @@ -253,4 +232,4 @@ final class PermissionData implements \JsonSerializable return $this; } -} \ No newline at end of file +} diff --git a/src/Entity/UserSystem/U2FKey.php b/src/Entity/UserSystem/U2FKey.php index 9c1c68a3..d1d864bc 100644 --- a/src/Entity/UserSystem/U2FKey.php +++ b/src/Entity/UserSystem/U2FKey.php @@ -22,20 +22,18 @@ declare(strict_types=1); namespace App\Entity\UserSystem; +use App\Entity\Contracts\TimeStampableInterface; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\TimestampTrait; use Doctrine\ORM\Mapping as ORM; use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface; +use Symfony\Component\Validator\Constraints\Length; -/** - * @ORM\Entity - * @ORM\Table(name="u2f_keys", - * uniqueConstraints={ - * @ORM\UniqueConstraint(name="user_unique",columns={"user_id", - * "key_handle"}) - * }) - * @ORM\HasLifecycleCallbacks() - */ -class U2FKey implements LegacyU2FKeyInterface +#[ORM\Entity] +#[ORM\HasLifecycleCallbacks] +#[ORM\Table(name: 'u2f_keys')] +#[ORM\UniqueConstraint(name: 'user_unique', columns: ['user_id', 'key_handle'])] +class U2FKey implements LegacyU2FKeyInterface, TimeStampableInterface { use TimestampTrait; @@ -43,50 +41,43 @@ class U2FKey implements LegacyU2FKeyInterface * We have to restrict the length here, as InnoDB only supports key index with max. 767 Bytes. * Max length of keyhandles should be 128. (According to U2F_MAX_KH_SIZE in FIDO example C code). * - * @ORM\Column(type="string", length=128) * * @var string **/ - public string $keyHandle; + #[ORM\Column(type: Types::STRING, length: 128)] + #[Length(max: 128)] + public string $keyHandle = ''; /** - * @ORM\Column(type="string") - * * @var string **/ - public string $publicKey; + #[ORM\Column(type: Types::STRING)] + public string $publicKey = ''; /** - * @ORM\Column(type="text") - * * @var string **/ - public string $certificate; + #[ORM\Column(type: Types::TEXT)] + public string $certificate = ''; /** - * @ORM\Column(type="string") - * - * @var int + * @var string **/ - public int $counter; + #[ORM\Column(type: Types::STRING)] + public string $counter = '0'; - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\GeneratedValue] protected int $id; /** - * @ORM\Column(type="string") - * * @var string **/ - protected string $name; + #[ORM\Column(type: Types::STRING)] + protected string $name = ''; - /** - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User", inversedBy="u2fKeys") - **/ + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'u2fKeys')] protected ?User $user = null; public function getKeyHandle(): string @@ -126,7 +117,7 @@ class U2FKey implements LegacyU2FKeyInterface return $this; } - public function getCounter(): int + public function getCounter(): string { return $this->counter; } @@ -151,9 +142,9 @@ class U2FKey implements LegacyU2FKeyInterface } /** - * Gets the user, this U2F key belongs to. + * Gets the user, this U2F key belongs to. */ - public function getUser(): User + public function getUser(): User|null { return $this->user; } diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 6c8aef60..b39bea4f 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -22,6 +22,23 @@ declare(strict_types=1); namespace App\Entity\UserSystem; +use Doctrine\Common\Collections\Criteria; +use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use App\ApiPlatform\Filter\LikeFilter; +use App\Entity\Attachments\Attachment; +use App\Repository\UserRepository; +use App\EntityListeners\TreeCacheInvalidationListener; +use App\Validator\Constraints\NoLockout; +use Doctrine\DBAL\Types\Types; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\UserAttachment; use App\Entity\Base\AbstractNamedDBElement; @@ -30,10 +47,11 @@ use App\Security\Interfaces\HasPermissionsInterface; use App\Validator\Constraints\Selectable; use App\Validator\Constraints\ValidPermission; use App\Validator\Constraints\ValidTheme; -use Hslavich\OneloginSamlBundle\Security\User\SamlUserInterface; use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface; +use Nbgrp\OneloginSamlBundle\Security\User\SamlUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints\Length; use Webauthn\PublicKeyCredentialUserEntity; use function count; use DateTime; @@ -53,15 +71,38 @@ use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface /** * This entity represents a user, which can log in and have permissions. - * Also this entity is able to save some informations about the user, like the names, email-address and other info. + * Also, this entity is able to save some information about the user, like the names, email-address and other info. + * @see \App\Tests\Entity\UserSystem\UserTest * - * @ORM\Entity(repositoryClass="App\Repository\UserRepository") - * @ORM\Table("`users`", indexes={ - * @ORM\Index(name="user_idx_username", columns={"name"}) - * }) - * @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"}) - * @UniqueEntity("name", message="validator.user.username_already_used") + * @extends AttachmentContainingDBElement */ +#[UniqueEntity('name', message: 'validator.user.username_already_used')] +#[ORM\Entity(repositoryClass: UserRepository::class)] +#[ORM\EntityListeners([TreeCacheInvalidationListener::class])] +#[ORM\Table('`users`')] +#[ORM\Index(columns: ['name'], name: 'user_idx_username')] +#[ORM\AttributeOverrides([ + new ORM\AttributeOverride(name: 'name', column: new ORM\Column(type: Types::STRING, length: 180, unique: true)) +])] +#[ApiResource( + shortName: 'User', + operations: [ + new Get( + openapi: new Operation(summary: 'Get information about the current user.'), + security: 'is_granted("read", object)' + ), + new GetCollection( + openapi: new Operation(summary: 'Get all users defined in the system.'), + security: 'is_granted("@users.read")' + ), + ], + normalizationContext: ['groups' => ['user:read'], 'openapi_definition_name' => 'Read'], +)] +#[ApiFilter(PropertyFilter::class)] +#[ApiFilter(LikeFilter::class, properties: ["name", "aboutMe"])] +#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])] +#[NoLockout(groups: ['permissions:edit'])] class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface, SamlUserInterface { @@ -70,220 +111,235 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * The User id of the anonymous user. */ - public const ID_ANONYMOUS = 1; + final public const ID_ANONYMOUS = 1; + + #[Groups(['user:read'])] + protected ?int $id = null; + + #[Groups(['user:read'])] + protected ?\DateTimeImmutable $lastModified = null; + + #[Groups(['user:read'])] + protected ?\DateTimeImmutable $addedDate = null; /** * @var bool Determines if the user is disabled (user can not log in) - * @ORM\Column(type="boolean") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'user:read'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $disabled = false; /** * @var string|null The theme - * @ORM\Column(type="string", name="config_theme", nullable=true) - * @ValidTheme() - * @Groups({"full", "import"}) */ + #[Groups(['full', 'import', 'user:read'])] + #[ORM\Column(name: 'config_theme', type: Types::STRING, nullable: true)] + #[ValidTheme] protected ?string $theme = null; /** * @var string|null the hash of a token the user must provide when he wants to reset his password - * @ORM\Column(type="string", nullable=true) */ + #[ORM\Column(type: Types::STRING, nullable: true)] protected ?string $pw_reset_token = null; - /** - * @ORM\Column(type="text", name="config_instock_comment_a") - */ + #[ORM\Column(name: 'config_instock_comment_a', type: Types::TEXT)] + #[Groups(['extended', 'full', 'import'])] protected string $instock_comment_a = ''; - /** - * @ORM\Column(type="text", name="config_instock_comment_w") - */ + #[ORM\Column(name: 'config_instock_comment_w', type: Types::TEXT)] + #[Groups(['extended', 'full', 'import'])] protected string $instock_comment_w = ''; /** - * @var string A self-description of the user - * @ORM\Column(type="text") - * @Groups({"full", "import"}) + * @var string A self-description of the user as markdown text */ + #[Groups(['full', 'import', 'user:read'])] + #[ORM\Column(type: Types::TEXT)] protected string $aboutMe = ''; /** @var int The version of the trusted device cookie. Used to invalidate all trusted device cookies at once. - * @ORM\Column(type="integer") */ + #[ORM\Column(type: Types::INTEGER)] protected int $trustedDeviceCookieVersion = 0; /** * @var string[]|null A list of backup codes that can be used, if the user has no access to its Google Authenticator device - * @ORM\Column(type="json") */ + #[ORM\Column(type: Types::JSON)] protected ?array $backupCodes = []; - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ - protected ?int $id = null; - /** * @var Group|null the group this user belongs to - * DO NOT PUT A fetch eager here! Otherwise you can not unset the group of a user! This seems to be some kind of bug in doctrine. Maybe this is fixed in future versions. - * @ORM\ManyToOne(targetEntity="Group", inversedBy="users") - * @ORM\JoinColumn(name="group_id", referencedColumnName="id") - * @Selectable() - * @Groups({"extended", "full", "import"}) + * DO NOT PUT A fetch eager here! Otherwise, you can not unset the group of a user! This seems to be some kind of bug in doctrine. Maybe this is fixed in future versions. */ + #[Groups(['extended', 'full', 'import', 'user:read'])] + #[ORM\ManyToOne(targetEntity: Group::class, inversedBy: 'users')] + #[ORM\JoinColumn(name: 'group_id')] + #[Selectable] + #[ApiProperty(readableLink: true, writableLink: false)] protected ?Group $group = null; /** - * @var string|null The secret used for google authenticator - * @ORM\Column(name="google_authenticator_secret", type="string", nullable=true) + * @var string|null The secret used for Google authenticator */ + #[ORM\Column(name: 'google_authenticator_secret', type: Types::STRING, nullable: true)] protected ?string $googleAuthenticatorSecret = null; /** * @var string|null The timezone the user prefers - * @ORM\Column(type="string", name="config_timezone", nullable=true) - * @Assert\Timezone() - * @Groups({"full", "import"}) */ + #[Assert\Timezone] + #[Groups(['full', 'import', 'user:read'])] + #[ORM\Column(name: 'config_timezone', type: Types::STRING, nullable: true)] protected ?string $timezone = ''; /** * @var string|null The language/locale the user prefers - * @ORM\Column(type="string", name="config_language", nullable=true) - * @Assert\Language() - * @Groups({"full", "import"}) */ + #[Assert\Language] + #[Groups(['full', 'import', 'user:read'])] + #[ORM\Column(name: 'config_language', type: Types::STRING, nullable: true)] protected ?string $language = ''; /** * @var string|null The email address of the user - * @ORM\Column(type="string", length=255, nullable=true) - * @Assert\Email() - * @Groups({"simple", "extended", "full", "import"}) */ + #[Assert\Email] + #[Groups(['simple', 'extended', 'full', 'import', 'user:read'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + #[Length(max: 255)] protected ?string $email = ''; /** * @var bool True if the user wants to show his email address on his (public) profile - * @ORM\Column(type="boolean", options={"default": false}) */ + #[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])] + #[Groups(['full', 'import', 'user:read'])] protected bool $show_email_on_profile = false; /** * @var string|null The department the user is working - * @ORM\Column(type="string", length=255, nullable=true) - * @Groups({"simple", "extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'user:read'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + #[Length(max: 255)] protected ?string $department = ''; /** * @var string|null The last name of the User - * @ORM\Column(type="string", length=255, nullable=true) - * @Groups({"simple", "extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'user:read'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + #[Length(max: 255)] protected ?string $last_name = ''; /** * @var string|null The first name of the User - * @ORM\Column(type="string", length=255, nullable=true) - * @Groups({"simple", "extended", "full", "import"}) */ + #[Groups(['simple', 'extended', 'full', 'import', 'user:read'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + #[Length(max: 255)] protected ?string $first_name = ''; /** * @var bool True if the user needs to change password after log in - * @ORM\Column(type="boolean") - * @Groups({"extended", "full", "import"}) */ + #[Groups(['extended', 'full', 'import', 'user:read'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $need_pw_change = true; /** * @var string|null The hashed password - * @ORM\Column(type="string", nullable=true) */ + #[ORM\Column(type: Types::STRING, nullable: true)] protected ?string $password = null; - /** - * @ORM\Column(type="string", length=180, unique=true) - * @Assert\NotBlank - * @Assert\Regex("/^[\w\.\+\-\$]+$/", message="user.invalid_username") - */ + #[Assert\NotBlank] + #[Assert\Regex('/^[\w\.\+\-\$]+[\w\.\+\-\$\@]*$/', message: 'user.invalid_username')] + #[Groups(['user:read'])] protected string $name = ''; /** - * @var array - * @ORM\Column(type="json") + * @var array|null */ + #[ORM\Column(type: Types::JSON)] protected ?array $settings = []; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\UserAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) - * @ORM\OrderBy({"name" = "ASC"}) */ - protected $attachments; + #[ORM\OneToMany(mappedBy: 'element', targetEntity: UserAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[ORM\OrderBy(['name' => Criteria::ASC])] + #[Groups(['user:read', 'user:write'])] + protected Collection $attachments; - /** @var DateTime|null The time when the backup codes were generated - * @ORM\Column(type="datetime", nullable=true) - * @Groups({"full"}) + #[ORM\ManyToOne(targetEntity: UserAttachment::class)] + #[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')] + #[Groups(['user:read', 'user:write'])] + protected ?Attachment $master_picture_attachment = null; + + /** @var \DateTimeImmutable|null The time when the backup codes were generated */ - protected ?DateTime $backupCodesGenerationDate = null; + #[Groups(['full'])] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + protected ?\DateTimeImmutable $backupCodesGenerationDate = null; /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\UserSystem\U2FKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true) */ - protected $u2fKeys; + #[ORM\OneToMany(mappedBy: 'user', targetEntity: U2FKey::class, cascade: ['REMOVE'], fetch: 'EXTRA_LAZY', orphanRemoval: true)] + protected Collection $u2fKeys; /** * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\UserSystem\WebauthnKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true) */ - protected $webauthn_keys; + #[ORM\OneToMany(mappedBy: 'user', targetEntity: WebauthnKey::class, cascade: ['REMOVE'], fetch: 'EXTRA_LAZY', orphanRemoval: true)] + protected Collection $webauthn_keys; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'user', targetEntity: ApiToken::class, cascade: ['REMOVE'], fetch: 'EXTRA_LAZY', orphanRemoval: true)] + private Collection $api_tokens; /** * @var Currency|null The currency the user wants to see prices in. * Dont use fetch=EAGER here, this will cause problems with setting the currency setting. * TODO: This is most likely a bug in doctrine/symfony related to the UniqueEntity constraint (it makes a db call). * TODO: Find a way to use fetch EAGER (this improves performance a bit) - * @ORM\ManyToOne(targetEntity="App\Entity\PriceInformations\Currency") - * @ORM\JoinColumn(name="currency_id", referencedColumnName="id") - * @Selectable() - * @Groups({"extended", "full", "import"}) */ - protected $currency; + #[Groups(['extended', 'full', 'import'])] + #[ORM\ManyToOne(targetEntity: Currency::class)] + #[ORM\JoinColumn(name: 'currency_id')] + #[Selectable] + protected ?Currency $currency = null; - /** - * @var PermissionData - * @ValidPermission() - * @ORM\Embedded(class="PermissionData", columnPrefix="permissions_") - * @Groups({"simple", "extended", "full", "import"}) - */ + #[Groups(['simple', 'extended', 'full', 'import'])] + #[ORM\Embedded(class: 'PermissionData', columnPrefix: 'permissions_')] + #[ValidPermission] protected ?PermissionData $permissions = null; /** - * @var DateTime the time until the password reset token is valid - * @ORM\Column(type="datetime", nullable=true, options={"default": null}) + * @var \DateTimeImmutable|null the time until the password reset token is valid */ - protected $pw_reset_expires; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + protected ?\DateTimeImmutable $pw_reset_expires = null; /** * @var bool True if the user was created by a SAML provider (and therefore cannot change its password) - * @ORM\Column(type="boolean") - * @Groups({"extended", "full"}) */ + #[Groups(['extended', 'full'])] + #[ORM\Column(type: Types::BOOLEAN)] protected bool $saml_user = false; public function __construct() { + $this->attachments = new ArrayCollection(); parent::__construct(); $this->permissions = new PermissionData(); $this->u2fKeys = new ArrayCollection(); $this->webauthn_keys = new ArrayCollection(); + $this->api_tokens = new ArrayCollection(); } /** @@ -292,7 +348,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe * * @return string */ - public function __toString() + public function __toString(): string { $tmp = $this->isDisabled() ? ' [DISABLED]' : ''; @@ -359,8 +415,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Sets the password hash for this user. - * - * @return User */ public function setPassword(string $password): self { @@ -398,8 +452,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Sets the currency the users prefers to see prices in. - * - * @return User */ public function setCurrency(?Currency $currency): self { @@ -409,7 +461,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * Checks if this user is disabled (user cannot login any more). + * Checks if this user is disabled (user cannot log in any more). * * @return bool true, if the user is disabled */ @@ -422,8 +474,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe * Sets the status if a user is disabled. * * @param bool $disabled true if the user should be disabled - * - * @return User */ public function setDisabled(bool $disabled): self { @@ -434,7 +484,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe public function getPermissions(): PermissionData { - if ($this->permissions === null) { + if (!$this->permissions instanceof PermissionData) { $this->permissions = new PermissionData(); } @@ -451,8 +501,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Set the status, if the user needs a password change. - * - * @return User */ public function setNeedPwChange(bool $need_pw_change): self { @@ -471,8 +519,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Sets the encrypted password reset token. - * - * @return User */ public function setPwResetToken(?string $pw_reset_token): self { @@ -482,19 +528,17 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * Gets the datetime when the password reset token expires. + * Gets the datetime when the password reset token expires. */ - public function getPwResetExpires(): DateTime + public function getPwResetExpires(): \DateTimeImmutable|null { return $this->pw_reset_expires; } /** * Sets the datetime when the password reset token expires. - * - * @return User */ - public function setPwResetExpires(DateTime $pw_reset_expires): self + public function setPwResetExpires(\DateTimeImmutable $pw_reset_expires): self { $this->pw_reset_expires = $pw_reset_expires; @@ -513,11 +557,12 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe * * @return string a string with the full name of this user */ + #[Groups(['user:read'])] public function getFullName(bool $including_username = false): string { $tmp = $this->getFirstName(); - //Dont add a space, if the name has only one part (it would look strange) - if (!empty($this->getFirstName()) && !empty($this->getLastName())) { + //Don't add a space, if the name has only one part (it would look strange) + if ($this->getFirstName() !== null && $this->getFirstName() !== '' && ($this->getLastName() !== null && $this->getLastName() !== '')) { $tmp .= ' '; } $tmp .= $this->getLastName(); @@ -604,8 +649,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe * Change the department of the user. * * @param string|null $department The new department - * - * @return User */ public function setDepartment(?string $department): self { @@ -640,7 +683,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Gets whether the email address of the user is shown on the public profile page. - * @return bool */ public function isShowEmailOnProfile(): bool { @@ -649,8 +691,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Sets whether the email address of the user is shown on the public profile page. - * @param bool $show_email_on_profile - * @return User */ public function setShowEmailOnProfile(bool $show_email_on_profile): User { @@ -662,7 +702,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Returns the about me text of the user. - * @return string */ public function getAboutMe(): string { @@ -671,8 +710,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Change the about me text of the user. - * @param string $aboutMe - * @return User */ public function setAboutMe(string $aboutMe): User { @@ -683,9 +720,9 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** - * Gets the language the user prefers (as 2 letter ISO code). + * Gets the language the user prefers (as 2-letter ISO code). * - * @return string|null The 2 letter ISO code of the preferred language (e.g. 'en' or 'de'). + * @return string|null The 2-letter ISO code of the preferred language (e.g. 'en' or 'de'). * If null is returned, the user has not specified a language and the server wide language should be used. */ public function getLanguage(): ?string @@ -696,10 +733,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Change the language the user prefers. * - * @param string|null $language The new language as 2 letter ISO code (e.g. 'en' or 'de'). - * Set to null, to use the system wide language. - * - * @return User + * @param string|null $language The new language as 2-letter ISO code (e.g. 'en' or 'de'). + * Set to null, to use the system-wide language. */ public function setLanguage(?string $language): self { @@ -744,8 +779,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Change the theme the user wants to see. * - * @param string|null $theme The name of the theme (See See self::AVAILABLE_THEMES for valid values). Set to null - * if the system wide theme should be used. + * @param string|null $theme The name of the theme (See self::AVAILABLE_THEMES for valid values). Set to null + * if the system-wide theme should be used. * * @return $this */ @@ -789,7 +824,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * Return the user name that should be shown in Google Authenticator. + * Return the username that should be shown in Google Authenticator. */ public function getGoogleAuthenticatorUsername(): string { @@ -858,17 +893,11 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe * @param string[] $codes An array containing the backup codes * * @return $this - * - * @throws Exception If an error with the datetime occurs */ public function setBackupCodes(array $codes): self { $this->backupCodes = $codes; - if (empty($codes)) { - $this->backupCodesGenerationDate = null; - } else { - $this->backupCodesGenerationDate = new DateTime(); - } + $this->backupCodesGenerationDate = $codes === [] ? null : new \DateTimeImmutable(); return $this; } @@ -876,7 +905,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Return the date when the backup codes were generated. */ - public function getBackupCodesGenerationDate(): ?DateTime + public function getBackupCodesGenerationDate(): ?\DateTimeImmutable { return $this->backupCodesGenerationDate; } @@ -893,7 +922,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Invalidate all trusted device tokens at once, by incrementing the token version. - * You have to flush the changes to database afterwards. + * You have to flush the changes to database afterward. */ public function invalidateTrustedDeviceTokens(): void { @@ -930,7 +959,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe { return new PublicKeyCredentialUserEntity( $this->getUsername(), - (string) $this->getId(), + (string) $this->getID(), $this->getFullName(), ); } @@ -947,7 +976,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Returns true, if the user was created by the SAML authentication. - * @return bool */ public function isSamlUser(): bool { @@ -956,8 +984,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe /** * Sets the saml_user flag. - * @param bool $saml_user - * @return User */ public function setSamlUser(bool $saml_user): User { @@ -965,9 +991,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe return $this; } - - - public function setSamlAttributes(array $attributes) + public function setSamlAttributes(array $attributes): void { //When mail attribute exists, set it if (isset($attributes['email'])) { @@ -996,4 +1020,34 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe $this->setEmail($attributes['urn:oid:1.2.840.113549.1.9.1'][0]); } } + + /** + * Return all API tokens of the user. + * @return Collection + */ + public function getApiTokens(): Collection + { + return $this->api_tokens; + } + + /** + * Add an API token to the user. + * @param ApiToken $apiToken + * @return void + */ + public function addApiToken(ApiToken $apiToken): void + { + $apiToken->setUser($this); + $this->api_tokens->add($apiToken); + } + + /** + * Remove an API token from the user. + * @param ApiToken $apiToken + * @return void + */ + public function removeApiToken(ApiToken $apiToken): void + { + $this->api_tokens->removeElement($apiToken); + } } diff --git a/src/Entity/UserSystem/WebauthnKey.php b/src/Entity/UserSystem/WebauthnKey.php index 5d5c654d..b2716e07 100644 --- a/src/Entity/UserSystem/WebauthnKey.php +++ b/src/Entity/UserSystem/WebauthnKey.php @@ -1,4 +1,7 @@ . */ - namespace App\Entity\UserSystem; +use App\Entity\Contracts\TimeStampableInterface; +use Doctrine\DBAL\Types\Types; use App\Entity\Base\TimestampTrait; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource; -/** - * @ORM\Table(name="webauthn_keys") - * @ORM\Entity() - * @ORM\HasLifecycleCallbacks() - */ -class WebauthnKey extends BasePublicKeyCredentialSource +#[ORM\Entity] +#[ORM\HasLifecycleCallbacks] +#[ORM\Table(name: 'webauthn_keys')] +class WebauthnKey extends BasePublicKeyCredentialSource implements TimeStampableInterface { use TimestampTrait; - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Id] + #[ORM\Column(type: Types::INTEGER)] + #[ORM\GeneratedValue] protected int $id; - /** - * @ORM\Column(type="string") - */ - protected string $name; + #[ORM\Column(type: Types::STRING)] + #[NotBlank] + #[Length(max: 255)] + protected string $name = ''; - /** - * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User", inversedBy="webauthn_keys") - **/ + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'webauthn_keys')] protected ?User $user = null; - /** - * @return string - */ + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + protected ?\DateTimeImmutable $last_time_used = null; + public function getName(): string { return $this->name; } - /** - * @param string $name - * @return WebauthnKey - */ public function setName(string $name): WebauthnKey { $this->name = $name; return $this; } - /** - * @return User|null - */ public function getUser(): ?User { return $this->user; } - /** - * @param User|null $user - * @return WebauthnKey - */ public function setUser(?User $user): WebauthnKey { $this->user = $user; return $this; } - /** - * @return int - */ public function getId(): int { return $this->id; } + /** + * Retrieve the last time when the key was used. + */ + public function getLastTimeUsed(): ?\DateTimeImmutable + { + return $this->last_time_used; + } - - + /** + * Update the last time when the key was used. + * @return void + */ + public function updateLastTimeUsed(): void + { + $this->last_time_used = new \DateTimeImmutable('now'); + } public static function fromRegistration(BasePublicKeyCredentialSource $registration): self { @@ -113,4 +112,4 @@ class WebauthnKey extends BasePublicKeyCredentialSource $registration->getOtherUI() ); } -} \ No newline at end of file +} diff --git a/src/EntityListeners/AttachmentDeleteListener.php b/src/EntityListeners/AttachmentDeleteListener.php index edf68e25..1f39b2d0 100644 --- a/src/EntityListeners/AttachmentDeleteListener.php +++ b/src/EntityListeners/AttachmentDeleteListener.php @@ -22,12 +22,12 @@ declare(strict_types=1); namespace App\EntityListeners; +use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\Attachment; use App\Services\Attachments\AttachmentManager; use App\Services\Attachments\AttachmentPathResolver; use App\Services\Attachments\AttachmentReverseSearch; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Event\PostRemoveEventArgs; use Doctrine\ORM\Event\PreRemoveEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; @@ -38,30 +38,22 @@ use SplFileInfo; /** * This listener watches for changes on attachments and deletes the files associated with an attachment, that are not - * used any more. This can happens after an attachment is delteted or the path is changed. + * used anymore. This can happen after an attachment is deleted or the path is changed. */ class AttachmentDeleteListener { - protected AttachmentReverseSearch $attachmentReverseSearch; - protected AttachmentManager $attachmentHelper; - protected AttachmentPathResolver $pathResolver; - - public function __construct(AttachmentReverseSearch $attachmentReverseSearch, AttachmentManager $attachmentHelper, AttachmentPathResolver $pathResolver) + public function __construct(protected AttachmentReverseSearch $attachmentReverseSearch, protected AttachmentManager $attachmentHelper, protected AttachmentPathResolver $pathResolver) { - $this->attachmentReverseSearch = $attachmentReverseSearch; - $this->attachmentHelper = $attachmentHelper; - $this->pathResolver = $pathResolver; } /** * Removes the file associated with the attachment, if the file associated with the attachment changes. - * - * @PreUpdate */ + #[PreUpdate] public function preUpdateHandler(Attachment $attachment, PreUpdateEventArgs $event): void { - if ($event->hasChangedField('path')) { - $old_path = $event->getOldValue('path'); + if ($event->hasChangedField('internal_path')) { + $old_path = $event->getOldValue('internal_path'); //Dont delete file if the attachment uses a builtin ressource: if (Attachment::checkIfBuiltin($old_path)) { @@ -82,15 +74,14 @@ class AttachmentDeleteListener /** * Ensure that attachments are not used in preview, so that they can be deleted (without integrity violation). - * - * @ORM\PreRemove() */ + #[ORM\PreRemove] public function preRemoveHandler(Attachment $attachment, PreRemoveEventArgs $event): void { //Ensure that the attachment that will be deleted, is not used as preview picture anymore... $attachment_holder = $attachment->getElement(); - if (null === $attachment_holder) { + if (!$attachment_holder instanceof AttachmentContainingDBElement) { return; } @@ -103,16 +94,15 @@ class AttachmentDeleteListener if (!$em instanceof EntityManagerInterface) { throw new \RuntimeException('Invalid EntityManagerInterface!'); } - $classMetadata = $em->getClassMetadata(get_class($attachment_holder)); + $classMetadata = $em->getClassMetadata($attachment_holder::class); $em->getUnitOfWork()->computeChangeSet($classMetadata, $attachment_holder); } } /** * Removes the file associated with the attachment, after the attachment was deleted. - * - * @PostRemove */ + #[PostRemove] public function postRemoveHandler(Attachment $attachment, PostRemoveEventArgs $event): void { //Dont delete file if the attachment uses a builtin ressource: @@ -122,7 +112,7 @@ class AttachmentDeleteListener $file = $this->attachmentHelper->attachmentToFile($attachment); //Only delete if the attachment has a valid file. - if (null !== $file) { + if ($file instanceof \SplFileInfo) { /* The original file has already been removed, so we have to decrease the threshold to zero, as any remaining attachment depends on this attachment, and we must not delete this file! */ $this->attachmentReverseSearch->deleteIfNotUsed($file, 0); diff --git a/src/EntityListeners/TreeCacheInvalidationListener.php b/src/EntityListeners/TreeCacheInvalidationListener.php index 017c8018..eae7ce35 100644 --- a/src/EntityListeners/TreeCacheInvalidationListener.php +++ b/src/EntityListeners/TreeCacheInvalidationListener.php @@ -24,56 +24,52 @@ namespace App\EntityListeners; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\LabelSystem\LabelProfile; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; -use App\Services\UserSystem\UserCacheKeyGenerator; -use Doctrine\ORM\Event\LifecycleEventArgs; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; +use Doctrine\ORM\Event\PostPersistEventArgs; +use Doctrine\ORM\Event\PostRemoveEventArgs; +use Doctrine\ORM\Event\PostUpdateEventArgs; use Doctrine\ORM\Mapping as ORM; -use function get_class; use Symfony\Contracts\Cache\TagAwareCacheInterface; class TreeCacheInvalidationListener { - protected TagAwareCacheInterface $cache; - protected UserCacheKeyGenerator $keyGenerator; - - public function __construct(TagAwareCacheInterface $treeCache, UserCacheKeyGenerator $keyGenerator) + public function __construct( + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected ElementCacheTagGenerator $tagGenerator + ) { - $this->cache = $treeCache; - $this->keyGenerator = $keyGenerator; } - /** - * @ORM\PostUpdate() - * @ORM\PostPersist() - * @ORM\PostRemove() - */ - public function invalidate(AbstractDBElement $element, LifecycleEventArgs $event): void + #[ORM\PostUpdate] + #[ORM\PostPersist] + #[ORM\PostRemove] + public function invalidate(AbstractDBElement $element, PostUpdateEventArgs|PostPersistEventArgs|PostRemoveEventArgs $event): void { - //If an element was changed, then invalidate all cached trees with this element class - if ($element instanceof AbstractStructuralDBElement || $element instanceof LabelProfile) { - $secure_class_name = str_replace('\\', '_', get_class($element)); - $this->cache->invalidateTags([$secure_class_name]); + //For all changes, we invalidate the cache for all elements of this class + $tags = [$this->tagGenerator->getElementTypeCacheTag($element)]; - //Trigger a sidebar reload for all users (see SidebarTreeUpdater service) - if(!$element instanceof LabelProfile) { - $this->cache->invalidateTags(['sidebar_tree_update']); - } + + //For changes on structural elements, we also invalidate the sidebar tree + if ($element instanceof AbstractStructuralDBElement) { + $tags[] = 'sidebar_tree_update'; } - //If a user change, then invalidate all cached trees for him + //For user changes, we invalidate the cache for this user if ($element instanceof User) { - $secure_class_name = str_replace('\\', '_', get_class($element)); - $tag = $this->keyGenerator->generateKey($element); - $this->cache->invalidateTags([$tag, $secure_class_name]); + $tags[] = $this->keyGenerator->generateKey($element); } /* If any group change, then invalidate all cached trees. Users Permissions can be inherited from groups, so a change in any group can cause big permisssion changes for users. So to be sure, invalidate all trees */ if ($element instanceof Group) { - $tag = 'groups'; - $this->cache->invalidateTags([$tag]); + $tags[] = 'groups'; } + + //Invalidate the cache for the given tags + $this->cache->invalidateTags($tags); } } diff --git a/src/EventListener/AddEditCommentRequestListener.php b/src/EventListener/AddEditCommentRequestListener.php new file mode 100644 index 00000000..33c72b3f --- /dev/null +++ b/src/EventListener/AddEditCommentRequestListener.php @@ -0,0 +1,62 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventListener; + +use App\Services\LogSystem\EventCommentHelper; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +#[AsEventListener] +class AddEditCommentRequestListener +{ + public function __construct(private readonly EventCommentHelper $helper) + { + + } + + public function __invoke(RequestEvent $event) + { + if (!$event->isMainRequest()) { + return; + } + $request = $event->getRequest(); + + //Do not add comment if the request is a GET request + if ($request->isMethod('GET')) { + return; + } + + //Check if the user tries to access a /api/ endpoint, if not skip + if (!str_contains($request->getPathInfo(), '/api/')) { + return; + } + + //Extract the comment from the query parameter + $comment = $request->query->getString('_comment', ''); + + if ($comment !== '') { + $this->helper->setMessage($comment); + } + } +} \ No newline at end of file diff --git a/src/EventListener/AllowSlowNaturalSortListener.php b/src/EventListener/AllowSlowNaturalSortListener.php new file mode 100644 index 00000000..02ec6144 --- /dev/null +++ b/src/EventListener/AllowSlowNaturalSortListener.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventListener; + +use App\Doctrine\Functions\Natsort; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +/** + * This is a workaround to the fact that we can not inject parameters into doctrine custom functions. + * Therefore we use this event listener to call the static function on the custom function, to inject the value, before + * any NATSORT function is called. + */ +#[AsEventListener] +class AllowSlowNaturalSortListener +{ + public function __construct( + #[Autowire(param: 'partdb.db.emulate_natural_sort')] + private readonly bool $allowNaturalSort) + { + } + + public function __invoke(RequestEvent $event) + { + Natsort::allowSlowNaturalSort($this->allowNaturalSort); + } +} \ No newline at end of file diff --git a/src/EventListener/ConsoleEnsureWebserverUserListener.php b/src/EventListener/ConsoleEnsureWebserverUserListener.php new file mode 100644 index 00000000..7c119304 --- /dev/null +++ b/src/EventListener/ConsoleEnsureWebserverUserListener.php @@ -0,0 +1,159 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventListener; + +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + +/** + * This event listener is called before any console command is executed and should ensure that the webserver + * user is used for all operations (and show a warning if not). This ensures that all files are created with the + * correct permissions. + * If the console is in non-interactive mode, a warning is shown, but the command is still executed. + */ +#[AsEventListener(ConsoleEvents::COMMAND)] +class ConsoleEnsureWebserverUserListener +{ + public function __construct( + #[Autowire('%kernel.project_dir%')] + private readonly string $project_root) + { + } + + public function __invoke(ConsoleCommandEvent $event): void + { + $input = $event->getInput(); + $io = new SymfonyStyle($event->getInput(), $event->getOutput()); + + //Check if we are (not) running as the webserver user + $webserver_user = $this->getWebserverUser(); + $running_user = $this->getRunningUser(); + + //Check if we are trying to run as root + if ($this->isRunningAsRoot()) { + //If the COMPOSER_ALLOW_SUPERUSER environment variable is set, we allow running as root + if ($_SERVER['COMPOSER_ALLOW_SUPERUSER'] ?? false) { + return; + } + + $io->warning('You are running this command as root. This is not recommended, as it can cause permission problems. Please run this command as the webserver user "'. ($webserver_user ?? '??') . '" instead.'); + $io->info('You might have already caused permission problems by running this command as wrong user. If you encounter issues with Part-DB, delete the var/cache directory completely and let it be recreated by Part-DB.'); + if ($input->isInteractive() && !$io->confirm('Do you want to continue?', false)) { + $event->disableCommand(); + } + + return; + } + + if ($webserver_user !== null && $running_user !== null && $webserver_user !== $running_user) { + $io->warning('You are running this command as the user "' . $running_user . '". This is not recommended, as it can cause permission problems. Please run this command as the webserver user "' . $webserver_user . '" instead.'); + $io->info('You might have already caused permission problems by running this command as wrong user. If you encounter issues with Part-DB, delete the var/cache directory completely and let it be recreated by Part-DB.'); + if ($input->isInteractive() && !$io->confirm('Do you want to continue?', false)) { + $event->disableCommand(); + } + + return; + } + } + + /** @noinspection PhpUndefinedFunctionInspection */ + private function isRunningAsRoot(): bool + { + //If we are on windows, we can't run as root + if (PHP_OS_FAMILY === 'Windows') { + return false; + } + + //Try to use the posix extension if available (Linux) + if (function_exists('posix_geteuid')) { + //Check if the current user is root + return posix_geteuid() === 0; + } + + //Otherwise we can't determine the username + return false; + } + + /** + * Determines the username of the user who started the current script if possible. + * Returns null if the username could not be determined. + * @return string|null + * @noinspection PhpUndefinedFunctionInspection + */ + private function getRunningUser(): ?string + { + //Try to use the posix extension if available (Linux) + if (function_exists('posix_geteuid') && function_exists('posix_getpwuid')) { + $id = posix_geteuid(); + + $user = posix_getpwuid($id); + //Try to get the username from the posix extension or return the id + return $user['name'] ?? ("ID: " . $id); + } + + //Otherwise we can't determine the username + return $_SERVER['USERNAME'] ?? $_SERVER['USER'] ?? null; + } + + private function getWebserverUser(): ?string + { + //Determine the webserver user, by checking who owns the uploads/ directory + $path_to_check = $this->project_root . '/uploads/'; + + //Determine the owner of this directory + if (!is_dir($path_to_check)) { + return null; + } + + //If we are on windows we need some special logic + if (PHP_OS_FAMILY === 'Windows') { + //If we have the COM extension available, we can use it to determine the owner + if (extension_loaded('com_dotnet')) { + /** @noinspection PhpUndefinedClassInspection */ + $su = new \COM("ADsSecurityUtility"); // Call interface + //@phpstan-ignore-next-line + $securityInfo = $su->GetSecurityDescriptor($path_to_check, 1, 1); // Call method + return $securityInfo->owner; // Get file owner + } + + //Otherwise we can't determine the owner + return null; + } + + //When we are on a POSIX system, we can use the fileowner function + $owner = fileowner($path_to_check); + + if (function_exists('posix_getpwuid')) { + $user = posix_getpwuid($owner); + //Try to get the username from the posix extension or return the id + return $user['name'] ?? ("ID: " . $owner); + } + + return null; + } + +} \ No newline at end of file diff --git a/src/EventListener/DisallowSearchEngineIndexingRequestListener.php b/src/EventListener/DisallowSearchEngineIndexingRequestListener.php new file mode 100644 index 00000000..b969b6b2 --- /dev/null +++ b/src/EventListener/DisallowSearchEngineIndexingRequestListener.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventListener; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpKernel\Event\ResponseEvent; + +#[AsEventListener] +class DisallowSearchEngineIndexingRequestListener +{ + private const HEADER_NAME = 'X-Robots-Tag'; + + private readonly bool $enabled; + + public function __construct(#[Autowire(param: 'partdb.demo_mode')] bool $demo_mode) + { + // Disable this listener in demo mode + $this->enabled = !$demo_mode; + } + + public function __invoke(ResponseEvent $event): void + { + //Skip if disabled + if (!$this->enabled) { + return; + } + + if (!$event->getResponse()->headers->has(self::HEADER_NAME)) { + $event->getResponse()->headers->set(self::HEADER_NAME, 'noindex'); + } + } +} \ No newline at end of file diff --git a/src/EventSubscriber/LogSystem/EventLoggerSubscriber.php b/src/EventListener/LogSystem/EventLoggerListener.php similarity index 81% rename from src/EventSubscriber/LogSystem/EventLoggerSubscriber.php rename to src/EventListener/LogSystem/EventLoggerListener.php index 1f38c66c..6fe3d8dc 100644 --- a/src/EventSubscriber/LogSystem/EventLoggerSubscriber.php +++ b/src/EventListener/LogSystem/EventLoggerListener.php @@ -20,7 +20,7 @@ declare(strict_types=1); -namespace App\EventSubscriber\LogSystem; +namespace App\EventListener\LogSystem; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; @@ -29,6 +29,7 @@ use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Entity\OAuthToken; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\PartLot; use App\Entity\PriceInformations\Orderdetail; @@ -37,7 +38,7 @@ use App\Entity\UserSystem\User; use App\Services\LogSystem\EventCommentHelper; use App\Services\LogSystem\EventLogger; use App\Services\LogSystem\EventUndoHelper; -use Doctrine\Common\EventSubscriber; +use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Event\PostFlushEventArgs; @@ -47,12 +48,15 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\SerializerInterface; /** - * This event subscriber write to event log when entities are changed, removed, created. + * This event subscriber writes to the event log when entities are changed, removed, created. */ -class EventLoggerSubscriber implements EventSubscriber +#[AsDoctrineListener(event: Events::onFlush)] +#[AsDoctrineListener(event: Events::postPersist)] +#[AsDoctrineListener(event: Events::postFlush)] +class EventLoggerListener { /** - * @var array The given fields will not be saved, because they contain sensitive informations + * @var array The given fields will not be saved, because they contain sensitive information */ protected const FIELD_BLACKLIST = [ User::class => ['password', 'need_pw_change', 'googleAuthenticatorSecret', 'backupCodes', 'trustedDeviceCookieVersion', 'pw_reset_token', 'backupCodesGenerationDate'], @@ -70,34 +74,19 @@ class EventLoggerSubscriber implements EventSubscriber ]; protected const MAX_STRING_LENGTH = 2000; + protected bool $save_new_data; - protected EventLogger $logger; - protected SerializerInterface $serializer; - protected EventCommentHelper $eventCommentHelper; - protected EventUndoHelper $eventUndoHelper; - protected bool $save_changed_fields; - protected bool $save_changed_data; - protected bool $save_removed_data; - protected PropertyAccessorInterface $propertyAccessor; - - public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper, - bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor, - EventUndoHelper $eventUndoHelper) + public function __construct(protected EventLogger $logger, protected SerializerInterface $serializer, protected EventCommentHelper $eventCommentHelper, + protected bool $save_changed_fields, protected bool $save_changed_data, protected bool $save_removed_data, bool $save_new_data, + protected PropertyAccessorInterface $propertyAccessor, protected EventUndoHelper $eventUndoHelper) { - $this->logger = $logger; - $this->serializer = $serializer; - $this->eventCommentHelper = $commentHelper; - $this->propertyAccessor = $propertyAccessor; - $this->eventUndoHelper = $eventUndoHelper; - - $this->save_changed_fields = $save_changed_fields; - $this->save_changed_data = $save_changed_data; - $this->save_removed_data = $save_removed_data; + //This option only makes sense if save_changed_data is true + $this->save_new_data = $save_new_data && $save_changed_data; } public function onFlush(OnFlushEventArgs $eventArgs): void { - $em = $eventArgs->getEntityManager(); + $em = $eventArgs->getObjectManager(); $uow = $em->getUnitOfWork(); /* @@ -128,7 +117,7 @@ class EventLoggerSubscriber implements EventSubscriber public function postPersist(LifecycleEventArgs $args): void { - //Create an log entry, we have to do this post persist, cause we have to know the ID + //Create a log entry, we have to do this post persist, because we have to know the ID /** @var AbstractDBElement $entity */ $entity = $args->getObject(); @@ -150,17 +139,18 @@ class EventLoggerSubscriber implements EventSubscriber $log->setTargetElementID($undoEvent->getDeletedElementID()); } } + $this->logger->log($log); } } public function postFlush(PostFlushEventArgs $args): void { - $em = $args->getEntityManager(); + $em = $args->getObjectManager(); $uow = $em->getUnitOfWork(); - // If the we have added any ElementCreatedLogEntries added in postPersist, we flush them here. + // If we have added any ElementCreatedLogEntries added in postPersist, we flush them here. $uow->computeChangeSets(); - if ($uow->hasPendingInsertions() || !empty($uow->getScheduledEntityUpdates())) { + if ($uow->hasPendingInsertions() || $uow->getScheduledEntityUpdates() !== []) { $em->flush(); } @@ -177,7 +167,7 @@ class EventLoggerSubscriber implements EventSubscriber public function hasFieldRestrictions(AbstractDBElement $element): bool { foreach (array_keys(static::FIELD_BLACKLIST) as $class) { - if (is_a($element, $class)) { + if ($element instanceof $class) { return true; } } @@ -191,24 +181,15 @@ class EventLoggerSubscriber implements EventSubscriber public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool { foreach (static::FIELD_BLACKLIST as $class => $blacklist) { - if (is_a($element, $class) && in_array($field_name, $blacklist, true)) { + if ($element instanceof $class && in_array($field_name, $blacklist, true)) { return false; } } - //By default allow every field. + //By default, allow every field. return true; } - public function getSubscribedEvents(): array - { - return[ - Events::onFlush, - Events::postPersist, - Events::postFlush, - ]; - } - protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void { $log = new ElementDeletedLogEntry($entity); @@ -227,11 +208,11 @@ class EventLoggerSubscriber implements EventSubscriber //Check if we have to log CollectionElementDeleted entries if ($this->save_changed_data) { - $metadata = $em->getClassMetadata(get_class($entity)); + $metadata = $em->getClassMetadata($entity::class); $mappings = $metadata->getAssociationMappings(); //Check if class is whitelisted for CollectionElementDeleted entry foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) { - if (is_a($entity, $class)) { + if ($entity instanceof $class) { //Check names foreach ($mappings as $field => $mapping) { if (in_array($field, $whitelist, true)) { @@ -257,7 +238,7 @@ class EventLoggerSubscriber implements EventSubscriber $changeSet = $uow->getEntityChangeSet($entity); //Skip log entry, if only the lastModified field has changed... - if (isset($changeSet['lastModified']) && count($changeSet)) { + if (count($changeSet) === 1 && isset($changeSet['lastModified'])) { return; } @@ -301,8 +282,25 @@ class EventLoggerSubscriber implements EventSubscriber }, ARRAY_FILTER_USE_BOTH); } + /** + * Restrict the length of every string in the given array to MAX_STRING_LENGTH, to save memory in the case of very + * long strings (e.g. images in notes) + */ + protected function fieldLengthRestrict(array $fields): array + { + return array_map( + static function ($value) { + if (is_string($value)) { + return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...'); + } + + return $value; + }, $fields); + } + protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, bool $element_deleted = false): void { + $new_data = null; $uow = $em->getUnitOfWork(); if (!$logEntry instanceof ElementEditedLogEntry && !$logEntry instanceof ElementDeletedLogEntry) { @@ -314,20 +312,24 @@ class EventLoggerSubscriber implements EventSubscriber } else { //Otherwise we have to get it from entity changeset $changeSet = $uow->getEntityChangeSet($entity); $old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0)); + //If save_new_data is enabled, we extract it from the change set + if ($this->save_new_data) { + $new_data = array_combine(array_keys($changeSet), array_column($changeSet, 1)); + } } $old_data = $this->filterFieldRestrictions($entity, $old_data); //Restrict length of string fields, to save memory... - $old_data = array_map( - static function ($value) { - if (is_string($value)) { - return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...'); - } - - return $value; - }, $old_data); + $old_data = $this->fieldLengthRestrict($old_data); $logEntry->setOldData($old_data); + + if ($new_data !== [] && $new_data !== null) { + $new_data = $this->filterFieldRestrictions($entity, $new_data); + $new_data = $this->fieldLengthRestrict($new_data); + + $logEntry->setNewData($new_data); + } } /** @@ -337,6 +339,11 @@ class EventLoggerSubscriber implements EventSubscriber */ protected function validEntity(object $entity): bool { + //Dont log OAuthTokens + if ($entity instanceof OAuthToken) { + return false; + } + //Dont log logentries itself! return $entity instanceof AbstractDBElement && !$entity instanceof AbstractLogEntry; } diff --git a/src/EventSubscriber/LogSystem/LogDBMigrationSubscriber.php b/src/EventListener/LogSystem/LogDBMigrationListener.php similarity index 81% rename from src/EventSubscriber/LogSystem/LogDBMigrationSubscriber.php rename to src/EventListener/LogSystem/LogDBMigrationListener.php index 610c0b5e..c8b60e18 100644 --- a/src/EventSubscriber/LogSystem/LogDBMigrationSubscriber.php +++ b/src/EventListener/LogSystem/LogDBMigrationListener.php @@ -20,11 +20,11 @@ declare(strict_types=1); -namespace App\EventSubscriber\LogSystem; +namespace App\EventListener\LogSystem; use App\Entity\LogSystem\DatabaseUpdatedLogEntry; use App\Services\LogSystem\EventLogger; -use Doctrine\Common\EventSubscriber; +use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\Migrations\DependencyFactory; use Doctrine\Migrations\Event\MigrationsEventArgs; use Doctrine\Migrations\Events; @@ -32,18 +32,15 @@ use Doctrine\Migrations\Events; /** * This subscriber logs databaseMigrations to Event log. */ -class LogDBMigrationSubscriber implements EventSubscriber +#[AsDoctrineListener(event: Events::onMigrationsMigrated)] +#[AsDoctrineListener(event: Events::onMigrationsMigrating)] +class LogDBMigrationListener { protected ?string $old_version = null; protected ?string $new_version = null; - protected EventLogger $eventLogger; - protected DependencyFactory $dependencyFactory; - - public function __construct(EventLogger $eventLogger, DependencyFactory $dependencyFactory) + public function __construct(protected EventLogger $eventLogger, protected DependencyFactory $dependencyFactory) { - $this->eventLogger = $eventLogger; - $this->dependencyFactory = $dependencyFactory; } public function onMigrationsMigrated(MigrationsEventArgs $args): void @@ -60,13 +57,13 @@ class LogDBMigrationSubscriber implements EventSubscriber $this->new_version = (string) $aliasResolver->resolveVersionAlias('current'); //After everything is done, write the results to DB log - $this->old_version = empty($this->old_version) ? 'legacy/empty' : $this->old_version; - $this->new_version = empty($this->new_version) ? 'unknown' : $this->new_version; + $this->old_version = $this->old_version === null || $this->old_version === '' ? 'legacy/empty' : $this->old_version; + $this->new_version = $this->new_version === '' ? 'unknown' : $this->new_version; try { $log = new DatabaseUpdatedLogEntry($this->old_version, $this->new_version); $this->eventLogger->logAndFlush($log); - } catch (\Throwable $exception) { + } catch (\Throwable) { //Ignore any exception occuring here... } } diff --git a/src/EventSubscriber/LogSystem/LogAccessDeniedSubscriber.php b/src/EventSubscriber/LogSystem/LogAccessDeniedSubscriber.php index a2b9f1ff..abe2f9ba 100644 --- a/src/EventSubscriber/LogSystem/LogAccessDeniedSubscriber.php +++ b/src/EventSubscriber/LogSystem/LogAccessDeniedSubscriber.php @@ -49,15 +49,12 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Security\Core\Exception\AccessDeniedException; /** - * Write to event log when a user tries to access an forbidden page and recevies an 403 Access Denied message. + * Write to event log when a user tries to access a forbidden page and receives an 403 Access Denied message. */ class LogAccessDeniedSubscriber implements EventSubscriberInterface { - private EventLogger $logger; - - public function __construct(EventLogger $logger) + public function __construct(private readonly EventLogger $logger) { - $this->logger = $logger; } public function onKernelException(ExceptionEvent $event): void diff --git a/src/EventSubscriber/LogSystem/LogoutLoggerListener.php b/src/EventSubscriber/LogSystem/LogLogoutEventSubscriber.php similarity index 70% rename from src/EventSubscriber/LogSystem/LogoutLoggerListener.php rename to src/EventSubscriber/LogSystem/LogLogoutEventSubscriber.php index 3b197b38..7e6d2da7 100644 --- a/src/EventSubscriber/LogSystem/LogoutLoggerListener.php +++ b/src/EventSubscriber/LogSystem/LogLogoutEventSubscriber.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\EventSubscriber\LogSystem; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use App\Entity\LogSystem\UserLogoutLogEntry; use App\Entity\UserSystem\User; use App\Services\LogSystem\EventLogger; @@ -30,27 +32,22 @@ use Symfony\Component\Security\Http\Event\LogoutEvent; /** * This handler logs to event log, if a user logs out. */ -class LogoutLoggerListener +final class LogLogoutEventSubscriber implements EventSubscriberInterface { - protected EventLogger $logger; - protected bool $gpdr_compliance; - - public function __construct(EventLogger $logger, bool $gpdr_compliance) + public function __construct(private readonly EventLogger $logger, private readonly bool $gdpr_compliance) { - $this->logger = $logger; - $this->gpdr_compliance = $gpdr_compliance; } - public function __invoke(LogoutEvent $event) + public function logLogout(LogoutEvent $event): void { $request = $event->getRequest(); $token = $event->getToken(); - if (null === $token) { + if (!$token instanceof TokenInterface) { return; } - $log = new UserLogoutLogEntry($request->getClientIp(), $this->gpdr_compliance); + $log = new UserLogoutLogEntry($request->getClientIp(), $this->gdpr_compliance); $user = $token->getUser(); if ($user instanceof User) { $log->setTargetElement($user); @@ -58,4 +55,12 @@ class LogoutLoggerListener $this->logger->logAndFlush($log); } + /** + * @return array + */ + public static function getSubscribedEvents(): array + { + return [LogoutEvent::class => 'logLogout']; + } + } diff --git a/src/EventSubscriber/LogSystem/SecurityEventLoggerSubscriber.php b/src/EventSubscriber/LogSystem/SecurityEventLoggerSubscriber.php index 78402633..7880a41d 100644 --- a/src/EventSubscriber/LogSystem/SecurityEventLoggerSubscriber.php +++ b/src/EventSubscriber/LogSystem/SecurityEventLoggerSubscriber.php @@ -41,6 +41,7 @@ declare(strict_types=1); namespace App\EventSubscriber\LogSystem; +use Symfony\Component\HttpFoundation\Request; use App\Entity\LogSystem\SecurityEventLogEntry; use App\Events\SecurityEvent; use App\Events\SecurityEvents; @@ -49,19 +50,12 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; /** - * This subscriber writes entries to log if an security related event happens (e.g. the user changes its password). + * This subscriber writes entries to log if a security related event happens (e.g. the user changes its password). */ final class SecurityEventLoggerSubscriber implements EventSubscriberInterface { - private RequestStack $requestStack; - private bool $gpdr_compliant; - private EventLogger $eventLogger; - - public function __construct(RequestStack $requestStack, EventLogger $eventLogger, bool $gpdr_compliance) + public function __construct(private readonly RequestStack $requestStack, private readonly EventLogger $eventLogger, private readonly bool $gdpr_compliance) { - $this->requestStack = $requestStack; - $this->gpdr_compliant = $gpdr_compliance; - $this->eventLogger = $eventLogger; } public static function getSubscribedEvents(): array @@ -76,6 +70,7 @@ final class SecurityEventLoggerSubscriber implements EventSubscriberInterface SecurityEvents::GOOGLE_DISABLED => 'google_disabled', SecurityEvents::GOOGLE_ENABLED => 'google_enabled', SecurityEvents::TFA_ADMIN_RESET => 'tfa_admin_reset', + SecurityEvents::USER_IMPERSONATED => 'user_impersonated', ]; } @@ -109,6 +104,11 @@ final class SecurityEventLoggerSubscriber implements EventSubscriberInterface $this->addLog(SecurityEvents::U2F_REMOVED, $event); } + public function user_impersonated(SecurityEvent $event): void + { + $this->addLog(SecurityEvents::USER_IMPERSONATED, $event); + } + public function u2f_added(SecurityEvent $event): void { $this->addLog(SecurityEvents::U2F_ADDED, $event); @@ -126,14 +126,14 @@ final class SecurityEventLoggerSubscriber implements EventSubscriberInterface private function addLog(string $type, SecurityEvent $event): void { - $anonymize = $this->gpdr_compliant; + $anonymize = $this->gdpr_compliance; $request = $this->requestStack->getCurrentRequest(); - if (null !== $request) { + if ($request instanceof Request) { $ip = $request->getClientIp() ?? 'unknown'; } else { $ip = 'Console'; - //Dont try to apply IP filter rules to non numeric string + //Don't try to apply IP filter rules to non-numeric string $anonymize = false; } diff --git a/src/EventSubscriber/RedirectToHttpsSubscriber.php b/src/EventSubscriber/RedirectToHttpsSubscriber.php new file mode 100644 index 00000000..7089109e --- /dev/null +++ b/src/EventSubscriber/RedirectToHttpsSubscriber.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventSubscriber; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Security\Http\HttpUtils; + +/** + * The purpose of this event listener is (if enabled) to redirect all requests to https. + */ +final class RedirectToHttpsSubscriber implements EventSubscriberInterface +{ + + public function __construct( + #[Autowire('%env(bool:REDIRECT_TO_HTTPS)%')] + private readonly bool $enabled, + private readonly HttpUtils $httpUtils) + { + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::REQUEST => 'onKernelRequest', + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + //If the feature is disabled, or we are not the main request, we do nothing + if (!$this->enabled || !$event->isMainRequest()) { + return; + } + + + $request = $event->getRequest(); + + //If the request is already https, we do nothing + if ($request->isSecure()) { + return; + } + + + //Change the request to https + $new_url = str_replace('http://', 'https://' ,$request->getUri()); + $event->setResponse($this->httpUtils->createRedirectResponse($event->getRequest(), $new_url)); + } +} \ No newline at end of file diff --git a/src/EventSubscriber/SetMailFromSubscriber.php b/src/EventSubscriber/SetMailFromSubscriber.php index 6c9f563c..3675c487 100644 --- a/src/EventSubscriber/SetMailFromSubscriber.php +++ b/src/EventSubscriber/SetMailFromSubscriber.php @@ -32,13 +32,8 @@ use Symfony\Component\Mime\Email; */ final class SetMailFromSubscriber implements EventSubscriberInterface { - private string $email; - private string $name; - - public function __construct(string $email, string $name) + public function __construct(private readonly string $email, private readonly string $name) { - $this->email = $email; - $this->name = $name; } public function onMessage(MessageEvent $event): void diff --git a/src/EventSubscriber/SwitchUserEventSubscriber.php b/src/EventSubscriber/SwitchUserEventSubscriber.php new file mode 100644 index 00000000..b68f6b4f --- /dev/null +++ b/src/EventSubscriber/SwitchUserEventSubscriber.php @@ -0,0 +1,64 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventSubscriber; + +use App\Entity\UserSystem\User; +use App\Events\SecurityEvent; +use App\Events\SecurityEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; +use Symfony\Component\Security\Http\Event\SwitchUserEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +class SwitchUserEventSubscriber implements EventSubscriberInterface +{ + + public function __construct(private readonly EventDispatcherInterface $eventDispatcher) + { + } + + public static function getSubscribedEvents(): array + { + return [ + 'security.switch_user' => 'onSwitchUser', + ]; + } + + public function onSwitchUser(SwitchUserEvent $event): void + { + $target_user = $event->getTargetUser(); + // We can only handle User objects + if (!$target_user instanceof User) { + return; + } + + //We are only interested in impersonation (not unimpersonation) + if (!$event->getToken() instanceof SwitchUserToken) { + return; + } + + $security_event = new SecurityEvent($target_user); + $this->eventDispatcher->dispatch($security_event, SecurityEvents::USER_IMPERSONATED); + } +} \ No newline at end of file diff --git a/src/EventSubscriber/SymfonyDebugToolbarSubscriber.php b/src/EventSubscriber/SymfonyDebugToolbarSubscriber.php index f6f4fc25..6f17e399 100644 --- a/src/EventSubscriber/SymfonyDebugToolbarSubscriber.php +++ b/src/EventSubscriber/SymfonyDebugToolbarSubscriber.php @@ -26,15 +26,12 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; /** - * This subscriber sets an Header in Debug mode that signals the Symfony Profiler to also update on Ajax requests. + * This subscriber sets a Header in Debug mode that signals the Symfony Profiler to also update on Ajax requests. */ final class SymfonyDebugToolbarSubscriber implements EventSubscriberInterface { - private bool $kernel_debug; - - public function __construct(bool $kernel_debug) + public function __construct(private readonly bool $kernel_debug_enabled) { - $this->kernel_debug = $kernel_debug; } /** @@ -62,7 +59,7 @@ final class SymfonyDebugToolbarSubscriber implements EventSubscriberInterface public function onKernelResponse(ResponseEvent $event): void { - if (!$this->kernel_debug) { + if (!$this->kernel_debug_enabled) { return; } diff --git a/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php b/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php index 5e18826b..d67a8e14 100644 --- a/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php +++ b/src/EventSubscriber/UserSystem/LoginSuccessSubscriber.php @@ -26,44 +26,36 @@ use App\Entity\LogSystem\UserLoginLogEntry; use App\Entity\UserSystem\User; use App\Services\LogSystem\EventLogger; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\SecurityEvents; use Symfony\Contracts\Translation\TranslatorInterface; /** - * This event listener shows an login successful flash to the user after login and write the login to event log. + * This event listener shows a login successful flash to the user after login and write the login to event log. */ final class LoginSuccessSubscriber implements EventSubscriberInterface { - private TranslatorInterface $translator; - private FlashBagInterface $flashBag; - private EventLogger $eventLogger; - private bool $gpdr_compliance; - - public function __construct(TranslatorInterface $translator, SessionInterface $session, EventLogger $eventLogger, bool $gpdr_compliance) + public function __construct(private readonly TranslatorInterface $translator, private readonly RequestStack $requestStack, private readonly EventLogger $eventLogger, private readonly bool $gdpr_compliance) { - /** @var Session $session */ - $this->translator = $translator; - $this->flashBag = $session->getFlashBag(); - $this->eventLogger = $eventLogger; - $this->gpdr_compliance = $gpdr_compliance; } public function onLogin(InteractiveLoginEvent $event): void { $ip = $event->getRequest()->getClientIp(); - $log = new UserLoginLogEntry($ip, $this->gpdr_compliance); + $log = new UserLoginLogEntry($ip, $this->gdpr_compliance); $user = $event->getAuthenticationToken()->getUser(); if ($user instanceof User && $user->getID()) { $log->setTargetElement($user); $this->eventLogger->logAndFlush($log); } + /** @var Session $session */ + $session = $this->requestStack->getSession(); + $flashBag = $session->getFlashBag(); - $this->flashBag->add('notice', $this->translator->trans('flash.login_successful')); + $flashBag->add('notice', $this->translator->trans('flash.login_successful')); } /** diff --git a/src/EventSubscriber/UserSystem/LogoutDisabledUserSubscriber.php b/src/EventSubscriber/UserSystem/LogoutDisabledUserSubscriber.php index 2ab3f8f7..33c5e919 100644 --- a/src/EventSubscriber/UserSystem/LogoutDisabledUserSubscriber.php +++ b/src/EventSubscriber/UserSystem/LogoutDisabledUserSubscriber.php @@ -22,35 +22,29 @@ declare(strict_types=1); namespace App\EventSubscriber\UserSystem; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\UserSystem\User; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Security; /** - * This subscriber is used to log out a disabled user, as soon as he to do an request. - * It is not possible for him to login again, afterwards. + * This subscriber is used to log out a disabled user, as soon as he to do a request. + * It is not possible for him to login again, afterward. */ final class LogoutDisabledUserSubscriber implements EventSubscriberInterface { - private Security $security; - private UrlGeneratorInterface $urlGenerator; - - public function __construct(Security $security, UrlGeneratorInterface $urlGenerator) + public function __construct(private readonly Security $security, private readonly UrlGeneratorInterface $urlGenerator) { - $this->security = $security; - - $this->urlGenerator = $urlGenerator; } public function onRequest(RequestEvent $event): void { $user = $this->security->getUser(); if ($user instanceof User && $user->isDisabled()) { - //Redirect to login + //Redirect to log in $response = new RedirectResponse($this->urlGenerator->generate('logout')); $event->setResponse($response); } diff --git a/src/EventSubscriber/UserSystem/PasswordChangeNeededSubscriber.php b/src/EventSubscriber/UserSystem/PasswordChangeNeededSubscriber.php index 98020f03..2eb32436 100644 --- a/src/EventSubscriber/UserSystem/PasswordChangeNeededSubscriber.php +++ b/src/EventSubscriber/UserSystem/PasswordChangeNeededSubscriber.php @@ -22,14 +22,14 @@ declare(strict_types=1); namespace App\EventSubscriber\UserSystem; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Http\HttpUtils; /** @@ -55,16 +55,9 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface * @var string The route the user will redirected to, if he needs to change this password */ public const REDIRECT_TARGET = 'user_settings'; - private Security $security; - private FlashBagInterface $flashBag; - private HttpUtils $httpUtils; - public function __construct(Security $security, SessionInterface $session, HttpUtils $httpUtils) + public function __construct(private readonly Security $security, private readonly HttpUtils $httpUtils) { - /** @var Session $session */ - $this->security = $security; - $this->flashBag = $session->getFlashBag(); - $this->httpUtils = $httpUtils; } /** @@ -84,8 +77,13 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface return; } + //If the user is impersonated, we don't need to redirect him + if ($this->security->isGranted('IS_IMPERSONATOR')) { + return; + } + //Abort if we dont need to redirect the user. - if (!$user->isNeedPwChange() && !static::TFARedirectNeeded($user)) { + if (!$user->isNeedPwChange() && !self::TFARedirectNeeded($user, $request)) { return; } @@ -99,17 +97,21 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface /* Dont redirect tree endpoints, as this would cause trouble and creates multiple flash warnigs for one page reload */ - if (false !== strpos($request->getUri(), '/tree/')) { + if (str_contains($request->getUri(), '/tree/')) { return; } + /** @var Session $session */ + $session = $request->getSession(); + $flashBag = $session->getFlashBag(); + //Show appropriate message to user about the reason he was redirected if ($user->isNeedPwChange()) { - $this->flashBag->add('warning', 'user.pw_change_needed.flash'); + $flashBag->add('warning', 'user.pw_change_needed.flash'); } - if (static::TFARedirectNeeded($user)) { - $this->flashBag->add('warning', 'user.2fa_needed.flash'); + if (self::TFARedirectNeeded($user, $request)) { + $flashBag->add('warning', 'user.2fa_needed.flash'); } $event->setResponse($this->httpUtils->createRedirectResponse($request, static::REDIRECT_TARGET)); @@ -119,16 +121,35 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface * Check if a redirect because of a missing 2FA method is needed. * That is the case if the group of the user enforces 2FA, but the user has neither Google Authenticator nor an * U2F key setup. + * The result is cached for some minutes in the session to prevent unnecessary database queries. * * @param User $user the user for which should be checked if it needs to be redirected * * @return bool true if the user needs to be redirected */ - public static function TFARedirectNeeded(User $user): bool + public static function TFARedirectNeeded(User $user, Request $request): bool { + //Check if when we have checked the user the last time + $session = $request->getSession(); + $last_check = $session->get('tfa_redirect_check', 0); + + //If we have checked the user already in the last 10 minutes, we don't need to check it again + if ($last_check > time() - 600) { + return false; + } + + //Otherwise we check the user again + $tfa_enabled = $user->isWebAuthnAuthenticatorEnabled() || $user->isGoogleAuthenticatorEnabled(); - return null !== $user->getGroup() && $user->getGroup()->isEnforce2FA() && !$tfa_enabled; + $result = $user->getGroup() instanceof Group && $user->getGroup()->isEnforce2FA() && !$tfa_enabled; + + //If no redirect is needed, we set the last check time to now + if (!$result) { + $session->set('tfa_redirect_check', time()); + } + + return $result; } public static function getSubscribedEvents(): array diff --git a/src/EventSubscriber/UserSystem/SetUserTimezoneSubscriber.php b/src/EventSubscriber/UserSystem/SetUserTimezoneSubscriber.php index c39d4b37..10ecaddf 100644 --- a/src/EventSubscriber/UserSystem/SetUserTimezoneSubscriber.php +++ b/src/EventSubscriber/UserSystem/SetUserTimezoneSubscriber.php @@ -23,23 +23,18 @@ declare(strict_types=1); namespace App\EventSubscriber\UserSystem; use App\Entity\UserSystem\User; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\Security\Core\Security; /** * The purpose of this event listener is to set the timezone to the one preferred by the user. */ final class SetUserTimezoneSubscriber implements EventSubscriberInterface { - private string $default_timezone; - private Security $security; - - public function __construct(string $timezone, Security $security) + public function __construct(private readonly string $default_timezone, private readonly Security $security) { - $this->default_timezone = $timezone; - $this->security = $security; } public function setTimeZone(ControllerEvent $event): void @@ -48,12 +43,12 @@ final class SetUserTimezoneSubscriber implements EventSubscriberInterface //Check if the user has set a timezone $user = $this->security->getUser(); - if ($user instanceof User && !empty($user->getTimezone())) { + if ($user instanceof User && ($user->getTimezone() !== null && $user->getTimezone() !== '')) { $timezone = $user->getTimezone(); } //Fill with default value if needed - if (null === $timezone && !empty($this->default_timezone)) { + if (null === $timezone && $this->default_timezone !== '') { $timezone = $this->default_timezone; } diff --git a/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php b/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php index 7d891df0..613bc6ec 100644 --- a/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php +++ b/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php @@ -1,4 +1,7 @@ . */ - namespace App\EventSubscriber\UserSystem; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\User\UserInterface; use App\Entity\UserSystem\User; use App\Services\LogSystem\EventCommentHelper; use App\Services\UserSystem\PermissionSchemaUpdater; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\Security\Core\Security; /** - * The purpose of this event subscriber is to check if the permission schema of the current user is up to date and upgrade it automatically if needed. + * The purpose of this event subscriber is to check if the permission schema of the current user is up-to-date and upgrade it automatically if needed. */ class UpgradePermissionsSchemaSubscriber implements EventSubscriberInterface { - private Security $security; - private PermissionSchemaUpdater $permissionSchemaUpdater; - private EntityManagerInterface $entityManager; - private FlashBagInterface $flashBag; - private EventCommentHelper $eventCommentHelper; - - public function __construct(Security $security, PermissionSchemaUpdater $permissionSchemaUpdater, EntityManagerInterface $entityManager, FlashBagInterface $flashBag, EventCommentHelper $eventCommentHelper) + public function __construct(private readonly Security $security, private readonly PermissionSchemaUpdater $permissionSchemaUpdater, private readonly EntityManagerInterface $entityManager, private readonly EventCommentHelper $eventCommentHelper) { - $this->security = $security; - $this->permissionSchemaUpdater = $permissionSchemaUpdater; - $this->entityManager = $entityManager; - $this->flashBag = $flashBag; - $this->eventCommentHelper = $eventCommentHelper; } public function onRequest(RequestEvent $event): void @@ -57,16 +49,25 @@ class UpgradePermissionsSchemaSubscriber implements EventSubscriberInterface } $user = $this->security->getUser(); - if (null === $user) { + if (!$user instanceof UserInterface) { //Retrieve anonymous user $user = $this->entityManager->getRepository(User::class)->getAnonymousUser(); } + /** @var Session $session */ + $session = $event->getRequest()->getSession(); + $flashBag = $session->getFlashBag(); + + //Check if the user is an instance of User, otherwise we can't upgrade the schema + if (!$user instanceof User) { + return; + } + if ($this->permissionSchemaUpdater->isSchemaUpdateNeeded($user)) { $this->eventCommentHelper->setMessage('Automatic permission schema update'); $this->permissionSchemaUpdater->userUpgradeSchemaRecursively($user); $this->entityManager->flush(); - $this->flashBag->add('notice', 'user.permissions_schema_updated'); + $flashBag->add('notice', 'user.permissions_schema_updated'); } } @@ -74,4 +75,4 @@ class UpgradePermissionsSchemaSubscriber implements EventSubscriberInterface { return [KernelEvents::REQUEST => 'onRequest']; } -} \ No newline at end of file +} diff --git a/src/Events/SecurityEvent.php b/src/Events/SecurityEvent.php index bf8056d3..883460a7 100644 --- a/src/Events/SecurityEvent.php +++ b/src/Events/SecurityEvent.php @@ -46,15 +46,12 @@ use Symfony\Contracts\EventDispatcher\Event; /** * This event is triggered when something security related to a user happens. - * For example when the password is reset or the an two factor authentication method was disabled. + * For example when the password is reset or the two-factor authentication method was disabled. */ class SecurityEvent extends Event { - protected User $targetUser; - - public function __construct(User $targetUser) + public function __construct(protected User $targetUser) { - $this->targetUser = $targetUser; } /** diff --git a/src/Events/SecurityEvents.php b/src/Events/SecurityEvents.php index 6ac1f882..f2e44c6f 100644 --- a/src/Events/SecurityEvents.php +++ b/src/Events/SecurityEvents.php @@ -43,13 +43,15 @@ namespace App\Events; class SecurityEvents { - public const PASSWORD_CHANGED = 'security.password_changed'; - public const PASSWORD_RESET = 'security.password_reset'; - public const BACKUP_KEYS_RESET = 'security.backup_keys_reset'; - public const U2F_ADDED = 'security.u2f_added'; - public const U2F_REMOVED = 'security.u2f_removed'; - public const GOOGLE_ENABLED = 'security.google_enabled'; - public const GOOGLE_DISABLED = 'security.google_disabled'; - public const TRUSTED_DEVICE_RESET = 'security.trusted_device_reset'; - public const TFA_ADMIN_RESET = 'security.2fa_admin_reset'; + final public const PASSWORD_CHANGED = 'security.password_changed'; + final public const PASSWORD_RESET = 'security.password_reset'; + final public const BACKUP_KEYS_RESET = 'security.backup_keys_reset'; + final public const U2F_ADDED = 'security.u2f_added'; + final public const U2F_REMOVED = 'security.u2f_removed'; + final public const GOOGLE_ENABLED = 'security.google_enabled'; + final public const GOOGLE_DISABLED = 'security.google_disabled'; + final public const TRUSTED_DEVICE_RESET = 'security.trusted_device_reset'; + final public const TFA_ADMIN_RESET = 'security.2fa_admin_reset'; + + final public const USER_IMPERSONATED = 'security.user_impersonated'; } diff --git a/src/Exceptions/InvalidRegexException.php b/src/Exceptions/InvalidRegexException.php new file mode 100644 index 00000000..aaa2a897 --- /dev/null +++ b/src/Exceptions/InvalidRegexException.php @@ -0,0 +1,74 @@ +. + */ +namespace App\Exceptions; + +use Doctrine\DBAL\Exception\DriverException; +use ErrorException; + +class InvalidRegexException extends \RuntimeException +{ + public function __construct(private readonly ?string $reason = null) + { + parent::__construct('Invalid regular expression'); + } + + /** + * Returns the reason for the exception (what the regex driver deemed invalid) + */ + public function getReason(): ?string + { + return $this->reason; + } + + /** + * Creates a new exception from a driver exception happening, when MySQL encounters an invalid regex + */ + public static function fromDriverException(DriverException $exception): self + { + //1139 means invalid regex error + if ($exception->getCode() !== 1139) { + throw new \InvalidArgumentException('The given exception is not a driver exception', 0, $exception); + } + + //Reason is the part after the erorr code + $reason = preg_replace('/^.*1139 /', '', $exception->getMessage()); + + return new self($reason); + } + + /** + * Creates a new exception from the errorException thrown by mb_ereg + */ + public static function fromMBRegexError(ErrorException $ex): self + { + //Ensure that the error is really a mb_ereg error + if ($ex->getSeverity() !== E_WARNING || !strpos($ex->getMessage(), 'mb_ereg()')) { + throw new \InvalidArgumentException('The given exception is not a mb_ereg error', 0, $ex); + } + + //Reason is the part after the erorr code + $reason = preg_replace('/^.*mb_ereg\(\): /', '', $ex->getMessage()); + + return new self($reason); + } +} diff --git a/src/Exceptions/TwigModeException.php b/src/Exceptions/TwigModeException.php index adcc86aa..b76d14d3 100644 --- a/src/Exceptions/TwigModeException.php +++ b/src/Exceptions/TwigModeException.php @@ -46,8 +46,23 @@ use Twig\Error\Error; class TwigModeException extends RuntimeException { + private const PROJECT_PATH = __DIR__ . '/../../'; + public function __construct(?Error $previous = null) { parent::__construct($previous->getMessage(), 0, $previous); } + + /** + * Returns the message of this exception, where it is tried to remove any sensitive information (like filepaths). + * @return string + */ + public function getSafeMessage(): string + { + //Resolve project root path + $projectPath = realpath(self::PROJECT_PATH); + + //Remove occurrences of the project path from the message + return str_replace($projectPath, '[Part-DB Root Folder]', $this->getMessage()); + } } diff --git a/src/Form/AdminPages/AttachmentTypeAdminForm.php b/src/Form/AdminPages/AttachmentTypeAdminForm.php index 57ba6fed..d777d4d4 100644 --- a/src/Form/AdminPages/AttachmentTypeAdminForm.php +++ b/src/Form/AdminPages/AttachmentTypeAdminForm.php @@ -22,21 +22,18 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Services\Attachments\FileTypeFilterTools; use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Security\Core\Security; class AttachmentTypeAdminForm extends BaseEntityAdminForm { - protected FileTypeFilterTools $filterTools; - - public function __construct(Security $security, FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(Security $security, protected FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper) { - $this->filterTools = $filterTools; parent::__construct($security, $eventCommentNeededHelper); } @@ -58,12 +55,8 @@ class AttachmentTypeAdminForm extends BaseEntityAdminForm //Normalize data before writing it to database $builder->get('filetype_filter')->addViewTransformer(new CallbackTransformer( - static function ($value) { - return $value; - }, - function ($value) { - return $this->filterTools->normalizeFilterString($value); - } + static fn($value) => $value, + fn($value) => $this->filterTools->normalizeFilterString($value) )); } } diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index 1a95a119..d1a0ffd0 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -22,18 +22,19 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use App\Entity\PriceInformations\Currency; +use App\Entity\ProjectSystem\Project; +use App\Entity\UserSystem\Group; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\LabelSystem\LabelProfile; -use App\Entity\Parameters\AbstractParameter; use App\Form\AttachmentFormType; use App\Form\ParameterType; use App\Form\Type\MasterPictureAttachmentType; use App\Form\Type\RichTextEditorType; use App\Form\Type\StructuralEntityType; use App\Services\LogSystem\EventCommentNeededHelper; -use FOS\CKEditorBundle\Form\Type\CKEditorType; -use function get_class; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; @@ -42,17 +43,11 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; class BaseEntityAdminForm extends AbstractType { - protected Security $security; - protected EventCommentNeededHelper $eventCommentNeededHelper; - - public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(protected Security $security, protected EventCommentNeededHelper $eventCommentNeededHelper) { - $this->security = $security; - $this->eventCommentNeededHelper = $eventCommentNeededHelper; } public function configureOptions(OptionsResolver $resolver): void @@ -84,7 +79,7 @@ class BaseEntityAdminForm extends AbstractType 'parent', StructuralEntityType::class, [ - 'class' => get_class($entity), + 'class' => $entity::class, 'required' => false, 'label' => 'parent.label', 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), @@ -118,6 +113,19 @@ class BaseEntityAdminForm extends AbstractType ); } + if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Currency)) { + $builder->add('alternative_names', TextType::class, [ + 'required' => false, + 'label' => 'entity.edit.alternative_names.label', + 'help' => 'entity.edit.alternative_names.help', + 'empty_data' => null, + 'attr' => [ + 'class' => 'tagsinput', + 'data-controller' => 'elements--tagsinput', + ] + ]); + } + $this->additionalFormElements($builder, $options, $entity); //Attachment section diff --git a/src/Form/AdminPages/CategoryAdminForm.php b/src/Form/AdminPages/CategoryAdminForm.php index 10a56646..44c1dede 100644 --- a/src/Form/AdminPages/CategoryAdminForm.php +++ b/src/Form/AdminPages/CategoryAdminForm.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; +use App\Form\Part\EDA\EDACategoryInfoType; use App\Form\Type\RichTextEditorType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -104,5 +105,11 @@ class CategoryAdminForm extends BaseEntityAdminForm ], 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); + + //EDA info + $builder->add('eda_info', EDACategoryInfoType::class, [ + 'label' => false, + 'required' => false, + ]); } } diff --git a/src/Form/AdminPages/CurrencyAdminForm.php b/src/Form/AdminPages/CurrencyAdminForm.php index 754d7c66..0fab055d 100644 --- a/src/Form/AdminPages/CurrencyAdminForm.php +++ b/src/Form/AdminPages/CurrencyAdminForm.php @@ -22,22 +22,19 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Form\Type\BigDecimalMoneyType; use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\Extension\Core\Type\CurrencyType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Security\Core\Security; class CurrencyAdminForm extends BaseEntityAdminForm { - private string $default_currency; - - public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, string $default_currency) + public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, private readonly string $base_currency) { parent::__construct($security, $eventCommentNeededHelper); - $this->default_currency = $default_currency; } protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void @@ -45,7 +42,7 @@ class CurrencyAdminForm extends BaseEntityAdminForm $is_new = null === $entity->getID(); $builder->add('iso_code', CurrencyType::class, [ - 'required' => false, + 'required' => true, 'label' => 'currency.edit.iso_code', 'preferred_choices' => ['EUR', 'USD', 'GBP', 'JPY', 'CNY'], 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), @@ -54,7 +51,7 @@ class CurrencyAdminForm extends BaseEntityAdminForm $builder->add('exchange_rate', BigDecimalMoneyType::class, [ 'required' => false, 'label' => 'currency.edit.exchange_rate', - 'currency' => $this->default_currency, + 'currency' => $this->base_currency, 'scale' => 6, 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); diff --git a/src/Form/AdminPages/FootprintAdminForm.php b/src/Form/AdminPages/FootprintAdminForm.php index a01e91bc..f96564d0 100644 --- a/src/Form/AdminPages/FootprintAdminForm.php +++ b/src/Form/AdminPages/FootprintAdminForm.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; +use App\Form\Part\EDA\EDAFootprintInfoType; use App\Form\Type\MasterPictureAttachmentType; use Symfony\Component\Form\FormBuilderInterface; @@ -37,5 +38,11 @@ class FootprintAdminForm extends BaseEntityAdminForm 'filter' => '3d_model', 'entity' => $entity, ]); + + //EDA info + $builder->add('eda_info', EDAFootprintInfoType::class, [ + 'label' => false, + 'required' => false, + ]); } } diff --git a/src/Form/AdminPages/ImportType.php b/src/Form/AdminPages/ImportType.php index ded1da2f..3e87812c 100644 --- a/src/Form/AdminPages/ImportType.php +++ b/src/Form/AdminPages/ImportType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parts\Category; use App\Entity\Parts\Part; @@ -33,15 +34,11 @@ use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Security\Core\Security; class ImportType extends AbstractType { - protected Security $security; - - public function __construct(Security $security) + public function __construct(protected Security $security) { - $this->security = $security; } public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/AdminPages/MassCreationForm.php b/src/Form/AdminPages/MassCreationForm.php index 27ee8287..4948cdd5 100644 --- a/src/Form/AdminPages/MassCreationForm.php +++ b/src/Form/AdminPages/MassCreationForm.php @@ -22,21 +22,18 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractStructuralDBElement; use App\Form\Type\StructuralEntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Security\Core\Security; class MassCreationForm extends AbstractType { - protected Security $security; - - public function __construct(Security $security) + public function __construct(protected Security $security) { - $this->security = $security; } public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/AdminPages/ProjectAdminForm.php b/src/Form/AdminPages/ProjectAdminForm.php index 3547d094..2d4683c9 100644 --- a/src/Form/AdminPages/ProjectAdminForm.php +++ b/src/Form/AdminPages/ProjectAdminForm.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; use App\Form\ProjectSystem\ProjectBOMEntryCollectionType; -use App\Form\ProjectSystem\ProjectBOMEntryType; use App\Form\Type\RichTextEditorType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\FormBuilderInterface; class ProjectAdminForm extends BaseEntityAdminForm @@ -61,4 +61,4 @@ class ProjectAdminForm extends BaseEntityAdminForm ], ]); } -} \ No newline at end of file +} diff --git a/src/Form/AdminPages/StorelocationAdminForm.php b/src/Form/AdminPages/StorelocationAdminForm.php index 9e24dcd3..c5c76b16 100644 --- a/src/Form/AdminPages/StorelocationAdminForm.php +++ b/src/Form/AdminPages/StorelocationAdminForm.php @@ -24,7 +24,6 @@ namespace App\Form\AdminPages; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Parts\MeasurementUnit; -use App\Entity\UserSystem\User; use App\Form\Type\StructuralEntityType; use App\Form\Type\UserSelectType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; diff --git a/src/Form/AdminPages/SupplierForm.php b/src/Form/AdminPages/SupplierForm.php index db798db8..34b3b27a 100644 --- a/src/Form/AdminPages/SupplierForm.php +++ b/src/Form/AdminPages/SupplierForm.php @@ -22,22 +22,19 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\PriceInformations\Currency; use App\Form\Type\BigDecimalMoneyType; use App\Form\Type\StructuralEntityType; use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Security\Core\Security; class SupplierForm extends CompanyForm { - protected string $default_currency; - - public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, string $default_currency) + public function __construct(Security $security, EventCommentNeededHelper $eventCommentNeededHelper, protected string $base_currency) { parent::__construct($security, $eventCommentNeededHelper); - $this->default_currency = $default_currency; } protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void @@ -56,7 +53,7 @@ class SupplierForm extends CompanyForm $builder->add('shipping_costs', BigDecimalMoneyType::class, [ 'required' => false, - 'currency' => $this->default_currency, + 'currency' => $this->base_currency, 'scale' => 3, 'label' => 'supplier.shipping_costs.label', 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), diff --git a/src/Form/AttachmentFormType.php b/src/Form/AttachmentFormType.php index 6f33fde0..957d692b 100644 --- a/src/Form/AttachmentFormType.php +++ b/src/Form/AttachmentFormType.php @@ -22,12 +22,12 @@ declare(strict_types=1); namespace App\Form; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Form\Type\StructuralEntityType; use App\Services\Attachments\AttachmentManager; use App\Services\Attachments\AttachmentSubmitHandler; -use App\Validator\Constraints\AllowedFileExtension; use App\Validator\Constraints\UrlOrBuiltin; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -42,33 +42,22 @@ use Symfony\Component\Form\FormView; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Constraints\File; -use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Url; use Symfony\Contracts\Translation\TranslatorInterface; class AttachmentFormType extends AbstractType { - protected AttachmentManager $attachment_helper; - protected UrlGeneratorInterface $urlGenerator; - protected bool $allow_attachments_download; - protected string $max_file_size; - protected Security $security; - protected AttachmentSubmitHandler $submitHandler; - protected TranslatorInterface $translator; - - public function __construct(AttachmentManager $attachmentHelper, UrlGeneratorInterface $urlGenerator, - Security $security, AttachmentSubmitHandler $submitHandler, TranslatorInterface $translator, - bool $allow_attachments_downloads, string $max_file_size) - { - $this->attachment_helper = $attachmentHelper; - $this->urlGenerator = $urlGenerator; - $this->allow_attachments_download = $allow_attachments_downloads; - $this->security = $security; - $this->submitHandler = $submitHandler; - $this->translator = $translator; - $this->max_file_size = $max_file_size; + public function __construct( + protected AttachmentManager $attachment_helper, + protected UrlGeneratorInterface $urlGenerator, + protected Security $security, + protected AttachmentSubmitHandler $submitHandler, + protected TranslatorInterface $translator, + protected bool $allow_attachments_download, + protected bool $download_by_default, + protected string $max_file_size + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -104,7 +93,8 @@ class AttachmentFormType extends AbstractType 'required' => false, 'attr' => [ 'data-controller' => 'elements--attachment-autocomplete', - 'data-autocomplete' => $this->urlGenerator->generate('typeahead_builtInRessources', ['query' => '__QUERY__']), + 'data-autocomplete' => $this->urlGenerator->generate('typeahead_builtInRessources', + ['query' => '__QUERY__']), //Disable browser autocomplete 'autocomplete' => 'off', ], @@ -151,6 +141,12 @@ class AttachmentFormType extends AbstractType } if (!$file instanceof UploadedFile) { + //When no file was uploaded, but a URL was entered, try to determine the attachment name from the URL + if ((trim($attachment->getName()) === '') && ($attachment->getURL() !== null && $attachment->getURL() !== '')) { + $name = basename(parse_url($attachment->getURL(), PHP_URL_PATH)); + $attachment->setName($name); + } + return; } @@ -163,7 +159,7 @@ class AttachmentFormType extends AbstractType } //If the name is empty, use the original file name as attachment name - if (empty($attachment->getName())) { + if ($attachment->getName() === '') { $attachment->setName($file->getClientOriginalName()); } }, 100000); @@ -178,6 +174,30 @@ class AttachmentFormType extends AbstractType } } ); + + //If the attachment should be downloaded by default (and is download allowed at all), register a listener, + // which sets the downloadURL checkbox to true for new attachments + if ($this->download_by_default && $this->allow_attachments_download) { + $builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void { + $form = $event->getForm(); + $attachment = $form->getData(); + + if (!$attachment instanceof Attachment && $attachment !== null) { + return; + } + + //If the attachment was not created yet, set the downloadURL checkbox to true + if ($attachment === null || $attachment->getId() === null) { + $checkbox = $form->get('downloadURL'); + //Ensure that the checkbox is not disabled + if ($checkbox->isDisabled()) { + return; + } + //Set the checkbox + $checkbox->setData(true); + } + }); + } } public function configureOptions(OptionsResolver $resolver): void @@ -189,7 +209,7 @@ class AttachmentFormType extends AbstractType ]); } - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { $view->vars['max_upload_size'] = $this->submitHandler->getMaximumAllowedUploadSize(); } diff --git a/src/Form/CollectionTypeExtension.php b/src/Form/CollectionTypeExtension.php index 3947f9d4..4fa93852 100644 --- a/src/Form/CollectionTypeExtension.php +++ b/src/Form/CollectionTypeExtension.php @@ -44,7 +44,6 @@ namespace App\Form; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use ReflectionClass; -use ReflectionException; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Event\PreSubmitEvent; use Symfony\Component\Form\Extension\Core\Type\CollectionType; @@ -68,11 +67,8 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; */ class CollectionTypeExtension extends AbstractTypeExtension { - protected PropertyAccessorInterface $propertyAccess; - - public function __construct(PropertyAccessorInterface $propertyAccess) + public function __construct(protected PropertyAccessorInterface $propertyAccess) { - $this->propertyAccess = $propertyAccess; } public static function getExtendedTypes(): iterable @@ -94,9 +90,7 @@ class CollectionTypeExtension extends AbstractTypeExtension //Set a unique prototype name, so that we can use nested collections $resolver->setDefaults([ - 'prototype_name' => function (Options $options) { - return '__name_'.uniqid("", false) . '__'; - }, + 'prototype_name' => fn(Options $options): string => '__name_'.uniqid("", false) . '__', ]); $resolver->setAllowedTypes('reindex_enable', 'bool'); @@ -104,7 +98,7 @@ class CollectionTypeExtension extends AbstractTypeExtension $resolver->setAllowedTypes('reindex_path', 'string'); } - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { parent::finishView($view, $form, $options); //Add prototype name to view, so that we can pass it to the stimulus controller @@ -157,7 +151,7 @@ class CollectionTypeExtension extends AbstractTypeExtension //The validator uses the number of the element as index, so we have to map the errors to the correct index $error_mapping = []; $n = 0; - foreach ($data as $key => $item) { + foreach (array_keys($data) as $key) { $error_mapping['['.$n.']'] = $key; $n++; } @@ -167,22 +161,22 @@ class CollectionTypeExtension extends AbstractTypeExtension /** * Set the option of the form. - * This a bit hacky cause we access private properties.... - * + * This a bit hacky because we access private properties.... + * @param FormConfigInterface $builder The form on which the option should be set + * @param string $option The option which should be changed + * @param mixed $value The new value */ - public function setOption(FormConfigInterface $builder, string $option, $value): void + public function setOption(FormConfigInterface $builder, string $option, mixed $value): void { if (!$builder instanceof FormConfigBuilder) { throw new \RuntimeException('This method only works with FormConfigBuilder instances.'); } - //We have to use FormConfigBuilder::class here, because options is private and not available in sub classes + //We have to use FormConfigBuilder::class here, because options is private and not available in subclasses $reflection = new ReflectionClass(FormConfigBuilder::class); $property = $reflection->getProperty('options'); - $property->setAccessible(true); $tmp = $property->getValue($builder); $tmp[$option] = $value; $property->setValue($builder, $tmp); - $property->setAccessible(false); } } diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php index 57967be7..ff80bd38 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters; use App\DataTables\Filters\AttachmentFilter; @@ -30,19 +32,16 @@ use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; use App\Entity\Attachments\LabelAttachment; use App\Entity\Attachments\PartAttachment; -use App\Entity\Attachments\StorelocationAttachment; +use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Supplier; -use App\Form\AdminPages\FootprintAdminForm; use App\Form\Filters\Constraints\BooleanConstraintType; use App\Form\Filters\Constraints\DateTimeConstraintType; use App\Form\Filters\Constraints\InstanceOfConstraintType; use App\Form\Filters\Constraints\NumberConstraintType; use App\Form\Filters\Constraints\StructuralEntityConstraintType; -use App\Form\Filters\Constraints\UserEntityConstraintType; use App\Form\Filters\Constraints\TextConstraintType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ResetType; @@ -61,7 +60,7 @@ class AttachmentFilterType extends AbstractType ]); } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('dbId', NumberConstraintType::class, [ 'label' => 'part.filter.dbId', @@ -86,7 +85,7 @@ class AttachmentFilterType extends AbstractType 'label_profile.label' => LabelAttachment::class, 'manufacturer.label' => Manufacturer::class, 'measurement_unit.label' => MeasurementUnit::class, - 'storelocation.label' => StorelocationAttachment::class, + 'storelocation.label' => StorageLocationAttachment::class, 'supplier.label' => SupplierAttachment::class, 'user.label' => UserAttachment::class, ] @@ -101,6 +100,15 @@ class AttachmentFilterType extends AbstractType 'label' => 'attachment.edit.show_in_table' ]); + $builder->add('originalFileName', TextConstraintType::class, [ + 'label' => 'attachment.file_name' + ]); + + $builder->add('externalLink', TextConstraintType::class, [ + 'label' => 'attachment.table.external_link' + ]); + + $builder->add('lastModified', DateTimeConstraintType::class, [ 'label' => 'lastModified' ]); @@ -117,4 +125,4 @@ class AttachmentFilterType extends AbstractType 'label' => 'filter.discard', ]); } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/BooleanConstraintType.php b/src/Form/Filters/Constraints/BooleanConstraintType.php index ebc5ce09..6669b5a7 100644 --- a/src/Form/Filters/Constraints/BooleanConstraintType.php +++ b/src/Form/Filters/Constraints/BooleanConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\BooleanConstraint; @@ -46,9 +48,9 @@ class BooleanConstraintType extends AbstractType ]); } - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { //Remove the label from the compound form, as the checkbox already has a label $view->vars['label'] = false; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/ChoiceConstraintType.php b/src/Form/Filters/Constraints/ChoiceConstraintType.php index 16014c7f..70d37b08 100644 --- a/src/Form/Filters/Constraints/ChoiceConstraintType.php +++ b/src/Form/Filters/Constraints/ChoiceConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\ChoiceConstraint; @@ -63,4 +65,4 @@ class ChoiceConstraintType extends AbstractType ]); } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/DateTimeConstraintType.php b/src/Form/Filters/Constraints/DateTimeConstraintType.php index f476f0ed..ffd3aafd 100644 --- a/src/Form/Filters/Constraints/DateTimeConstraintType.php +++ b/src/Form/Filters/Constraints/DateTimeConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\DateTimeConstraint; -use App\DataTables\Filters\Constraints\NumberConstraint; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; -use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; @@ -86,10 +86,10 @@ class DateTimeConstraintType extends AbstractType ]); } - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { parent::buildView($view, $form, $options); $view->vars['text_suffix'] = $options['text_suffix']; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/EnumConstraintType.php b/src/Form/Filters/Constraints/EnumConstraintType.php new file mode 100644 index 00000000..59259169 --- /dev/null +++ b/src/Form/Filters/Constraints/EnumConstraintType.php @@ -0,0 +1,73 @@ +. + */ +namespace App\Form\Filters\Constraints; + +use App\DataTables\Filters\Constraints\ChoiceConstraint; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class EnumConstraintType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('enum_class'); + $resolver->setAllowedTypes('enum_class', 'string'); + + $resolver->setRequired('choice_label'); + $resolver->setAllowedTypes('choice_label', ['string', 'callable']); + + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => ChoiceConstraint::class, + ]); + + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'filter.choice_constraint.operator.ANY' => 'ANY', + 'filter.choice_constraint.operator.NONE' => 'NONE', + ]; + + $builder->add('operator', ChoiceType::class, [ + 'choices' => $choices, + 'required' => false, + ]); + + $builder->add('value', EnumType::class, [ + 'class' => $options['enum_class'], + 'choice_label' => $options['choice_label'], + 'required' => false, + 'multiple' => true, + 'attr' => [ + 'data-controller' => 'elements--select-multiple', + ] + ]); + } + +} diff --git a/src/Form/Filters/Constraints/InstanceOfConstraintType.php b/src/Form/Filters/Constraints/InstanceOfConstraintType.php index 4a53e0f5..02de15e5 100644 --- a/src/Form/Filters/Constraints/InstanceOfConstraintType.php +++ b/src/Form/Filters/Constraints/InstanceOfConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\InstanceOfConstraint; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class InstanceOfConstraintType extends AbstractType { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $entityManager) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $entityManager; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('data_class', InstanceOfConstraint::class); } - public function getParent() + public function getParent(): string { return ChoiceConstraintType::class; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/NumberConstraintType.php b/src/Form/Filters/Constraints/NumberConstraintType.php index cbfd6897..ad792fa0 100644 --- a/src/Form/Filters/Constraints/NumberConstraintType.php +++ b/src/Form/Filters/Constraints/NumberConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\NumberConstraint; @@ -47,7 +49,7 @@ class NumberConstraintType extends AbstractType $resolver->setDefaults([ 'compound' => true, 'data_class' => NumberConstraint::class, - 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + 'text_suffix' => '', // A suffix which is attached as text-append to the input group. This can for example be used for units 'min' => null, 'max' => null, @@ -91,10 +93,8 @@ class NumberConstraintType extends AbstractType ]); } - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { - parent::buildView($view, $form, $options); - $view->vars['text_suffix'] = $options['text_suffix']; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/ParameterConstraintType.php b/src/Form/Filters/Constraints/ParameterConstraintType.php index f9f2b311..3c3b396d 100644 --- a/src/Form/Filters/Constraints/ParameterConstraintType.php +++ b/src/Form/Filters/Constraints/ParameterConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\Part\ParameterConstraint; -use Svg\Tag\Text; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\SearchType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -49,10 +50,12 @@ class ParameterConstraintType extends AbstractType $builder->add('unit', SearchType::class, [ 'required' => false, + 'empty_data' => '', ]); $builder->add('symbol', SearchType::class, [ - 'required' => false + 'required' => false, + 'empty_data' => '', ]); $builder->add('value_text', TextConstraintType::class, [ @@ -69,7 +72,6 @@ class ParameterConstraintType extends AbstractType * Ensure that the data is never null, but use an empty ParameterConstraint instead */ $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { - $form = $event->getForm(); $data = $event->getData(); if ($data === null) { @@ -77,4 +79,4 @@ class ParameterConstraintType extends AbstractType } }); } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/ParameterValueConstraintType.php b/src/Form/Filters/Constraints/ParameterValueConstraintType.php index a99fd2a4..01fbca5c 100644 --- a/src/Form/Filters/Constraints/ParameterValueConstraintType.php +++ b/src/Form/Filters/Constraints/ParameterValueConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; class ParameterValueConstraintType extends NumberConstraintType @@ -48,4 +50,4 @@ class ParameterValueConstraintType extends NumberConstraintType { return NumberConstraintType::class; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/StructuralEntityConstraintType.php b/src/Form/Filters/Constraints/StructuralEntityConstraintType.php index c9d6f25a..5191881b 100644 --- a/src/Form/Filters/Constraints/StructuralEntityConstraintType.php +++ b/src/Form/Filters/Constraints/StructuralEntityConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\EntityConstraint; @@ -36,7 +38,7 @@ class StructuralEntityConstraintType extends AbstractType $resolver->setDefaults([ 'compound' => true, 'data_class' => EntityConstraint::class, - 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + 'text_suffix' => '', // A suffix which is attached as text-append to the input group. This can for example be used for units ]); $resolver->setRequired('entity_class'); @@ -65,9 +67,9 @@ class StructuralEntityConstraintType extends AbstractType ]); } - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { parent::buildView($view, $form, $options); $view->vars['text_suffix'] = $options['text_suffix']; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/TagsConstraintType.php b/src/Form/Filters/Constraints/TagsConstraintType.php index e6134a65..0a7661dd 100644 --- a/src/Form/Filters/Constraints/TagsConstraintType.php +++ b/src/Form/Filters/Constraints/TagsConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\Part\TagsConstraint; @@ -30,11 +32,8 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class TagsConstraintType extends AbstractType { - protected UrlGeneratorInterface $urlGenerator; - - public function __construct(UrlGeneratorInterface $urlGenerator) + public function __construct(protected UrlGeneratorInterface $urlGenerator) { - $this->urlGenerator = $urlGenerator; } public function configureOptions(OptionsResolver $resolver): void @@ -71,4 +70,4 @@ class TagsConstraintType extends AbstractType 'required' => false, ]); } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/TextConstraintType.php b/src/Form/Filters/Constraints/TextConstraintType.php index b9415977..c1007cf9 100644 --- a/src/Form/Filters/Constraints/TextConstraintType.php +++ b/src/Form/Filters/Constraints/TextConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\TextConstraint; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\SearchType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; @@ -70,10 +71,10 @@ class TextConstraintType extends AbstractType ]); } - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { parent::buildView($view, $form, $options); $view->vars['text_suffix'] = $options['text_suffix']; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/Constraints/UserEntityConstraintType.php b/src/Form/Filters/Constraints/UserEntityConstraintType.php index 706d7cea..8c82e0d8 100644 --- a/src/Form/Filters/Constraints/UserEntityConstraintType.php +++ b/src/Form/Filters/Constraints/UserEntityConstraintType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters\Constraints; use App\DataTables\Filters\Constraints\EntityConstraint; -use App\Entity\UserSystem\User; -use App\Form\Type\StructuralEntityType; use App\Form\Type\UserSelectType; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; class UserEntityConstraintType extends AbstractType @@ -40,7 +38,7 @@ class UserEntityConstraintType extends AbstractType $resolver->setDefaults([ 'compound' => true, 'data_class' => EntityConstraint::class, - 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + 'text_suffix' => '', //A suffix which is attached as text-append to the input group. This can for example be used for units ]); } @@ -64,9 +62,9 @@ class UserEntityConstraintType extends AbstractType ]); } - public function buildView(FormView $view, FormInterface $form, array $options) + public function buildView(FormView $view, FormInterface $form, array $options): void { parent::buildView($view, $form, $options); $view->vars['text_suffix'] = $options['text_suffix']; } -} \ No newline at end of file +} diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php index f7b460b6..42b367b7 100644 --- a/src/Form/Filters/LogFilterType.php +++ b/src/Form/Filters/LogFilterType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters; use App\DataTables\Filters\LogFilter; -use App\Entity\Attachments\Attachment; -use App\Entity\Attachments\AttachmentType; +use App\Entity\LogSystem\LogLevel; +use App\Entity\LogSystem\LogTargetType; use App\Entity\LogSystem\PartStockChangedLogEntry; -use App\Entity\ProjectSystem\Project; -use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Entity\LabelSystem\LabelProfile; -use App\Entity\LogSystem\AbstractLogEntry; use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\DatabaseUpdatedLogEntry; use App\Entity\LogSystem\ElementCreatedLogEntry; @@ -38,25 +36,10 @@ use App\Entity\LogSystem\SecurityEventLogEntry; use App\Entity\LogSystem\UserLoginLogEntry; use App\Entity\LogSystem\UserLogoutLogEntry; use App\Entity\LogSystem\UserNotAllowedLogEntry; -use App\Entity\Parameters\AbstractParameter; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; -use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Part; -use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; -use App\Entity\Parts\Supplier; -use App\Entity\PriceInformations\Currency; -use App\Entity\PriceInformations\Orderdetail; -use App\Entity\PriceInformations\Pricedetail; -use App\Entity\UserSystem\Group; -use App\Entity\UserSystem\User; -use App\Form\Filters\Constraints\ChoiceConstraintType; use App\Form\Filters\Constraints\DateTimeConstraintType; +use App\Form\Filters\Constraints\EnumConstraintType; use App\Form\Filters\Constraints\InstanceOfConstraintType; use App\Form\Filters\Constraints\NumberConstraintType; -use App\Form\Filters\Constraints\StructuralEntityConstraintType; use App\Form\Filters\Constraints\UserEntityConstraintType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ResetType; @@ -66,17 +49,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class LogFilterType extends AbstractType { - protected const LEVEL_CHOICES = [ - 'log.level.debug' => AbstractLogEntry::LEVEL_DEBUG, - 'log.level.info' => AbstractLogEntry::LEVEL_INFO, - 'log.level.notice' => AbstractLogEntry::LEVEL_NOTICE, - 'log.level.warning' => AbstractLogEntry::LEVEL_WARNING, - 'log.level.error' => AbstractLogEntry::LEVEL_ERROR, - 'log.level.critical' => AbstractLogEntry::LEVEL_CRITICAL, - 'log.level.alert' => AbstractLogEntry::LEVEL_ALERT, - 'log.level.emergency' => AbstractLogEntry::LEVEL_EMERGENCY, - ]; - protected const TARGET_TYPE_CHOICES = [ 'log.type.collection_element_deleted' => CollectionElementDeleted::class, 'log.type.database_updated' => DatabaseUpdatedLogEntry::class, @@ -102,7 +74,7 @@ class LogFilterType extends AbstractType ]); } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('dbId', NumberConstraintType::class, [ 'label' => 'part.filter.dbId', @@ -116,9 +88,10 @@ class LogFilterType extends AbstractType - $builder->add('level', ChoiceConstraintType::class, [ + $builder->add('level', EnumConstraintType::class, [ 'label' => 'log.level', - 'choices' => self::LEVEL_CHOICES, + 'enum_class' => LogLevel::class, + 'choice_label' => fn(LogLevel $level): string => 'log.level.' . $level->toPSR3LevelString(), ]); $builder->add('eventType', InstanceOfConstraintType::class, [ @@ -130,29 +103,32 @@ class LogFilterType extends AbstractType 'label' => 'log.user', ]); - $builder->add('targetType', ChoiceConstraintType::class, [ + $builder->add('targetType', EnumConstraintType::class, [ 'label' => 'log.target_type', - 'choices' => [ - 'user.label' => AbstractLogEntry::targetTypeClassToID(User::class), - 'attachment.label' => AbstractLogEntry::targetTypeClassToID(Attachment::class), - 'attachment_type.label' => AbstractLogEntry::targetTypeClassToID(AttachmentType::class), - 'category.label' => AbstractLogEntry::targetTypeClassToID(Category::class), - 'project.label' => AbstractLogEntry::targetTypeClassToID(Project::class), - 'project_bom_entry.label' => AbstractLogEntry::targetTypeClassToID(ProjectBOMEntry::class), - 'footprint.label' => AbstractLogEntry::targetTypeClassToID(Footprint::class), - 'group.label' => AbstractLogEntry::targetTypeClassToID(Group::class), - 'manufacturer.label' => AbstractLogEntry::targetTypeClassToID(Manufacturer::class), - 'part.label' => AbstractLogEntry::targetTypeClassToID(Part::class), - 'storelocation.label' => AbstractLogEntry::targetTypeClassToID(Storelocation::class), - 'supplier.label' => AbstractLogEntry::targetTypeClassToID(Supplier::class), - 'part_lot.label' => AbstractLogEntry::targetTypeClassToID(PartLot::class), - 'currency.label' => AbstractLogEntry::targetTypeClassToID(Currency::class), - 'orderdetail.label' => AbstractLogEntry::targetTypeClassToID(Orderdetail::class), - 'pricedetail.label' => AbstractLogEntry::targetTypeClassToID(Pricedetail::class), - 'measurement_unit.label' => AbstractLogEntry::targetTypeClassToID(MeasurementUnit::class), - 'parameter.label' => AbstractLogEntry::targetTypeClassToID(AbstractParameter::class), - 'label_profile.label' => AbstractLogEntry::targetTypeClassToID(LabelProfile::class), - ] + 'enum_class' => LogTargetType::class, + 'choice_label' => fn(LogTargetType $type): string => match ($type) { + LogTargetType::NONE => 'log.target_type.none', + LogTargetType::USER => 'user.label', + LogTargetType::ATTACHMENT => 'attachment.label', + LogTargetType::ATTACHMENT_TYPE => 'attachment_type.label', + LogTargetType::CATEGORY => 'category.label', + LogTargetType::PROJECT => 'project.label', + LogTargetType::BOM_ENTRY => 'project_bom_entry.label', + LogTargetType::FOOTPRINT => 'footprint.label', + LogTargetType::GROUP => 'group.label', + LogTargetType::MANUFACTURER => 'manufacturer.label', + LogTargetType::PART => 'part.label', + LogTargetType::STORELOCATION => 'storelocation.label', + LogTargetType::SUPPLIER => 'supplier.label', + LogTargetType::PART_LOT => 'part_lot.label', + LogTargetType::CURRENCY => 'currency.label', + LogTargetType::ORDERDETAIL => 'orderdetail.label', + LogTargetType::PRICEDETAIL => 'pricedetail.label', + LogTargetType::MEASUREMENT_UNIT => 'measurement_unit.label', + LogTargetType::PARAMETER => 'parameter.label', + LogTargetType::LABEL_PROFILE => 'label_profile.label', + LogTargetType::PART_ASSOCIATION => 'part_association.label', + }, ]); $builder->add('targetId', NumberConstraintType::class, [ @@ -169,4 +145,4 @@ class LogFilterType extends AbstractType 'label' => 'filter.discard', ]); } -} \ No newline at end of file +} diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index 249b0c1c..dfe449d1 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Filters; use App\DataTables\Filters\Constraints\Part\ParameterConstraint; @@ -27,7 +29,9 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\ProjectSystem\Project; use App\Form\Filters\Constraints\BooleanConstraintType; use App\Form\Filters\Constraints\ChoiceConstraintType; use App\Form\Filters\Constraints\DateTimeConstraintType; @@ -37,19 +41,21 @@ use App\Form\Filters\Constraints\StructuralEntityConstraintType; use App\Form\Filters\Constraints\TagsConstraintType; use App\Form\Filters\Constraints\TextConstraintType; use App\Form\Filters\Constraints\UserEntityConstraintType; -use Svg\Tag\Text; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; -use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\ResetType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\SubmitButton; use Symfony\Component\OptionsResolver\OptionsResolver; class PartFilterType extends AbstractType { + public function __construct(private readonly Security $security) + { + } + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ @@ -59,7 +65,7 @@ class PartFilterType extends AbstractType ]); } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { /* * Common tab @@ -170,13 +176,13 @@ class PartFilterType extends AbstractType $builder->add('supplier', StructuralEntityConstraintType::class, [ 'label' => 'supplier.label', - 'entity_class' => Manufacturer::class + 'entity_class' => Supplier::class ]); $builder->add('orderdetailsCount', NumberConstraintType::class, [ 'label' => 'part.filter.orderdetails_count', - 'step' => 1, - 'min' => 0, + 'step' => 1, + 'min' => 0, ]); $builder->add('obsolete', BooleanConstraintType::class, [ @@ -188,7 +194,7 @@ class PartFilterType extends AbstractType */ $builder->add('storelocation', StructuralEntityConstraintType::class, [ 'label' => 'storelocation.label', - 'entity_class' => Storelocation::class + 'entity_class' => StorageLocation::class ]); $builder->add('minAmount', NumberConstraintType::class, [ @@ -268,6 +274,31 @@ class PartFilterType extends AbstractType 'min' => 0, ]); + /************************************************************************** + * Project tab + **************************************************************************/ + if ($this->security->isGranted('read', Project::class)) { + $builder + ->add('project', StructuralEntityConstraintType::class, [ + 'label' => 'project.label', + 'entity_class' => Project::class + ]) + ->add('bomQuantity', NumberConstraintType::class, [ + 'label' => 'project.bom.quantity', + 'min' => 0, + 'step' => "any", + ]) + ->add('bomName', TextConstraintType::class, [ + 'label' => 'project.bom.name', + ]) + ->add('bomComment', TextConstraintType::class, [ + 'label' => 'project.bom.comment', + ]) + ; + + } + + $builder->add('submit', SubmitType::class, [ 'label' => 'filter.submit', ]); @@ -277,4 +308,4 @@ class PartFilterType extends AbstractType ]); } -} \ No newline at end of file +} diff --git a/src/Form/InfoProviderSystem/PartSearchType.php b/src/Form/InfoProviderSystem/PartSearchType.php new file mode 100644 index 00000000..9d582ca4 --- /dev/null +++ b/src/Form/InfoProviderSystem/PartSearchType.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\InfoProviderSystem; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\SearchType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; + +class PartSearchType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('keyword', SearchType::class, [ + 'label' => 'info_providers.search.keyword', + ]); + $builder->add('providers', ProviderSelectType::class, [ + 'label' => 'info_providers.search.providers', + 'help' => 'info_providers.search.providers.help', + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'info_providers.search.submit' + ]); + } +} \ No newline at end of file diff --git a/src/Form/InfoProviderSystem/ProviderSelectType.php b/src/Form/InfoProviderSystem/ProviderSelectType.php new file mode 100644 index 00000000..a9373390 --- /dev/null +++ b/src/Form/InfoProviderSystem/ProviderSelectType.php @@ -0,0 +1,56 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\InfoProviderSystem; + +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class ProviderSelectType extends AbstractType +{ + public function __construct(private readonly ProviderRegistry $providerRegistry) + { + + } + + public function getParent(): string + { + return ChoiceType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'choices' => $this->providerRegistry->getActiveProviders(), + 'choice_label' => ChoiceList::label($this, static fn (?InfoProviderInterface $choice) => $choice?->getProviderInfo()['name']), + 'choice_value' => ChoiceList::value($this, static fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()), + + 'multiple' => true, + ]); + } + +} \ No newline at end of file diff --git a/src/Form/LabelOptionsType.php b/src/Form/LabelOptionsType.php index 5af69ce6..ad458374 100644 --- a/src/Form/LabelOptionsType.php +++ b/src/Form/LabelOptionsType.php @@ -41,23 +41,23 @@ declare(strict_types=1); namespace App\Form; +use App\Entity\LabelSystem\BarcodeType; +use App\Entity\LabelSystem\LabelProcessMode; +use App\Entity\LabelSystem\LabelSupportedElement; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\LabelSystem\LabelOptions; use App\Form\Type\RichTextEditorType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; class LabelOptionsType extends AbstractType { - private Security $security; - - public function __construct(Security $security) + public function __construct(private readonly Security $security) { - $this->security = $security; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -81,31 +81,33 @@ class LabelOptionsType extends AbstractType ], ]); - $builder->add('supported_element', ChoiceType::class, [ + $builder->add('supported_element', EnumType::class, [ 'label' => 'label_options.supported_elements.label', - 'choices' => [ - 'part.label' => 'part', - 'part_lot.label' => 'part_lot', - 'storelocation.label' => 'storelocation', - ], + 'class' => LabelSupportedElement::class, + 'choice_label' => fn(LabelSupportedElement $choice) => match($choice) { + LabelSupportedElement::PART => 'part.label', + LabelSupportedElement::PART_LOT => 'part_lot.label', + LabelSupportedElement::STORELOCATION => 'storelocation.label', + }, ]); - $builder->add('barcode_type', ChoiceType::class, [ + $builder->add('barcode_type', EnumType::class, [ 'label' => 'label_options.barcode_type.label', 'empty_data' => 'none', - 'choices' => [ - 'label_options.barcode_type.none' => 'none', - 'label_options.barcode_type.qr' => 'qr', - 'label_options.barcode_type.code128' => 'code128', - 'label_options.barcode_type.code39' => 'code39', - 'label_options.barcode_type.code93' => 'code93', - 'label_options.barcode_type.datamatrix' => 'datamatrix', - ], - 'group_by' => static function ($choice, $key, $value) { - if (in_array($choice, ['qr', 'datamatrix'], true)) { + 'class' => BarcodeType::class, + 'choice_label' => fn(BarcodeType $choice) => match($choice) { + BarcodeType::NONE => 'label_options.barcode_type.none', + BarcodeType::QR => 'label_options.barcode_type.qr', + BarcodeType::CODE128 => 'label_options.barcode_type.code128', + BarcodeType::CODE39 => 'label_options.barcode_type.code39', + BarcodeType::CODE93 => 'label_options.barcode_type.code93', + BarcodeType::DATAMATRIX => 'label_options.barcode_type.datamatrix', + }, + 'group_by' => static function (BarcodeType $choice, $key, $value): ?string { + if ($choice->is2D()) { return 'label_options.barcode_type.2D'; } - if (in_array($choice, ['code39', 'code93', 'code128'], true)) { + if ($choice->is1D()) { return 'label_options.barcode_type.1D'; } @@ -132,12 +134,13 @@ class LabelOptionsType extends AbstractType 'required' => false, ]); - $builder->add('lines_mode', ChoiceType::class, [ + $builder->add('process_mode', EnumType::class, [ 'label' => 'label_options.lines_mode.label', - 'choices' => [ - 'label_options.lines_mode.html' => 'html', - 'label.options.lines_mode.twig' => 'twig', - ], + 'class' => LabelProcessMode::class, + 'choice_label' => fn(LabelProcessMode $choice) => match($choice) { + LabelProcessMode::PLACEHOLDER => 'label_options.lines_mode.html', + LabelProcessMode::TWIG => 'label.options.lines_mode.twig', + }, 'help' => 'label_options.lines_mode.help', 'help_html' => true, 'expanded' => true, diff --git a/src/Form/LabelSystem/LabelDialogType.php b/src/Form/LabelSystem/LabelDialogType.php index 9bc3dfdf..f2710b19 100644 --- a/src/Form/LabelSystem/LabelDialogType.php +++ b/src/Form/LabelSystem/LabelDialogType.php @@ -41,6 +41,7 @@ declare(strict_types=1); namespace App\Form\LabelSystem; +use Symfony\Bundle\SecurityBundle\Security; use App\Form\LabelOptionsType; use App\Validator\Constraints\Misc\ValidRange; use Symfony\Component\Form\AbstractType; @@ -48,15 +49,11 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; class LabelDialogType extends AbstractType { - protected Security $security; - - public function __construct(Security $security) + public function __construct(protected Security $security) { - $this->security = $security; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -74,6 +71,22 @@ class LabelDialogType extends AbstractType 'label' => false, 'disabled' => !$this->security->isGranted('@labels.edit_options') || $options['disable_options'], ]); + + $builder->add('save_profile_name', TextType::class, [ + 'required' => false, + 'attr' =>[ + 'placeholder' => 'label_generator.save_profile_name', + ] + ]); + + $builder->add('save_profile', SubmitType::class, [ + 'label' => 'label_generator.save_profile', + 'disabled' => !$this->security->isGranted('@labels.create_profiles'), + 'attr' => [ + 'class' => 'btn btn-outline-success' + ] + ]); + $builder->add('update', SubmitType::class, [ 'label' => 'label_generator.update', ]); diff --git a/src/Form/LabelSystem/ScanDialogType.php b/src/Form/LabelSystem/ScanDialogType.php index 163ee9c2..13ff8e6f 100644 --- a/src/Form/LabelSystem/ScanDialogType.php +++ b/src/Form/LabelSystem/ScanDialogType.php @@ -41,7 +41,10 @@ declare(strict_types=1); namespace App\Form\LabelSystem; +use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -53,12 +56,34 @@ class ScanDialogType extends AbstractType { $builder->add('input', TextType::class, [ 'label' => 'scan_dialog.input', + //Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters + 'trim' => false, 'attr' => [ 'autofocus' => true, 'id' => 'scan_dialog_input', ], ]); + $builder->add('mode', EnumType::class, [ + 'label' => 'scan_dialog.mode', + 'expanded' => true, + 'class' => BarcodeSourceType::class, + 'required' => false, + 'placeholder' => 'scan_dialog.mode.auto', + 'choice_label' => fn (?BarcodeSourceType $enum) => match($enum) { + null => 'scan_dialog.mode.auto', + BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal', + BarcodeSourceType::IPN => 'scan_dialog.mode.ipn', + BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user', + BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp' + }, + ]); + + $builder->add('info_mode', CheckboxType::class, [ + 'label' => 'scan_dialog.info_mode', + 'required' => false, + ]); + $builder->add('submit', SubmitType::class, [ 'label' => 'scan_dialog.submit', ]); diff --git a/src/Form/ParameterType.php b/src/Form/ParameterType.php index 09293b97..4c2174ae 100644 --- a/src/Form/ParameterType.php +++ b/src/Form/ParameterType.php @@ -50,9 +50,10 @@ use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; use App\Entity\Parameters\PartParameter; -use App\Entity\Parameters\StorelocationParameter; +use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; use App\Entity\Parts\MeasurementUnit; +use App\Form\Type\ExponentialNumberType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -93,7 +94,7 @@ class ParameterType extends AbstractType ], ]); - $builder->add('value_max', NumberType::class, [ + $builder->add('value_max', ExponentialNumberType::class, [ 'label' => false, 'required' => false, 'html5' => true, @@ -101,10 +102,10 @@ class ParameterType extends AbstractType 'step' => 'any', 'placeholder' => 'parameters.max.placeholder', 'class' => 'form-control-sm', - 'style' => 'max-width: 12ch;', + 'style' => 'max-width: 25ch;', ], ]); - $builder->add('value_min', NumberType::class, [ + $builder->add('value_min', ExponentialNumberType::class, [ 'label' => false, 'required' => false, 'html5' => true, @@ -112,10 +113,10 @@ class ParameterType extends AbstractType 'step' => 'any', 'placeholder' => 'parameters.min.placeholder', 'class' => 'form-control-sm', - 'style' => 'max-width: 12ch;', + 'style' => 'max-width: 25ch;', ], ]); - $builder->add('value_typical', NumberType::class, [ + $builder->add('value_typical', ExponentialNumberType::class, [ 'label' => false, 'required' => false, 'html5' => true, @@ -123,7 +124,7 @@ class ParameterType extends AbstractType 'step' => 'any', 'placeholder' => 'parameters.typical.placeholder', 'class' => 'form-control-sm', - 'style' => 'max-width: 12ch;', + 'style' => 'max-width: 25ch;', ], ]); $builder->add('unit', TextType::class, [ @@ -148,7 +149,7 @@ class ParameterType extends AbstractType ]); } - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { //By default use part parameters for autocomplete $view->vars['type'] = 'part'; @@ -163,7 +164,7 @@ class ParameterType extends AbstractType GroupParameter::class => 'group', ManufacturerParameter::class => 'manufacturer', MeasurementUnit::class => 'measurement_unit', - StorelocationParameter::class => 'storelocation', + StorageLocationParameter::class => 'storelocation', SupplierParameter::class => 'supplier', ]; diff --git a/src/Form/Part/EDA/EDACategoryInfoType.php b/src/Form/Part/EDA/EDACategoryInfoType.php new file mode 100644 index 00000000..f45bd697 --- /dev/null +++ b/src/Form/Part/EDA/EDACategoryInfoType.php @@ -0,0 +1,87 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Part\EDA; + +use App\Entity\EDA\EDACategoryInfo; +use App\Form\Type\TriStateCheckboxType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +use function Symfony\Component\Translation\t; + +class EDACategoryInfoType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('reference_prefix', TextType::class, [ + 'label' => 'eda_info.reference_prefix', + 'attr' => [ + 'placeholder' => t('eda_info.reference_prefix.placeholder'), + ] + ] + ) + ->add('visibility', TriStateCheckboxType::class, [ + 'help' => 'eda_info.visibility.help', + 'label' => 'eda_info.visibility', + ]) + ->add('exclude_from_bom', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_bom', + 'label_attr' => [ + 'class' => 'checkbox-inline' + ] + ]) + ->add('exclude_from_board', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_board', + 'label_attr' => [ + 'class' => 'checkbox-inline' + ] + ]) + ->add('exclude_from_sim', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_sim', + 'label_attr' => [ + 'class' => 'checkbox-inline' + ] + ]) + ->add('kicad_symbol', KicadFieldAutocompleteType::class, [ + 'label' => 'eda_info.kicad_symbol', + 'type' => KicadFieldAutocompleteType::TYPE_SYMBOL, + 'attr' => [ + 'placeholder' => t('eda_info.kicad_symbol.placeholder'), + ] + ]) + ; + + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => EDACategoryInfo::class, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Part/EDA/EDAFootprintInfoType.php b/src/Form/Part/EDA/EDAFootprintInfoType.php new file mode 100644 index 00000000..bdfa346c --- /dev/null +++ b/src/Form/Part/EDA/EDAFootprintInfoType.php @@ -0,0 +1,55 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Part\EDA; + +use App\Entity\EDA\EDAFootprintInfo; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +use function Symfony\Component\Translation\t; + +class EDAFootprintInfoType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('kicad_footprint', KicadFieldAutocompleteType::class, [ + 'type' => KicadFieldAutocompleteType::TYPE_FOOTPRINT, + 'label' => 'eda_info.kicad_footprint', + 'attr' => [ + 'placeholder' => t('eda_info.kicad_footprint.placeholder'), + ] + ]); + + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => EDAFootprintInfo::class, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Part/EDA/EDAPartInfoType.php b/src/Form/Part/EDA/EDAPartInfoType.php new file mode 100644 index 00000000..e8cac681 --- /dev/null +++ b/src/Form/Part/EDA/EDAPartInfoType.php @@ -0,0 +1,97 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Part\EDA; + +use App\Entity\EDA\EDAPartInfo; +use App\Form\Type\TriStateCheckboxType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +use function Symfony\Component\Translation\t; + +class EDAPartInfoType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('reference_prefix', TextType::class, [ + 'label' => 'eda_info.reference_prefix', + 'attr' => [ + 'placeholder' => t('eda_info.reference_prefix.placeholder'), + ] + ] + ) + ->add('value', TextType::class, [ + 'label' => 'eda_info.value', + 'attr' => [ + 'placeholder' => t('eda_info.value.placeholder'), + ] + ]) + ->add('visibility', TriStateCheckboxType::class, [ + 'help' => 'eda_info.visibility.help', + 'label' => 'eda_info.visibility', + ]) + ->add('exclude_from_bom', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_bom', + 'label_attr' => [ + 'class' => 'checkbox-inline' + ] + ]) + ->add('exclude_from_board', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_board', + 'label_attr' => [ + 'class' => 'checkbox-inline' + ] + ]) + ->add('exclude_from_sim', TriStateCheckboxType::class, [ + 'label' => 'eda_info.exclude_from_sim', + 'label_attr' => [ + 'class' => 'checkbox-inline' + ] + ]) + ->add('kicad_symbol', KicadFieldAutocompleteType::class, [ + 'label' => 'eda_info.kicad_symbol', + 'type' => KicadFieldAutocompleteType::TYPE_SYMBOL, + 'attr' => [ + 'placeholder' => t('eda_info.kicad_symbol.placeholder'), + ] + ]) + ->add('kicad_footprint', KicadFieldAutocompleteType::class, [ + 'label' => 'eda_info.kicad_footprint', + 'type' => KicadFieldAutocompleteType::TYPE_FOOTPRINT, + 'attr' => [ + 'placeholder' => t('eda_info.kicad_footprint.placeholder'), + ] + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => EDAPartInfo::class, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Part/EDA/KicadFieldAutocompleteType.php b/src/Form/Part/EDA/KicadFieldAutocompleteType.php new file mode 100644 index 00000000..50de81d0 --- /dev/null +++ b/src/Form/Part/EDA/KicadFieldAutocompleteType.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Part\EDA; + +use App\Form\Type\StaticFileAutocompleteType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * This is a specialized version of the StaticFileAutocompleteType, which loads the different types of Kicad lists. + */ +class KicadFieldAutocompleteType extends AbstractType +{ + public const TYPE_FOOTPRINT = 'footprint'; + public const TYPE_SYMBOL = 'symbol'; + + //Do not use a leading slash here! otherwise it will not work under prefixed reverse proxies + public const FOOTPRINT_PATH = 'kicad/footprints.txt'; + public const SYMBOL_PATH = 'kicad/symbols.txt'; + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('type'); + $resolver->setAllowedValues('type', [self::TYPE_SYMBOL, self::TYPE_FOOTPRINT]); + + $resolver->setDefaults([ + 'file' => fn(Options $options) => match ($options['type']) { + self::TYPE_FOOTPRINT => self::FOOTPRINT_PATH, + self::TYPE_SYMBOL => self::SYMBOL_PATH, + default => throw new \InvalidArgumentException('Invalid type'), + } + ]); + } + + public function getParent(): string + { + return StaticFileAutocompleteType::class; + } +} \ No newline at end of file diff --git a/src/Form/Part/OrderdetailType.php b/src/Form/Part/OrderdetailType.php index 8489bc73..53240821 100644 --- a/src/Form/Part/OrderdetailType.php +++ b/src/Form/Part/OrderdetailType.php @@ -22,12 +22,12 @@ declare(strict_types=1); namespace App\Form\Part; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use App\Form\Type\StructuralEntityType; -use App\Form\WorkaroundCollectionType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; @@ -37,15 +37,11 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; class OrderdetailType extends AbstractType { - protected Security $security; - - public function __construct(Security $security) + public function __construct(protected Security $security) { - $this->security = $security; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -83,7 +79,7 @@ class OrderdetailType extends AbstractType $orderdetail = $event->getData(); $dummy_pricedetail = new Pricedetail(); - if (null !== $orderdetail && null !== $orderdetail->getSupplier()) { + if ($orderdetail instanceof Orderdetail && $orderdetail->getSupplier() instanceof Supplier) { $dummy_pricedetail->setCurrency($orderdetail->getSupplier()->getDefaultCurrency()); } diff --git a/src/Form/Part/PartAssociationType.php b/src/Form/Part/PartAssociationType.php new file mode 100644 index 00000000..bf9ec4f9 --- /dev/null +++ b/src/Form/Part/PartAssociationType.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Part; + +use App\Entity\Parts\AssociationType; +use App\Entity\Parts\PartAssociation; +use App\Form\Type\PartSelectType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class PartAssociationType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('other', PartSelectType::class, [ + 'label' => 'part_association.edit.other_part', + ]) + ->add('type', EnumType::class, [ + 'class' => AssociationType::class, + 'label' => 'part_association.edit.type', + 'choice_label' => fn(AssociationType $type) => $type->getTranslationKey(), + 'help' => 'part_association.edit.type.help', + 'attr' => [ + 'data-pages--association-edit-type-select-target' => 'select' + ] + ]) + ->add('other_type', TextType::class, [ + 'required' => false, + 'label' => 'part_association.edit.other_type', + 'row_attr' => [ + 'data-pages--association-edit-type-select-target' => 'display' + ] + ]) + ->add('comment', TextType::class, [ + 'required' => false, + 'label' => 'part_association.edit.comment' + ]) + ; + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => PartAssociation::class, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index ef9bd60c..b1d2ebea 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -27,21 +27,24 @@ use App\Entity\Parameters\PartParameter; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Orderdetail; use App\Form\AttachmentFormType; use App\Form\ParameterType; +use App\Form\Part\EDA\EDAPartInfoType; use App\Form\Type\MasterPictureAttachmentType; use App\Form\Type\RichTextEditorType; use App\Form\Type\SIUnitType; use App\Form\Type\StructuralEntityType; -use App\Form\WorkaroundCollectionType; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\LogSystem\EventCommentNeededHelper; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\ResetType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -49,19 +52,11 @@ use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Security; class PartBaseType extends AbstractType { - protected Security $security; - protected UrlGeneratorInterface $urlGenerator; - protected EventCommentNeededHelper $event_comment_needed_helper; - - public function __construct(Security $security, UrlGeneratorInterface $urlGenerator, EventCommentNeededHelper $event_comment_needed_helper) + public function __construct(protected Security $security, protected UrlGeneratorInterface $urlGenerator, protected EventCommentNeededHelper $event_comment_needed_helper) { - $this->security = $security; - $this->urlGenerator = $urlGenerator; - $this->event_comment_needed_helper = $event_comment_needed_helper; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -70,14 +65,8 @@ class PartBaseType extends AbstractType $part = $builder->getData(); $new_part = null === $part->getID(); - $status_choices = [ - 'm_status.unknown' => '', - 'm_status.announced' => 'announced', - 'm_status.active' => 'active', - 'm_status.nrfnd' => 'nrfnd', - 'm_status.eol' => 'eol', - 'm_status.discontinued' => 'discontinued', - ]; + /** @var PartDetailDTO|null $dto */ + $dto = $options['info_provider_dto']; //Common section $builder @@ -109,13 +98,17 @@ class PartBaseType extends AbstractType ->add('category', StructuralEntityType::class, [ 'class' => Category::class, 'allow_add' => $this->security->isGranted('@categories.create'), + 'dto_value' => $dto?->category, 'label' => 'part.edit.category', 'disable_not_selectable' => true, + //Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity) + 'required' => !$new_part, ]) ->add('footprint', StructuralEntityType::class, [ 'class' => Footprint::class, 'required' => false, 'label' => 'part.edit.footprint', + 'dto_value' => $dto?->footprint, 'allow_add' => $this->security->isGranted('@footprints.create'), 'disable_not_selectable' => true, ]) @@ -136,6 +129,7 @@ class PartBaseType extends AbstractType 'required' => false, 'label' => 'part.edit.manufacturer.label', 'allow_add' => $this->security->isGranted('@manufacturers.create'), + 'dto_value' => $dto?->manufacturer, 'disable_not_selectable' => true, ]) ->add('manufacturer_product_url', UrlType::class, [ @@ -148,9 +142,10 @@ class PartBaseType extends AbstractType 'empty_data' => '', 'label' => 'part.edit.mpn', ]) - ->add('manufacturing_status', ChoiceType::class, [ + ->add('manufacturing_status', EnumType::class, [ 'label' => 'part.edit.manufacturing_status', - 'choices' => $status_choices, + 'class' => ManufacturingStatus::class, + 'choice_label' => fn (ManufacturingStatus $status) => $status->toTranslationKey(), 'required' => false, ]); @@ -251,6 +246,22 @@ class PartBaseType extends AbstractType ], ]); + //Part associations + $builder->add('associated_parts_as_owner', CollectionType::class, [ + 'entry_type' => PartAssociationType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'reindex_enable' => true, + 'label' => false, + 'by_reference' => false, + ]); + + //EDA info + $builder->add('eda_info', EDAPartInfoType::class, [ + 'label' => false, + 'required' => false, + ]); + $builder->add('log_comment', TextType::class, [ 'label' => 'edit.log_comment', 'mapped' => false, @@ -281,10 +292,15 @@ class PartBaseType extends AbstractType ->add('reset', ResetType::class, ['label' => 'part.edit.reset']); } + + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Part::class, + 'info_provider_dto' => null, ]); + + $resolver->setAllowedTypes('info_provider_dto', [PartDetailDTO::class, 'null']); } } diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index 2c46df1a..7d545340 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -22,9 +22,10 @@ declare(strict_types=1); namespace App\Form\Part; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Form\Type\SIUnitType; use App\Form\Type\StructuralEntityType; use App\Form\Type\UserSelectType; @@ -34,15 +35,11 @@ use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; class PartLotType extends AbstractType { - protected Security $security; - - public function __construct(Security $security) + public function __construct(protected Security $security) { - $this->security = $security; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -57,7 +54,7 @@ class PartLotType extends AbstractType ]); $builder->add('storage_location', StructuralEntityType::class, [ - 'class' => Storelocation::class, + 'class' => StorageLocation::class, 'label' => 'part_lot.edit.location', 'required' => false, 'disable_not_selectable' => true, @@ -83,7 +80,7 @@ class PartLotType extends AbstractType 'required' => false, ]); - $builder->add('expirationDate', DateType::class, [ + $builder->add('expiration_date', DateType::class, [ 'label' => 'part_lot.edit.expiration_date', 'attr' => [], 'widget' => 'single_text', @@ -105,6 +102,14 @@ class PartLotType extends AbstractType 'required' => false, 'help' => 'part_lot.owner.help', ]); + + $builder->add('user_barcode', TextType::class, [ + 'label' => 'part_lot.edit.user_barcode', + 'help' => 'part_lot.edit.vendor_barcode.help', + 'required' => false, + //Do not remove whitespace chars on the beginning and end of the string + 'trim' => false, + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/Part/PricedetailType.php b/src/Form/Part/PricedetailType.php index c8df4c71..cabb112d 100644 --- a/src/Form/Part/PricedetailType.php +++ b/src/Form/Part/PricedetailType.php @@ -27,12 +27,18 @@ use App\Entity\PriceInformations\Pricedetail; use App\Form\Type\BigDecimalNumberType; use App\Form\Type\CurrencyEntityType; use App\Form\Type\SIUnitType; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class PricedetailType extends AbstractType { + + public function __construct(private readonly Security $security) + { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { //No labels needed, we define translation in templates @@ -63,6 +69,7 @@ class PricedetailType extends AbstractType 'required' => false, 'label' => false, 'short' => true, + 'allow_add' => $this->security->isGranted('@currencies.create'), ]); } diff --git a/src/Form/PasswordTypeExtension.php b/src/Form/PasswordTypeExtension.php new file mode 100644 index 00000000..64711c53 --- /dev/null +++ b/src/Form/PasswordTypeExtension.php @@ -0,0 +1,56 @@ +. + */ +namespace App\Form; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Purpose of this class is to add the setting 'password_estimator' to the PasswordType. + */ +class PasswordTypeExtension extends AbstractTypeExtension +{ + + public static function getExtendedTypes(): iterable + { + return [PasswordType::class]; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'password_estimator' => false, + ]); + + $resolver->setAllowedTypes('password_estimator', 'bool'); + } + + public function finishView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['password_estimator'] = $options['password_estimator']; + } + +} diff --git a/src/Form/Permissions/PermissionGroupType.php b/src/Form/Permissions/PermissionGroupType.php index c395337f..f3f7ffec 100644 --- a/src/Form/Permissions/PermissionGroupType.php +++ b/src/Form/Permissions/PermissionGroupType.php @@ -30,12 +30,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class PermissionGroupType extends AbstractType { - protected PermissionManager $resolver; protected array $perm_structure; - public function __construct(PermissionManager $resolver) + public function __construct(protected PermissionManager $resolver) { - $this->resolver = $resolver; $this->perm_structure = $resolver->getPermissionStructure(); } @@ -68,9 +66,7 @@ class PermissionGroupType extends AbstractType { parent::configureOptions($resolver); - $resolver->setDefault('group_name', static function (Options $options) { - return trim($options['name']); - }); + $resolver->setDefault('group_name', static fn(Options $options): string => trim((string) $options['name'])); $resolver->setDefault('inherit', false); diff --git a/src/Form/Permissions/PermissionType.php b/src/Form/Permissions/PermissionType.php index 804fed0a..ab5ee86b 100644 --- a/src/Form/Permissions/PermissionType.php +++ b/src/Form/Permissions/PermissionType.php @@ -33,12 +33,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class PermissionType extends AbstractType { - protected PermissionManager $resolver; protected array $perm_structure; - public function __construct(PermissionManager $resolver) + public function __construct(protected PermissionManager $resolver) { - $this->resolver = $resolver; $this->perm_structure = $resolver->getPermissionStructure(); } @@ -46,9 +44,7 @@ class PermissionType extends AbstractType { parent::configureOptions($resolver); - $resolver->setDefault('perm_name', static function (Options $options) { - return $options['name']; - }); + $resolver->setDefault('perm_name', static fn(Options $options) => $options['name']); $resolver->setDefault('label', function (Options $options) { if (!empty($this->perm_structure['perms'][$options['perm_name']]['label'])) { @@ -58,9 +54,7 @@ class PermissionType extends AbstractType return $options['name']; }); - $resolver->setDefault('multi_checkbox', static function (Options $options) { - return !$options['disabled']; - }); + $resolver->setDefault('multi_checkbox', static fn(Options $options) => !$options['disabled']); $resolver->setDefaults([ 'inherit' => false, diff --git a/src/Form/Permissions/PermissionsMapper.php b/src/Form/Permissions/PermissionsMapper.php index c08d2954..d4b937bc 100644 --- a/src/Form/Permissions/PermissionsMapper.php +++ b/src/Form/Permissions/PermissionsMapper.php @@ -34,25 +34,20 @@ use Traversable; */ final class PermissionsMapper implements DataMapperInterface { - private PermissionManager $resolver; - private bool $inherit; - - public function __construct(PermissionManager $resolver, bool $inherit = false) + public function __construct(private readonly PermissionManager $resolver, private readonly bool $inherit = false) { - $this->inherit = $inherit; - $this->resolver = $resolver; } /** - * Maps the view data of a compound form to its children. + * Maps the view data of a compound form to its children. * - * The method is responsible for calling {@link FormInterface::setData()} - * on the children of compound forms, defining their underlying model data. + * The method is responsible for calling {@link FormInterface::setData()} + * on the children of compound forms, defining their underlying model data. * * @param mixed $viewData View data of the compound form being initialized - * @param FormInterface[]|Traversable $forms A list of {@link FormInterface} instances + * @param Traversable $forms A list of {@link FormInterface} instances */ - public function mapDataToForms($viewData, $forms): void + public function mapDataToForms(mixed $viewData, \Traversable $forms): void { foreach ($forms as $form) { if ($this->inherit) { @@ -73,33 +68,33 @@ final class PermissionsMapper implements DataMapperInterface } /** - * Maps the model data of a list of children forms into the view data of their parent. + * Maps the model data of a list of children forms into the view data of their parent. * - * This is the internal cascade call of FormInterface::submit for compound forms, since they - * cannot be bound to any input nor the request as scalar, but their children may: + * This is the internal cascade call of FormInterface::submit for compound forms, since they + * cannot be bound to any input nor the request as scalar, but their children may: * - * $compoundForm->submit($arrayOfChildrenViewData) - * // inside: - * $childForm->submit($childViewData); - * // for each entry, do the same and/or reverse transform - * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData) - * // then reverse transform + * $compoundForm->submit($arrayOfChildrenViewData) + * // inside: + * $childForm->submit($childViewData); + * // for each entry, do the same and/or reverse transform + * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData) + * // then reverse transform * - * When a simple form is submitted the following is happening: + * When a simple form is submitted the following is happening: * - * $simpleForm->submit($submittedViewData) - * // inside: - * $this->viewData = $submittedViewData - * // then reverse transform + * $simpleForm->submit($submittedViewData) + * // inside: + * $this->viewData = $submittedViewData + * // then reverse transform * - * The model data can be an array or an object, so this second argument is always passed - * by reference. + * The model data can be an array or an object, so this second argument is always passed + * by reference. * - * @param FormInterface[]|Traversable $forms A list of {@link FormInterface} instances + * @param Traversable $forms A list of {@link FormInterface} instances * @param mixed $viewData The compound form's view data that get mapped * its children model data */ - public function mapFormsToData($forms, &$viewData): void + public function mapFormsToData(\Traversable $forms, mixed &$viewData): void { if ($this->inherit) { throw new RuntimeException('The permission type is readonly when it is showing read only data!'); diff --git a/src/Form/Permissions/PermissionsType.php b/src/Form/Permissions/PermissionsType.php index e5688729..86fdbc2c 100644 --- a/src/Form/Permissions/PermissionsType.php +++ b/src/Form/Permissions/PermissionsType.php @@ -33,12 +33,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class PermissionsType extends AbstractType { - protected PermissionManager $resolver; protected array $perm_structure; - public function __construct(PermissionManager $resolver) + public function __construct(protected PermissionManager $resolver) { - $this->resolver = $resolver; $this->perm_structure = $resolver->getPermissionStructure(); } @@ -47,6 +45,7 @@ class PermissionsType extends AbstractType $resolver->setDefaults([ 'show_legend' => true, 'show_presets' => false, + 'show_dependency_notice' => static fn(Options $options) => !$options['disabled'], 'constraints' => static function (Options $options) { if (!$options['disabled']) { return [new NoLockout()]; @@ -62,6 +61,7 @@ class PermissionsType extends AbstractType { $view->vars['show_legend'] = $options['show_legend']; $view->vars['show_presets'] = $options['show_presets']; + $view->vars['show_dependency_notice'] = $options['show_dependency_notice']; } public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Form/ProjectSystem/ProjectAddPartsType.php b/src/Form/ProjectSystem/ProjectAddPartsType.php new file mode 100644 index 00000000..61f72c41 --- /dev/null +++ b/src/Form/ProjectSystem/ProjectAddPartsType.php @@ -0,0 +1,88 @@ +. + */ +namespace App\Form\ProjectSystem; + +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Form\Type\StructuralEntityType; +use App\Validator\Constraints\UniqueObjectCollection; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\NotNull; + +class ProjectAddPartsType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('project', StructuralEntityType::class, [ + 'class' => Project::class, + 'required' => true, + 'disabled' => $options['project'] instanceof Project, //If a project is given, disable the field + 'data' => $options['project'], + 'constraints' => [ + new NotNull() + ] + ]); + $builder->add('bom_entries', ProjectBOMEntryCollectionType::class, [ + 'entry_options' => [ + 'constraints' => [ + new UniqueEntity(fields: ['part', 'project'], message: 'project.bom_entry.part_already_in_bom', + entityClass: ProjectBOMEntry::class), + new UniqueEntity(fields: ['name', 'project'], message: 'project.bom_entry.name_already_in_bom', + entityClass: ProjectBOMEntry::class, ignoreNull: true), + ] + ], + 'constraints' => [ + new UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part']), + new UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name']), + ] + ]); + $builder->add('submit', SubmitType::class, ['label' => 'save']); + + //After submit set the project for all bom entries, so that it can be validated properly + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + /** @var Project $project */ + $project = $form->get('project')->getData(); + $bom_entries = $form->get('bom_entries')->getData(); + + foreach ($bom_entries as $bom_entry) { + $bom_entry->setProject($project); + } + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'project' => null, + ]); + + $resolver->setAllowedTypes('project', ['null', Project::class]); + } +} diff --git a/src/Form/ProjectSystem/ProjectBOMEntryCollectionType.php b/src/Form/ProjectSystem/ProjectBOMEntryCollectionType.php index 71c745c7..53ec5f70 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryCollectionType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryCollectionType.php @@ -1,5 +1,7 @@ setDefaults([ 'entry_type' => ProjectBOMEntryType::class, @@ -27,9 +29,4 @@ class ProjectBOMEntryCollectionType extends AbstractType 'label' => false, ]); } - - public function getBlockPrefix() - { - return 'project_bom_entry_collection'; - } -} \ No newline at end of file +} diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index 49292235..cac362fb 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -1,19 +1,17 @@ ProjectBOMEntry::class, ]); } - - - public function getBlockPrefix() - { - return 'project_bom_entry'; - } -} \ No newline at end of file +} diff --git a/src/Form/ProjectSystem/ProjectBuildType.php b/src/Form/ProjectSystem/ProjectBuildType.php index 3758bb21..2b7b52e2 100644 --- a/src/Form/ProjectSystem/ProjectBuildType.php +++ b/src/Form/ProjectSystem/ProjectBuildType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\ProjectSystem; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Form\Type\PartLotSelectType; use App\Form\Type\SIUnitType; @@ -34,18 +38,14 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; class ProjectBuildType extends AbstractType implements DataMapperInterface { - private Security $security; - - public function __construct(Security $security) + public function __construct(private readonly Security $security) { - $this->security = $security; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'compound' => true, @@ -53,7 +53,7 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface ]); } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->setDataMapper($this); @@ -62,6 +62,15 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface 'disabled' => !$this->security->isGranted('@parts_stock.withdraw'), ]); + $builder->add('dontCheckQuantity', CheckboxType::class, [ + 'label' => 'project.build.dont_check_quantity', + 'help' => 'project.build.dont_check_quantity.help', + 'required' => false, + 'attr' => [ + 'data-controller' => 'pages--dont-check-quantity-checkbox' + ] + ]); + $builder->add('comment', TextType::class, [ 'label' => 'part.info.withdraw_modal.comment', 'help' => 'part.info.withdraw_modal.comment.hint', @@ -79,10 +88,10 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface $form->add('addBuildsToBuildsPart', CheckboxType::class, [ 'label' => 'project.build.add_builds_to_builds_part', 'required' => false, - 'disabled' => $build_request->getProject()->getBuildPart() === null, + 'disabled' => !$build_request->getProject()->getBuildPart() instanceof Part, ]); - if ($build_request->getProject()->getBuildPart()) { + if ($build_request->getProject()->getBuildPart() instanceof Part) { $form->add('buildsPartLot', PartLotSelectType::class, [ 'label' => 'project.build.builds_part_lot', 'required' => false, @@ -106,7 +115,7 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface }); } - public function mapDataToForms($data, \Traversable $forms) + public function mapDataToForms($data, \Traversable $forms): void { if (!$data instanceof ProjectBuildRequest) { throw new \RuntimeException('Data must be an instance of ' . ProjectBuildRequest::class); @@ -124,6 +133,7 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface } $forms['comment']->setData($data->getComment()); + $forms['dontCheckQuantity']->setData($data->isDontCheckQuantity()); $forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart()); if (isset($forms['buildsPartLot'])) { $forms['buildsPartLot']->setData($data->getBuildsPartLot()); @@ -131,7 +141,7 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface } - public function mapFormsToData(\Traversable $forms, &$data) + public function mapFormsToData(\Traversable $forms, &$data): void { if (!$data instanceof ProjectBuildRequest) { throw new \RuntimeException('Data must be an instance of ' . ProjectBuildRequest::class); @@ -145,17 +155,19 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface $matches = []; if (preg_match('/^lot_(\d+)$/', $key, $matches)) { $lot_id = (int) $matches[1]; - $data->setLotWithdrawAmount($lot_id, $form->getData()); + $data->setLotWithdrawAmount($lot_id, (float) $form->getData()); } } $data->setComment($forms['comment']->getData()); + $data->setDontCheckQuantity($forms['dontCheckQuantity']->getData()); + if (isset($forms['buildsPartLot'])) { $lot = $forms['buildsPartLot']->getData(); if (!$lot) { //When the user selected "Create new lot", create a new lot $lot = new PartLot(); $description = 'Build ' . date('Y-m-d H:i:s'); - if (!empty($data->getComment())) { + if ($data->getComment() !== '') { $description .= ' (' . $data->getComment() . ')'; } $lot->setDescription($description); @@ -168,4 +180,4 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface //This has to be set after the builds part lot, so that it can disable the option $data->setAddBuildsToBuildsPart($forms['addBuildsToBuildsPart']->getData()); } -} \ No newline at end of file +} diff --git a/src/Form/TFAGoogleSettingsType.php b/src/Form/TFAGoogleSettingsType.php index e00ba494..7917f705 100644 --- a/src/Form/TFAGoogleSettingsType.php +++ b/src/Form/TFAGoogleSettingsType.php @@ -53,6 +53,7 @@ class TFAGoogleSettingsType extends AbstractType 'google_confirmation', TextType::class, [ + 'label' => 'tfa.check.code.confirmation', 'mapped' => false, 'attr' => [ 'maxlength' => '6', @@ -60,7 +61,7 @@ class TFAGoogleSettingsType extends AbstractType 'pattern' => '\d*', 'autocomplete' => 'off', ], - 'constraints' => [new ValidGoogleAuthCode()], + 'constraints' => [new ValidGoogleAuthCode(groups: ["google_authenticator"])], ] ); @@ -92,6 +93,7 @@ class TFAGoogleSettingsType extends AbstractType { $resolver->setDefaults([ 'data_class' => User::class, + 'validation_groups' => ['google_authenticator'], ]); } } diff --git a/src/Form/Type/BigDecimalMoneyType.php b/src/Form/Type/BigDecimalMoneyType.php index 2fb8d7ee..189416ff 100644 --- a/src/Form/Type/BigDecimalMoneyType.php +++ b/src/Form/Type/BigDecimalMoneyType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Type; use Brick\Math\BigDecimal; @@ -28,7 +30,7 @@ use Symfony\Component\Form\FormBuilderInterface; class BigDecimalMoneyType extends AbstractType implements DataTransformerInterface { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addModelTransformer($this); } diff --git a/src/Form/Type/BigDecimalNumberType.php b/src/Form/Type/BigDecimalNumberType.php index 8ee0911e..c225f0a4 100644 --- a/src/Form/Type/BigDecimalNumberType.php +++ b/src/Form/Type/BigDecimalNumberType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Type; use Brick\Math\BigDecimal; @@ -28,7 +30,7 @@ use Symfony\Component\Form\FormBuilderInterface; class BigDecimalNumberType extends AbstractType implements DataTransformerInterface { - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addModelTransformer($this); } diff --git a/src/Form/Type/CurrencyEntityType.php b/src/Form/Type/CurrencyEntityType.php index 856acc9e..07f0a9f8 100644 --- a/src/Form/Type/CurrencyEntityType.php +++ b/src/Form/Type/CurrencyEntityType.php @@ -22,14 +22,10 @@ declare(strict_types=1); namespace App\Form\Type; -use App\Entity\Attachments\AttachmentType; -use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\PriceInformations\Currency; use App\Form\Type\Helper\StructuralEntityChoiceHelper; -use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; -use RuntimeException; use Symfony\Component\Intl\Currencies; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -40,12 +36,9 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class CurrencyEntityType extends StructuralEntityType { - protected ?string $base_currency; - - public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, TranslatorInterface $translator, StructuralEntityChoiceHelper $choiceHelper, ?string $base_currency) + public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, TranslatorInterface $translator, StructuralEntityChoiceHelper $choiceHelper, protected ?string $base_currency) { parent::__construct($em, $builder, $translator, $choiceHelper); - $this->base_currency = $base_currency; } public function configureOptions(OptionsResolver $resolver): void @@ -60,14 +53,10 @@ class CurrencyEntityType extends StructuralEntityType // This options allows you to override the currency shown for the null value $resolver->setDefault('base_currency', null); - $resolver->setDefault('choice_attr', function (Options $options) { - return function ($choice) use ($options) { - return $this->choice_helper->generateChoiceAttrCurrency($choice, $options); - }; - }); + $resolver->setDefault('choice_attr', fn(Options $options) => fn($choice) => $this->choice_helper->generateChoiceAttrCurrency($choice, $options)); $resolver->setDefault('empty_message', function (Options $options) { - //By default we use the global base currency: + //By default, we use the global base currency: $iso_code = $this->base_currency; if ($options['base_currency']) { //Allow to override it @@ -79,7 +68,7 @@ class CurrencyEntityType extends StructuralEntityType $resolver->setDefault('used_to_select_parent', false); - //If short is set to true, then the name of the entity will only shown in the dropdown list not in the selected value. + //If short is set to true, then the name of the entity will only show in the dropdown list not in the selected value. $resolver->setDefault('short', false); } } diff --git a/src/Form/Type/ExponentialNumberType.php b/src/Form/Type/ExponentialNumberType.php new file mode 100644 index 00000000..f566afbb --- /dev/null +++ b/src/Form/Type/ExponentialNumberType.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Type; + +use App\Form\Type\Helper\ExponentialNumberTransformer; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Similar to the NumberType, but formats small values in scienfitic notation instead of rounding it to 0, like NumberType + */ +class ExponentialNumberType extends AbstractType +{ + public function getParent(): string + { + return NumberType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + //We want to allow the full precision of the number, so disable rounding + 'scale' => null, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->resetViewTransformers(); + + $builder->addViewTransformer(new ExponentialNumberTransformer( + $options['scale'], + $options['grouping'], + $options['rounding_mode'], + $options['html5'] ? 'en' : null + )); + } +} \ No newline at end of file diff --git a/src/Form/Type/Helper/ExponentialNumberTransformer.php b/src/Form/Type/Helper/ExponentialNumberTransformer.php new file mode 100644 index 00000000..ee2f4a4c --- /dev/null +++ b/src/Form/Type/Helper/ExponentialNumberTransformer.php @@ -0,0 +1,113 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Type\Helper; + +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer; + +/** + * This transformer formats small values in scienfitic notation instead of rounding it to 0, like the default + * NumberFormatter. + */ +class ExponentialNumberTransformer extends NumberToLocalizedStringTransformer +{ + public function __construct( + private ?int $scale = null, + ?bool $grouping = false, + ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, + protected ?string $locale = null + ) { + //Set scale to null, to disable rounding of values + parent::__construct($scale, $grouping, $roundingMode, $locale); + } + + /** + * Transforms a number type into localized number. + * + * @param int|float|null $value Number value + * + * @throws TransformationFailedException if the given value is not numeric + * or if the value cannot be transformed + */ + public function transform(mixed $value): string + { + if (null === $value) { + return ''; + } + + if (!is_numeric($value)) { + throw new TransformationFailedException('Expected a numeric.'); + } + + //If the value is too small, the number formatter would return 0, therfore use exponential notation for small numbers + if (abs($value) < 1e-3) { + $formatter = $this->getScientificNumberFormatter(); + } else { + $formatter = $this->getNumberFormatter(); + } + + + + $value = $formatter->format($value); + + if (intl_is_failure($formatter->getErrorCode())) { + throw new TransformationFailedException($formatter->getErrorMessage()); + } + + // Convert non-breaking and narrow non-breaking spaces to normal ones + $value = str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value); + + return $value; + } + + protected function getScientificNumberFormatter(): \NumberFormatter + { + $formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::SCIENTIFIC); + + if (null !== $this->scale) { + $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $this->scale); + $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode); + } + + $formatter->setAttribute(\NumberFormatter::GROUPING_USED, (int) $this->grouping); + + return $formatter; + } + + protected function getNumberFormatter(): \NumberFormatter + { + $formatter = parent::getNumberFormatter(); + + //Unset the fraction digits, as we don't want to round the number + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, 0); + if (null !== $this->scale) { + $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $this->scale); + } else { + $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 100); + } + + + return $formatter; + } +} \ No newline at end of file diff --git a/src/Form/Type/Helper/StructuralEntityChoiceHelper.php b/src/Form/Type/Helper/StructuralEntityChoiceHelper.php index 6448a734..1210d188 100644 --- a/src/Form/Type/Helper/StructuralEntityChoiceHelper.php +++ b/src/Form/Type/Helper/StructuralEntityChoiceHelper.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Type\Helper; +use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Contracts\HasMasterAttachmentInterface; use App\Entity\PriceInformations\Currency; -use App\Entity\UserSystem\User; -use App\Form\Type\MasterPictureAttachmentType; use App\Services\Attachments\AttachmentURLGenerator; -use RuntimeException; use Symfony\Component\Intl\Currencies; use Symfony\Component\OptionsResolver\Options; use Symfony\Contracts\Translation\TranslatorInterface; @@ -37,22 +37,15 @@ use Symfony\Contracts\Translation\TranslatorInterface; class StructuralEntityChoiceHelper { - private AttachmentURLGenerator $attachmentURLGenerator; - private TranslatorInterface $translator; - - public function __construct(AttachmentURLGenerator $attachmentURLGenerator, TranslatorInterface $translator) + public function __construct(private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly TranslatorInterface $translator) { - $this->attachmentURLGenerator = $attachmentURLGenerator; - $this->translator = $translator; } /** * Generates the choice attributes for the given AbstractStructuralDBElement. - * @param AbstractNamedDBElement $choice - * @param Options|array $options - * @return array|string[] + * @return array */ - public function generateChoiceAttr(AbstractNamedDBElement $choice, $options): array + public function generateChoiceAttr(AbstractNamedDBElement $choice, Options|array $options): array { $tmp = [ 'data-level' => 0, @@ -72,65 +65,54 @@ class StructuralEntityChoiceHelper $level = $choice->getLevel(); /** @var AbstractStructuralDBElement|null $parent */ $parent = $options['subentities_of'] ?? null; - if (null !== $parent) { + if ($parent instanceof AbstractStructuralDBElement) { $level -= $parent->getLevel() - 1; } - $tmp += [ - 'data-level' => $level, - 'data-parent' => $choice->getParent() ? $choice->getParent()->getFullPath() : null, - 'data-path' => $choice->getFullPath('->'), - ]; + $tmp['data-level'] = $level; + $tmp['data-parent'] = $choice->getParent() instanceof AbstractStructuralDBElement ? $choice->getParent()->getFullPath() : null; + $tmp['data-path'] = $choice->getFullPath('->'); } if ($choice instanceof HasMasterAttachmentInterface) { - $tmp['data-image'] = $choice->getMasterPictureAttachment() ? + $tmp['data-image'] = ($choice->getMasterPictureAttachment() instanceof Attachment + && $choice->getMasterPictureAttachment()->isPicture()) ? $this->attachmentURLGenerator->getThumbnailURL($choice->getMasterPictureAttachment(), 'thumbnail_xs') : null ; } - if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) { + if ($choice instanceof AttachmentType && $choice->getFiletypeFilter() !== '') { $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()]; } + //Show entities that are not added to DB yet separately from other entities + $tmp['data-not_in_db_yet'] = $choice->getID() === null; + return $tmp; } /** * Generates the choice attributes for the given AbstractStructuralDBElement. - * @param Currency $choice - * @param Options|array $options * @return array|string[] */ - public function generateChoiceAttrCurrency(Currency $choice, $options): array + public function generateChoiceAttrCurrency(Currency $choice, Options|array $options): array { $tmp = $this->generateChoiceAttr($choice, $options); + $symbol = $choice->getIsoCode() === '' ? null : Currencies::getSymbol($choice->getIsoCode()); + $tmp['data-short'] = $options['short'] ? $symbol : $choice->getName(); - if(!empty($choice->getIsoCode())) { - $symbol = Currencies::getSymbol($choice->getIsoCode()); - } else { - $symbol = null; - } + //Show entities that are not added to DB yet separately from other entities + $tmp['data-not_in_db_yet'] = $choice->getID() === null; - if ($options['short']) { - $tmp['data-short'] = $symbol; - } else { - $tmp['data-short'] = $choice->getName(); - } - - $tmp += [ + return $tmp + [ 'data-symbol' => $symbol, ]; - - return $tmp; } /** * Returns the choice label for the given AbstractStructuralDBElement. - * @param AbstractNamedDBElement $choice - * @return string */ public function generateChoiceLabel(AbstractNamedDBElement $choice): string { @@ -139,36 +121,31 @@ class StructuralEntityChoiceHelper /** * Returns the choice value for the given AbstractStructuralDBElement. - * @param AbstractNamedDBElement|null $element - * @return string|int|null */ - public function generateChoiceValue(?AbstractNamedDBElement $element) + public function generateChoiceValue(?AbstractNamedDBElement $element): string|int|null { - if ($element === null) { + if (!$element instanceof AbstractNamedDBElement) { return null; } /** * Do not change the structure below, even when inspection says it can be replaced with a null coalescing operator. - * It is important that the value returned here for a existing element is an int, and for a new element a string. - * I dont really understand why, but it seems to be important for the choice_loader to work correctly. + * It is important that the value returned here for an existing element is an int, and for a new element a string. + * I don't really understand why, but it seems to be important for the choice_loader to work correctly. * So please do not change this! */ if ($element->getID() === null) { if ($element instanceof AbstractStructuralDBElement) { //Must be the same as the separator in the choice_loader, otherwise this will not work! - return $element->getFullPath('->'); + return '$%$' . $element->getFullPath('->'); } - return $element->getName(); + // '$%$' is the indicator prefix for a new entity + return '$%$' . $element->getName(); } return $element->getID(); } - /** - * @param AbstractDBElement $element - * @return string|null - */ public function generateGroupBy(AbstractDBElement $element): ?string { //Show entities that are not added to DB yet separately from other entities @@ -178,4 +155,4 @@ class StructuralEntityChoiceHelper return null; } -} \ No newline at end of file +} diff --git a/src/Form/Type/Helper/StructuralEntityChoiceLoader.php b/src/Form/Type/Helper/StructuralEntityChoiceLoader.php index 6bca91b4..e2e4e841 100644 --- a/src/Form/Type/Helper/StructuralEntityChoiceLoader.php +++ b/src/Form/Type/Helper/StructuralEntityChoiceLoader.php @@ -1,4 +1,7 @@ options = $options; - $this->builder = $builder; - $this->entityManager = $entityManager; + private ?AbstractNamedDBElement $starting_element = null; + + private ?FormInterface $form = null; + + public function __construct( + private readonly Options $options, + private readonly NodesListBuilder $builder, + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator + ) { } protected function loadChoices(): iterable { - $tmp = []; + //If the starting_element is set and not persisted yet, add it to the list + $tmp = $this->starting_element !== null && $this->starting_element->getID() === null ? [$this->starting_element] : []; + if ($this->additional_element) { $tmp = $this->createNewEntitiesFromValue($this->additional_element); $this->additional_element = null; @@ -56,26 +68,51 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader public function createNewEntitiesFromValue(string $value): array { - if (!$this->options['allow_add']) { - throw new \RuntimeException('Cannot create new entity, because allow_add is not enabled!'); - } - if (trim($value) === '') { throw new \InvalidArgumentException('Cannot create new entity, because the name is empty!'); } + //Check if the value is matching the starting value element, we use the choice_value option to get the name of the starting element + if ($this->starting_element !== null + && $this->starting_element->getID() === null //Element must not be persisted yet + && $this->options['choice_value']($this->starting_element) === $value) { + //Then reuse the starting element + $this->entityManager->persist($this->starting_element); + return [$this->starting_element]; + } + + + if (!$this->options['allow_add']) { + //If we have a form, add an error to it, to improve the user experience + if ($this->form !== null) { + $this->form->addError( + new FormError($this->translator->trans('entity.select.creating_new_entities_not_allowed') + ) + ); + } else { + throw new \RuntimeException('Cannot create new entity, because allow_add is not enabled!'); + } + } + + + /** @var class-string $class */ $class = $this->options['class']; - /** @var StructuralDBElementRepository $repo */ + + /** @var StructuralDBElementRepository $repo */ $repo = $this->entityManager->getRepository($class); + $entities = $repo->getNewEntityFromPath($value, '->'); $results = []; - foreach($entities as $entity) { + foreach ($entities as $entity) { //If the entity is newly created (ID null), add it as result and persist it. if ($entity->getID() === null) { - $this->entityManager->persist($entity); + //Only persist the entities if it is allowed + if ($this->options['allow_add']) { + $this->entityManager->persist($entity); + } $results[] = $entity; } } @@ -93,4 +130,50 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader return $this->additional_element; } -} \ No newline at end of file + /** + * Gets the initial value used to populate the field. + * @return AbstractNamedDBElement|null + */ + public function getStartingElement(): ?AbstractNamedDBElement + { + return $this->starting_element; + } + + /** + * Sets the form that this loader is bound to. + * @param FormInterface|null $form + * @return void + */ + public function setForm(?FormInterface $form): void + { + $this->form = $form; + } + + /** + * Sets the initial value used to populate the field. This will always be an allowed value. + * @param AbstractNamedDBElement|null $starting_element + * @return StructuralEntityChoiceLoader + */ + public function setStartingElement(?AbstractNamedDBElement $starting_element): StructuralEntityChoiceLoader + { + $this->starting_element = $starting_element; + return $this; + } + + protected function doLoadChoicesForValues(array $values, ?callable $value): array + { + // Normalize the data (remove whitespaces around the arrow sign) and leading/trailing whitespaces + // This is required so that the value that is generated for an new entity based on its name structure is + // the same as the value that is generated for the same entity after it is persisted. + // Otherwise, errors occurs that the element could not be found. + foreach ($values as &$data) { + $data = trim((string) $data); + $data = preg_replace('/\s*->\s*/', '->', $data); + } + unset ($data); + + return $this->loadChoiceList($value)->getChoicesForValues($values); + } + + +} diff --git a/src/Form/Type/MasterPictureAttachmentType.php b/src/Form/Type/MasterPictureAttachmentType.php index 7aba9cc6..b5edbd55 100644 --- a/src/Form/Type/MasterPictureAttachmentType.php +++ b/src/Form/Type/MasterPictureAttachmentType.php @@ -42,32 +42,28 @@ class MasterPictureAttachmentType extends AbstractType $resolver->setDefaults([ 'filter' => 'picture', 'choice_translation_domain' => false, - 'choice_attr' => static function (Options $options) { - return static function ($choice, $key, $value) use ($options) { - /** @var Attachment $choice */ - $tmp = ['data-subtext' => $choice->getFilename() ?? 'URL']; + 'choice_attr' => static fn(Options $options) => static function ($choice, $key, $value) use ($options) { + /** @var Attachment $choice */ + $tmp = ['data-subtext' => $choice->getFilename() ?? 'URL']; - if ('picture' === $options['filter'] && !$choice->isPicture()) { - $tmp += ['disabled' => 'disabled']; - } elseif ('3d_model' === $options['filter'] && !$choice->is3DModel()) { - $tmp += ['disabled' => 'disabled']; - } + if ('picture' === $options['filter'] && !$choice->isPicture()) { + $tmp += ['disabled' => 'disabled']; + } elseif ('3d_model' === $options['filter'] && !$choice->is3DModel()) { + $tmp += ['disabled' => 'disabled']; + } - return $tmp; - }; + return $tmp; }, 'choice_label' => 'name', - 'choice_loader' => static function (Options $options) { - return new CallbackChoiceLoader( - static function () use ($options) { - $entity = $options['entity']; - if (!$entity instanceof AttachmentContainingDBElement) { - throw new RuntimeException('$entity must have Attachments! (be of type AttachmentContainingDBElement)'); - } + 'choice_loader' => static fn(Options $options) => new CallbackChoiceLoader( + static function () use ($options) { + $entity = $options['entity']; + if (!$entity instanceof AttachmentContainingDBElement) { + throw new RuntimeException('$entity must have Attachments! (be of type AttachmentContainingDBElement)'); + } - return $entity->getAttachments()->toArray(); - }); - }, + return $entity->getAttachments()->toArray(); + }), ]); $resolver->setAllowedValues('filter', ['', 'picture', '3d_model']); diff --git a/src/Form/Type/PartLotSelectType.php b/src/Form/Type/PartLotSelectType.php index 61e119b0..c68535a7 100644 --- a/src/Form/Type/PartLotSelectType.php +++ b/src/Form/Type/PartLotSelectType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Type; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Doctrine\ORM\EntityRepository; @@ -31,29 +34,23 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class PartLotSelectType extends AbstractType { - public function getParent() + public function getParent(): string { return EntityType::class; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('part'); $resolver->setAllowedTypes('part', Part::class); $resolver->setDefaults([ 'class' => PartLot::class, - 'choice_label' => ChoiceList::label($this, function (PartLot $part_lot) { - return ($part_lot->getStorageLocation() ? $part_lot->getStorageLocation()->getFullPath() : '') - . ' (' . $part_lot->getName() . '): ' . $part_lot->getAmount(); - }), - 'query_builder' => function (Options $options) { - return function (EntityRepository $er) use ($options) { - return $er->createQueryBuilder('l') - ->where('l.part = :part') - ->setParameter('part', $options['part']); - }; - } + 'choice_label' => ChoiceList::label($this, static fn(PartLot $part_lot): string => ($part_lot->getStorageLocation() instanceof StorageLocation ? $part_lot->getStorageLocation()->getFullPath() : '') + . ' (' . $part_lot->getName() . '): ' . $part_lot->getAmount()), + 'query_builder' => fn(Options $options) => static fn(EntityRepository $er) => $er->createQueryBuilder('l') + ->where('l.part = :part') + ->setParameter('part', $options['part']) ]); } -} \ No newline at end of file +} diff --git a/src/Form/Type/PartSelectType.php b/src/Form/Type/PartSelectType.php index 5ca543f7..34b8fc7c 100644 --- a/src/Form/Type/PartSelectType.php +++ b/src/Form/Type/PartSelectType.php @@ -1,7 +1,12 @@ urlGenerator = $urlGenerator; - $this->em = $em; - $this->previewGenerator = $previewGenerator; - $this->attachmentURLGenerator = $attachmentURLGenerator; } - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { - //At initialization we have to fill the form element with our selected data, so the user can see it + //At initialization, we have to fill the form element with our selected data, so the user can see it $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) { $form = $event->getForm(); $config = $form->getConfig()->getOptions(); @@ -73,7 +68,7 @@ class PartSelectType extends AbstractType implements DataMapperInterface $builder->setDataMapper($this); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'class' => Part::class, @@ -96,10 +91,10 @@ class PartSelectType extends AbstractType implements DataMapperInterface $resolver->setDefaults([ //Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request 'choice_attr' => ChoiceList::attr($this, function (?Part $part) { - if($part) { + if($part instanceof Part) { //Determine the picture to show: $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($part); - if ($preview_attachment !== null) { + if ($preview_attachment instanceof Attachment) { $preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); } else { @@ -107,31 +102,26 @@ class PartSelectType extends AbstractType implements DataMapperInterface } } - return $part ? [ + return $part instanceof Part ? [ 'data-description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), - 'data-category' => $part->getCategory() ? $part->getCategory()->getName() : '', - 'data-footprint' => $part->getFootprint() ? $part->getFootprint()->getName() : '', + 'data-category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : '', + 'data-footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'data-image' => $preview_url, ] : []; }) ]); } - public function getBlockPrefix() - { - return 'part_select'; - } - - public function mapDataToForms($data, $forms) + public function mapDataToForms($data, \Traversable $forms): void { $form = current(iterator_to_array($forms, false)); $form->setData($data); } - public function mapFormsToData($forms, &$data) + public function mapFormsToData(\Traversable $forms, &$data): void { $form = current(iterator_to_array($forms, false)); $data = $form->getData(); } -} \ No newline at end of file +} diff --git a/src/Form/Type/RichTextEditorType.php b/src/Form/Type/RichTextEditorType.php index a572fa2e..50683b0c 100644 --- a/src/Form/Type/RichTextEditorType.php +++ b/src/Form/Type/RichTextEditorType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Type; use Symfony\Component\Form\AbstractType; @@ -28,7 +30,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class RichTextEditorType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); // TODO: Change the autogenerated stub @@ -39,12 +41,7 @@ class RichTextEditorType extends AbstractType } - public function getBlockPrefix(): string - { - return 'rich_text_editor'; - } - - public function finishView(FormView $view, FormInterface $form, array $options) + public function finishView(FormView $view, FormInterface $form, array $options): void { $view->vars['attr'] = array_merge($view->vars['attr'], $this->optionsToAttrArray($options)); @@ -53,22 +50,17 @@ class RichTextEditorType extends AbstractType protected function optionsToAttrArray(array $options): array { - $tmp = []; - - //Set novalidate attribute, or we will get problems that form can not be submitted as textarea is not focusable - $tmp['novalidate'] = 'novalidate'; - - $tmp['data-mode'] = $options['mode']; - - //Add our data-controller element to the textarea - $tmp['data-controller'] = 'elements--ckeditor'; - - - return $tmp; + return [ + //Set novalidate attribute, or we will get problems that form can not be submitted as textarea is not focusable + 'novalidate' => 'novalidate', + 'data-mode' => $options['mode'], + //Add our data-controller element to the textarea + 'data-controller' => 'elements--ckeditor', + ]; } public function getParent(): string { return TextareaType::class; } -} \ No newline at end of file +} diff --git a/src/Form/Type/SIUnitType.php b/src/Form/Type/SIUnitType.php index bfec23e2..52bcaa3d 100644 --- a/src/Form/Type/SIUnitType.php +++ b/src/Form/Type/SIUnitType.php @@ -38,11 +38,8 @@ use Traversable; final class SIUnitType extends AbstractType implements DataMapperInterface { - protected SIFormatter $si_formatter; - - public function __construct(SIFormatter $SIFormatter) + public function __construct(protected SIFormatter $si_formatter) { - $this->si_formatter = $SIFormatter; } public function configureOptions(OptionsResolver $resolver): void @@ -91,7 +88,7 @@ final class SIUnitType extends AbstractType implements DataMapperInterface $resolver->setDefaults([ 'min' => 0, 'max' => '', - 'step' => static function (Options $options) { + 'step' => static function (Options $options): int|string { if (true === $options['is_integer']) { return 1; } @@ -138,7 +135,7 @@ final class SIUnitType extends AbstractType implements DataMapperInterface //Check if we need to make this thing small if (isset($options['attr']['class'])) { - $view->vars['sm'] = false !== strpos($options['attr']['class'], 'form-control-sm'); + $view->vars['sm'] = str_contains((string) $options['attr']['class'], 'form-control-sm'); } $view->vars['unit'] = $options['unit']; @@ -146,17 +143,17 @@ final class SIUnitType extends AbstractType implements DataMapperInterface } /** - * Maps the view data of a compound form to its children. + * Maps the view data of a compound form to its children. * - * The method is responsible for calling {@link FormInterface::setData()} - * on the children of compound forms, defining their underlying model data. + * The method is responsible for calling {@link FormInterface::setData()} + * on the children of compound forms, defining their underlying model data. * * @param mixed $viewData View data of the compound form being initialized - * @param FormInterface[]|Traversable $forms A list of {@link FormInterface} instances + * @param Traversable $forms A list of {@link FormInterface} instances * * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported */ - public function mapDataToForms($viewData, $forms): void + public function mapDataToForms(mixed $viewData, \Traversable $forms): void { $forms = iterator_to_array($forms); @@ -179,35 +176,35 @@ final class SIUnitType extends AbstractType implements DataMapperInterface } /** - * Maps the model data of a list of children forms into the view data of their parent. + * Maps the model data of a list of children forms into the view data of their parent. * - * This is the internal cascade call of FormInterface::submit for compound forms, since they - * cannot be bound to any input nor the request as scalar, but their children may: + * This is the internal cascade call of FormInterface::submit for compound forms, since they + * cannot be bound to any input nor the request as scalar, but their children may: * - * $compoundForm->submit($arrayOfChildrenViewData) - * // inside: - * $childForm->submit($childViewData); - * // for each entry, do the same and/or reverse transform - * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData) - * // then reverse transform + * $compoundForm->submit($arrayOfChildrenViewData) + * // inside: + * $childForm->submit($childViewData); + * // for each entry, do the same and/or reverse transform + * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData) + * // then reverse transform * - * When a simple form is submitted the following is happening: + * When a simple form is submitted the following is happening: * - * $simpleForm->submit($submittedViewData) - * // inside: - * $this->viewData = $submittedViewData - * // then reverse transform + * $simpleForm->submit($submittedViewData) + * // inside: + * $this->viewData = $submittedViewData + * // then reverse transform * - * The model data can be an array or an object, so this second argument is always passed - * by reference. + * The model data can be an array or an object, so this second argument is always passed + * by reference. * - * @param FormInterface[]|Traversable $forms A list of {@link FormInterface} instances + * @param Traversable $forms A list of {@link FormInterface} instances * @param mixed $viewData The compound form's view data that get mapped * its children model data * * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported */ - public function mapFormsToData($forms, &$viewData): void + public function mapFormsToData(\Traversable $forms, mixed &$viewData): void { //Convert both fields to a single float value. diff --git a/src/Form/Type/StaticFileAutocompleteType.php b/src/Form/Type/StaticFileAutocompleteType.php new file mode 100644 index 00000000..4d483e2a --- /dev/null +++ b/src/Form/Type/StaticFileAutocompleteType.php @@ -0,0 +1,63 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Type; + +use Symfony\Component\Asset\Packages; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Implements a text type with autocomplete functionality based on a static file, containing a list of autocomplete + * suggestions. + * Other values are allowed, but the user can select from the list of suggestions. + * The file must be located in the public directory! + */ +class StaticFileAutocompleteType extends AbstractType +{ + public function __construct( + private readonly Packages $assets + ) { + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('file'); + $resolver->setAllowedTypes('file', 'string'); + } + + public function getParent(): string + { + return TextType::class; + } + + public function finishView(FormView $view, FormInterface $form, array $options): void + { + //Add the data-controller and data-url attributes to the form field + $view->vars['attr']['data-controller'] = 'elements--static-file-autocomplete'; + $view->vars['attr']['data-url'] = $this->assets->getUrl($options['file']); + } +} \ No newline at end of file diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index b58caef7..1018eeeb 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -22,29 +22,19 @@ declare(strict_types=1); namespace App\Form\Type; -use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; -use App\Entity\Base\AbstractStructuralDBElement; use App\Form\Type\Helper\StructuralEntityChoiceHelper; use App\Form\Type\Helper\StructuralEntityChoiceLoader; -use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; -use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; -use Symfony\Component\Form\Event\PostSubmitEvent; use Symfony\Component\Form\Event\PreSubmitEvent; -use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\AtLeastOneOf; -use Symfony\Component\Validator\Constraints\IsNull; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Contracts\Translation\TranslatorInterface; @@ -54,31 +44,21 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class StructuralEntityType extends AbstractType { - protected EntityManagerInterface $em; - protected TranslatorInterface $translator; - protected StructuralEntityChoiceHelper $choice_helper; - - /** - * @var NodesListBuilder - */ - protected NodesListBuilder $builder; - - public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, TranslatorInterface $translator, StructuralEntityChoiceHelper $choice_helper) + public function __construct(protected EntityManagerInterface $em, protected NodesListBuilder $builder, protected TranslatorInterface $translator, protected StructuralEntityChoiceHelper $choice_helper) { - $this->em = $em; - $this->builder = $builder; - $this->translator = $translator; - $this->choice_helper = $choice_helper; } public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) { - //When the data contains non-digit characters, we assume that the user entered a new element. + //When the data starts with "$%$", we assume that the user entered a new element. //In that case we add the new element to our choice_loader $data = $event->getData(); - if (null === $data || !is_string($data) || $data === "" || ctype_digit($data)) { + if (is_string($data) && str_starts_with($data, '$%$')) { + //Extract the real name from the data + $data = substr($data, 3); + } else { return; } @@ -87,15 +67,12 @@ class StructuralEntityType extends AbstractType $choice_loader = $options['choice_loader']; if ($choice_loader instanceof StructuralEntityChoiceLoader) { $choice_loader->setAdditionalElement($data); + $choice_loader->setForm($form); } }); $builder->addModelTransformer(new CallbackTransformer( - function ($value) use ($options) { - return $this->modelTransform($value, $options); - }, function ($value) use ($options) { - return $this->modelReverseTransform($value, $options); - })); + fn($value) => $this->modelTransform($value, $options), fn($value) => $this->modelReverseTransform($value, $options))); } public function configureOptions(OptionsResolver $resolver): void @@ -106,29 +83,15 @@ class StructuralEntityType extends AbstractType 'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext 'subentities_of' => null, //Only show entities with the given parent class 'disable_not_selectable' => false, //Disable entries with not selectable property - 'choice_value' => function (?AbstractNamedDBElement $element) { - return $this->choice_helper->generateChoiceValue($element); - }, //Use the element id as option value and for comparing items - 'choice_loader' => function (Options $options) { - return new StructuralEntityChoiceLoader($options, $this->builder, $this->em); - }, - 'choice_label' => function (Options $options) { - return function ($choice, $key, $value) { - return $this->choice_helper->generateChoiceLabel($choice); - }; - }, - 'choice_attr' => function (Options $options) { - return function ($choice, $key, $value) use ($options) { - return $this->choice_helper->generateChoiceAttr($choice, $options); - }; - }, - 'group_by' => function (AbstractNamedDBElement $element) { - return $this->choice_helper->generateGroupBy($element); - }, + 'choice_value' => fn(?AbstractNamedDBElement $element) => $this->choice_helper->generateChoiceValue($element), //Use the element id as option value and for comparing items + 'choice_loader' => fn(Options $options) => new StructuralEntityChoiceLoader($options, $this->builder, $this->em, $this->translator), + 'choice_label' => fn(Options $options) => fn($choice, $key, $value) => $this->choice_helper->generateChoiceLabel($choice), + 'choice_attr' => fn(Options $options) => fn($choice, $key, $value) => $this->choice_helper->generateChoiceAttr($choice, $options), + 'group_by' => fn(AbstractNamedDBElement $element) => $this->choice_helper->generateGroupBy($element), 'choice_translation_domain' => false, //Don't translate the entity names ]); - //Set the constraints for the case that allow add is enabled (we then have to check that the new element is valid) + //Set the constraints for the case that allow to add is enabled (we then have to check that the new element is valid) $resolver->setNormalizer('constraints', function (Options $options, $value) { if ($options['allow_add']) { $value[] = new Valid(); @@ -141,6 +104,13 @@ class StructuralEntityType extends AbstractType $resolver->setDefault('controller', 'elements--structural-entity-select'); + //Options for DTO values + $resolver->setDefault('dto_value', null); + $resolver->setAllowedTypes('dto_value', ['null', 'string']); + //If no help text is explicitly set, we use the dto value as help text and show it as html + $resolver->setDefault('help', fn(Options $options) => $this->dtoText($options['dto_value'])); + $resolver->setDefault('help_html', fn(Options $options) => $options['dto_value'] !== null); + $resolver->setDefault('attr', function (Options $options) { $tmp = [ 'data-controller' => $options['controller'], @@ -155,6 +125,16 @@ class StructuralEntityType extends AbstractType }); } + private function dtoText(?string $text): ?string + { + if ($text === null) { + return null; + } + + $result = '' . $this->translator->trans('info_providers.form.help_prefix') . ': '; + + return $result . htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ; + } public function getParent(): string { @@ -163,14 +143,19 @@ class StructuralEntityType extends AbstractType public function modelTransform($value, array $options) { + $choice_loader = $options['choice_loader']; + if ($choice_loader instanceof StructuralEntityChoiceLoader) { + $choice_loader->setStartingElement($value); + } + return $value; } public function modelReverseTransform($value, array $options) { /* This step is important in combination with the caching! - The elements deserialized from cache, are not known to Doctrinte ORM any more, so doctrine thinks, - that the entity has changed (and so throws an exception about non-persited entities). + The elements deserialized from cache, are not known to Doctrine ORM anymore, so doctrine thinks, + that the entity has changed (and so throws an exception about non-persisted entities). This function just retrieves a fresh copy of the entity from database, so doctrine detect correctly that no change happened. The performance impact of this should be very small in comparison of the boost, caused by the caching. diff --git a/src/Form/Type/ThemeChoiceType.php b/src/Form/Type/ThemeChoiceType.php index 88843903..7cdc0aa9 100644 --- a/src/Form/Type/ThemeChoiceType.php +++ b/src/Form/Type/ThemeChoiceType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Type; -use App\Entity\UserSystem\User; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\OptionsResolver; class ThemeChoiceType extends AbstractType { - private array $available_themes; - - public function __construct(array $available_themes) + public function __construct(private readonly array $available_themes) { - $this->available_themes = $available_themes; } - public function getParent() + public function getParent(): string { return ChoiceType::class; } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'choices' => $this->available_themes, - 'choice_label' => static function ($entity, $key, $value) { - return $value; - }, + 'choice_label' => static fn($entity, $key, $value) => $value, 'choice_translation_domain' => false, 'placeholder' => 'user_settings.theme.placeholder' ]); } -} \ No newline at end of file +} diff --git a/src/Form/Type/TriStateCheckboxType.php b/src/Form/Type/TriStateCheckboxType.php index 6e8dafc4..4523a839 100644 --- a/src/Form/Type/TriStateCheckboxType.php +++ b/src/Form/Type/TriStateCheckboxType.php @@ -99,9 +99,8 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer * * @return mixed The value in the transformed representation * - * @throws TransformationFailedException when the transformation fails */ - public function transform($value) + public function transform(mixed $value) { if (true === $value) { return 'true'; @@ -142,22 +141,14 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer * @param mixed $value The value in the transformed representation * * @return mixed The value in the original representation - * - * @throws TransformationFailedException when the transformation fails */ - public function reverseTransform($value) + public function reverseTransform(mixed $value) { - switch ($value) { - case 'true': - return true; - case 'false': - return false; - case 'indeterminate': - case 'null': - case '': - return null; - default: - throw new InvalidArgumentException('Invalid value encountered!: '.$value); - } + return match ($value) { + 'true' => true, + 'false' => false, + 'indeterminate', 'null', '' => null, + default => throw new InvalidArgumentException('Invalid value encountered!: '.$value), + }; } } diff --git a/src/Form/Type/UserSelectType.php b/src/Form/Type/UserSelectType.php index c9c0830c..8862cdf7 100644 --- a/src/Form/Type/UserSelectType.php +++ b/src/Form/Type/UserSelectType.php @@ -1,4 +1,7 @@ . */ - namespace App\Form\Type; use App\Entity\UserSystem\User; @@ -28,20 +30,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class UserSelectType extends AbstractType { - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'class' => User::class, - 'choice_label' => function (Options $options) { - return function (User $choice, $key, $value) { - return $choice->getFullName(true); - }; - }, + 'choice_label' => fn(Options $options) => static fn(User $choice, $key, $value) => $choice->getFullName(true), ]); } - public function getParent() + public function getParent(): string { return StructuralEntityType::class; } -} \ No newline at end of file +} diff --git a/src/Form/UserAdminForm.php b/src/Form/UserAdminForm.php index ce9cab04..864bcf6b 100644 --- a/src/Form/UserAdminForm.php +++ b/src/Form/UserAdminForm.php @@ -22,8 +22,8 @@ declare(strict_types=1); namespace App\Form; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; -use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Form\Permissions\PermissionsType; @@ -34,7 +34,6 @@ use App\Form\Type\StructuralEntityType; use App\Form\Type\ThemeChoiceType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\LanguageType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; @@ -45,16 +44,12 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TimezoneType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Constraints\Length; class UserAdminForm extends AbstractType { - protected Security $security; - - public function __construct(Security $security) + public function __construct(protected Security $security) { - $this->security = $security; } public function configureOptions(OptionsResolver $resolver): void @@ -62,6 +57,8 @@ class UserAdminForm extends AbstractType parent::configureOptions($resolver); // TODO: Change the autogenerated stub $resolver->setRequired('attachment_class'); $resolver->setDefault('parameter_class', false); + + $resolver->setDefault('validation_groups', ['Default', 'permissions:edit']); } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -172,6 +169,7 @@ class UserAdminForm extends AbstractType 'type' => PasswordType::class, 'first_options' => [ 'label' => 'user.settings.pw_new.label', + 'password_estimator' => true, ], 'second_options' => [ 'label' => 'user.settings.pw_confirm.label', diff --git a/src/Form/UserSettingsType.php b/src/Form/UserSettingsType.php index cf75b0f8..05f63df4 100644 --- a/src/Form/UserSettingsType.php +++ b/src/Form/UserSettingsType.php @@ -22,14 +22,15 @@ declare(strict_types=1); namespace App\Form; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\UserSystem\User; use App\Form\Type\CurrencyEntityType; use App\Form\Type\RichTextEditorType; use App\Form\Type\ThemeChoiceType; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Event\PreSetDataEvent; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\LanguageType; @@ -40,18 +41,14 @@ use Symfony\Component\Form\Extension\Core\Type\TimezoneType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Constraints\File; class UserSettingsType extends AbstractType { - protected Security $security; - protected bool $demo_mode; - - public function __construct(Security $security, bool $demo_mode) + public function __construct(protected Security $security, + protected bool $demo_mode, + #[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages) { - $this->security = $security; - $this->demo_mode = $demo_mode; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -96,7 +93,7 @@ class UserSettingsType extends AbstractType ], 'constraints' => [ new File([ - 'maxSize' => '2M', + 'maxSize' => '5M', ]), ], ]) @@ -115,7 +112,7 @@ class UserSettingsType extends AbstractType 'required' => false, 'placeholder' => 'user_settings.language.placeholder', 'label' => 'user.language_select', - 'preferred_choices' => ['en', 'de'], + 'preferred_choices' => $this->preferred_languages, ]) ->add('timezone', TimezoneType::class, [ 'disabled' => $this->demo_mode, diff --git a/src/Helpers/BBCodeToMarkdownConverter.php b/src/Helpers/BBCodeToMarkdownConverter.php index 9ee4dff1..922e6a7e 100644 --- a/src/Helpers/BBCodeToMarkdownConverter.php +++ b/src/Helpers/BBCodeToMarkdownConverter.php @@ -24,8 +24,10 @@ namespace App\Helpers; use League\HTMLToMarkdown\HtmlConverter; use s9e\TextFormatter\Bundles\Forum as TextFormatter; -use SebastianBergmann\CodeCoverage\Report\Text; +/** + * @see \App\Tests\Helpers\BBCodeToMarkdownConverterTest + */ class BBCodeToMarkdownConverter { protected HtmlConverter $html_to_markdown; diff --git a/src/Helpers/FilenameSanatizer.php b/src/Helpers/FilenameSanatizer.php new file mode 100644 index 00000000..f6744b1a --- /dev/null +++ b/src/Helpers/FilenameSanatizer.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Helpers; + +/** + * This class provides functions to sanitize filenames. + */ +class FilenameSanatizer +{ + /** + * Converts a given filename to a version, which is guaranteed to be safe to use on all filesystems. + * This function is adapted from https://stackoverflow.com/a/42058764/21879970 + * @param string $filename + * @return string + */ + public static function sanitizeFilename(string $filename): string + { + //Convert to ASCII + $filename = iconv('UTF-8', 'ASCII//TRANSLIT', $filename); + + $filename = preg_replace( + '~ + [<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + [\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx + [\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN + [#\[\]@!$&\'()+,;=]| # URI reserved https://www.rfc-editor.org/rfc/rfc3986#section-2.2 + [{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt + ~x', + '-', $filename); + + // avoids ".", ".." or ".hiddenFiles" + $filename = ltrim((string) $filename, '.-'); + //Limit filename length to 255 bytes + $ext = pathinfo($filename, PATHINFO_EXTENSION); + return mb_strcut(pathinfo($filename, PATHINFO_FILENAME), 0, 255 - ($ext !== '' && $ext !== '0' ? strlen($ext) + 1 : 0), mb_detect_encoding($filename)) . ($ext !== '' && $ext !== '0' ? '.' . $ext : ''); + } +} \ No newline at end of file diff --git a/src/Helpers/IPAnonymizer.php b/src/Helpers/IPAnonymizer.php new file mode 100644 index 00000000..9662852f --- /dev/null +++ b/src/Helpers/IPAnonymizer.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Helpers; + +use Symfony\Component\HttpFoundation\IpUtils; + +/** + * Utils to assist with IP anonymization. + * The IPUtils::anonymize has a certain edgecase with local-link addresses, which is handled here. + * See: https://github.com/Part-DB/Part-DB-server/issues/782 + */ +final class IPAnonymizer +{ + public static function anonymize(string $ip): string + { + /** + * If the IP contains a % symbol, then it is a local-link address with scoping according to RFC 4007 + * In that case, we only care about the part before the % symbol, as the following functions, can only work with + * the IP address itself. As the scope can leak information (containing interface name), we do not want to + * include it in our anonymized IP data. + */ + if (str_contains($ip, '%')) { + $ip = substr($ip, 0, strpos($ip, '%')); + } + + return IpUtils::anonymize($ip); + } +} \ No newline at end of file diff --git a/src/Helpers/LabelResponse.php b/src/Helpers/LabelResponse.php index 1dbb947b..2973eb7e 100644 --- a/src/Helpers/LabelResponse.php +++ b/src/Helpers/LabelResponse.php @@ -54,7 +54,7 @@ class LabelResponse extends Response parent::__construct($content, $status, $headers); } - public function setContent($content): self + public function setContent($content): static { parent::setContent($content); @@ -64,7 +64,7 @@ class LabelResponse extends Response return $this; } - public function prepare(Request $request): self + public function prepare(Request $request): static { parent::prepare($request); @@ -84,7 +84,7 @@ class LabelResponse extends Response */ public function setAutoLastModified(): LabelResponse { - $this->setLastModified(new DateTime()); + $this->setLastModified(new \DateTimeImmutable()); return $this; } @@ -110,7 +110,7 @@ class LabelResponse extends Response */ public function setContentDisposition(string $disposition, string $filename, string $filenameFallback = ''): self { - if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || false !== strpos($filename, '%'))) { + if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) { $encoding = mb_detect_encoding($filename, null, true) ?: '8bit'; for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { diff --git a/src/Helpers/Projects/ProjectBuildRequest.php b/src/Helpers/Projects/ProjectBuildRequest.php index 093581f4..430d37b5 100644 --- a/src/Helpers/Projects/ProjectBuildRequest.php +++ b/src/Helpers/Projects/ProjectBuildRequest.php @@ -1,4 +1,7 @@ . */ - namespace App\Helpers\Projects; +use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Validator\Constraints\ProjectSystem\ValidProjectBuildRequest; /** - * @ValidProjectBuildRequest() + * @see \App\Tests\Helpers\Projects\ProjectBuildRequestTest */ +#[ValidProjectBuildRequest] final class ProjectBuildRequest { - private Project $project; - private int $number_of_builds; + private readonly int $number_of_builds; /** * @var array @@ -44,23 +47,23 @@ final class ProjectBuildRequest private bool $add_build_to_builds_part = false; + private bool $dont_check_quantity = false; + /** * @param Project $project The project that should be build * @param int $number_of_builds The number of builds that should be created */ - public function __construct(Project $project, int $number_of_builds) + public function __construct(private readonly Project $project, int $number_of_builds) { if ($number_of_builds < 1) { throw new \InvalidArgumentException('Number of builds must be at least 1!'); } - - $this->project = $project; $this->number_of_builds = $number_of_builds; $this->initializeArray(); //By default, use the first available lot of builds part if there is one. - if($project->getBuildPart() !== null) { + if($project->getBuildPart() instanceof Part) { $this->add_build_to_builds_part = true; foreach( $project->getBuildPart()->getPartLots() as $lot) { if (!$lot->isInstockUnknown()) { @@ -89,8 +92,6 @@ final class ProjectBuildRequest /** * Ensure that the projectBOMEntry belongs to the project, otherwise throw an exception. - * @param ProjectBOMEntry $entry - * @return void */ private function ensureBOMEntryValid(ProjectBOMEntry $entry): void { @@ -101,7 +102,6 @@ final class ProjectBuildRequest /** * Returns the partlot where the builds should be added to, or null if it should not be added to any lot. - * @return PartLot|null */ public function getBuildsPartLot(): ?PartLot { @@ -110,7 +110,6 @@ final class ProjectBuildRequest /** * Return if the builds should be added to the builds part of this project as new stock - * @return bool */ public function getAddBuildsToBuildsPart(): bool { @@ -119,7 +118,6 @@ final class ProjectBuildRequest /** * Set if the builds should be added to the builds part of this project as new stock - * @param bool $new_value * @return $this */ public function setAddBuildsToBuildsPart(bool $new_value): self @@ -136,17 +134,16 @@ final class ProjectBuildRequest /** * Set the partlot where the builds should be added to, or null if it should not be added to any lot. * The part lot must belong to the project build part, or an exception is thrown! - * @param PartLot|null $new_part_lot * @return $this */ public function setBuildsPartLot(?PartLot $new_part_lot): self { //Ensure that this new_part_lot belongs to the project - if (($new_part_lot !== null && $new_part_lot->getPart() !== $this->project->getBuildPart()) || $this->project->getBuildPart() === null) { + if (($new_part_lot instanceof PartLot && $new_part_lot->getPart() !== $this->project->getBuildPart()) || !$this->project->getBuildPart() instanceof Part) { throw new \InvalidArgumentException('The given part lot does not belong to the projects build part!'); } - if ($new_part_lot !== null) { + if ($new_part_lot instanceof PartLot) { $this->setAddBuildsToBuildsPart(true); } @@ -157,7 +154,6 @@ final class ProjectBuildRequest /** * Returns the comment where the user can write additional information about the build. - * @return string */ public function getComment(): string { @@ -166,7 +162,6 @@ final class ProjectBuildRequest /** * Sets the comment where the user can write additional information about the build. - * @param string $comment */ public function setComment(string $comment): void { @@ -175,18 +170,11 @@ final class ProjectBuildRequest /** * Returns the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. - * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdraw amount should be get - * @return float + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got */ - public function getLotWithdrawAmount($lot): float + public function getLotWithdrawAmount(PartLot|int $lot): float { - if ($lot instanceof PartLot) { - $lot_id = $lot->getID(); - } elseif (is_int($lot)) { - $lot_id = $lot; - } else { - throw new \InvalidArgumentException('The given lot must be an instance of PartLot or an ID of a PartLot!'); - } + $lot_id = $lot instanceof PartLot ? $lot->getID() : $lot; if (! array_key_exists($lot_id, $this->withdraw_amounts)) { throw new \InvalidArgumentException('The given lot is not in the withdraw amounts array!'); @@ -197,11 +185,10 @@ final class ProjectBuildRequest /** * Sets the amount of parts that should be withdrawn from the given lot for the corresponding BOM entry. - * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdraw amount should be get - * @param float $amount + * @param PartLot|int $lot The part lot (or the ID of the part lot) for which the withdrawal amount should be got * @return $this */ - public function setLotWithdrawAmount($lot, float $amount): self + public function setLotWithdrawAmount(PartLot|int $lot, float $amount): self { if ($lot instanceof PartLot) { $lot_id = $lot->getID(); @@ -218,8 +205,6 @@ final class ProjectBuildRequest /** * Returns the sum of all withdraw amounts for the given BOM entry. - * @param ProjectBOMEntry $entry - * @return float */ public function getWithdrawAmountSum(ProjectBOMEntry $entry): float { @@ -239,14 +224,13 @@ final class ProjectBuildRequest /** * Returns the number of available lots to take stock from for the given BOM entry. - * @param ProjectBOMEntry $projectBOMEntry * @return PartLot[]|null Returns null if the entry is a non-part BOM entry */ public function getPartLotsForBOMEntry(ProjectBOMEntry $projectBOMEntry): ?array { $this->ensureBOMEntryValid($projectBOMEntry); - if ($projectBOMEntry->getPart() === null) { + if (!$projectBOMEntry->getPart() instanceof Part) { return null; } @@ -256,8 +240,6 @@ final class ProjectBuildRequest /** * Returns the needed amount of parts for the given BOM entry. - * @param ProjectBOMEntry $entry - * @return float */ public function getNeededAmountForBOMEntry(ProjectBOMEntry $entry): float { @@ -267,7 +249,7 @@ final class ProjectBuildRequest } /** - * Returns the list of all bom entries that have to be build. + * Returns the list of all bom entries that have to be built. * @return ProjectBOMEntry[] */ public function getBomEntries(): array @@ -276,19 +258,16 @@ final class ProjectBuildRequest } /** - * Returns the all part bom entries that have to be build. + * Returns the all part bom entries that have to be built. * @return ProjectBOMEntry[] */ public function getPartBomEntries(): array { - return $this->project->getBomEntries()->filter(function (ProjectBOMEntry $entry) { - return $entry->isPartBomEntry(); - })->toArray(); + return $this->project->getBomEntries()->filter(fn(ProjectBOMEntry $entry) => $entry->isPartBomEntry())->toArray(); } /** * Returns which project should be build - * @return Project */ public function getProject(): Project { @@ -297,10 +276,31 @@ final class ProjectBuildRequest /** * Returns the number of builds that should be created. - * @return int */ public function getNumberOfBuilds(): int { return $this->number_of_builds; } -} \ No newline at end of file + + /** + * If Set to true, the given withdraw amounts are used without any checks for requirements. + * @return bool + */ + public function isDontCheckQuantity(): bool + { + return $this->dont_check_quantity; + } + + /** + * Set to true, the given withdraw amounts are used without any checks for requirements. + * @param bool $dont_check_quantity + * @return $this + */ + public function setDontCheckQuantity(bool $dont_check_quantity): ProjectBuildRequest + { + $this->dont_check_quantity = $dont_check_quantity; + return $this; + } + + +} diff --git a/src/Helpers/Trees/TreeViewNode.php b/src/Helpers/Trees/TreeViewNode.php index 89e4a02e..0c5fcdce 100644 --- a/src/Helpers/Trees/TreeViewNode.php +++ b/src/Helpers/Trees/TreeViewNode.php @@ -30,17 +30,13 @@ use JsonSerializable; */ final class TreeViewNode implements JsonSerializable { - private $text; - private $href; - private $nodes; + private ?TreeViewNodeState $state = null; - private $state = null; + private ?array $tags = null; - private $tags; + private ?int $id = null; - private $id; - - private $icon; + private ?string $icon = null; /** * Creates a new TreeView node with the given parameters. @@ -51,12 +47,8 @@ final class TreeViewNode implements JsonSerializable * @param array|null $nodes An array containing other TreeViewNodes. They will be used as children nodes of the * newly created nodes. Set to null, if it should not have children. */ - public function __construct(string $text, ?string $href = null, ?array $nodes = null) + public function __construct(private string $text, private ?string $href = null, private ?array $nodes = null) { - $this->text = $text; - $this->href = $href; - $this->nodes = $nodes; - //$this->state = new TreeViewNodeState(); } @@ -94,8 +86,6 @@ final class TreeViewNode implements JsonSerializable * Sets the node text. * * @param string $text the new node text - * - * @return TreeViewNode */ public function setText(string $text): self { @@ -116,8 +106,6 @@ final class TreeViewNode implements JsonSerializable * Sets the href link. * * @param string|null $href the new href link - * - * @return TreeViewNode */ public function setHref(?string $href): self { @@ -140,8 +128,6 @@ final class TreeViewNode implements JsonSerializable * Sets the children nodes. * * @param array|null $nodes The new children nodes - * - * @return TreeViewNode */ public function setNodes(?array $nodes): self { @@ -165,7 +151,7 @@ final class TreeViewNode implements JsonSerializable public function setDisabled(?bool $disabled): self { //Lazy loading of state, so it does not need to get serialized and transfered, when it is empty. - if (null === $this->state) { + if (!$this->state instanceof TreeViewNodeState) { $this->state = new TreeViewNodeState(); } @@ -177,7 +163,7 @@ final class TreeViewNode implements JsonSerializable public function setSelected(?bool $selected): self { //Lazy loading of state, so it does not need to get serialized and transfered, when it is empty. - if (null === $this->state) { + if (!$this->state instanceof TreeViewNodeState) { $this->state = new TreeViewNodeState(); } @@ -189,7 +175,7 @@ final class TreeViewNode implements JsonSerializable public function setExpanded(?bool $selected = true): self { //Lazy loading of state, so it does not need to get serialized and transfered, when it is empty. - if (null === $this->state) { + if (!$this->state instanceof TreeViewNodeState) { $this->state = new TreeViewNodeState(); } @@ -215,17 +201,11 @@ final class TreeViewNode implements JsonSerializable return $this; } - /** - * @return string|null - */ public function getIcon(): ?string { return $this->icon; } - /** - * @param string|null $icon - */ public function setIcon(?string $icon): self { $this->icon = $icon; @@ -252,7 +232,7 @@ final class TreeViewNode implements JsonSerializable $ret['nodes'] = $this->nodes; } - if (null !== $this->state) { + if ($this->state instanceof TreeViewNodeState) { $ret['state'] = $this->state; } diff --git a/src/Helpers/Trees/TreeViewNodeIterator.php b/src/Helpers/Trees/TreeViewNodeIterator.php index 073218c0..ab8b4907 100644 --- a/src/Helpers/Trees/TreeViewNodeIterator.php +++ b/src/Helpers/Trees/TreeViewNodeIterator.php @@ -40,7 +40,7 @@ final class TreeViewNodeIterator extends ArrayIterator implements RecursiveItera /** @var TreeViewNode $element */ $element = $this->current(); - return !empty($element->getNodes()); + return $element->getNodes() !== null && $element->getNodes() !== []; } public function getChildren(): TreeViewNodeIterator diff --git a/src/Helpers/Trees/TreeViewNodeState.php b/src/Helpers/Trees/TreeViewNodeState.php index 92b4611c..727135bc 100644 --- a/src/Helpers/Trees/TreeViewNodeState.php +++ b/src/Helpers/Trees/TreeViewNodeState.php @@ -29,17 +29,17 @@ final class TreeViewNodeState implements JsonSerializable /** * @var bool|null */ - private $disabled = null; + private ?bool $disabled = null; /** * @var bool|null */ - private $expanded = null; + private ?bool $expanded = null; /** * @var bool|null */ - private $selected = null; + private ?bool $selected = null; public function getDisabled(): ?bool { diff --git a/src/Helpers/TrinaryLogicHelper.php b/src/Helpers/TrinaryLogicHelper.php new file mode 100644 index 00000000..f4b460de --- /dev/null +++ b/src/Helpers/TrinaryLogicHelper.php @@ -0,0 +1,107 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Helpers; + +/** + * Helper functions for logic operations with trinary logic. + * True and false are represented as classical boolean values, undefined is represented as null. + * @see \App\Tests\Helpers\TrinaryLogicHelperTest + */ +class TrinaryLogicHelper +{ + + /** + * Implements the trinary logic NOT. + * @param bool|null $a + * @return bool|null + */ + public static function not(?bool $a): ?bool + { + if ($a === null) { + return null; + } + return !$a; + } + + + /** + * Returns the trinary logic OR of the given parameters. At least one parameter is required. + * @param bool|null ...$args + * @return bool|null + */ + public static function or(?bool ...$args): ?bool + { + if (count($args) === 0) { + throw new \LogicException('At least one parameter is required.'); + } + + // The trinary or is the maximum of the integer representation of the parameters. + return self::intToBool( + max(array_map(self::boolToInt(...), $args)) + ); + } + + /** + * Returns the trinary logic AND of the given parameters. At least one parameter is required. + * @param bool|null ...$args + * @return bool|null + */ + public static function and(?bool ...$args): ?bool + { + if (count($args) === 0) { + throw new \LogicException('At least one parameter is required.'); + } + + // The trinary and is the minimum of the integer representation of the parameters. + return self::intToBool( + min(array_map(self::boolToInt(...), $args)) + ); + } + + /** + * Convert the trinary bool to an integer, where true is 1, false is -1 and null is 0. + * @param bool|null $a + * @return int + */ + private static function boolToInt(?bool $a): int + { + if ($a === null) { + return 0; + } + return $a ? 1 : -1; + } + + /** + * Convert the integer to a trinary bool, where 1 is true, -1 is false and 0 is null. + * @param int $a + * @return bool|null + */ + private static function intToBool(int $a): ?bool + { + if ($a === 0) { + return null; + } + return $a > 0; + } +} \ No newline at end of file diff --git a/src/Kernel.php b/src/Kernel.php index a406b6c8..97c7a69e 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -1,4 +1,7 @@ . */ - namespace App; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; diff --git a/src/Migration/AbstractMultiPlatformMigration.php b/src/Migration/AbstractMultiPlatformMigration.php index 38b86644..bc2b3f19 100644 --- a/src/Migration/AbstractMultiPlatformMigration.php +++ b/src/Migration/AbstractMultiPlatformMigration.php @@ -1,4 +1,7 @@ . */ - namespace App\Migration; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Driver\AbstractMySQLDriver; -use Doctrine\DBAL\Driver\AbstractSQLiteDriver; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; -use Doctrine\DBAL\Platforms\MariaDBPlatform; -use Doctrine\DBAL\Platforms\MySQLPlatform; -use Doctrine\DBAL\Platforms\SqlitePlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; use Psr\Log\LoggerInterface; abstract class AbstractMultiPlatformMigration extends AbstractMigration { - public const ADMIN_PW_LENGTH = 10; + final public const ADMIN_PW_LENGTH = 10; protected string $admin_pw = ''; - protected LoggerInterface $logger; - - public function __construct(Connection $connection, LoggerInterface $logger) + /** @noinspection SenselessProxyMethodInspection + * This method is required to redefine the logger type hint to protected + */ + public function __construct(Connection $connection, protected LoggerInterface $logger) { - $this->logger = $logger; - AbstractMigration::__construct($connection, $logger); + parent::__construct($connection, $logger); } public function up(Schema $schema): void { $db_type = $this->getDatabaseType(); - switch ($db_type) { - case 'mysql': - $this->mySQLUp($schema); - break; - case 'sqlite': - $this->sqLiteUp($schema); - break; - default: - $this->abortIf(true, "Database type '$db_type' is not supported!"); - break; - } + match ($db_type) { + 'mysql' => $this->mySQLUp($schema), + 'sqlite' => $this->sqLiteUp($schema), + 'postgresql' => $this->postgreSQLUp($schema), + default => $this->abortIf(true, "Database type '$db_type' is not supported!"), + }; } public function down(Schema $schema): void { $db_type = $this->getDatabaseType(); - switch ($db_type) { - case 'mysql': - $this->mySQLDown($schema); - break; - case 'sqlite': - $this->sqLiteDown($schema); - break; - default: - $this->abortIf(true, "Database type is not supported!"); - break; - } + match ($db_type) { + 'mysql' => $this->mySQLDown($schema), + 'sqlite' => $this->sqLiteDown($schema), + 'postgresql' => $this->postgreSQLDown($schema), + default => $this->abortIf(true, "Database type is not supported!"), + }; } /** - * Gets the legacy Part-DB version number. Returns 0, if target database is not an legacy Part-DB database. + * Gets the legacy Part-DB version number. Returns 0, if target database is not a legacy Part-DB database. */ public function getOldDBVersion(): int { @@ -96,7 +84,7 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration return 0; } return (int) $version; - } catch (Exception $dBALException) { + } catch (Exception) { //when the table was not found, we can proceed, because we have an empty DB! return 0; } @@ -108,7 +96,7 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration */ public function getInitalAdminPW(): string { - if (empty($this->admin_pw)) { + if ($this->admin_pw === '') { if (!empty($_ENV['INITIAL_ADMIN_PW'])) { $this->admin_pw = $_ENV['INITIAL_ADMIN_PW']; } else { @@ -116,15 +104,15 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration } } - //As we dont have access to container, just use the default PHP pw hash function - return password_hash($this->admin_pw, PASSWORD_DEFAULT); + //As we don't have access to container, just use the default PHP pw hash function + return password_hash((string) $this->admin_pw, PASSWORD_DEFAULT); } public function postUp(Schema $schema): void { parent::postUp($schema); - if (!empty($this->admin_pw)) { + if ($this->admin_pw !== '') { $this->logger->warning(''); $this->logger->warning('The initial password for the "admin" user is: '.$this->admin_pw.''); $this->logger->warning(''); @@ -134,8 +122,6 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration /** * Checks if a foreign key on a table exists in the database. * This method is only supported for MySQL/MariaDB databases yet! - * @param string $table - * @param string $fk_name * @return bool Returns true, if the foreign key exists * @throws Exception */ @@ -152,6 +138,24 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration return $result > 0; } + /** + * Checks if a column exists in a table. + * @return bool Returns true, if the column exists + * @throws Exception + */ + public function doesColumnExist(string $table, string $column_name): bool + { + $db_type = $this->getDatabaseType(); + if ($db_type !== 'mysql') { + throw new \RuntimeException('This method is only supported for MySQL/MariaDB databases!'); + } + + $sql = "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = '$column_name'"; + $result = (int) $this->connection->fetchOne($sql); + + return $result > 0; + } + /** * Returns the database type of the used database. * @return string|null Returns 'mysql' for MySQL/MariaDB and 'sqlite' for SQLite. Returns null if unknown type @@ -162,10 +166,14 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration return 'mysql'; } - if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { + if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) { return 'sqlite'; } + if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) { + return 'postgresql'; + } + return null; } @@ -176,4 +184,8 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration abstract public function sqLiteUp(Schema $schema): void; abstract public function sqLiteDown(Schema $schema): void; + + abstract public function postgreSQLUp(Schema $schema): void; + + abstract public function postgreSQLDown(Schema $schema): void; } diff --git a/src/Migration/WithPermPresetsTrait.php b/src/Migration/WithPermPresetsTrait.php new file mode 100644 index 00000000..44bc4510 --- /dev/null +++ b/src/Migration/WithPermPresetsTrait.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Migration; + +use App\Entity\UserSystem\PermissionData; +use App\Security\Interfaces\HasPermissionsInterface; +use App\Services\UserSystem\PermissionPresetsHelper; +use Symfony\Component\DependencyInjection\ContainerInterface; + +trait WithPermPresetsTrait +{ + private ?ContainerInterface $container = null; + private ?PermissionPresetsHelper $permission_presets_helper = null; + + private function getJSONPermDataFromPreset(string $preset): string + { + if ($this->permission_presets_helper === null) { + throw new \RuntimeException('PermissionPresetsHelper not set! There seems to be some issue with the dependency injection!'); + } + + //Create a virtual user on which we can apply the preset + $user = new class implements HasPermissionsInterface { + + public PermissionData $perm_data; + + public function __construct() + { + $this->perm_data = new PermissionData(); + } + + public function getPermissions(): PermissionData + { + return $this->perm_data; + } + }; + + //Apply the preset to the virtual user + $this->permission_presets_helper->applyPreset($user, $preset); + + //And return the json data + return json_encode($user->getPermissions()); + } + + public function setContainer(?ContainerInterface $container = null): void + { + if ($container !== null) { + $this->container = $container; + $this->permission_presets_helper = $container->get(PermissionPresetsHelper::class); + } + } +} \ No newline at end of file diff --git a/src/Repository/AbstractPartsContainingRepository.php b/src/Repository/AbstractPartsContainingRepository.php index 1bf4c31c..4fd0bbff 100644 --- a/src/Repository/AbstractPartsContainingRepository.php +++ b/src/Repository/AbstractPartsContainingRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository; use App\Entity\Base\AbstractPartsContainingDBElement; @@ -25,19 +27,24 @@ use App\Entity\Base\PartsContainingRepositoryInterface; use App\Entity\Parts\Part; use InvalidArgumentException; +/** + * @template TEntityClass of AbstractPartsContainingDBElement + * @extends StructuralDBElementRepository + */ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepository implements PartsContainingRepositoryInterface { + /** @var int The maximum number of levels for which we can recurse before throwing an error */ private const RECURSION_LIMIT = 50; /** * Returns all parts associated with this element. * * @param object $element the element for which the parts should be determined - * @param array $order_by The order of the parts. Format ['name' => 'ASC'] + * @param string $nameOrderDirection the direction in which the parts should be ordered by name, either ASC or DESC * * @return Part[] */ - abstract public function getParts(object $element, array $order_by = ['name' => 'ASC']): array; + abstract public function getParts(object $element, string $nameOrderDirection = "ASC"): array; /** * Gets the count of the parts associated with this element. @@ -50,11 +57,31 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo /** * Returns the count of the parts associated with this element and all its children. * Please be aware that this function is pretty slow on large trees! - * @param AbstractPartsContainingDBElement $element * @return int */ public function getPartsCountRecursive(AbstractPartsContainingDBElement $element): int { + return $this->getPartsCountRecursiveWithDepthN($element, self::RECURSION_LIMIT); + } + + public function getPartsRecursive(AbstractPartsContainingDBElement $element): array + { + return $this->getPartsRecursiveWithDepthN($element, self::RECURSION_LIMIT); + } + + /** + * The implementation of the recursive function to get the parts count. + * This function is used to limit the recursion depth (remaining_depth is decreased on each call). + * If the recursion limit is reached (remaining_depth <= 0), a RuntimeException is thrown. + * @internal This function is not intended to be called directly, use getPartsCountRecursive() instead. + * @return int + */ + protected function getPartsCountRecursiveWithDepthN(AbstractPartsContainingDBElement $element, int $remaining_depth): int + { + if ($remaining_depth <= 0) { + throw new \RuntimeException('Recursion limit reached!'); + } + $count = $this->getPartsCount($element); //If the element is its own parent, we have a loop in the tree, so we stop here. @@ -62,18 +89,31 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo return 0; } - $n = 0; foreach ($element->getChildren() as $child) { - $count += $this->getPartsCountRecursive($child); - if ($n++ > self::RECURSION_LIMIT) { - throw new \RuntimeException('Recursion limit reached!'); - } + $count += $this->getPartsCountRecursiveWithDepthN($child, $remaining_depth - 1); } return $count; } - protected function getPartsByField(object $element, array $order_by, string $field_name): array + protected function getPartsRecursiveWithDepthN(AbstractPartsContainingDBElement $element, int $remaining_depth): array + { + if ($remaining_depth <= 0) { + throw new \RuntimeException('Recursion limit reached!'); + } + + //Add direct parts + $parts = $this->getParts($element); + + //Then iterate over all children and add their parts + foreach ($element->getChildren() as $child) { + $parts = array_merge($parts, $this->getPartsRecursiveWithDepthN($child, $remaining_depth - 1)); + } + + return $parts; + } + + protected function getPartsByField(object $element, string $nameOrderDirection, string $field_name): array { if (!$element instanceof AbstractPartsContainingDBElement) { throw new InvalidArgumentException('$element must be an instance of AbstractPartContainingDBElement!'); @@ -81,7 +121,14 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo $repo = $this->getEntityManager()->getRepository(Part::class); - return $repo->findBy([$field_name => $element], $order_by); + //Build a query builder to get the parts with a custom order by + + $qb = $repo->createQueryBuilder('part') + ->where('part.'.$field_name.' = :element') + ->setParameter('element', $element) + ->orderBy('NATSORT(part.name)', $nameOrderDirection); + + return $qb->getQuery()->getResult(); } protected function getPartsCountByField(object $element, string $field_name): int diff --git a/src/Repository/AttachmentContainingDBElementRepository.php b/src/Repository/AttachmentContainingDBElementRepository.php new file mode 100644 index 00000000..40869662 --- /dev/null +++ b/src/Repository/AttachmentContainingDBElementRepository.php @@ -0,0 +1,83 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Repository; + +use App\Doctrine\Helpers\FieldHelper; +use App\Entity\Attachments\AttachmentContainingDBElement; +use Doctrine\ORM\Mapping\ClassMetadata; + +/** + * @template TEntityClass of AttachmentContainingDBElement + * @extends NamedDBElementRepository + * @see \App\Tests\Repository\AttachmentContainingDBElementRepositoryTest + */ +class AttachmentContainingDBElementRepository extends NamedDBElementRepository +{ + /** + * @var array This array is used to cache the results of getElementsAndPreviewAttachmentByIDs function. + */ + private array $elementsAndPreviewAttachmentCache = []; + + /** + * Similar to the findByIDInMatchingOrder function, but it also hints to doctrine that the master picture attachment should be fetched eagerly. + * @param array $ids + * @return array + * @phpstan-return array + */ + public function getElementsAndPreviewAttachmentByIDs(array $ids): array + { + //If no IDs are given, return an empty array + if (count($ids) === 0) { + return []; + } + + //Convert the ids to a string + $cache_key = implode(',', $ids); + + //Check if the result is already cached + if (isset($this->elementsAndPreviewAttachmentCache[$cache_key])) { + return $this->elementsAndPreviewAttachmentCache[$cache_key]; + } + + $qb = $this->createQueryBuilder('element') + ->select('element') + ->where('element.id IN (?1)') + //Order the results in the same order as the IDs in the input array (mysql supports this native, for SQLite we emulate it) + ->setParameter(1, $ids); + + //Order the results in the same order as the IDs in the input array + FieldHelper::addOrderByFieldParam($qb, 'element.id', 1); + + $q = $qb->getQuery(); + + $q->setFetchMode($this->getEntityName(), 'master_picture_attachment', ClassMetadata::FETCH_EAGER); + + $result = $q->getResult(); + + //Cache the result + $this->elementsAndPreviewAttachmentCache[$cache_key] = $result; + + return $result; + } +} \ No newline at end of file diff --git a/src/Repository/AttachmentRepository.php b/src/Repository/AttachmentRepository.php index 9ad9191b..4fc0abc9 100644 --- a/src/Repository/AttachmentRepository.php +++ b/src/Repository/AttachmentRepository.php @@ -41,9 +41,14 @@ declare(strict_types=1); namespace App\Repository; +use App\Entity\Attachments\Attachment; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; +/** + * @template TEntityClass of Attachment + * @extends DBElementRepository + */ class AttachmentRepository extends DBElementRepository { /** @@ -53,15 +58,15 @@ class AttachmentRepository extends DBElementRepository { $qb = $this->createQueryBuilder('attachment'); $qb->select('COUNT(attachment)') - ->where('attachment.path LIKE :like'); - $qb->setParameter('like', '\\%SECURE\\%%'); + ->where('attachment.internal_path LIKE :like ESCAPE \'#\''); + $qb->setParameter('like', '#%SECURE#%%'); $query = $qb->getQuery(); return (int) $query->getSingleScalarResult(); } /** - * Gets the count of all external attachments (attachments only containing an URL). + * Gets the count of all external attachments (attachments containing only an external path). * * @throws NoResultException * @throws NonUniqueResultException @@ -70,17 +75,16 @@ class AttachmentRepository extends DBElementRepository { $qb = $this->createQueryBuilder('attachment'); $qb->select('COUNT(attachment)') - ->where('attachment.path LIKE :http') - ->orWhere('attachment.path LIKE :https'); - $qb->setParameter('http', 'http://%'); - $qb->setParameter('https', 'https://%'); + ->where('attachment.external_path IS NOT NULL') + ->andWhere('attachment.internal_path IS NULL'); + $query = $qb->getQuery(); return (int) $query->getSingleScalarResult(); } /** - * Gets the count of all attachments where an user uploaded an file. + * Gets the count of all attachments where a user uploaded a file or a file was downloaded from an external source. * * @throws NoResultException * @throws NonUniqueResultException @@ -89,12 +93,12 @@ class AttachmentRepository extends DBElementRepository { $qb = $this->createQueryBuilder('attachment'); $qb->select('COUNT(attachment)') - ->where('attachment.path LIKE :base') - ->orWhere('attachment.path LIKE :media') - ->orWhere('attachment.path LIKE :secure'); - $qb->setParameter('secure', '\\%SECURE\\%%'); - $qb->setParameter('base', '\\%BASE\\%%'); - $qb->setParameter('media', '\\%MEDIA\\%%'); + ->where('attachment.internal_path LIKE :base ESCAPE \'#\'') + ->orWhere('attachment.internal_path LIKE :media ESCAPE \'#\'') + ->orWhere('attachment.internal_path LIKE :secure ESCAPE \'#\''); + $qb->setParameter('secure', '#%SECURE#%%'); + $qb->setParameter('base', '#%BASE#%%'); + $qb->setParameter('media', '#%MEDIA#%%'); $query = $qb->getQuery(); return (int) $query->getSingleScalarResult(); diff --git a/src/Repository/CurrencyRepository.php b/src/Repository/CurrencyRepository.php new file mode 100644 index 00000000..63473229 --- /dev/null +++ b/src/Repository/CurrencyRepository.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Repository; + +use App\Entity\PriceInformations\Currency; +use Symfony\Component\Intl\Currencies; + +/** + * @extends StructuralDBElementRepository + */ +class CurrencyRepository extends StructuralDBElementRepository +{ + /** + * Finds or create a currency with the given ISO code. + * @param string $iso_code + * @return Currency + */ + public function findOrCreateByISOCode(string $iso_code): Currency + { + //Normalize ISO code + $iso_code = strtoupper($iso_code); + + //Try to find currency + $currency = $this->findOneBy(['iso_code' => $iso_code]); + if ($currency !== null) { + return $currency; + } + + //Create currency if it does not exist + $name = Currencies::getName($iso_code); + + $currency = $this->findOrCreateForInfoProvider($name); + $currency->setIsoCode($iso_code); + + return $currency; + } +} \ No newline at end of file diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php index 0f7024b6..2437e848 100644 --- a/src/Repository/DBElementRepository.php +++ b/src/Repository/DBElementRepository.php @@ -41,23 +41,32 @@ declare(strict_types=1); namespace App\Repository; +use App\Doctrine\Helpers\FieldHelper; use App\Entity\Base\AbstractDBElement; use Doctrine\ORM\EntityRepository; use ReflectionClass; +/** + * @template TEntityClass of AbstractDBElement + * @extends EntityRepository + * @see \App\Tests\Repository\DBElementRepositoryTest + */ class DBElementRepository extends EntityRepository { + private array $find_elements_by_id_cache = []; + /** * Changes the ID of the given element to a new value. * You should only use it to undelete former existing elements, everything else is most likely a bad idea! * * @param AbstractDBElement $element The element whose ID should be changed - * @param int $new_id The new ID + * @phpstan-param TEntityClass $element + * @param int $new_id The new ID */ public function changeID(AbstractDBElement $element, int $new_id): void { $qb = $this->createQueryBuilder('element'); - $q = $qb->update(get_class($element), 'element') + $q = $qb->update($element::class, 'element') ->set('element.id', $new_id) ->where('element.id = ?1') ->setParameter(1, $element->getID()) @@ -71,8 +80,9 @@ class DBElementRepository extends EntityRepository /** * Find all elements that match a list of IDs. - * + * They are ordered by IDs in an ascending order. * @return AbstractDBElement[] + * @phpstan-return list */ public function getElementsFromIDArray(array $ids): array { @@ -80,14 +90,66 @@ class DBElementRepository extends EntityRepository $q = $qb->select('element') ->where('element.id IN (?1)') ->setParameter(1, $ids) + ->orderBy('element.id', 'ASC') ->getQuery(); return $q->getResult(); } + /** + * Returns the elements with the given IDs in the same order, as they were given in the input array. + * + * @param array $ids + * @return array + */ + public function findByIDInMatchingOrder(array $ids): array + { + //If no IDs are given, return an empty array + if (count($ids) === 0) { + return []; + } + + $cache_key = implode(',', $ids); + + //Check if the result is already cached + if (isset($this->find_elements_by_id_cache[$cache_key])) { + return $this->find_elements_by_id_cache[$cache_key]; + } + + //Otherwise do the query + $qb = $this->createQueryBuilder('element'); + $qb->select('element') + ->where('element.id IN (?1)') + ->setParameter(1, $ids); + + //Order the results in the same order as the IDs in the input array + FieldHelper::addOrderByFieldParam($qb, 'element.id', 1); + + $q = $qb->getQuery(); + + $result = $q->getResult(); + + //Cache the result + $this->find_elements_by_id_cache[$cache_key] = $result; + + return $result; + } + + /** + * The elements in the result array will be sorted, so that their order of their IDs matches the order of the IDs in the input array. + * @param array $result_array + * @phpstan-param list $result_array + * @param int[] $ids + * @return void + */ + protected function sortResultArrayByIDArray(array &$result_array, array $ids): void + { + usort($result_array, static fn(AbstractDBElement $a, AbstractDBElement $b) => array_search($a->getID(), $ids, true) <=> array_search($b->getID(), $ids, true)); + } + protected function setField(AbstractDBElement $element, string $field, int $new_value): void { - $reflection = new ReflectionClass(get_class($element)); + $reflection = new ReflectionClass($element::class); $property = $reflection->getProperty($field); $property->setAccessible(true); $property->setValue($element, $new_value); diff --git a/src/Repository/LabelProfileRepository.php b/src/Repository/LabelProfileRepository.php index 76372f34..ad9e40f1 100644 --- a/src/Repository/LabelProfileRepository.php +++ b/src/Repository/LabelProfileRepository.php @@ -43,21 +43,21 @@ namespace App\Repository; use App\Entity\LabelSystem\LabelOptions; use App\Entity\LabelSystem\LabelProfile; +use App\Entity\LabelSystem\LabelSupportedElement; use App\Helpers\Trees\TreeViewNode; -use InvalidArgumentException; +/** + * @template TEntityClass of LabelProfile + * @extends NamedDBElementRepository + */ class LabelProfileRepository extends NamedDBElementRepository { /** * Find the profiles that are shown in the dropdown for the given type. * You should maybe use the cached version of this in LabelProfileDropdownHelper. */ - public function getDropdownProfiles(string $type): array + public function getDropdownProfiles(LabelSupportedElement $type): array { - if (!in_array($type, LabelOptions::SUPPORTED_ELEMENTS, true)) { - throw new InvalidArgumentException('Invalid supported_element type given.'); - } - return $this->findBy([ 'options.supported_element' => $type, 'show_in_dropdown' => true, @@ -74,7 +74,7 @@ class LabelProfileRepository extends NamedDBElementRepository { $result = []; - foreach (LabelOptions::SUPPORTED_ELEMENTS as $type) { + foreach (LabelSupportedElement::cases() as $type) { $type_children = []; $entities = $this->findForSupportedElement($type); foreach ($entities as $entity) { @@ -84,9 +84,9 @@ class LabelProfileRepository extends NamedDBElementRepository $type_children[] = $node; } - if (!empty($type_children)) { + if ($type_children !== []) { //Use default label e.g. 'part_label'. $$ marks that it will be translated in TreeViewGenerator - $tmp = new TreeViewNode('$$'.$type.'.label', null, $type_children); + $tmp = new TreeViewNode('$$'.$type->value.'.label', null, $type_children); $result[] = $tmp; } @@ -98,42 +98,35 @@ class LabelProfileRepository extends NamedDBElementRepository /** * Find all LabelProfiles that can be used with the given type. * - * @param string $type see LabelOptions::SUPPORTED_ELEMENTS for valid values + * @param LabelSupportedElement $type see LabelOptions::SUPPORTED_ELEMENTS for valid values * @param array $order_by The way the results should be sorted. By default ordered by */ - public function findForSupportedElement(string $type, array $order_by = ['name' => 'ASC']): array + public function findForSupportedElement(LabelSupportedElement $type, array $order_by = ['name' => 'ASC']): array { - if (!in_array($type, LabelOptions::SUPPORTED_ELEMENTS, true)) { - throw new InvalidArgumentException('Invalid supported_element type given.'); - } - return $this->findBy(['options.supported_element' => $type], $order_by); } /** * Returns all LabelProfiles that can be used for parts - * @return array */ public function getPartLabelProfiles(): array { - return $this->getDropdownProfiles('part'); + return $this->getDropdownProfiles(LabelSupportedElement::PART); } /** * Returns all LabelProfiles that can be used for part lots - * @return array */ public function getPartLotsLabelProfiles(): array { - return $this->getDropdownProfiles('part_lot'); + return $this->getDropdownProfiles(LabelSupportedElement::PART_LOT); } /** * Returns all LabelProfiles that can be used for storelocations - * @return array */ public function getStorelocationsLabelProfiles(): array { - return $this->getDropdownProfiles('storelocation'); + return $this->getDropdownProfiles(LabelSupportedElement::STORELOCATION); } } diff --git a/src/Repository/LogEntryRepository.php b/src/Repository/LogEntryRepository.php index 857aac5b..6850d06b 100644 --- a/src/Repository/LogEntryRepository.php +++ b/src/Repository/LogEntryRepository.php @@ -28,10 +28,14 @@ use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Entity\LogSystem\LogTargetType; use App\Entity\UserSystem\User; -use DateTime; use RuntimeException; +/** + * @template TEntityClass of AbstractLogEntry + * @extends DBElementRepository + */ class LogEntryRepository extends DBElementRepository { public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array @@ -41,7 +45,7 @@ class LogEntryRepository extends DBElementRepository /** @var AbstractDBElement $element */ $element = $criteria['target']; $criteria['target_id'] = $element->getID(); - $criteria['target_type'] = AbstractLogEntry::targetTypeClassToID(get_class($element)); + $criteria['target_type'] = LogTargetType::fromElementClass($element); unset($criteria['target']); } @@ -52,15 +56,16 @@ class LogEntryRepository extends DBElementRepository * Find log entries associated with the given element (the history of the element). * * @param AbstractDBElement $element The element for which the history should be generated - * @param string $order By default, the newest entries are shown first. Change this to ASC to show the oldest entries first. - * @param null $limit - * @param null $offset + * @param string $order By default, the newest entries are shown first. Change this to ASC to show the oldest entries first. + * @param int|null $limit + * @param int|null $offset * * @return AbstractLogEntry[] */ - public function getElementHistory(AbstractDBElement $element, string $order = 'DESC', $limit = null, $offset = null): array + public function getElementHistory(AbstractDBElement $element, string $order = 'DESC', ?int $limit = null, ?int $offset = null): array { - return $this->findBy(['element' => $element], ['timestamp' => $order], $limit, $offset); + //@phpstan-ignore-next-line Target is parsed dynamically in findBy + return $this->findBy(['target' => $element], ['timestamp' => $order], $limit, $offset); } /** @@ -80,10 +85,8 @@ class LogEntryRepository extends DBElementRepository ->orderBy('log.timestamp', 'DESC') ->setMaxResults(1); - $qb->setParameters([ - 'target_type' => AbstractLogEntry::targetTypeClassToID($class), - 'target_id' => $id, - ]); + $qb->setParameter('target_type', LogTargetType::fromElementClass($class)); + $qb->setParameter('target_id', $id); $query = $qb->getQuery(); @@ -100,27 +103,26 @@ class LogEntryRepository extends DBElementRepository * Gets all log entries that are related to time travelling. * * @param AbstractDBElement $element The element for which the time travel data should be retrieved - * @param DateTime $until Back to which timestamp should the data be get (including the timestamp) + * @param \DateTimeInterface $until Back to which timestamp should the data be got (including the timestamp) * * @return AbstractLogEntry[] */ - public function getTimetravelDataForElement(AbstractDBElement $element, DateTime $until): array + public function getTimetravelDataForElement(AbstractDBElement $element, \DateTimeInterface $until): array { $qb = $this->createQueryBuilder('log'); $qb->select('log') - //->where('log INSTANCE OF App\Entity\LogSystem\ElementEditedLogEntry') ->where('log INSTANCE OF '.ElementEditedLogEntry::class) ->orWhere('log INSTANCE OF '.CollectionElementDeleted::class) ->andWhere('log.target_type = :target_type') ->andWhere('log.target_id = :target_id') ->andWhere('log.timestamp >= :until') - ->orderBy('log.timestamp', 'DESC'); + ->orderBy('log.timestamp', 'DESC') + ; + + $qb->setParameter('target_type', LogTargetType::fromElementClass($element)); + $qb->setParameter('target_id', $element->getID()); + $qb->setParameter('until', $until); - $qb->setParameters([ - 'target_type' => AbstractLogEntry::targetTypeClassToID(get_class($element)), - 'target_id' => $element->getID(), - 'until' => $until, - ]); $query = $qb->getQuery(); @@ -132,7 +134,7 @@ class LogEntryRepository extends DBElementRepository * * @return bool True if the element existed at the given timestamp */ - public function getElementExistedAtTimestamp(AbstractDBElement $element, DateTime $timestamp): bool + public function getElementExistedAtTimestamp(AbstractDBElement $element, \DateTimeInterface $timestamp): bool { $qb = $this->createQueryBuilder('log'); $qb->select('count(log)') @@ -140,28 +142,25 @@ class LogEntryRepository extends DBElementRepository ->andWhere('log.target_type = :target_type') ->andWhere('log.target_id = :target_id') ->andWhere('log.timestamp >= :until') - ->orderBy('log.timestamp', 'DESC'); + ; - $qb->setParameters([ - 'target_type' => AbstractLogEntry::targetTypeClassToID(get_class($element)), - 'target_id' => $element->getID(), - 'until' => $timestamp, - ]); + $qb->setParameter('target_type', LogTargetType::fromElementClass($element)); + $qb->setParameter('target_id', $element->getID()); + $qb->setParameter('until', $timestamp); $query = $qb->getQuery(); $count = $query->getSingleScalarResult(); - return !($count > 0); + return $count <= 0; } /** * Gets the last log entries ordered by timestamp. * - * @param string $order - * @param null $limit - * @param null $offset + * @param int|null $limit + * @param int|null $offset */ - public function getLogsOrderedByTimestamp(string $order = 'DESC', $limit = null, $offset = null): array + public function getLogsOrderedByTimestamp(string $order = 'DESC', ?int $limit = null, ?int $offset = null): array { return $this->findBy([], ['timestamp' => $order], $limit, $offset); } @@ -205,23 +204,38 @@ class LogEntryRepository extends DBElementRepository return $this->getLastUser($element, ElementCreatedLogEntry::class); } - protected function getLastUser(AbstractDBElement $element, string $class): ?User + /** + * Returns the last user that has created a log entry with the given class on the given element. + * @param AbstractDBElement $element + * @param string $log_class + * @return User|null + */ + protected function getLastUser(AbstractDBElement $element, string $log_class): ?User { $qb = $this->createQueryBuilder('log'); + /** + * The select and join with user here are important, to get true null user values, if the user was deleted. + * This happens for sqlite database, before the SET NULL constraint was added, and doctrine generates a proxy + * entity which fails to resolve, without this line. + * This was the cause of issue #414 (https://github.com/Part-DB/Part-DB-server/issues/414) + */ $qb->select('log') - //->where('log INSTANCE OF App\Entity\LogSystem\ElementEditedLogEntry') - ->where('log INSTANCE OF '.$class) + ->addSelect('user') + ->where('log INSTANCE OF '.$log_class) + ->leftJoin('log.user', 'user') ->andWhere('log.target_type = :target_type') ->andWhere('log.target_id = :target_id') - ->orderBy('log.timestamp', 'DESC'); + ->orderBy('log.timestamp', 'DESC') + //Use id as fallback, if timestamp is the same (higher id means newer entry) + ->addOrderBy('log.id', 'DESC') + ; - $qb->setParameters([ - 'target_type' => AbstractLogEntry::targetTypeClassToID(get_class($element)), - 'target_id' => $element->getID(), - ]); + $qb->setParameter('target_type', LogTargetType::fromElementClass($element)); + $qb->setParameter('target_id', $element->getID()); $query = $qb->getQuery(); $query->setMaxResults(1); + /** @var AbstractLogEntry[] $results */ $results = $query->execute(); if (isset($results[0])) { diff --git a/src/Repository/NamedDBElementRepository.php b/src/Repository/NamedDBElementRepository.php index 8387d47c..8c78cb5e 100644 --- a/src/Repository/NamedDBElementRepository.php +++ b/src/Repository/NamedDBElementRepository.php @@ -26,6 +26,11 @@ use App\Entity\Base\AbstractNamedDBElement; use App\Entity\UserSystem\User; use App\Helpers\Trees\TreeViewNode; +/** + * @template TEntityClass of AbstractNamedDBElement + * @extends DBElementRepository + * @see \App\Tests\Repository\NamedDBElementRepositoryTest + */ class NamedDBElementRepository extends DBElementRepository { /** @@ -38,7 +43,7 @@ class NamedDBElementRepository extends DBElementRepository { $result = []; - $entities = $this->findBy([], ['name' => 'ASC']); + $entities = $this->getFlatList(); foreach ($entities as $entity) { /** @var AbstractNamedDBElement $entity */ $node = new TreeViewNode($entity->getName(), null, null); @@ -47,7 +52,7 @@ class NamedDBElementRepository extends DBElementRepository if ($entity instanceof User) { if ($entity->isDisabled()) { - //If this is an user, then add a badge when it is disabled + //If this is a user, then add a badge when it is disabled $node->setIcon('fa-fw fa-treeview fa-solid fa-user-lock text-muted'); } if ($entity->isSamlUser()) { @@ -61,12 +66,17 @@ class NamedDBElementRepository extends DBElementRepository } /** - * Returns the list of all nodes to use in a select box. + * Returns a flattened list of all nodes, sorted by name in natural order. * @return AbstractNamedDBElement[] + * @phpstan-return array */ - public function toNodesList(): array + public function getFlatList(): array { - //All nodes are sorted by name - return $this->findBy([], ['name' => 'ASC']); + $qb = $this->createQueryBuilder('e'); + $q = $qb->select('e') + ->orderBy('NATSORT(e.name)', 'ASC') + ->getQuery(); + + return $q->getResult(); } } diff --git a/src/Repository/ParameterRepository.php b/src/Repository/ParameterRepository.php index 37420306..6c6c867d 100644 --- a/src/Repository/ParameterRepository.php +++ b/src/Repository/ParameterRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository; +use App\Entity\Parameters\AbstractParameter; + +/** + * @template TEntityClass of AbstractParameter + * @extends DBElementRepository + */ class ParameterRepository extends DBElementRepository { /** * Find parameters using a parameter name * @param string $name The name to search for * @param bool $exact True, if only exact names should match. False, if the name just needs to be contained in the parameter name - * @param int $max_results - * @return array + * @phpstan-return array */ public function autocompleteParamName(string $name, bool $exact = false, int $max_results = 50): array { @@ -37,7 +44,7 @@ class ParameterRepository extends DBElementRepository ->select('parameter.name') ->addSelect('parameter.symbol') ->addSelect('parameter.unit') - ->where('parameter.name LIKE :name'); + ->where('ILIKE(parameter.name, :name) = TRUE'); if ($exact) { $qb->setParameter('name', $name); } else { @@ -48,4 +55,4 @@ class ParameterRepository extends DBElementRepository return $qb->getQuery()->getArrayResult(); } -} \ No newline at end of file +} diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index 5dfb8f45..edccd74b 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -22,15 +22,19 @@ declare(strict_types=1); namespace App\Repository; +use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; +/** + * @extends NamedDBElementRepository + */ class PartRepository extends NamedDBElementRepository { /** - * Gets the summed up instock of all parts (only parts without an measurent unit). + * Gets the summed up instock of all parts (only parts without a measurement unit). * * @throws NoResultException * @throws NonUniqueResultException @@ -49,7 +53,7 @@ class PartRepository extends NamedDBElementRepository } /** - * Gets the number of parts that has price informations. + * Gets the number of parts that has price information. * * @throws NoResultException * @throws NonUniqueResultException @@ -67,6 +71,9 @@ class PartRepository extends NamedDBElementRepository return (int) ($query->getSingleScalarResult() ?? 0); } + /** + * @return Part[] + */ public function autocompleteSearch(string $query, int $max_limits = 50): array { $qb = $this->createQueryBuilder('part'); @@ -74,16 +81,16 @@ class PartRepository extends NamedDBElementRepository ->leftJoin('part.category', 'category') ->leftJoin('part.footprint', 'footprint') - ->where('part.name LIKE :query') - ->orWhere('part.description LIKE :query') - ->orWhere('category.name LIKE :query') - ->orWhere('footprint.name LIKE :query') + ->where('ILIKE(part.name, :query) = TRUE') + ->orWhere('ILIKE(part.description, :query) = TRUE') + ->orWhere('ILIKE(category.name, :query) = TRUE') + ->orWhere('ILIKE(footprint.name, :query) = TRUE') ; $qb->setParameter('query', '%'.$query.'%'); $qb->setMaxResults($max_limits); - $qb->orderBy('part.name', 'ASC'); + $qb->orderBy('NATSORT(part.name)', 'ASC'); return $qb->getQuery()->getResult(); } diff --git a/src/Repository/Parts/CategoryRepository.php b/src/Repository/Parts/CategoryRepository.php index c472d8d6..9bbb1e37 100644 --- a/src/Repository/Parts/CategoryRepository.php +++ b/src/Repository/Parts/CategoryRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository\Parts; use App\Entity\Parts\Category; @@ -26,13 +28,13 @@ use InvalidArgumentException; class CategoryRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Category) { throw new InvalidArgumentException('$element must be an Category!'); } - return $this->getPartsByField($element, $order_by, 'category'); + return $this->getPartsByField($element, $nameOrderDirection, 'category'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/DeviceRepository.php b/src/Repository/Parts/DeviceRepository.php index dc5d5acc..442c91e5 100644 --- a/src/Repository/Parts/DeviceRepository.php +++ b/src/Repository/Parts/DeviceRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository\Parts; -use App\Entity\Base\AbstractPartsContainingDBElement; use App\Entity\ProjectSystem\Project; -use App\Entity\Parts\Category; -use App\Entity\Parts\Part; -use App\Repository\AbstractPartsContainingRepository; use App\Repository\StructuralDBElementRepository; use InvalidArgumentException; @@ -53,4 +51,4 @@ class DeviceRepository extends StructuralDBElementRepository //Prevent user from deleting devices, to not accidentally remove filled devices from old versions return 1; } -} \ No newline at end of file +} diff --git a/src/Repository/Parts/FootprintRepository.php b/src/Repository/Parts/FootprintRepository.php index 72c25003..8934831a 100644 --- a/src/Repository/Parts/FootprintRepository.php +++ b/src/Repository/Parts/FootprintRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository\Parts; use App\Entity\Parts\Footprint; @@ -26,13 +28,13 @@ use InvalidArgumentException; class FootprintRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Footprint) { throw new InvalidArgumentException('$element must be an Footprint!'); } - return $this->getPartsByField($element, $order_by, 'footprint'); + return $this->getPartsByField($element, $nameOrderDirection, 'footprint'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/ManufacturerRepository.php b/src/Repository/Parts/ManufacturerRepository.php index aa4d8fec..1a838710 100644 --- a/src/Repository/Parts/ManufacturerRepository.php +++ b/src/Repository/Parts/ManufacturerRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository\Parts; use App\Entity\Parts\Manufacturer; @@ -26,13 +28,13 @@ use InvalidArgumentException; class ManufacturerRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Manufacturer) { throw new InvalidArgumentException('$element must be an Manufacturer!'); } - return $this->getPartsByField($element, $order_by, 'manufacturer'); + return $this->getPartsByField($element, $nameOrderDirection, 'manufacturer'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/MeasurementUnitRepository.php b/src/Repository/Parts/MeasurementUnitRepository.php index 80d32743..c581f751 100644 --- a/src/Repository/Parts/MeasurementUnitRepository.php +++ b/src/Repository/Parts/MeasurementUnitRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository\Parts; use App\Entity\Parts\MeasurementUnit; @@ -26,13 +28,13 @@ use InvalidArgumentException; class MeasurementUnitRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof MeasurementUnit) { throw new InvalidArgumentException('$element must be an MeasurementUnit!'); } - return $this->getPartsByField($element, $order_by, 'partUnit'); + return $this->getPartsByField($element, $nameOrderDirection, 'partUnit'); } public function getPartsCount(object $element): int diff --git a/src/Repository/Parts/StorelocationRepository.php b/src/Repository/Parts/StorelocationRepository.php index c0c432be..82317868 100644 --- a/src/Repository/Parts/StorelocationRepository.php +++ b/src/Repository/Parts/StorelocationRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository\Parts; use App\Entity\Parts\Part; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Repository\AbstractPartsContainingRepository; use Doctrine\ORM\QueryBuilder; use InvalidArgumentException; class StorelocationRepository extends AbstractPartsContainingRepository { - /** - * @param object $element - * @param array $order_by - * @return array - */ - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { - if (!$element instanceof Storelocation) { + if (!$element instanceof StorageLocation) { throw new InvalidArgumentException('$element must be an Storelocation!'); } @@ -45,18 +42,16 @@ class StorelocationRepository extends AbstractPartsContainingRepository ->from(Part::class, 'part') ->leftJoin('part.partLots', 'lots') ->where('lots.storage_location = ?1') - ->setParameter(1, $element); - - foreach ($order_by as $field => $order) { - $qb->addOrderBy('part.'.$field, $order); - } + ->setParameter(1, $element) + ->orderBy('NATSORT(part.name)', $nameOrderDirection) + ; return $qb->getQuery()->getResult(); } public function getPartsCount(object $element): int { - if (!$element instanceof Storelocation) { + if (!$element instanceof StorageLocation) { throw new InvalidArgumentException('$element must be an Storelocation!'); } diff --git a/src/Repository/Parts/SupplierRepository.php b/src/Repository/Parts/SupplierRepository.php index 0a2e2c8f..393ae593 100644 --- a/src/Repository/Parts/SupplierRepository.php +++ b/src/Repository/Parts/SupplierRepository.php @@ -1,4 +1,7 @@ . */ - namespace App\Repository\Parts; use App\Entity\Parts\Part; @@ -28,7 +30,7 @@ use InvalidArgumentException; class SupplierRepository extends AbstractPartsContainingRepository { - public function getParts(object $element, array $order_by = ['name' => 'ASC']): array + public function getParts(object $element, string $nameOrderDirection = "ASC"): array { if (!$element instanceof Supplier) { throw new InvalidArgumentException('$element must be an Supplier!'); @@ -40,11 +42,9 @@ class SupplierRepository extends AbstractPartsContainingRepository ->from(Part::class, 'part') ->leftJoin('part.orderdetails', 'orderdetail') ->where('orderdetail.supplier = ?1') - ->setParameter(1, $element); - - foreach ($order_by as $field => $order) { - $qb->addOrderBy('part.'.$field, $order); - } + ->setParameter(1, $element) + ->orderBy('NATSORT(part.name)', $nameOrderDirection) + ; return $qb->getQuery()->getResult(); } diff --git a/src/Repository/StructuralDBElementRepository.php b/src/Repository/StructuralDBElementRepository.php index d7dae474..781c7622 100644 --- a/src/Repository/StructuralDBElementRepository.php +++ b/src/Repository/StructuralDBElementRepository.php @@ -27,7 +27,12 @@ use App\Helpers\Trees\StructuralDBElementIterator; use App\Helpers\Trees\TreeViewNode; use RecursiveIteratorIterator; -class StructuralDBElementRepository extends NamedDBElementRepository +/** + * @see \App\Tests\Repository\StructuralDBElementRepositoryTest + * @template TEntityClass of AbstractStructuralDBElement + * @extends AttachmentContainingDBElementRepository + */ +class StructuralDBElementRepository extends AttachmentContainingDBElementRepository { /** * @var array An array containing all new entities created by getNewEntityByPath. @@ -35,6 +40,28 @@ class StructuralDBElementRepository extends NamedDBElementRepository */ private array $new_entity_cache = []; + /** + * Finds all nodes for the given parent node, ordered by name in a natural sort way + * @param AbstractStructuralDBElement|null $parent + * @param string $nameOrdering The ordering of the names. Either ASC or DESC + * @return array + */ + public function findNodesForParent(?AbstractStructuralDBElement $parent, string $nameOrdering = "ASC"): array + { + $qb = $this->createQueryBuilder('e'); + $qb->select('e') + ->orderBy('NATSORT(e.name)', $nameOrdering); + + if ($parent !== null) { + $qb->where('e.parent = :parent') + ->setParameter('parent', $parent); + } else { + $qb->where('e.parent IS NULL'); + } + //@phpstan-ignore-next-line [parent is only defined by the sub classes] + return $qb->getQuery()->getResult(); + } + /** * Finds all nodes without a parent node. They are our root nodes. * @@ -42,14 +69,15 @@ class StructuralDBElementRepository extends NamedDBElementRepository */ public function findRootNodes(): array { - return $this->findBy(['parent' => null], ['name' => 'ASC']); + return $this->findNodesForParent(null); } /** * Gets a tree of TreeViewNode elements. The root elements has $parent as parent. * The treeview is generic, that means the href are null and ID values are set. * - * @param AbstractStructuralDBElement|null $parent the parent the root elements should have + * @param AbstractStructuralDBElement|null $parent the parent the root elements should have + * @phpstan-param TEntityClass|null $parent * * @return TreeViewNode[] */ @@ -57,7 +85,7 @@ class StructuralDBElementRepository extends NamedDBElementRepository { $result = []; - $entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); + $entities = $this->findNodesForParent($parent); foreach ($entities as $entity) { /** @var AbstractStructuralDBElement $entity */ //Make a recursive call to find all children nodes @@ -75,20 +103,21 @@ class StructuralDBElementRepository extends NamedDBElementRepository * Gets a flattened hierarchical tree. Useful for generating option lists. * * @param AbstractStructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root - * + * @phpstan-param TEntityClass|null $parent * @return AbstractStructuralDBElement[] a flattened list containing the tree elements + * @phpstan-return array */ - public function toNodesList(?AbstractStructuralDBElement $parent = null): array + public function getFlatList(?AbstractStructuralDBElement $parent = null): array { $result = []; - $entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); + $entities = $this->findNodesForParent($parent); $elementIterator = new StructuralDBElementIterator($entities); $recursiveIterator = new RecursiveIteratorIterator($elementIterator, RecursiveIteratorIterator::SELF_FIRST); //$result = iterator_to_array($recursiveIterator); - //We can not use iterator_to_array here or we get only the parent elements + //We can not use iterator_to_array here, or we get only the parent elements foreach ($recursiveIterator as $item) { $result[] = $item; } @@ -100,9 +129,8 @@ class StructuralDBElementRepository extends NamedDBElementRepository * Creates a structure of AbstractStructuralDBElements from a path separated by $separator, which splits the various levels. * This function will try to use existing elements, if they are already in the database. If not, they will be created. * An array of the created elements will be returned, with the last element being the deepest element. - * @param string $path - * @param string $separator * @return AbstractStructuralDBElement[] + * @phpstan-return array */ public function getNewEntityFromPath(string $path, string $separator = '->'): array { @@ -118,12 +146,12 @@ class StructuralDBElementRepository extends NamedDBElementRepository $entity = $this->getNewEntityFromCache($name, $parent); //See if we already have an element with this name and parent in the database - if (!$entity) { + if (!$entity instanceof AbstractStructuralDBElement) { $entity = $this->findOneBy(['name' => $name, 'parent' => $parent]); } if (null === $entity) { $class = $this->getClassName(); - /** @var AbstractStructuralDBElement $entity */ + /** @var TEntityClass $entity */ $entity = new $class; $entity->setName($name); $entity->setParent($parent); @@ -140,11 +168,8 @@ class StructuralDBElementRepository extends NamedDBElementRepository private function getNewEntityFromCache(string $name, ?AbstractStructuralDBElement $parent): ?AbstractStructuralDBElement { - $key = $parent ? $parent->getFullPath('%->%').'%->%'.$name : $name; - if (isset($this->new_entity_cache[$key])) { - return $this->new_entity_cache[$key]; - } - return null; + $key = $parent instanceof AbstractStructuralDBElement ? $parent->getFullPath('%->%').'%->%'.$name : $name; + return $this->new_entity_cache[$key] ?? null; } private function setNewEntityToCache(AbstractStructuralDBElement $entity): void @@ -157,9 +182,8 @@ class StructuralDBElementRepository extends NamedDBElementRepository * Returns an element of AbstractStructuralDBElements queried from a path separated by $separator, which splits the various levels. * An array of the created elements will be returned, with the last element being the deepest element. * If no element was found, an empty array will be returned. - * @param string $path - * @param string $separator * @return AbstractStructuralDBElement[] + * @phpstan-return array */ public function getEntityByPath(string $path, string $separator = '->'): array { @@ -183,4 +207,74 @@ class StructuralDBElementRepository extends NamedDBElementRepository return $result; } + + /** + * Finds the element with the given name for the use with the InfoProvider System + * The name search is a bit more fuzzy than the normal findByName, because it is case-insensitive and ignores special characters. + * Also, it will try to find the element using the additional names field, of the elements. + * @param string $name + * @return AbstractStructuralDBElement|null + * @phpstan-return TEntityClass|null + */ + public function findForInfoProvider(string $name): ?AbstractStructuralDBElement + { + //First try to find the element by name + $qb = $this->createQueryBuilder('e'); + //Use lowercase conversion to be case-insensitive + $qb->where($qb->expr()->like('LOWER(e.name)', 'LOWER(:name)')); + + $qb->setParameter('name', $name); + + $result = $qb->getQuery()->getResult(); + + if (count($result) === 1) { + return $result[0]; + } + + //If we have no result, try to find the element by alternative names + $qb = $this->createQueryBuilder('e'); + //Use lowercase conversion to be case-insensitive + $qb->where($qb->expr()->like('LOWER(e.alternative_names)', 'LOWER(:name)')); + $qb->setParameter('name', '%'.$name.',%'); + + $result = $qb->getQuery()->getResult(); + + if (count($result) >= 1) { + return $result[0]; + } + + //If we find nothing, return null + return null; + } + + /** + * Similar to findForInfoProvider, but will create a new element with the given name if none was found. + * @param string $name + * @return AbstractStructuralDBElement + * @phpstan-return TEntityClass + */ + public function findOrCreateForInfoProvider(string $name): AbstractStructuralDBElement + { + $entity = $this->findForInfoProvider($name); + if (null === $entity) { + + //Try to find if we already have an element cached for this name + $entity = $this->getNewEntityFromCache($name, null); + if ($entity !== null) { + return $entity; + } + + $class = $this->getClassName(); + /** @var TEntityClass $entity */ + $entity = new $class; + $entity->setName($name); + + //Set the found name to the alternative names, so the entity can be easily renamed later + $entity->setAlternativeNames($name); + + $this->setNewEntityToCache($entity); + } + + return $entity; + } } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index b0ccd964..bbaa2b39 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -23,7 +23,10 @@ declare(strict_types=1); namespace App\Repository; use App\Entity\UserSystem\User; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\Query\Parameter; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -32,18 +35,21 @@ use Symfony\Component\Security\Core\User\UserInterface; * @method User|null findOneBy(array $criteria, array $orderBy = null) * @method User[] findAll() * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @extends NamedDBElementRepository + * @see \App\Tests\Repository\UserRepositoryTest */ final class UserRepository extends NamedDBElementRepository implements PasswordUpgraderInterface { - protected $anonymous_user; + protected ?User $anonymous_user = null; /** * Returns the anonymous user. * The result is cached, so the database is only called once, after the anonymous user was found. + * @return User|null The user if it is existing, null if no one matched the criteria */ public function getAnonymousUser(): ?User { - if (null === $this->anonymous_user) { + if (!$this->anonymous_user instanceof User) { $this->anonymous_user = $this->findOneBy([ 'id' => User::ID_ANONYMOUS, ]); @@ -52,6 +58,30 @@ final class UserRepository extends NamedDBElementRepository implements PasswordU return $this->anonymous_user; } + /** + * Find a user by its username. + * @param string $username + * @return User|null + */ + public function findByUsername(string $username): ?User + { + if ($username === '') { + return null; + } + + $qb = $this->createQueryBuilder('u'); + $qb->select('u') + ->where('u.name = (:name)'); + + $qb->setParameter('name', $username); + + try { + return $qb->getQuery()->getOneOrNullResult(); + } catch (NonUniqueResultException) { + return null; + } + } + /** * Find a user by its name or its email. Useful for login or password reset purposes. * @@ -61,7 +91,7 @@ final class UserRepository extends NamedDBElementRepository implements PasswordU */ public function findByEmailOrName(string $name_or_password): ?User { - if (empty($name_or_password)) { + if ($name_or_password === '') { return null; } @@ -70,19 +100,17 @@ final class UserRepository extends NamedDBElementRepository implements PasswordU ->where('u.name = (:name)') ->orWhere('u.email = (:email)'); - $qb->setParameters([ - 'email' => $name_or_password, - 'name' => $name_or_password, - ]); + $qb->setParameter('email', $name_or_password); + $qb->setParameter('name', $name_or_password); try { return $qb->getQuery()->getOneOrNullResult(); - } catch (NonUniqueResultException $nonUniqueResultException) { + } catch (NonUniqueResultException) { return null; } } - public function upgradePassword(UserInterface $user, string $newHashedPassword): void + public function upgradePassword(UserInterface|PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { if ($user instanceof User) { $user->setPassword($newHashedPassword); diff --git a/assets/controllers/pages/u2f_register_controller.js b/src/Repository/UserSystem/ApiTokenRepository.php similarity index 74% rename from assets/controllers/pages/u2f_register_controller.js rename to src/Repository/UserSystem/ApiTokenRepository.php index ffa4c3f2..609014f3 100644 --- a/assets/controllers/pages/u2f_register_controller.js +++ b/src/Repository/UserSystem/ApiTokenRepository.php @@ -1,7 +1,8 @@ +. */ -import {Controller} from "@hotwired/stimulus"; +declare(strict_types=1); -export default class extends Controller + +namespace App\Repository\UserSystem; + +use Doctrine\ORM\EntityRepository; + +class ApiTokenRepository extends EntityRepository { - connect() { - this.element.onclick = function() { - window.u2fauth.register(); - } - } -} +} \ No newline at end of file diff --git a/src/Security/ApiTokenAuthenticatedToken.php b/src/Security/ApiTokenAuthenticatedToken.php new file mode 100644 index 00000000..2f186e63 --- /dev/null +++ b/src/Security/ApiTokenAuthenticatedToken.php @@ -0,0 +1,52 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security; + +use App\Entity\UserSystem\ApiToken; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; + +class ApiTokenAuthenticatedToken extends PostAuthenticationToken +{ + public function __construct(UserInterface $user, string $firewallName, array $roles, private readonly ApiToken $apiToken) + { + //Add roles for the API + $roles[] = 'ROLE_API_AUTHENTICATED'; + + //Add roles based on the token level + $roles = array_merge($roles, $apiToken->getLevel()->getAdditionalRoles()); + + + parent::__construct($user, $firewallName, array_unique($roles)); + } + + /** + * Returns the API token that was used to authenticate the user. + * @return ApiToken + */ + public function getApiToken(): ApiToken + { + return $this->apiToken; + } +} \ No newline at end of file diff --git a/src/Security/ApiTokenAuthenticator.php b/src/Security/ApiTokenAuthenticator.php new file mode 100644 index 00000000..a52b1f7c --- /dev/null +++ b/src/Security/ApiTokenAuthenticator.php @@ -0,0 +1,156 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security; + +use App\Entity\UserSystem\ApiToken; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; +use Symfony\Component\Security\Http\AccessToken\AccessTokenExtractorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Authenticator similar to the builtin AccessTokenAuthenticator, but we return a Token here which contains information + * about the used token. + */ +class ApiTokenAuthenticator implements AuthenticatorInterface +{ + public function __construct( + #[Autowire(service: 'security.access_token_extractor.main')] + private readonly AccessTokenExtractorInterface $accessTokenExtractor, + private readonly TranslatorInterface $translator, + private readonly EntityManagerInterface $entityManager, + private readonly string $realm = 'api', + ) { + } + + /** + * Gets the ApiToken belonging to the given accessToken string. + * If the token is invalid or expired, an exception is thrown and authentication fails. + * @param string $accessToken + * @return ApiToken + */ + private function getTokenFromString(#[\SensitiveParameter] string $accessToken): ApiToken + { + $repo = $this->entityManager->getRepository(ApiToken::class); + $token = $repo->findOneBy(['token' => $accessToken]); + + if (!$token instanceof ApiToken) { + throw new BadCredentialsException(); + } + + if (!$token->isValid()) { + throw new CustomUserMessageAuthenticationException('Token expired'); + } + + $old_time = $token->getLastTimeUsed(); + //Set the last used date of the token + $token->setLastTimeUsed(new \DateTimeImmutable()); + //Only flush the token if the last used date change is more than 10 minutes + //For performance reasons we don't want to flush the token every time it is used, but only if it is used more than 10 minutes after the last time it was used + //If a flush is later in the code we don't want to flush the token again + if ($old_time === null || $old_time->diff($token->getLastTimeUsed())->i > 10) { + $this->entityManager->flush(); + } + + return $token; + } + + public function supports(Request $request): ?bool + { + return null === $this->accessTokenExtractor->extractAccessToken($request) ? false : null; + } + + public function authenticate(Request $request): Passport + { + $accessToken = $this->accessTokenExtractor->extractAccessToken($request); + if (!$accessToken) { + throw new BadCredentialsException('Invalid credentials.'); + } + + $apiToken = $this->getTokenFromString($accessToken); + $userBadge = new UserBadge($apiToken->getUser()?->getUserIdentifier() ?? throw new BadCredentialsException('Invalid credentials.')); + $apiBadge = new ApiTokenBadge($apiToken); + + return new SelfValidatingPassport($userBadge, [$apiBadge]); + } + + public function createToken(Passport $passport, string $firewallName): TokenInterface + { + return new ApiTokenAuthenticatedToken( + $passport->getUser(), + $firewallName, + $passport->getUser()->getRoles(), + $passport->getBadge(ApiTokenBadge::class)?->getApiToken() ?? throw new \LogicException('Passport does not contain an API token.') + ); + } + + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + $errorMessage = $this->translator->trans($exception->getMessageKey(), $exception->getMessageData(), + 'security'); + + return new Response( + null, + Response::HTTP_UNAUTHORIZED, + ['WWW-Authenticate' => $this->getAuthenticateHeader($errorMessage)] + ); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3 + */ + private function getAuthenticateHeader(?string $errorDescription = null): string + { + $data = [ + 'realm' => $this->realm, + 'error' => 'invalid_token', + 'error_description' => $errorDescription, + ]; + $values = []; + foreach ($data as $k => $v) { + if (null === $v || '' === $v) { + continue; + } + $values[] = sprintf('%s="%s"', $k, $v); + } + + return sprintf('Bearer %s', implode(',', $values)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } +} \ No newline at end of file diff --git a/src/Security/ApiTokenBadge.php b/src/Security/ApiTokenBadge.php new file mode 100644 index 00000000..d2429a06 --- /dev/null +++ b/src/Security/ApiTokenBadge.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security; + +use App\Entity\UserSystem\ApiToken; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; + +class ApiTokenBadge implements BadgeInterface +{ + + /** + * @param ApiToken $apiToken + */ + public function __construct(private readonly ApiToken $apiToken) + { + } + + /** + * @return ApiToken The token that was used to authenticate the user + */ + public function getApiToken(): ApiToken + { + return $this->apiToken; + } + + public function isResolved(): bool + { + return true; + } +} \ No newline at end of file diff --git a/src/Security/AuthenticationEntryPoint.php b/src/Security/AuthenticationEntryPoint.php new file mode 100644 index 00000000..41f624b2 --- /dev/null +++ b/src/Security/AuthenticationEntryPoint.php @@ -0,0 +1,89 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + +use function Symfony\Component\Translation\t; + +/** + * This class decides, what to do, when a user tries to access a page, that requires authentication and he is not + * authenticated / logged in yet. + * For browser requests, the user is redirected to the login page, for API requests, a 401 response with a JSON encoded + * message is returned. + */ +class AuthenticationEntryPoint implements AuthenticationEntryPointInterface +{ + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + ) { + } + + public function start(Request $request, ?AuthenticationException $authException = null): Response + { + //Check if the request is an API request + if ($this->isJSONRequest($request)) { + //If it is, we return a 401 response with a JSON body + return new JsonResponse([ + 'title' => 'Unauthorized', + 'detail' => 'Authentication is required. Please pass a valid API token in the Authorization header.', + ], Response::HTTP_UNAUTHORIZED); + } + + //Otherwise we redirect to the login page + + //Add a nice flash message to make it clear what happened + if ($request->getSession() instanceof Session) { + $request->getSession()->getFlashBag()->add('error', t('login.flash.access_denied_please_login')); + } + + return new RedirectResponse($this->urlGenerator->generate('login')); + } + + private function isJSONRequest(Request $request): bool + { + //If either the content type or accept header is a json type, we assume it is an API request + $contentType = $request->headers->get('Content-Type'); + $accept = $request->headers->get('Accept'); + + $tmp = false; + + if ($contentType !== null) { + $tmp = str_contains($contentType, 'json'); + } + + if ($accept !== null) { + $tmp = $tmp || str_contains($accept, 'json'); + } + + return $tmp; + } +} \ No newline at end of file diff --git a/src/Security/EnsureSAMLUserForSAMLLoginChecker.php b/src/Security/EnsureSAMLUserForSAMLLoginChecker.php index 1d4c3cfe..0ebf893c 100644 --- a/src/Security/EnsureSAMLUserForSAMLLoginChecker.php +++ b/src/Security/EnsureSAMLUserForSAMLLoginChecker.php @@ -1,4 +1,7 @@ . */ - namespace App\Security; use App\Entity\UserSystem\User; -use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; +use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; -use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @see \App\Tests\Security\EnsureSAMLUserForSAMLLoginCheckerTest + */ class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface { - private TranslatorInterface $translator; - - public function __construct(TranslatorInterface $translator) + public function __construct(private readonly TranslatorInterface $translator) { - $this->translator = $translator; } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ AuthenticationSuccessEvent::class => 'onAuthenticationSuccess', @@ -49,15 +51,21 @@ class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface $token = $event->getAuthenticationToken(); $user = $token->getUser(); - //If we are using SAML, we need to check that the user is a SAML user. - if ($token instanceof SamlToken) { - if ($user instanceof User && !$user->isSAMLUser()) { - throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml', [], 'security')); - } - } else { //Ensure that you can not login locally with a SAML user (even if this should not happen, as the password is not set) - if ($user instanceof User && $user->isSamlUser()) { - throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security')); - } + //Do not check for anonymous users + if (!$user instanceof User) { + return; + } + + //Do not allow SAML users to login as local user + if ($token instanceof SamlToken && !$user->isSamlUser()) { + throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml', + [], 'security')); + } + + //Do not allow local users to login as SAML user via local username and password + if ($token instanceof UsernamePasswordToken && $user->isSamlUser()) { + //Ensure that you can not login locally with a SAML user (even though this should not happen, as the password is not set) + throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security')); } } -} \ No newline at end of file +} diff --git a/src/Security/SamlUserFactory.php b/src/Security/SamlUserFactory.php index 39e67c0c..312be859 100644 --- a/src/Security/SamlUserFactory.php +++ b/src/Security/SamlUserFactory.php @@ -1,4 +1,7 @@ . */ - namespace App\Security; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use Doctrine\ORM\EntityManagerInterface; -use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; -use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface; +use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken; +use Nbgrp\OneloginSamlBundle\Security\User\SamlUserFactoryInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\User\UserInterface; +/** + * @see \App\Tests\Security\SamlUserFactoryTest + */ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterface { - private EntityManagerInterface $em; - private array $saml_role_mapping; - private bool $update_group_on_login; + private readonly array $saml_role_mapping; - public function __construct(EntityManagerInterface $entityManager, ?array $saml_role_mapping, bool $update_group_on_login) + public function __construct(private readonly EntityManagerInterface $em, ?array $saml_role_mapping, private readonly bool $update_group_on_login) { - $this->em = $entityManager; - if ($saml_role_mapping) { - $this->saml_role_mapping = $saml_role_mapping; - } else { - $this->saml_role_mapping = []; - } - $this->update_group_on_login = $update_group_on_login; + $this->saml_role_mapping = $saml_role_mapping ?: []; } - public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!'; + final public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!'; public function createUser($username, array $attributes = []): UserInterface { @@ -70,8 +67,6 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf /** * This method is called after a successful authentication. It is used to update the group of the user, * based on the new SAML attributes. - * @param AuthenticationSuccessEvent $event - * @return void */ public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void { @@ -98,7 +93,6 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf /** * Maps the given SAML attributes to a local group. * @param array $attributes The SAML attributes - * @return Group|null */ public function mapSAMLAttributesToLocalGroup(array $attributes): ?Group { @@ -109,7 +103,7 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf //Check if we can find a group with the given ID if ($group_id !== null) { $group = $this->em->find(Group::class, $group_id); - if ($group !== null) { + if ($group instanceof Group) { return $group; } } @@ -122,12 +116,12 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf * Maps a list of SAML roles to a local group ID. * The first available mapping will be used (so the order of the $map is important, first match wins). * @param array $roles The list of SAML roles - * @param array $map|null The mapping from SAML roles. If null, the global mapping will be used. + * @param array|null $map The mapping from SAML roles. If null, the global mapping will be used. * @return int|null The ID of the local group or null if no mapping was found. */ - public function mapSAMLRolesToLocalGroupID(array $roles, array $map = null): ?int + public function mapSAMLRolesToLocalGroupID(array $roles, ?array $map = null): ?int { - $map = $map ?? $this->saml_role_mapping; + $map ??= $this->saml_role_mapping; //Iterate over the mapping (from first to last) and check if we have a match foreach ($map as $saml_role => $group_id) { @@ -156,4 +150,4 @@ class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterf AuthenticationSuccessEvent::class => 'onAuthenticationSuccess', ]; } -} \ No newline at end of file +} diff --git a/src/Security/TwoFactor/WebauthnKeyLastUseTwoFactorProvider.php b/src/Security/TwoFactor/WebauthnKeyLastUseTwoFactorProvider.php new file mode 100644 index 00000000..9bfa691d --- /dev/null +++ b/src/Security/TwoFactor/WebauthnKeyLastUseTwoFactorProvider.php @@ -0,0 +1,106 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security\TwoFactor; + +use App\Entity\UserSystem\WebauthnKey; +use Doctrine\ORM\EntityManagerInterface; +use Jbtronics\TFAWebauthn\Services\UserPublicKeyCredentialSourceRepository; +use Jbtronics\TFAWebauthn\Services\WebauthnProvider; +use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorFormRendererInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + +/** + * This class decorates the Webauthn TwoFactorProvider and adds additional logic which allows us to set a last used date + * on the used webauthn key, which can be viewed in the user settings. + */ +#[AsDecorator('jbtronics_webauthn_tfa.two_factor_provider')] +class WebauthnKeyLastUseTwoFactorProvider implements TwoFactorProviderInterface +{ + + public function __construct( + #[AutowireDecorated] + private readonly TwoFactorProviderInterface $decorated, + private readonly EntityManagerInterface $entityManager, + #[Autowire(service: 'jbtronics_webauthn_tfa.user_public_key_source_repo')] + private readonly UserPublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, + #[Autowire(service: 'jbtronics_webauthn_tfa.webauthn_provider')] + private readonly WebauthnProvider $webauthnProvider, + ) + { + } + + public function beginAuthentication(AuthenticationContextInterface $context): bool + { + return $this->decorated->beginAuthentication($context); + } + + public function prepareAuthentication(object $user): void + { + $this->decorated->prepareAuthentication($user); + } + + public function validateAuthenticationCode(object $user, string $authenticationCode): bool + { + //Try to extract the used webauthn key from the code + $webauthnKey = $this->getWebauthnKeyFromCode($authenticationCode); + + //Perform the actual validation like normal + $tmp = $this->decorated->validateAuthenticationCode($user, $authenticationCode); + + //Update the last used date of the webauthn key, if the validation was successful + if($tmp && $webauthnKey !== null) { + $webauthnKey->updateLastTimeUsed(); + $this->entityManager->flush(); + } + + return $tmp; + } + + public function getFormRenderer(): TwoFactorFormRendererInterface + { + return $this->decorated->getFormRenderer(); + } + + private function getWebauthnKeyFromCode(string $authenticationCode): ?WebauthnKey + { + $publicKeyCredentialLoader = $this->webauthnProvider->getPublicKeyCredentialLoader(); + + //Try to load the public key credential from the code + $publicKeyCredential = $publicKeyCredentialLoader->load($authenticationCode); + + //Find the credential source for the given credential id + $publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($publicKeyCredential->rawId); + + //If the credential source is not an instance of WebauthnKey, return null + if(!($publicKeyCredentialSource instanceof WebauthnKey)) { + return null; + } + + return $publicKeyCredentialSource; + } +} \ No newline at end of file diff --git a/src/Security/UserChecker.php b/src/Security/UserChecker.php index ae8e3f34..16afb37e 100644 --- a/src/Security/UserChecker.php +++ b/src/Security/UserChecker.php @@ -25,28 +25,25 @@ namespace App\Security; use App\Entity\UserSystem\User; use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; -use Symfony\Component\Security\Core\Exception\DisabledException; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @see \App\Tests\Security\UserCheckerTest + */ final class UserChecker implements UserCheckerInterface { - private TranslatorInterface $translator; - - public function __construct(TranslatorInterface $translator) + public function __construct(private readonly TranslatorInterface $translator) { - $this->translator = $translator; } /** * Checks the user account before authentication. - * - * @throws AccountStatusException */ public function checkPreAuth(UserInterface $user): void { - // TODO: Implement checkPreAuth() method. + //We don't need to check the user before authentication, just implemented to fulfill the interface } /** @@ -60,7 +57,7 @@ final class UserChecker implements UserCheckerInterface return; } - //Check if user is disabled. Then dont allow login + //Check if user is disabled. Then don't allow login if ($user->isDisabled()) { //throw new DisabledException(); throw new CustomUserMessageAccountStatusException($this->translator->trans('user.login_error.user_disabled', [], 'security')); diff --git a/src/Security/Voter/AttachmentVoter.php b/src/Security/Voter/AttachmentVoter.php index 135ba57f..c2b17053 100644 --- a/src/Security/Voter/AttachmentVoter.php +++ b/src/Security/Voter/AttachmentVoter.php @@ -22,57 +22,109 @@ declare(strict_types=1); namespace App\Security\Voter; +use App\Services\UserSystem\VoterHelper; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\Attachment; -use App\Entity\UserSystem\User; -use App\Services\UserSystem\PermissionManager; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Security\Core\Security; +use App\Entity\Attachments\AttachmentTypeAttachment; +use App\Entity\Attachments\CategoryAttachment; +use App\Entity\Attachments\CurrencyAttachment; +use App\Entity\Attachments\FootprintAttachment; +use App\Entity\Attachments\GroupAttachment; +use App\Entity\Attachments\ManufacturerAttachment; +use App\Entity\Attachments\MeasurementUnitAttachment; +use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\ProjectAttachment; +use App\Entity\Attachments\StorageLocationAttachment; +use App\Entity\Attachments\SupplierAttachment; +use App\Entity\Attachments\UserAttachment; +use RuntimeException; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; use function in_array; -class AttachmentVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class AttachmentVoter extends Voter { - protected $security; + private const ALLOWED_ATTRIBUTES = ['read', 'view', 'edit', 'delete', 'create', 'show_private', 'show_history']; - public function __construct(PermissionManager $resolver, EntityManagerInterface $entityManager, Security $security) + public function __construct(private readonly Security $security, private readonly VoterHelper $helper) { - parent::__construct($resolver, $entityManager); - $this->security = $security; } - /** - * Similar to voteOnAttribute, but checking for the anonymous user is already done. - * The current user (or the anonymous user) is passed by $user. - * - * @param string $attribute - */ - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - //return $this->resolver->inherit($user, 'attachments', $attribute) ?? false; - //If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element - $target_element = $subject->getElement(); - if (! $subject instanceof Attachment || null === $target_element) { + //This voter only works for attachments + if (!is_a($subject, Attachment::class, true)) { return false; } - //Depending on the operation delegate either to the attachments element or to the attachment permission - switch ($attribute) { - //We can view the attachment if we can view the element - case 'read': - case 'view': - return $this->security->isGranted('read', $target_element); - //We can edit/create/delete the attachment if we can edit the element - case 'edit': - case 'create': - case 'delete': - return $this->security->isGranted('edit', $target_element); - - case 'show_private': - return $this->resolver->inherit($user, 'attachments', 'show_private') ?? false; + if ($attribute === 'show_private') { + return $this->helper->isGranted($token, 'attachments', 'show_private'); } - throw new \RuntimeException('Encountered unknown attribute "'.$attribute.'" in AttachmentVoter!'); + + if (is_object($subject)) { + //If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element + $target_element = $subject->getElement(); + if ($target_element instanceof AttachmentContainingDBElement) { + return $this->security->isGranted($this->mapOperation($attribute), $target_element); + } + } + + if (is_string($subject)) { + //If we do not have a concrete element (or we just got a string as value), we delegate to the different categories + if (is_a($subject, AttachmentTypeAttachment::class, true)) { + $param = 'attachment_types'; + } elseif (is_a($subject, CategoryAttachment::class, true)) { + $param = 'categories'; + } elseif (is_a($subject, CurrencyAttachment::class, true)) { + $param = 'currencies'; + } elseif (is_a($subject, ProjectAttachment::class, true)) { + $param = 'projects'; + } elseif (is_a($subject, FootprintAttachment::class, true)) { + $param = 'footprints'; + } elseif (is_a($subject, GroupAttachment::class, true)) { + $param = 'groups'; + } elseif (is_a($subject, ManufacturerAttachment::class, true)) { + $param = 'manufacturers'; + } elseif (is_a($subject, MeasurementUnitAttachment::class, true)) { + $param = 'measurement_units'; + } elseif (is_a($subject, PartAttachment::class, true)) { + $param = 'parts'; + } elseif (is_a($subject, StorageLocationAttachment::class, true)) { + $param = 'storelocations'; + } elseif (is_a($subject, SupplierAttachment::class, true)) { + $param = 'suppliers'; + } elseif (is_a($subject, UserAttachment::class, true)) { + $param = 'users'; + } elseif ($subject === Attachment::class) { + //If the subject was deleted, we can not determine the type properly, so we just use the parts permission + $param = 'parts'; + } + else { + throw new RuntimeException('Encountered unknown Parameter type: ' . $subject); + } + + return $this->helper->isGranted($token, $param, $this->mapOperation($attribute)); + } + + return false; + } + + private function mapOperation(string $attribute): string + { + return match ($attribute) { + 'read', 'view' => 'read', + 'edit', 'create', 'delete' => 'edit', + 'show_history' => 'show_history', + default => throw new \RuntimeException('Encountered unknown attribute "'.$attribute.'" in AttachmentVoter!'), + }; } /** @@ -83,14 +135,24 @@ class AttachmentVoter extends ExtendedVoter * * @return bool True if the attribute and subject are supported, false otherwise */ - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { if (is_a($subject, Attachment::class, true)) { //These are the allowed attributes - return in_array($attribute, ['read', 'view', 'edit', 'delete', 'create', 'show_private'], true); + return in_array($attribute, self::ALLOWED_ATTRIBUTES, true); } //Allow class name as subject return false; } + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, self::ALLOWED_ATTRIBUTES, true); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, Attachment::class, true); + } } diff --git a/src/Security/Voter/BOMEntryVoter.php b/src/Security/Voter/BOMEntryVoter.php new file mode 100644 index 00000000..121c8172 --- /dev/null +++ b/src/Security/Voter/BOMEntryVoter.php @@ -0,0 +1,90 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security\Voter; + +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; + +/** + * @phpstan-extends Voter + */ +class BOMEntryVoter extends Voter +{ + + private const ALLOWED_ATTRIBUTES = ['read', 'view', 'edit', 'delete', 'create', 'show_history']; + + public function __construct(private readonly Security $security) + { + } + + protected function supports(string $attribute, mixed $subject): bool + { + return $this->supportsAttribute($attribute) && is_a($subject, ProjectBOMEntry::class, true); + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + { + if (!is_a($subject, ProjectBOMEntry::class, true)) { + return false; + } + + if (is_object($subject)) { + $project = $subject->getProject(); + + //Allow everything if the project was not set yet + if ($project === null) { + return true; + } + } else { + //If a string was given, use the general project permissions to resolve permissions + $project = Project::class; + } + + //Entry can be read if the user has read access to the project + if ($attribute === 'read') { + return $this->security->isGranted('read', $project); + } + + //History can be shown if the user has show_history access to the project + if ($attribute === 'show_history') { + return $this->security->isGranted('show_history', $project); + } + + //Everything else can be done if the user has edit access to the project + return $this->security->isGranted('edit', $project); + } + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, self::ALLOWED_ATTRIBUTES, true); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, ProjectBOMEntry::class, true); + } +} \ No newline at end of file diff --git a/src/Security/Voter/ExtendedVoter.php b/src/Security/Voter/ExtendedVoter.php deleted file mode 100644 index 825d768c..00000000 --- a/src/Security/Voter/ExtendedVoter.php +++ /dev/null @@ -1,75 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Security\Voter; - -use App\Entity\UserSystem\User; -use App\Repository\UserRepository; -use App\Services\UserSystem\PermissionManager; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Authorization\Voter\Voter; - -/** - * The purpose of this class is, to use the anonymous user from DB in the case, that nobody is logged in. - */ -abstract class ExtendedVoter extends Voter -{ - protected EntityManagerInterface $entityManager; - protected PermissionManager $resolver; - - public function __construct(PermissionManager $resolver, EntityManagerInterface $entityManager) - { - $this->resolver = $resolver; - $this->entityManager = $entityManager; - } - - final protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool - { - $user = $token->getUser(); - - //An allowed user is not allowed to do anything... - if ($user instanceof User && $user->isDisabled()) { - return false; - } - - // if the user is anonymous (meaning $user is null), we use the anonymous user. - if (!$user instanceof User) { - /** @var UserRepository $repo */ - $repo = $this->entityManager->getRepository(User::class); - $user = $repo->getAnonymousUser(); - if (null === $user) { - return false; - } - } - - return $this->voteOnUser($attribute, $subject, $user); - } - - /** - * Similar to voteOnAttribute, but checking for the anonymous user is already done. - * The current user (or the anonymous user) is passed by $user. - * - * @param string $attribute - */ - abstract protected function voteOnUser(string $attribute, $subject, User $user): bool; -} diff --git a/src/Security/Voter/GroupVoter.php b/src/Security/Voter/GroupVoter.php index e1e21543..34839d38 100644 --- a/src/Security/Voter/GroupVoter.php +++ b/src/Security/Voter/GroupVoter.php @@ -23,19 +23,29 @@ declare(strict_types=1); namespace App\Security\Voter; use App\Entity\UserSystem\Group; -use App\Entity\UserSystem\User; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class GroupVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class GroupVoter extends Voter { + + public function __construct(private readonly VoterHelper $helper) + { + } + /** * Similar to voteOnAttribute, but checking for the anonymous user is already done. * The current user (or the anonymous user) is passed by $user. * * @param string $attribute */ - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - return $this->resolver->inherit($user, 'groups', $attribute) ?? false; + return $this->helper->isGranted($token, 'groups', $attribute); } /** @@ -46,12 +56,22 @@ class GroupVoter extends ExtendedVoter * * @return bool True if the attribute and subject are supported, false otherwise */ - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { if (is_a($subject, Group::class, true)) { - return $this->resolver->isValidOperation('groups', $attribute); + return $this->helper->isValidOperation('groups', $attribute); } return false; } + + public function supportsAttribute(string $attribute): bool + { + return $this->helper->isValidOperation('groups', $attribute); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, Group::class, true); + } } diff --git a/src/Security/Voter/HasAccessPermissionsVoter.php b/src/Security/Voter/HasAccessPermissionsVoter.php new file mode 100644 index 00000000..bd466d07 --- /dev/null +++ b/src/Security/Voter/HasAccessPermissionsVoter.php @@ -0,0 +1,59 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security\Voter; + +use App\Services\UserSystem\PermissionManager; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; + +/** + * This voter implements a virtual role, which can be used if the user has any permission set to allowed. + * We use this to restrict access to the homepage. + * @phpstan-extends Voter + */ +final class HasAccessPermissionsVoter extends Voter +{ + public const ROLE = "HAS_ACCESS_PERMISSIONS"; + + public function __construct(private readonly PermissionManager $permissionManager, private readonly VoterHelper $helper) + { + } + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + $user = $this->helper->resolveUser($token); + return $this->permissionManager->hasAnyPermissionSetToAllowInherited($user); + } + + protected function supports(string $attribute, mixed $subject): bool + { + return $attribute === self::ROLE; + } + + public function supportsAttribute(string $attribute): bool + { + return $attribute === self::ROLE; + } +} \ No newline at end of file diff --git a/src/Security/Voter/ImpersonateUserVoter.php b/src/Security/Voter/ImpersonateUserVoter.php new file mode 100644 index 00000000..edf55c62 --- /dev/null +++ b/src/Security/Voter/ImpersonateUserVoter.php @@ -0,0 +1,64 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security\Voter; + +use App\Entity\UserSystem\User; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * This voter implements a virtual role, which can be used if the user has any permission set to allowed. + * We use this to restrict access to the homepage. + * @phpstan-extends Voter + */ +final class ImpersonateUserVoter extends Voter +{ + + public function __construct(private readonly VoterHelper $helper) + { + } + + protected function supports(string $attribute, mixed $subject): bool + { + return $attribute === 'CAN_SWITCH_USER' + && $subject instanceof UserInterface; + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + { + return $this->helper->isGranted($token, 'users', 'impersonate'); + } + + public function supportsAttribute(string $attribute): bool + { + return $attribute === 'CAN_SWITCH_USER'; + } + + public function supportsType(string $subjectType): bool + { + return is_a($subjectType, User::class, true); + } +} \ No newline at end of file diff --git a/src/Security/Voter/LabelProfileVoter.php b/src/Security/Voter/LabelProfileVoter.php index 5a3699a2..47505bf9 100644 --- a/src/Security/Voter/LabelProfileVoter.php +++ b/src/Security/Voter/LabelProfileVoter.php @@ -42,9 +42,14 @@ declare(strict_types=1); namespace App\Security\Voter; use App\Entity\LabelSystem\LabelProfile; -use App\Entity\UserSystem\User; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class LabelProfileVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class LabelProfileVoter extends Voter { protected const MAPPING = [ 'read' => 'read_profiles', @@ -55,21 +60,34 @@ class LabelProfileVoter extends ExtendedVoter 'revert_element' => 'revert_element', ]; - protected function voteOnUser(string $attribute, $subject, User $user): bool + public function __construct(private readonly VoterHelper $helper) + {} + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - return $this->resolver->inherit($user, 'labels', self::MAPPING[$attribute]) ?? false; + return $this->helper->isGranted($token, 'labels', self::MAPPING[$attribute]); } protected function supports($attribute, $subject): bool { - if ($subject instanceof LabelProfile) { + if (is_a($subject, LabelProfile::class, true)) { if (!isset(self::MAPPING[$attribute])) { return false; } - return $this->resolver->isValidOperation('labels', self::MAPPING[$attribute]); + return $this->helper->isValidOperation('labels', self::MAPPING[$attribute]); } return false; } + + public function supportsAttribute(string $attribute): bool + { + return isset(self::MAPPING[$attribute]); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, LabelProfile::class, true); + } } diff --git a/src/Security/Voter/LogEntryVoter.php b/src/Security/Voter/LogEntryVoter.php index b03cd99f..08bc3b70 100644 --- a/src/Security/Voter/LogEntryVoter.php +++ b/src/Security/Voter/LogEntryVoter.php @@ -22,29 +22,56 @@ declare(strict_types=1); namespace App\Security\Voter; +use App\Services\UserSystem\VoterHelper; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\LogSystem\AbstractLogEntry; -use App\Entity\UserSystem\User; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class LogEntryVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class LogEntryVoter extends Voter { - public const ALLOWED_OPS = ['read', 'delete']; + final public const ALLOWED_OPS = ['read', 'show_details', 'delete']; - protected function voteOnUser(string $attribute, $subject, User $user): bool + public function __construct(private readonly Security $security, private readonly VoterHelper $helper) { + } + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + $user = $this->helper->resolveUser($token); + + if (!$subject instanceof AbstractLogEntry) { + throw new \InvalidArgumentException('The subject must be an instance of '.AbstractLogEntry::class); + } + if ('delete' === $attribute) { - return $this->resolver->inherit($user, 'system', 'delete_logs') ?? false; + return $this->helper->isGranted($token, 'system', 'delete_logs'); } if ('read' === $attribute) { //Allow read of the users own log entries if ( $subject->getUser() === $user - && $this->resolver->inherit($user, 'self', 'show_logs') + && $this->helper->isGranted($token, 'self', 'show_logs') ) { return true; } - return $this->resolver->inherit($user, 'system', 'show_logs') ?? false; + return $this->helper->isGranted($token, 'system', 'show_logs'); + } + + if ('show_details' === $attribute) { + //To view details of a element related log entry, the user needs to be able to view the history of this entity type + $targetClass = $subject->getTargetClass(); + if (null !== $targetClass) { + return $this->security->isGranted('show_history', $targetClass); + } + + //In other cases, this behaves like the read permission + return $this->voteOnAttribute('read', $subject, $token); } return false; @@ -53,9 +80,19 @@ class LogEntryVoter extends ExtendedVoter protected function supports($attribute, $subject): bool { if ($subject instanceof AbstractLogEntry) { - return in_array($subject, static::ALLOWED_OPS, true); + return in_array($attribute, static::ALLOWED_OPS, true); } return false; } + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, static::ALLOWED_OPS, true); + } + + public function supportsType(string $subjectType): bool + { + return is_a($subjectType, AbstractLogEntry::class, true); + } } diff --git a/src/Security/Voter/OrderdetailVoter.php b/src/Security/Voter/OrderdetailVoter.php index eaeea11d..20843b9a 100644 --- a/src/Security/Voter/OrderdetailVoter.php +++ b/src/Security/Voter/OrderdetailVoter.php @@ -41,52 +41,41 @@ declare(strict_types=1); namespace App\Security\Voter; +use App\Services\UserSystem\VoterHelper; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Parts\Part; use App\Entity\PriceInformations\Orderdetail; -use App\Entity\UserSystem\User; -use App\Services\UserSystem\PermissionManager; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class OrderdetailVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class OrderdetailVoter extends Voter { - protected Security $security; - - public function __construct(PermissionManager $resolver, EntityManagerInterface $entityManager, Security $security) + public function __construct(private readonly Security $security, private readonly VoterHelper $helper) { - parent::__construct($resolver, $entityManager); - $this->security = $security; } protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { if (! is_a($subject, Orderdetail::class, true)) { throw new \RuntimeException('This voter can only handle Orderdetail objects!'); } - switch ($attribute) { - case 'read': - $operation = 'read'; - break; - case 'edit': //As long as we can edit, we can also edit orderdetails - case 'create': - case 'delete': - $operation = 'edit'; - break; - case 'show_history': - $operation = 'show_history'; - break; - case 'revert_element': - $operation = 'revert_element'; - break; - default: - throw new \RuntimeException('Encountered unknown operation "'.$attribute.'"!'); - } + $operation = match ($attribute) { + 'read' => 'read', + 'edit', 'create', 'delete' => 'edit', + 'show_history' => 'show_history', + 'revert_element' => 'revert_element', + default => throw new \RuntimeException('Encountered unknown operation "'.$attribute.'"!'), + }; //If we have no part associated use the generic part permission - if (is_string($subject) || $subject->getPart() === null) { - return $this->resolver->inherit($user, 'parts', $operation) ?? false; + if (is_string($subject) || !$subject->getPart() instanceof Part) { + return $this->helper->isGranted($token, 'parts', $operation); } //Otherwise vote on the part @@ -101,4 +90,14 @@ class OrderdetailVoter extends ExtendedVoter return false; } + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, self::ALLOWED_PERMS, true); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, Orderdetail::class, true); + } } diff --git a/src/Security/Voter/ParameterVoter.php b/src/Security/Voter/ParameterVoter.php index 525a75b6..8ee2b9f5 100644 --- a/src/Security/Voter/ParameterVoter.php +++ b/src/Security/Voter/ParameterVoter.php @@ -1,4 +1,7 @@ . */ - namespace App\Security\Voter; +use App\Services\UserSystem\VoterHelper; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Base\AbstractDBElement; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; @@ -30,102 +35,101 @@ use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; use App\Entity\Parameters\MeasurementUnitParameter; use App\Entity\Parameters\PartParameter; -use App\Entity\Parameters\StorelocationParameter; +use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; -use App\Entity\UserSystem\User; -use App\Services\UserSystem\PermissionManager; -use Doctrine\ORM\EntityManagerInterface; use RuntimeException; -use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class ParameterVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class ParameterVoter extends Voter { - protected Security $security; + private const ALLOWED_ATTRIBUTES = ['read', 'edit', 'delete', 'create', 'show_history', 'revert_element']; - public function __construct(PermissionManager $resolver, EntityManagerInterface $entityManager, Security $security) + public function __construct(private readonly Security $security, private readonly VoterHelper $helper) { - $this->security = $security; - parent::__construct($resolver, $entityManager); } - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { //return $this->resolver->inherit($user, 'attachments', $attribute) ?? false; - if (!$subject instanceof AbstractParameter) { + if (!is_a($subject, AbstractParameter::class, true)) { return false; } - //If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element - $target_element = $subject->getElement(); - if ($target_element !== null) { - //Depending on the operation delegate either to the attachments element or to the attachment permission + if (is_object($subject)) { + //If the attachment has no element (which should not happen), we deny access, as we can not determine if the user is allowed to access the associated element + $target_element = $subject->getElement(); + if ($target_element instanceof AbstractDBElement) { + $operation = match ($attribute) { + 'read', 'view' => 'read', + 'edit', 'create', 'delete' => 'edit', + 'show_history' => 'show_history', + 'revert_element' => 'revert_element', + default => throw new RuntimeException('Unknown operation: '.$attribute), + }; - - switch ($attribute) { - //We can view the attachment if we can view the element - case 'read': - case 'view': - $operation = 'read'; - break; - //We can edit/create/delete the attachment if we can edit the element - case 'edit': - case 'create': - case 'delete': - $operation = 'edit'; - break; - case 'show_history': - $operation = 'show_history'; - break; - case 'revert_element': - $operation = 'revert_element'; - break; - default: - throw new RuntimeException('Unknown operation: '.$attribute); + return $this->security->isGranted($operation, $target_element); } - - return $this->security->isGranted($operation, $target_element); } - //If we do not have a concrete element, we delegate to the different categories - if ($subject instanceof AttachmentTypeParameter) { + //If we do not have a concrete element (or we just got a string as value), we delegate to the different categories + if (is_a($subject, AttachmentTypeParameter::class, true)) { $param = 'attachment_types'; - } elseif ($subject instanceof CategoryParameter) { + } elseif (is_a($subject, CategoryParameter::class, true)) { $param = 'categories'; - } elseif ($subject instanceof CurrencyParameter) { + } elseif (is_a($subject, CurrencyParameter::class, true)) { $param = 'currencies'; - } elseif ($subject instanceof ProjectParameter) { + } elseif (is_a($subject, ProjectParameter::class, true)) { $param = 'projects'; - } elseif ($subject instanceof FootprintParameter) { + } elseif (is_a($subject, FootprintParameter::class, true)) { $param = 'footprints'; - } elseif ($subject instanceof GroupParameter) { + } elseif (is_a($subject, GroupParameter::class, true)) { $param = 'groups'; - } elseif ($subject instanceof ManufacturerParameter) { + } elseif (is_a($subject, ManufacturerParameter::class, true)) { $param = 'manufacturers'; - } elseif ($subject instanceof MeasurementUnitParameter) { + } elseif (is_a($subject, MeasurementUnitParameter::class, true)) { $param = 'measurement_units'; - } elseif ($subject instanceof PartParameter) { + } elseif (is_a($subject, PartParameter::class, true)) { $param = 'parts'; - } elseif ($subject instanceof StorelocationParameter) { + } elseif (is_a($subject, StorageLocationParameter::class, true)) { $param = 'storelocations'; - } elseif ($subject instanceof SupplierParameter) { + } elseif (is_a($subject, SupplierParameter::class, true)) { $param = 'suppliers'; - } else { - throw new RuntimeException('Encountered unknown Parameter type: ' . get_class($subject)); + } elseif ($subject === AbstractParameter::class) { + //If the subject was deleted, we can not determine the type properly, so we just use the parts permission + $param = 'parts'; + } + else { + throw new RuntimeException('Encountered unknown Parameter type: ' . (is_object($subject) ? $subject::class : $subject)); } - return $this->resolver->inherit($user, $param, $attribute) ?? false; + return $this->helper->isGranted($token, $param, $attribute); } - protected function supports(string $attribute, $subject) + protected function supports(string $attribute, $subject): bool { if (is_a($subject, AbstractParameter::class, true)) { //These are the allowed attributes - return in_array($attribute, ['read', 'edit', 'delete', 'create', 'show_history', 'revert_element'], true); + return in_array($attribute, self::ALLOWED_ATTRIBUTES, true); } //Allow class name as subject return false; } -} \ No newline at end of file + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, self::ALLOWED_ATTRIBUTES, true); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, AbstractParameter::class, true); + } + +} diff --git a/src/Security/Voter/PartAssociationVoter.php b/src/Security/Voter/PartAssociationVoter.php new file mode 100644 index 00000000..7678b67a --- /dev/null +++ b/src/Security/Voter/PartAssociationVoter.php @@ -0,0 +1,105 @@ +. + */ + +declare(strict_types=1); + +/** + * 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 . + */ + +namespace App\Security\Voter; + +use App\Entity\Parts\PartAssociation; +use App\Services\UserSystem\VoterHelper; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Parts\Part; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; + +/** + * This voter handles permissions for part associations. + * The permissions are inherited from the part. + * @phpstan-extends Voter + */ +final class PartAssociationVoter extends Voter +{ + public function __construct(private readonly Security $security, private readonly VoterHelper $helper) + { + } + + protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + if (!is_string($subject) && !$subject instanceof PartAssociation) { + throw new \RuntimeException('Invalid subject type!'); + } + + $operation = match ($attribute) { + 'read' => 'read', + 'edit', 'create', 'delete' => 'edit', + 'show_history' => 'show_history', + 'revert_element' => 'revert_element', + default => throw new \RuntimeException('Encountered unknown operation "'.$attribute.'"!'), + }; + + //If we have no part associated use the generic part permission + if (is_string($subject) || !$subject->getOwner() instanceof Part) { + return $this->helper->isGranted($token, 'parts', $operation); + } + + //Otherwise vote on the part + return $this->security->isGranted($attribute, $subject->getOwner()); + } + + protected function supports($attribute, $subject): bool + { + if (is_a($subject, PartAssociation::class, true)) { + return in_array($attribute, self::ALLOWED_PERMS, true); + } + + return false; + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, PartAssociation::class, true); + } + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, self::ALLOWED_PERMS, true); + } +} diff --git a/src/Security/Voter/PartLotVoter.php b/src/Security/Voter/PartLotVoter.php index 0c70e629..a64473c8 100644 --- a/src/Security/Voter/PartLotVoter.php +++ b/src/Security/Voter/PartLotVoter.php @@ -41,33 +41,31 @@ declare(strict_types=1); namespace App\Security\Voter; +use App\Services\UserSystem\VoterHelper; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\UserSystem\User; -use App\Services\UserSystem\PermissionManager; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class PartLotVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class PartLotVoter extends Voter { - protected Security $security; - - public function __construct(PermissionManager $resolver, EntityManagerInterface $entityManager, Security $security) + public function __construct(private readonly Security $security, private readonly VoterHelper $helper) { - parent::__construct($resolver, $entityManager); - $this->security = $security; } protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move']; - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - if (! is_a($subject, PartLot::class, true)) { - throw new \RuntimeException('This voter can only handle PartLot objects!'); - } + $user = $this->helper->resolveUser($token); - if (in_array($attribute, ['withdraw', 'add', 'move'])) + if (in_array($attribute, ['withdraw', 'add', 'move'], true)) { - $base_permission = $this->resolver->inherit($user, 'parts_stock', $attribute) ?? false; + $base_permission = $this->helper->isGranted($token, 'parts_stock', $attribute); $lot_permission = true; //If the lot has an owner, we need to check if the user is the owner of the lot to be allowed to withdraw it. @@ -78,28 +76,17 @@ class PartLotVoter extends ExtendedVoter return $base_permission && $lot_permission; } - switch ($attribute) { - case 'read': - $operation = 'read'; - break; - case 'edit': //As long as we can edit, we can also edit orderdetails - case 'create': - case 'delete': - $operation = 'edit'; - break; - case 'show_history': - $operation = 'show_history'; - break; - case 'revert_element': - $operation = 'revert_element'; - break; - default: - throw new \RuntimeException('Encountered unknown operation "'.$attribute.'"!'); - } + $operation = match ($attribute) { + 'read' => 'read', + 'edit', 'create', 'delete' => 'edit', + 'show_history' => 'show_history', + 'revert_element' => 'revert_element', + default => throw new \RuntimeException('Encountered unknown operation "'.$attribute.'"!'), + }; //If we have no part associated use the generic part permission - if (is_string($subject) || $subject->getPart() === null) { - return $this->resolver->inherit($user, 'parts', $operation) ?? false; + if (is_string($subject) || !$subject->getPart() instanceof Part) { + return $this->helper->isGranted($token, 'parts', $operation); } //Otherwise vote on the part @@ -114,4 +101,14 @@ class PartLotVoter extends ExtendedVoter return false; } + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, self::ALLOWED_PERMS, true); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, PartLot::class, true); + } } diff --git a/src/Security/Voter/PartVoter.php b/src/Security/Voter/PartVoter.php index fb1e3a38..ef70b6ce 100644 --- a/src/Security/Voter/PartVoter.php +++ b/src/Security/Voter/PartVoter.php @@ -23,30 +23,48 @@ declare(strict_types=1); namespace App\Security\Voter; use App\Entity\Parts\Part; -use App\Entity\UserSystem\User; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** * A Voter that votes on Part entities. * * See parts permissions for valid operations. + * + * @phpstan-extends Voter */ -class PartVoter extends ExtendedVoter +final class PartVoter extends Voter { - public const READ = 'read'; + final public const READ = 'read'; + + public function __construct(private readonly VoterHelper $helper) + { + } protected function supports($attribute, $subject): bool { if (is_a($subject, Part::class, true)) { - return $this->resolver->isValidOperation('parts', $attribute); + return $this->helper->isValidOperation('parts', $attribute); } //Allow class name as subject return false; } - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { //Null concealing operator means, that no - return $this->resolver->inherit($user, 'parts', $attribute) ?? false; + return $this->helper->isGranted($token, 'parts', $attribute); + } + + public function supportsAttribute(string $attribute): bool + { + return $this->helper->isValidOperation('parts', $attribute); + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, Part::class, true); } } diff --git a/src/Security/Voter/PermissionVoter.php b/src/Security/Voter/PermissionVoter.php index 018c4f92..c6ec1b3d 100644 --- a/src/Security/Voter/PermissionVoter.php +++ b/src/Security/Voter/PermissionVoter.php @@ -22,27 +22,35 @@ declare(strict_types=1); namespace App\Security\Voter; -use App\Entity\UserSystem\User; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** * This voter allows you to directly check permissions from the permission structure, without passing an object. * This use the syntax like "@permission.op" * However you should use the "normal" object based voters if possible, because they are needed for a future ACL system. + * @phpstan-extends Voter */ -class PermissionVoter extends ExtendedVoter +final class PermissionVoter extends Voter { - /** - * Similar to voteOnAttribute, but checking for the anonymous user is already done. - * The current user (or the anonymous user) is passed by $user. - * - * @param string $attribute - */ - protected function voteOnUser(string $attribute, $subject, User $user): bool + public function __construct(private readonly VoterHelper $helper) + { + + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $attribute = ltrim($attribute, '@'); [$perm, $op] = explode('.', $attribute); - return $this->resolver->inherit($user, $perm, $op) ?? false; + return $this->helper->isGranted($token, $perm, $op); + } + + public function supportsAttribute(string $attribute): bool + { + //Check if the attribute has the form '@permission.operation' + return preg_match('#^@\\w+\\.\\w+$#', $attribute) === 1; } /** @@ -53,14 +61,14 @@ class PermissionVoter extends ExtendedVoter * * @return bool True if the attribute and subject are supported, false otherwise */ - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { //Check if the attribute has the form @permission.operation if (preg_match('#^@\\w+\\.\\w+$#', $attribute)) { $attribute = ltrim($attribute, '@'); [$perm, $op] = explode('.', $attribute); - $valid = $this->resolver->isValidOperation($perm, $op); + $valid = $this->helper->isValidOperation($perm, $op); //if an invalid operation is encountered, throw an exception so the developer knows it if(!$valid) { diff --git a/src/Security/Voter/PricedetailVoter.php b/src/Security/Voter/PricedetailVoter.php index eb4a81aa..681b73b7 100644 --- a/src/Security/Voter/PricedetailVoter.php +++ b/src/Security/Voter/PricedetailVoter.php @@ -41,52 +41,38 @@ declare(strict_types=1); namespace App\Security\Voter; +use App\Services\UserSystem\VoterHelper; +use Symfony\Bundle\SecurityBundle\Security; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\Parts\Part; use App\Entity\PriceInformations\Pricedetail; -use App\Entity\UserSystem\User; -use App\Services\UserSystem\PermissionManager; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class PricedetailVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class PricedetailVoter extends Voter { - protected Security $security; - - public function __construct(PermissionManager $resolver, EntityManagerInterface $entityManager, Security $security) + public function __construct(private readonly Security $security, private readonly VoterHelper $helper) { - parent::__construct($resolver, $entityManager); - $this->security = $security; } protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element']; - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { - if (!is_a($subject, Pricedetail::class, true)) { - throw new \RuntimeException('This voter can only handle Pricedetails objects!'); - } - - switch ($attribute) { - case 'read': - $operation = 'read'; - break; - case 'edit': //As long as we can edit, we can also edit orderdetails - case 'create': - case 'delete': - $operation = 'edit'; - break; - case 'show_history': - $operation = 'show_history'; - break; - case 'revert_element': - $operation = 'revert_element'; - break; - default: - throw new \RuntimeException('Encountered unknown operation "'.$attribute.'"!'); - } + $operation = match ($attribute) { + 'read' => 'read', + 'edit', 'create', 'delete' => 'edit', + 'show_history' => 'show_history', + 'revert_element' => 'revert_element', + default => throw new \RuntimeException('Encountered unknown operation "'.$attribute.'"!'), + }; //If we have no part associated use the generic part permission - if (is_string($subject) || $subject->getOrderdetail() === null || $subject->getOrderdetail()->getPart() === null) { - return $this->resolver->inherit($user, 'parts', $operation) ?? false; + if (is_string($subject) || !$subject->getOrderdetail() instanceof Orderdetail || !$subject->getOrderdetail()->getPart() instanceof Part) { + return $this->helper->isGranted($token, 'parts', $operation); } //Otherwise vote on the part @@ -101,4 +87,14 @@ class PricedetailVoter extends ExtendedVoter return false; } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, Pricedetail::class, true); + } + + public function supportsAttribute(string $attribute): bool + { + return in_array($attribute, self::ALLOWED_PERMS, true); + } } diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index df88e113..2417b796 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -28,14 +28,19 @@ use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; -use App\Entity\UserSystem\User; -use function get_class; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; + use function is_object; -class StructureVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class StructureVoter extends Voter { protected const OBJ_PERM_MAP = [ AttachmentType::class => 'attachment_types', @@ -43,12 +48,16 @@ class StructureVoter extends ExtendedVoter Project::class => 'projects', Footprint::class => 'footprints', Manufacturer::class => 'manufacturers', - Storelocation::class => 'storelocations', + StorageLocation::class => 'storelocations', Supplier::class => 'suppliers', Currency::class => 'currencies', MeasurementUnit::class => 'measurement_units', ]; + public function __construct(private readonly VoterHelper $helper) + { + } + /** * Determines if the attribute and subject are supported by this voter. * @@ -57,31 +66,32 @@ class StructureVoter extends ExtendedVoter * * @return bool True if the attribute and subject are supported, false otherwise */ - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { if (is_object($subject) || is_string($subject)) { $permission_name = $this->instanceToPermissionName($subject); //If permission name is null, then the subject is not supported - return (null !== $permission_name) && $this->resolver->isValidOperation($permission_name, $attribute); + return (null !== $permission_name) && $this->helper->isValidOperation($permission_name, $attribute); } return false; } + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || $this->instanceToPermissionName($subjectType) !== null; + } + /** - * Maps a instance type to the permission name. + * Maps an instance type to the permission name. * - * @param object|string $subject The subject for which the permission name should be generated + * @param object|string $subject The subject for which the permission name should be generated * * @return string|null the name of the permission for the subject's type or null, if the subject is not supported */ - protected function instanceToPermissionName($subject): ?string + protected function instanceToPermissionName(object|string $subject): ?string { - if (!is_string($subject)) { - $class_name = get_class($subject); - } else { - $class_name = $subject; - } + $class_name = is_string($subject) ? $subject : $subject::class; //If it is existing in index, we can skip the loop if (isset(static::OBJ_PERM_MAP[$class_name])) { @@ -103,10 +113,10 @@ class StructureVoter extends ExtendedVoter * * @param string $attribute */ - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { $permission_name = $this->instanceToPermissionName($subject); //Just resolve the permission - return $this->resolver->inherit($user, $permission_name, $attribute) ?? false; + return $this->helper->isGranted($token, $permission_name, $attribute); } } diff --git a/src/Security/Voter/UserVoter.php b/src/Security/Voter/UserVoter.php index a311e4db..b41c1a40 100644 --- a/src/Security/Voter/UserVoter.php +++ b/src/Security/Voter/UserVoter.php @@ -23,10 +23,22 @@ declare(strict_types=1); namespace App\Security\Voter; use App\Entity\UserSystem\User; +use App\Services\UserSystem\PermissionManager; +use App\Services\UserSystem\VoterHelper; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; + use function in_array; -class UserVoter extends ExtendedVoter +/** + * @phpstan-extends Voter + */ +final class UserVoter extends Voter { + public function __construct(private readonly VoterHelper $helper, private readonly PermissionManager $resolver) + { + } + /** * Determines if the attribute and subject are supported by this voter. * @@ -35,7 +47,7 @@ class UserVoter extends ExtendedVoter * * @return bool True if the attribute and subject are supported, false otherwise */ - protected function supports(string $attribute, $subject): bool + protected function supports(string $attribute, mixed $subject): bool { if (is_a($subject, User::class, true)) { return in_array($attribute, @@ -44,21 +56,33 @@ class UserVoter extends ExtendedVoter $this->resolver->listOperationsForPermission('self'), ['info'] ), - false + true ); } return false; } + public function supportsAttribute(string $attribute): bool + { + return $this->helper->isValidOperation('users', $attribute) || $this->helper->isValidOperation('self', $attribute) || $attribute === 'info'; + } + + public function supportsType(string $subjectType): bool + { + return $subjectType === 'string' || is_a($subjectType, User::class, true); + } + /** * Similar to voteOnAttribute, but checking for the anonymous user is already done. * The current user (or the anonymous user) is passed by $user. * * @param string $attribute */ - protected function voteOnUser(string $attribute, $subject, User $user): bool + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { + $user = $this->helper->resolveUser($token); + if ($attribute === 'info') { //Every logged-in user (non-anonymous) can see the info pages of other users if (!$user->isAnonymousUser()) { @@ -71,18 +95,18 @@ class UserVoter extends ExtendedVoter //Check if the checked user is the user itself if (($subject instanceof User) && $subject->getID() === $user->getID() && - $this->resolver->isValidOperation('self', $attribute)) { + $this->helper->isValidOperation('self', $attribute)) { //Then we also need to check the self permission - $tmp = $this->resolver->inherit($user, 'self', $attribute) ?? false; + $tmp = $this->helper->isGranted($token, 'self', $attribute); //But if the self value is not allowed then use just the user value: if ($tmp) { return $tmp; } } - //Else just check users permission: - if ($this->resolver->isValidOperation('users', $attribute)) { - return $this->resolver->inherit($user, 'users', $attribute) ?? false; + //Else just check user permission: + if ($this->helper->isValidOperation('users', $attribute)) { + return $this->helper->isGranted($token, 'users', $attribute); } return false; diff --git a/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php b/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php new file mode 100644 index 00000000..8283dbbe --- /dev/null +++ b/src/Serializer/APIPlatform/DetermineTypeFromElementIRIDenormalizer.php @@ -0,0 +1,124 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Serializer\APIPlatform; + +use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; +use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use App\Entity\Attachments\Attachment; +use App\Entity\Parameters\AbstractParameter; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +/** + * The purpose of this normalizer is to automatically add the _type discriminator field for the Attachment and AbstractParameter classes + * based on the element IRI. + * So that for a request pointing for a part element, an PartAttachment is automatically created. + * This highly improves UX and is the expected behavior. + */ +class DetermineTypeFromElementIRIDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface +{ + private const SUPPORTED_CLASSES = [ + Attachment::class, + AbstractParameter::class + ]; + + use DenormalizerAwareTrait; + + private const ALREADY_CALLED = self::class . '::ALREADY_CALLED'; + + public function __construct(private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) + { + } + + /** + * This functions add the _type discriminator to the input array if necessary automatically from the given element IRI. + * @param array $input + * @param Operation $operation + * @return array + * @throws ResourceClassNotFoundException + */ + private function addTypeDiscriminatorIfNecessary(array $input, Operation $operation): array + { + + //We only want to modify POST requests + if (!$operation instanceof Post) { + return $input; + } + + + //Ignore if the _type variable is already set + if (isset($input['_type'])) { + return $input; + } + + if (!isset($input['element']) || !is_string($input['element'])) { + return $input; + } + + //Retrieve the element + $element = $this->iriConverter->getResourceFromIri($input['element']); + + //Retrieve the short name of the operation + $type = $this->resourceMetadataCollectionFactory->create($element::class)->getOperation()->getShortName(); + $input['_type'] = $type; + + return $input; + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): object + { + //If we are on API platform, we want to add the type discriminator if necessary + if (!isset($data['_type']) && isset($context['operation'])) { + $data = $this->addTypeDiscriminatorIfNecessary($data, $context['operation']); + } + + $context[self::ALREADY_CALLED] = true; + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + //Only denormalize if the _type discriminator is not set and the class is supported and we not have already called this function + return !isset($context[self::ALREADY_CALLED]) + && is_array($data) + && !isset($data['_type']) + && in_array($type, self::SUPPORTED_CLASSES, true); + } + + public function getSupportedTypes(?string $format): array + { + $tmp = []; + + foreach (self::SUPPORTED_CLASSES as $class) { + $tmp[$class] = false; + } + + return $tmp; + } +} \ No newline at end of file diff --git a/src/Serializer/APIPlatform/OverrideClassDenormalizer.php b/src/Serializer/APIPlatform/OverrideClassDenormalizer.php new file mode 100644 index 00000000..c8155abc --- /dev/null +++ b/src/Serializer/APIPlatform/OverrideClassDenormalizer.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Serializer\APIPlatform; + +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +/** + * The idea of this denormalizer is to allow to override the type of the object created using a certain context key. + * This is required to resolve the issue of the serializer/API platform not correctly being able to determine the type + * of the "element" properties of the Attachment and Parameter subclasses. + */ +class OverrideClassDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface +{ + use DenormalizerAwareTrait; + + public const CONTEXT_KEY = '__override_type__'; + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + //Deserialize the data with the overridden type + $overrideType = $context[self::CONTEXT_KEY]; + unset($context[self::CONTEXT_KEY]); + + return $this->denormalizer->denormalize($data, $overrideType, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return isset($context[self::CONTEXT_KEY]); + } + + public function getSupportedTypes(?string $format): array + { + return [ + '*' => false, + ]; + } +} \ No newline at end of file diff --git a/src/Serializer/APIPlatform/SkippableItemNormalizer.php b/src/Serializer/APIPlatform/SkippableItemNormalizer.php new file mode 100644 index 00000000..5568c4cb --- /dev/null +++ b/src/Serializer/APIPlatform/SkippableItemNormalizer.php @@ -0,0 +1,90 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Serializer\APIPlatform; + +use ApiPlatform\Serializer\ItemNormalizer; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * This class decorates API Platform's ItemNormalizer to allow skipping the normalization process by setting the + * DISABLE_ITEM_NORMALIZER context key to true. This is useful for all kind of serialization operations, where the API + * Platform subsystem should not be used. + */ +#[AsDecorator("api_platform.serializer.normalizer.item")] +class SkippableItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface +{ + + public const DISABLE_ITEM_NORMALIZER = 'DISABLE_ITEM_NORMALIZER'; + + public function __construct(private readonly ItemNormalizer $inner) + { + + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + return $this->inner->denormalize($data, $type, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + if ($context[self::DISABLE_ITEM_NORMALIZER] ?? false) { + return false; + } + + return $this->inner->supportsDenormalization($data, $type, $format, $context); + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): float|int|bool|\ArrayObject|array|string|null + { + return $this->inner->normalize($object, $format, $context); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + if ($context[self::DISABLE_ITEM_NORMALIZER] ?? false) { + return false; + } + + return $this->inner->supportsNormalization($data, $format, $context); + } + + public function setSerializer(SerializerInterface $serializer): void + { + $this->inner->setSerializer($serializer); + } + + public function getSupportedTypes(?string $format): array + { + //Don't cache results, as we check for the context + return [ + 'object' => false + ]; + } +} \ No newline at end of file diff --git a/src/Serializer/AttachmentNormalizer.php b/src/Serializer/AttachmentNormalizer.php new file mode 100644 index 00000000..bd791d04 --- /dev/null +++ b/src/Serializer/AttachmentNormalizer.php @@ -0,0 +1,84 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Serializer; + +use App\Entity\Attachments\Attachment; +use App\Services\Attachments\AttachmentURLGenerator; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class AttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterface +{ + + use NormalizerAwareTrait; + + private const ALREADY_CALLED = 'ATTACHMENT_NORMALIZER_ALREADY_CALLED'; + + public function __construct( + private readonly AttachmentURLGenerator $attachmentURLGenerator, + ) + { + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array|null + { + if (!$object instanceof Attachment) { + throw new \InvalidArgumentException('This normalizer only supports Attachment objects!'); + } + + //Prevent loops, by adding a flag to the context + $context[self::ALREADY_CALLED] = true; + + $data = $this->normalizer->normalize($object, $format, $context); + $data['internal_path'] = $this->attachmentURLGenerator->getInternalViewURL($object); + + //Add thumbnail url if the attachment is a picture + $data['thumbnail_url'] = $object->isPicture() ? $this->attachmentURLGenerator->getThumbnailURL($object) : null; + + //For backwards compatibility reasons + //Deprecated: Use internal_path and external_path instead + $data['media_url'] = $data['internal_path'] ?? $object->getExternalPath(); + + return $data; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + // avoid recursion: only call once per object + if (isset($context[self::ALREADY_CALLED])) { + return false; + } + + return $data instanceof Attachment; + } + + public function getSupportedTypes(?string $format): array + { + return [ + //We depend on the context to determine if we should normalize or not + Attachment::class => false, + ]; + } +} \ No newline at end of file diff --git a/src/Serializer/BigNumberNormalizer.php b/src/Serializer/BigNumberNormalizer.php index fc352cf1..10cedfa5 100644 --- a/src/Serializer/BigNumberNormalizer.php +++ b/src/Serializer/BigNumberNormalizer.php @@ -1,4 +1,7 @@ . */ - namespace App\Serializer; use Brick\Math\BigDecimal; use Brick\Math\BigNumber; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -class BigNumberNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +/** + * @see \App\Tests\Serializer\BigNumberNormalizerTest + */ +class BigNumberNormalizer implements NormalizerInterface, DenormalizerInterface { - public function supportsNormalization($data, string $format = null): bool + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { return $data instanceof BigNumber; } - public function normalize($object, string $format = null, array $context = []): string + public function normalize($object, ?string $format = null, array $context = []): string { if (!$object instanceof BigNumber) { throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!'); @@ -43,8 +47,29 @@ class BigNumberNormalizer implements NormalizerInterface, CacheableSupportsMetho return (string) $object; } - public function hasCacheableSupportsMethod(): bool + /** + * @return bool[] + */ + public function getSupportedTypes(?string $format): array { - return true; + return [ + BigNumber::class => true, + BigDecimal::class => true, + ]; } -} \ No newline at end of file + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): BigNumber|null + { + if (!is_a($type, BigNumber::class, true)) { + throw new \InvalidArgumentException('This normalizer only supports BigNumber objects!'); + } + + return $type::of($data); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + //data must be a string or a number (int, float, etc.) and the type must be BigNumber or BigDecimal + return (is_string($data) || is_numeric($data)) && (is_subclass_of($type, BigNumber::class)); + } +} diff --git a/src/Serializer/PartNormalizer.php b/src/Serializer/PartNormalizer.php index ef844acf..9050abfc 100644 --- a/src/Serializer/PartNormalizer.php +++ b/src/Serializer/PartNormalizer.php @@ -1,4 +1,7 @@ . */ - namespace App\Serializer; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use Brick\Math\BigDecimal; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; -class PartNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface +/** + * @see \App\Tests\Serializer\PartNormalizerTest + */ +class PartNormalizer implements NormalizerInterface, DenormalizerInterface, NormalizerAwareInterface, DenormalizerAwareInterface { + + use NormalizerAwareTrait; + use DenormalizerAwareTrait; + + private const ALREADY_CALLED = 'PART_NORMALIZER_ALREADY_CALLED'; + private const DENORMALIZE_KEY_MAPPING = [ 'notes' => 'comment', 'quantity' => 'instock', @@ -44,26 +57,31 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach 'storage_location' => 'storelocation', ]; - private ObjectNormalizer $normalizer; - private StructuralElementFromNameDenormalizer $locationDenormalizer; - - public function __construct(ObjectNormalizer $normalizer, StructuralElementFromNameDenormalizer $locationDenormalizer) + public function __construct( + private readonly StructuralElementFromNameDenormalizer $locationDenormalizer, + ) { - $this->normalizer = $normalizer; - $this->locationDenormalizer = $locationDenormalizer; } - public function supportsNormalization($data, string $format = null): bool + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { - return $data instanceof Part; + //We only remove the type field for CSV export + return !isset($context[self::ALREADY_CALLED]) && $format === 'csv' && $data instanceof Part ; } - public function normalize($object, string $format = null, array $context = []) + public function normalize($object, ?string $format = null, array $context = []): array { if (!$object instanceof Part) { throw new \InvalidArgumentException('This normalizer only supports Part objects!'); } + $context[self::ALREADY_CALLED] = true; + + //Prevent exception in API Platform + if ($object->getID() === null) { + $context['iri'] = 'not-persisted'; + } + $data = $this->normalizer->normalize($object, $format, $context); //Remove type field for CSV export @@ -71,14 +89,19 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach unset($data['type']); } - $data['total_instock'] = $object->getAmountSum(); - return $data; } - public function supportsDenormalization($data, string $type, string $format = null): bool + public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool { - return is_array($data) && is_a($type, Part::class, true); + //Only denormalize if we are doing a file import operation + if (!($context['partdb_import'] ?? false)) { + return false; + } + + //Only make the denormalizer available on import operations + return !isset($context[self::ALREADY_CALLED]) + && is_array($data) && is_a($type, Part::class, true); } private function normalizeKeys(array &$data): array @@ -94,7 +117,7 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach return $data; } - public function denormalize($data, string $type, string $format = null, array $context = []) + public function denormalize($data, string $type, ?string $format = null, array $context = []): ?Part { $this->normalizeKeys($data); @@ -114,18 +137,20 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach $data['minamount'] = 0.0; } - $object = $this->normalizer->denormalize($data, $type, $format, $context); + $context[self::ALREADY_CALLED] = true; + + $object = $this->denormalizer->denormalize($data, $type, $format, $context); if (!$object instanceof Part) { throw new \InvalidArgumentException('This normalizer only supports Part objects!'); } - if ((isset($data['instock']) && trim($data['instock']) !== "") || (isset($data['storelocation']) && trim($data['storelocation']) !== "")) { + if ((isset($data['instock']) && trim((string) $data['instock']) !== "") || (isset($data['storelocation']) && trim((string) $data['storelocation']) !== "")) { $partLot = new PartLot(); if (isset($data['instock']) && $data['instock'] !== "") { //Replace comma with dot - $instock = (float) str_replace(',', '.', $data['instock']); + $instock = (float) str_replace(',', '.', (string) $data['instock']); $partLot->setAmount($instock); } else { @@ -133,7 +158,7 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach } if (isset($data['storelocation']) && $data['storelocation'] !== "") { - $location = $this->locationDenormalizer->denormalize($data['storelocation'], Storelocation::class, $format, $context); + $location = $this->locationDenormalizer->denormalize($data['storelocation'], StorageLocation::class, $format, $context); $partLot->setStorageLocation($location); } @@ -143,7 +168,7 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach if (isset($data['supplier']) && $data['supplier'] !== "") { $supplier = $this->locationDenormalizer->denormalize($data['supplier'], Supplier::class, $format, $context); - if ($supplier) { + if ($supplier !== null) { $orderdetail = new Orderdetail(); $orderdetail->setSupplier($supplier); @@ -157,7 +182,7 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach $pricedetail = new Pricedetail(); $pricedetail->setMinDiscountQuantity(1); $pricedetail->setPriceRelatedQuantity(1); - $price = BigDecimal::of(str_replace(',', '.', $data['price'])); + $price = BigDecimal::of(str_replace(',', '.', (string) $data['price'])); $pricedetail->setPrice($price); $orderdetail->addPricedetail($pricedetail); @@ -168,9 +193,14 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Cach return $object; } - public function hasCacheableSupportsMethod(): bool + /** + * @return bool[] + */ + public function getSupportedTypes(?string $format): array { //Must be false, because we rely on is_array($data) in supportsDenormalization() - return false; + return [ + Part::class => false, + ]; } -} \ No newline at end of file +} diff --git a/src/Serializer/StructuralElementDenormalizer.php b/src/Serializer/StructuralElementDenormalizer.php index bd832f1f..d9b03ae7 100644 --- a/src/Serializer/StructuralElementDenormalizer.php +++ b/src/Serializer/StructuralElementDenormalizer.php @@ -1,4 +1,7 @@ . */ - namespace App\Serializer; use App\Entity\Base\AbstractStructuralDBElement; use App\Repository\StructuralDBElementRepository; +use App\Serializer\APIPlatform\SkippableItemNormalizer; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; -class StructuralElementDenormalizer implements ContextAwareDenormalizerInterface, CacheableSupportsMethodInterface +/** + * @see \App\Tests\Serializer\StructuralElementDenormalizerTest + */ +class StructuralElementDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface { - private DenormalizerInterface $normalizer; - private EntityManagerInterface $entityManager; + use DenormalizerAwareTrait; + + private const ALREADY_CALLED = 'STRUCTURAL_DENORMALIZER_ALREADY_CALLED'; private array $object_cache = []; - public function __construct(ObjectNormalizer $normalizer, EntityManagerInterface $entityManager) + public function __construct( + private readonly EntityManagerInterface $entityManager) { - $this->normalizer = $normalizer; - $this->entityManager = $entityManager; } - public function supportsDenormalization($data, string $type, string $format = null, array $context = []) + public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool { + //Only denormalize if we are doing a file import operation + if (!($context['partdb_import'] ?? false)) { + return false; + } + + //If we already handled this object, skip it + if (isset($context[self::ALREADY_CALLED]) + && is_array($context[self::ALREADY_CALLED]) + && in_array($data, $context[self::ALREADY_CALLED], true)) { + return false; + } + return is_array($data) && is_subclass_of($type, AbstractStructuralDBElement::class) - //Only denormalize if we are doing an file import operation - && in_array('import', $context['groups'] ?? []); + //Only denormalize if we are doing a file import operation + && in_array('import', $context['groups'] ?? [], true); } - public function denormalize($data, string $type, string $format = null, array $context = []) + /** + * @template T of AbstractStructuralDBElement + * @param $data + * @phpstan-param class-string $type + * @param string|null $format + * @param array $context + * @return AbstractStructuralDBElement|null + * @phpstan-return T|null + */ + public function denormalize($data, string $type, ?string $format = null, array $context = []): ?AbstractStructuralDBElement { + //Do not use API Platform's denormalizer + $context[SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER] = true; + + if (!isset($context[self::ALREADY_CALLED])) { + $context[self::ALREADY_CALLED] = []; + } + + $context[self::ALREADY_CALLED][] = $data; + + /** @var AbstractStructuralDBElement $deserialized_entity */ - $deserialized_entity = $this->normalizer->denormalize($data, $type, $format, $context); + $deserialized_entity = $this->denormalizer->denormalize($data, $type, $format, $context); //Check if we already have the entity in the database (via path) - /** @var StructuralDBElementRepository $repo */ + /** @var StructuralDBElementRepository $repo */ $repo = $this->entityManager->getRepository($type); $path = $deserialized_entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW); $db_elements = $repo->getEntityByPath($path, AbstractStructuralDBElement::PATH_DELIMITER_ARROW); - if ($db_elements) { + if ($db_elements !== []) { //We already have the entity in the database, so we can return it return end($db_elements); } @@ -70,7 +107,7 @@ class StructuralElementDenormalizer implements ContextAwareDenormalizerInterface //Check if we have created the entity in this request before (so we don't create multiple entities for the same path) //Entities get saved in the cache by type and path - //We use a different cache for this then the objects created by a string value (saved in repo). However that should not be a problem + //We use a different cache for this then the objects created by a string value (saved in repo). However, that should not be a problem //unless the user data has mixed structure between json data and a string path if (isset($this->object_cache[$type][$path])) { return $this->object_cache[$type][$path]; @@ -85,8 +122,11 @@ class StructuralElementDenormalizer implements ContextAwareDenormalizerInterface return $deserialized_entity; } - public function hasCacheableSupportsMethod(): bool + public function getSupportedTypes(): array { - return false; + //Must be false, because we use in_array in supportsDenormalization + return [ + AbstractStructuralDBElement::class => false, + ]; } -} \ No newline at end of file +} diff --git a/src/Serializer/StructuralElementFromNameDenormalizer.php b/src/Serializer/StructuralElementFromNameDenormalizer.php index ac035946..1d7255b7 100644 --- a/src/Serializer/StructuralElementFromNameDenormalizer.php +++ b/src/Serializer/StructuralElementFromNameDenormalizer.php @@ -1,4 +1,7 @@ . */ - namespace App\Serializer; use App\Entity\Base\AbstractStructuralDBElement; -use App\Form\Type\StructuralEntityType; use App\Repository\StructuralDBElementRepository; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -class StructuralElementFromNameDenormalizer implements DenormalizerInterface, CacheableSupportsMethodInterface +/** + * @see \App\Tests\Serializer\StructuralElementFromNameDenormalizerTest + */ +class StructuralElementFromNameDenormalizer implements DenormalizerInterface { - private EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $em) + public function __construct(private readonly EntityManagerInterface $em) { - $this->em = $em; } - public function supportsDenormalization($data, string $type, string $format = null) + public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool { + //Only denormalize if we are doing a file import operation + if (!($context['partdb_import'] ?? false)) { + return false; + } + return is_string($data) && is_subclass_of($type, AbstractStructuralDBElement::class); } - public function denormalize($data, string $type, string $format = null, array $context = []) + /** + * @template T of AbstractStructuralDBElement + * @phpstan-param class-string $type + * @phpstan-return T|null + */ + public function denormalize($data, string $type, ?string $format = null, array $context = []): AbstractStructuralDBElement|null { //Retrieve the repository for the given type - /** @var StructuralDBElementRepository $repo */ + /** @var StructuralDBElementRepository $repo */ $repo = $this->em->getRepository($type); $path_delimiter = $context['path_delimiter'] ?? '->'; @@ -56,22 +65,27 @@ class StructuralElementFromNameDenormalizer implements DenormalizerInterface, Ca foreach ($elements as $element) { $this->em->persist($element); } - if (empty($elements)) { + if ($elements === []) { return null; } return end($elements); } $elements = $repo->getEntityByPath($data, $path_delimiter); - if (empty($elements)) { + if ($elements === []) { return null; } return end($elements); } - public function hasCacheableSupportsMethod(): bool + /** + * @return bool[] + */ + public function getSupportedTypes(?string $format): array { - //Must be false, because we do a is_string check on data in supportsDenormalization - return false; + //Cachable value Must be false, because we do an is_string check on data in supportsDenormalization + return [ + AbstractStructuralDBElement::class => false + ]; } -} \ No newline at end of file +} diff --git a/src/Serializer/StructuralElementNormalizer.php b/src/Serializer/StructuralElementNormalizer.php index c412fabd..e73f69be 100644 --- a/src/Serializer/StructuralElementNormalizer.php +++ b/src/Serializer/StructuralElementNormalizer.php @@ -1,4 +1,7 @@ . */ - namespace App\Serializer; use App\Entity\Base\AbstractStructuralDBElement; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; -use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; -class StructuralElementNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +/** + * @see \App\Tests\Serializer\StructuralElementNormalizerTest + */ +class StructuralElementNormalizer implements NormalizerInterface { - private NormalizerInterface $normalizer; - - public function __construct(ObjectNormalizer $normalizer) + public function __construct( + #[Autowire(service: ObjectNormalizer::class)]private readonly NormalizerInterface $normalizer + ) { - $this->normalizer = $normalizer; } - public function supportsNormalization($data, string $format = null): bool + public function supportsNormalization($data, ?string $format = null, array $context = []): bool { + //Only normalize if we are doing a file export operation + if (!($context['partdb_export'] ?? false)) { + return false; + } + return $data instanceof AbstractStructuralDBElement; } - public function normalize($object, string $format = null, array $context = []) + public function normalize($object, ?string $format = null, array $context = []): mixed { if (!$object instanceof AbstractStructuralDBElement) { throw new \InvalidArgumentException('This normalizer only supports AbstractStructural objects!'); @@ -49,6 +56,11 @@ class StructuralElementNormalizer implements NormalizerInterface, CacheableSuppo $data = $this->normalizer->normalize($object, $format, $context); + //If the data is not an array, we can't do anything with it + if (!is_array($data)) { + return $data; + } + //Remove type field for CSV export if ($format === 'csv') { unset($data['type']); @@ -59,8 +71,13 @@ class StructuralElementNormalizer implements NormalizerInterface, CacheableSuppo return $data; } - public function hasCacheableSupportsMethod(): bool + /** + * @return bool[] + */ + public function getSupportedTypes(?string $format): array { - return true; + return [ + AbstractStructuralDBElement::class => true, + ]; } -} \ No newline at end of file +} diff --git a/src/Services/Attachments/AttachmentManager.php b/src/Services/Attachments/AttachmentManager.php index bddea30b..1075141b 100644 --- a/src/Services/Attachments/AttachmentManager.php +++ b/src/Services/Attachments/AttachmentManager.php @@ -35,11 +35,8 @@ use function strlen; */ class AttachmentManager { - protected AttachmentPathResolver $pathResolver; - - public function __construct(AttachmentPathResolver $pathResolver) + public function __construct(protected AttachmentPathResolver $pathResolver) { - $this->pathResolver = $pathResolver; } /** @@ -47,35 +44,31 @@ class AttachmentManager * * @param Attachment $attachment The attachment for which the file should be generated * - * @return SplFileInfo|null The fileinfo for the attachment file. Null, if the attachment is external or has + * @return SplFileInfo|null The fileinfo for the attachment file. Null, if the attachment is only external or has * invalid file. */ public function attachmentToFile(Attachment $attachment): ?SplFileInfo { - if ($attachment->isExternal() || !$this->isFileExisting($attachment)) { + if (!$this->isInternalFileExisting($attachment)) { return null; } - return new SplFileInfo($this->toAbsoluteFilePath($attachment)); + return new SplFileInfo($this->toAbsoluteInternalFilePath($attachment)); } /** - * Returns the absolute filepath of the attachment. Null is returned, if the attachment is externally saved, - * or is not existing. + * Returns the absolute filepath to the internal copy of the attachment. Null is returned, if the attachment is + * only externally saved, or is not existing. * * @param Attachment $attachment The attachment for which the filepath should be determined */ - public function toAbsoluteFilePath(Attachment $attachment): ?string + public function toAbsoluteInternalFilePath(Attachment $attachment): ?string { - if (empty($attachment->getPath())) { + if (!$attachment->hasInternal()){ return null; } - if ($attachment->isExternal()) { - return null; - } - - $path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); + $path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); //realpath does not work with null as argument if (null === $path) { @@ -92,8 +85,8 @@ class AttachmentManager } /** - * Checks if the file in this attachement is existing. This works for files on the HDD, and for URLs - * (it's not checked if the ressource behind the URL is really existing, so for every external attachment true is returned). + * Checks if the file in this attachment is existing. This works for files on the HDD, and for URLs + * (it's not checked if the resource behind the URL is really existing, so for every external attachment true is returned). * * @param Attachment $attachment The attachment for which the existence should be checked * @@ -101,15 +94,23 @@ class AttachmentManager */ public function isFileExisting(Attachment $attachment): bool { - if (empty($attachment->getPath())) { - return false; - } - - if ($attachment->isExternal()) { + if($attachment->hasExternal()){ return true; } + return $this->isInternalFileExisting($attachment); + } - $absolute_path = $this->toAbsoluteFilePath($attachment); + /** + * Checks if the internal file in this attachment is existing. Returns false if the attachment doesn't have an + * internal file. + * + * @param Attachment $attachment The attachment for which the existence should be checked + * + * @return bool true if the file is existing + */ + public function isInternalFileExisting(Attachment $attachment): bool + { + $absolute_path = $this->toAbsoluteInternalFilePath($attachment); if (null === $absolute_path) { return false; @@ -120,27 +121,23 @@ class AttachmentManager /** * Returns the filesize of the attachments in bytes. - * For external attachments or not existing attachments, null is returned. + * For purely external attachments or inexistent attachments, null is returned. * * @param Attachment $attachment the filesize for which the filesize should be calculated */ public function getFileSize(Attachment $attachment): ?int { - if ($attachment->isExternal()) { + if (!$this->isInternalFileExisting($attachment)) { return null; } - if (!$this->isFileExisting($attachment)) { - return null; - } - - $tmp = filesize($this->toAbsoluteFilePath($attachment)); + $tmp = filesize($this->toAbsoluteInternalFilePath($attachment)); return false !== $tmp ? $tmp : null; } /** - * Returns a human readable version of the attachment file size. + * Returns a human-readable version of the attachment file size. * For external attachments, null is returned. * * @param int $decimals The number of decimals numbers that should be printed diff --git a/src/Services/Attachments/AttachmentPathResolver.php b/src/Services/Attachments/AttachmentPathResolver.php index 9617024e..1b52c89b 100644 --- a/src/Services/Attachments/AttachmentPathResolver.php +++ b/src/Services/Attachments/AttachmentPathResolver.php @@ -22,24 +22,22 @@ declare(strict_types=1); namespace App\Services\Attachments; -use FontLib\Table\Type\maxp; use const DIRECTORY_SEPARATOR; use Symfony\Component\Filesystem\Filesystem; /** * This service converts the relative pathes for attachments saved in database (like %MEDIA%/img.jpg) to real pathes * an vice versa. + * @see \App\Tests\Services\Attachments\AttachmentPathResolverTest */ class AttachmentPathResolver { - protected string $project_dir; - - protected ?string $media_path; - protected ?string $footprints_path; + protected string $media_path; + protected string $footprints_path; protected ?string $models_path; protected ?string $secure_path; - protected array $placeholders; + protected array $placeholders = ['%MEDIA%', '%BASE%/data/media', '%FOOTPRINTS%', '%FOOTPRINTS_3D%', '%SECURE%']; protected array $pathes; protected array $placeholders_regex; protected array $pathes_regex; @@ -53,18 +51,13 @@ class AttachmentPathResolver * Set to null if this ressource should be disabled. * @param string|null $models_path set to null if this ressource should be disabled */ - public function __construct(string $project_dir, string $media_path, string $secure_path, ?string $footprints_path, ?string $models_path) + public function __construct(protected string $project_dir, string $media_path, string $secure_path, ?string $footprints_path, ?string $models_path) { - $this->project_dir = $project_dir; - - //Determine the path for our ressources - $this->media_path = $this->parameterToAbsolutePath($media_path); - $this->footprints_path = $this->parameterToAbsolutePath($footprints_path); + //Determine the path for our resources + $this->media_path = $this->parameterToAbsolutePath($media_path) ?? throw new \InvalidArgumentException('The media path must be set and valid!'); + $this->secure_path = $this->parameterToAbsolutePath($secure_path) ?? throw new \InvalidArgumentException('The secure path must be set and valid!'); + $this->footprints_path = $this->parameterToAbsolutePath($footprints_path) ; $this->models_path = $this->parameterToAbsolutePath($models_path); - $this->secure_path = $this->parameterToAbsolutePath($secure_path); - - //Here we define the valid placeholders and their replacement values - $this->placeholders = ['%MEDIA%', '%BASE%/data/media', '%FOOTPRINTS%', '%FOOTPRINTS_3D%', '%SECURE%']; $this->pathes = [$this->media_path, $this->media_path, $this->footprints_path, $this->models_path, $this->secure_path]; //Remove all disabled placeholders @@ -122,19 +115,23 @@ class AttachmentPathResolver * Converts an relative placeholder filepath (with %MEDIA% or older %BASE%) to an absolute filepath on disk. * The directory separator is always /. Relative pathes are not realy possible (.. is striped). * - * @param string $placeholder_path the filepath with placeholder for which the real path should be determined + * @param string|null $placeholder_path the filepath with placeholder for which the real path should be determined * * @return string|null The absolute real path of the file, or null if the placeholder path is invalid */ - public function placeholderToRealPath(string $placeholder_path): ?string + public function placeholderToRealPath(?string $placeholder_path): ?string { + if (null === $placeholder_path) { + return null; + } + //The new attachments use %MEDIA% as placeholders, which is the directory set in media_directory //Older path entries are given via %BASE% which was the project root $count = 0; //When path is a footprint we have to first run the string through our lecagy german mapping functions - if (strpos($placeholder_path, '%FOOTPRINTS%') !== false) { + if (str_contains($placeholder_path, '%FOOTPRINTS%')) { $placeholder_path = $this->convertOldFootprintPath($placeholder_path); } @@ -146,12 +143,12 @@ class AttachmentPathResolver } //If we have now have a placeholder left, the string is invalid: - if (preg_match('#%\w+%#', $placeholder_path)) { + if (preg_match('#%\w+%#', (string) $placeholder_path)) { return null; } //Path is invalid if path is directory traversal - if (false !== strpos($placeholder_path, '..')) { + if (str_contains((string) $placeholder_path, '..')) { return null; } @@ -190,7 +187,7 @@ class AttachmentPathResolver } //If the new string does not begin with a placeholder, it is invalid - if (!preg_match('#^%\w+%#', $real_path)) { + if (!preg_match('#^%\w+%#', (string) $real_path)) { return null; } @@ -198,7 +195,7 @@ class AttachmentPathResolver } /** - * The path where uploaded attachments is stored. + * The path where uploaded attachments is stored. * * @return string the absolute path to the media folder */ @@ -208,8 +205,8 @@ class AttachmentPathResolver } /** - * The path where secured attachments are stored. Must not be located in public/ folder, so it can only be accessed - * via the attachment controller. + * The path where secured attachments are stored. Must not be located in public/ folder, so it can only be accessed + * via the attachment controller. * * @return string the absolute path to the secure path */ @@ -221,7 +218,7 @@ class AttachmentPathResolver /** * The string where the builtin footprints are stored. * - * @return string|null The absolute path to the footprints folder. Null if built footprints were disabled. + * @return string|null The absolute path to the footprints' folder. Null if built footprints were disabled. */ public function getFootprintsPath(): ?string { @@ -231,7 +228,7 @@ class AttachmentPathResolver /** * The string where the builtin 3D models are stored. * - * @return string|null The absolute path to the models folder. Null if builtin models were disabled. + * @return string|null The absolute path to the models' folder. Null if builtin models were disabled. */ public function getModelsPath(): ?string { @@ -248,7 +245,7 @@ class AttachmentPathResolver $ret = []; foreach ($array as $item) { - $item = str_replace(['\\'], ['/'], $item); + $item = str_replace(['\\'], ['/'], (string) $item); $ret[] = '/'.preg_quote($item, '/').'/'; } diff --git a/src/Services/Attachments/AttachmentReverseSearch.php b/src/Services/Attachments/AttachmentReverseSearch.php index 34a6b929..e05192d0 100644 --- a/src/Services/Attachments/AttachmentReverseSearch.php +++ b/src/Services/Attachments/AttachmentReverseSearch.php @@ -30,22 +30,12 @@ use SplFileInfo; use Symfony\Component\Filesystem\Filesystem; /** - * This service provides functions to find attachments via an reverse search based on a file. + * This service provides functions to find attachments via a reverse search based on a file. */ class AttachmentReverseSearch { - protected EntityManagerInterface $em; - protected AttachmentPathResolver $pathResolver; - protected CacheManager $cacheManager; - protected AttachmentURLGenerator $attachmentURLGenerator; - - public function __construct(EntityManagerInterface $em, AttachmentPathResolver $pathResolver, - CacheManager $cacheManager, AttachmentURLGenerator $attachmentURLGenerator) + public function __construct(protected EntityManagerInterface $em, protected AttachmentPathResolver $pathResolver, protected CacheManager $cacheManager, protected AttachmentURLGenerator $attachmentURLGenerator) { - $this->em = $em; - $this->pathResolver = $pathResolver; - $this->cacheManager = $cacheManager; - $this->attachmentURLGenerator = $attachmentURLGenerator; } /** @@ -53,7 +43,7 @@ class AttachmentReverseSearch * * @param SplFileInfo $file The file for which is searched * - * @return Attachment[] an list of attachments that use the given file + * @return Attachment[] a list of attachments that use the given file */ public function findAttachmentsByFile(SplFileInfo $file): array { @@ -65,7 +55,7 @@ class AttachmentReverseSearch $repo = $this->em->getRepository(Attachment::class); return $repo->findBy([ - 'path' => [$relative_path_new, $relative_path_old], + 'internal_path' => [$relative_path_new, $relative_path_old], ]); } @@ -75,11 +65,11 @@ class AttachmentReverseSearch * @param SplFileInfo $file The file that should be removed * @param int $threshold the threshold used, to determine if a file should be deleted or not * - * @return bool True, if the file was delete. False if not. + * @return bool True, if the file was deleted. False if not. */ public function deleteIfNotUsed(SplFileInfo $file, int $threshold = 1): bool { - /* When the file is used more then $threshold times, don't delete it */ + /* When the file is used more than $threshold times, don't delete it */ if (count($this->findAttachmentsByFile($file)) > $threshold) { return false; } diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index 65e5c7db..89457cea 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -26,6 +26,7 @@ use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; +use App\Entity\Attachments\AttachmentUpload; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; use App\Entity\Attachments\LabelAttachment; @@ -35,18 +36,18 @@ use App\Entity\Attachments\GroupAttachment; use App\Entity\Attachments\ManufacturerAttachment; use App\Entity\Attachments\MeasurementUnitAttachment; use App\Entity\Attachments\PartAttachment; -use App\Entity\Attachments\StorelocationAttachment; +use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; use App\Exceptions\AttachmentDownloadException; +use Hshn\Base64EncodedFile\HttpFoundation\File\Base64EncodedFile; +use Hshn\Base64EncodedFile\HttpFoundation\File\UploadedBase64EncodedFile; use const DIRECTORY_SEPARATOR; -use function get_class; use InvalidArgumentException; use RuntimeException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Mime\MimeTypesInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -55,16 +56,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; */ class AttachmentSubmitHandler { - protected AttachmentPathResolver $pathResolver; protected array $folder_mapping; - protected bool $allow_attachments_downloads; - protected HttpClientInterface $httpClient; - protected MimeTypesInterface $mimeTypes; - protected FileTypeFilterTools $filterTools; - /** - * @var string The user configured maximum upload size. This is a string like "10M" or "1G" and will be converted to - */ - protected string $max_upload_size; private ?int $max_upload_size_bytes = null; @@ -72,18 +64,13 @@ class AttachmentSubmitHandler 'asp', 'cgi', 'py', 'pl', 'exe', 'aspx', 'js', 'mjs', 'jsp', 'css', 'jar', 'html', 'htm', 'shtm', 'shtml', 'htaccess', 'htpasswd', '']; - public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads, - HttpClientInterface $httpClient, MimeTypesInterface $mimeTypes, - FileTypeFilterTools $filterTools, string $max_upload_size) + public function __construct(protected AttachmentPathResolver $pathResolver, protected bool $allow_attachments_downloads, + protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes, protected readonly SVGSanitizer $SVGSanitizer, + protected FileTypeFilterTools $filterTools, /** + * @var string The user configured maximum upload size. This is a string like "10M" or "1G" and will be converted to + */ + protected string $max_upload_size) { - $this->pathResolver = $pathResolver; - $this->allow_attachments_downloads = $allow_attachments_downloads; - $this->httpClient = $httpClient; - $this->mimeTypes = $mimeTypes; - $this->max_upload_size = $max_upload_size; - - $this->filterTools = $filterTools; - //The mapping used to determine which folder will be used for an attachment type $this->folder_mapping = [ PartAttachment::class => 'part', @@ -95,7 +82,7 @@ class AttachmentSubmitHandler GroupAttachment::class => 'group', ManufacturerAttachment::class => 'manufacturer', MeasurementUnitAttachment::class => 'measurement_unit', - StorelocationAttachment::class => 'storelocation', + StorageLocationAttachment::class => 'storelocation', SupplierAttachment::class => 'supplier', UserAttachment::class => 'user', LabelAttachment::class => 'label_profile', @@ -108,8 +95,8 @@ class AttachmentSubmitHandler */ public function isValidFileExtension(AttachmentType $attachment_type, UploadedFile $uploadedFile): bool { - //Only validate if the attachment type has specified an filetype filter: - if (empty($attachment_type->getFiletypeFilter())) { + //Only validate if the attachment type has specified a filetype filter: + if ($attachment_type->getFiletypeFilter() === '') { return true; } @@ -121,10 +108,10 @@ class AttachmentSubmitHandler /** * Generates a filename for the given attachment and extension. - * The filename contains a random id, so every time this function is called you get an unique name. + * The filename contains a random id, so every time this function is called you get a unique name. * * @param Attachment $attachment The attachment that should be used for generating an attachment - * @param string $extension The extension that the new file should have (must only contain chars allowed in pathes) + * @param string $extension The extension that the new file should have (must only contain chars allowed in paths) * * @return string the new filename */ @@ -136,7 +123,7 @@ class AttachmentSubmitHandler $extension ); - //Use the (sanatized) attachment name as an filename part + //Use the (sanatized) attachment name as a filename part $safeName = transliterator_transliterate( 'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $attachment->getName() @@ -155,84 +142,118 @@ class AttachmentSubmitHandler */ public function generateAttachmentPath(Attachment $attachment, bool $secure_upload = false): string { - if ($secure_upload) { - $base_path = $this->pathResolver->getSecurePath(); + $base_path = $secure_upload ? $this->pathResolver->getSecurePath() : $this->pathResolver->getMediaPath(); + + //Ensure the attachment has an assigned element + if (!$attachment->getElement() instanceof AttachmentContainingDBElement) { + throw new InvalidArgumentException('The given attachment is not assigned to an element! An element is needed to generate a path!'); + } + + //Determine the folder prefix for the given attachment class: + $prefix = null; + //Check if we can use the class name dire + if (isset($this->folder_mapping[$attachment::class])) { + $prefix = $this->folder_mapping[$attachment::class]; } else { - $base_path = $this->pathResolver->getMediaPath(); + //If not, check for instance of: + foreach ($this->folder_mapping as $class => $folder) { + if ($attachment instanceof $class) { + $prefix = $folder; + break; + } + } } //Ensure the given attachment class is known to mapping - if (!isset($this->folder_mapping[get_class($attachment)])) { - throw new InvalidArgumentException('The given attachment class is not known! The passed class was: '.get_class($attachment)); - } - //Ensure the attachment has an assigned element - if (null === $attachment->getElement()) { - throw new InvalidArgumentException('The given attachment is not assigned to an element! An element is needed to generate a path!'); + if (!$prefix) { + throw new InvalidArgumentException('The given attachment class is not known! The passed class was: '.$attachment::class); } //Build path return $base_path.DIRECTORY_SEPARATOR //Base path - .$this->folder_mapping[get_class($attachment)].DIRECTORY_SEPARATOR.$attachment->getElement()->getID(); + .$prefix.DIRECTORY_SEPARATOR.$attachment->getElement()->getID(); } /** - * Handle the submit of an attachment form. + * Handle submission of an attachment form. * This function will move the uploaded file or download the URL file to server, if needed. * * @param Attachment $attachment the attachment that should be used for handling - * @param UploadedFile|null $file If given, that file will be moved to the right location - * @param array $options The options to use with the upload. Here you can specify that an URL should be downloaded, - * or an file should be moved to a secure location. + * @param AttachmentUpload|null $upload The upload options DTO. If it is null, it will be tried to get from the attachment option * * @return Attachment The attachment with the new filename (same instance as passed $attachment) */ - public function handleFormSubmit(Attachment $attachment, ?UploadedFile $file, array $options = []): Attachment + public function handleUpload(Attachment $attachment, ?AttachmentUpload $upload): Attachment { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); + if ($upload === null) { + $upload = $attachment->getUpload(); + if ($upload === null) { + throw new InvalidArgumentException('No upload options given and no upload options set in attachment!'); + } + } + + $file = $upload->file; + + //If no file was uploaded, but we have base64 encoded data, create a file from it + if (!$file && $upload->data !== null) { + $file = new UploadedBase64EncodedFile(new Base64EncodedFile($upload->data), $upload->filename ?? 'base64'); + } + + //By default we assume a public upload + $secure_attachment = $upload->private ?? false; //When a file is given then upload it, otherwise check if we need to download the URL - if ($file) { - $this->upload($attachment, $file, $options); - } elseif ($options['download_url'] && $attachment->isExternal()) { - $this->downloadURL($attachment, $options); + if ($file instanceof UploadedFile) { + + $this->upload($attachment, $file, $secure_attachment); + } elseif ($upload->downloadUrl && $attachment->hasExternal()) { + $this->downloadURL($attachment, $secure_attachment); } //Move the attachment files to secure location (and back) if needed - $this->moveFile($attachment, $options['secure_attachment']); + $this->moveFile($attachment, $secure_attachment); + + //Sanitize the SVG if needed + $this->sanitizeSVGAttachment($attachment); //Rename blacklisted (unsecure) files to a better extension $this->renameBlacklistedExtensions($attachment); - //Check if we should assign this attachment to master picture - //this is only possible if the attachment is new (not yet persisted to DB) - if ($options['become_preview_if_empty'] && null === $attachment->getID() && $attachment->isPicture()) { - $element = $attachment->getElement(); - if ($element instanceof AttachmentContainingDBElement && null === $element->getMasterPictureAttachment()) { + //Set / Unset the master picture attachment / preview image + $element = $attachment->getElement(); + if ($element instanceof AttachmentContainingDBElement) { + //Make this attachment the master picture if needed and this was requested + if ($upload->becomePreviewIfEmpty + && $element->getMasterPictureAttachment() === null //Element must not have an preview image yet + && null === $attachment->getID() //Attachment must be null + && $attachment->isPicture() //Attachment must be a picture + ) { $element->setMasterPictureAttachment($attachment); } + + //If this attachment is the master picture, but is not a picture anymore, dont use it as master picture anymore + if ($element->getMasterPictureAttachment() === $attachment && !$attachment->isPicture()) { + $element->setMasterPictureAttachment(null); + } } return $attachment; } /** - * Rename attachments with an unsafe extension (meaning files which would be runned by a to a safe one. - * @param Attachment $attachment - * @return Attachment + * Rename attachments with an unsafe extension (meaning files which would be run by a to a safe one). */ protected function renameBlacklistedExtensions(Attachment $attachment): Attachment { //We can not do anything on builtins or external ressources - if ($attachment->isBuiltIn() || $attachment->isExternal()) { + if ($attachment->isBuiltIn() || !$attachment->hasInternal()) { return $attachment; } //Determine the old filepath - $old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); - if (empty($old_path) || !file_exists($old_path)) { + $old_path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); + if ($old_path === null || $old_path === '' || !file_exists($old_path)) { return $attachment; } $filename = basename($old_path); @@ -240,45 +261,34 @@ class AttachmentSubmitHandler //Check if the extension is blacklisted and replace the file extension with txt if needed - if(in_array($ext, self::BLACKLISTED_EXTENSIONS)) { + if(in_array($ext, self::BLACKLISTED_EXTENSIONS, true)) { $new_path = $this->generateAttachmentPath($attachment, $attachment->isSecure()) - .DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'txt'); + .DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'txt'); //Move file to new directory $fs = new Filesystem(); $fs->rename($old_path, $new_path); //Update the attachment - $attachment->setPath($this->pathResolver->realPathToPlaceholder($new_path)); + $attachment->setInternalPath($this->pathResolver->realPathToPlaceholder($new_path)); } return $attachment; } - protected function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - //If no preview image was set yet, the new uploaded file will become the preview image - 'become_preview_if_empty' => true, - //When an URL is given download the URL - 'download_url' => false, - 'secure_attachment' => false, - ]); - } - /** - * Move the given attachment to secure location (or back to public folder) if needed. + * Move the internal copy of the given attachment to a secure location (or back to public folder) if needed. * * @param Attachment $attachment the attachment for which the file should be moved * @param bool $secure_location this value determines, if the attachment is moved to the secure or public folder * - * @return Attachment The attachment with the updated filepath + * @return Attachment The attachment with the updated internal filepath */ protected function moveFile(Attachment $attachment, bool $secure_location): Attachment { //We can not do anything on builtins or external ressources - if ($attachment->isBuiltIn() || $attachment->isExternal()) { + if ($attachment->isBuiltIn() || !$attachment->hasInternal()) { return $attachment; } @@ -288,12 +298,12 @@ class AttachmentSubmitHandler } //Determine the old filepath - $old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); + $old_path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); if (!file_exists($old_path)) { return $attachment; } - $filename = basename($old_path); + $filename = basename((string) $old_path); //If the basename is not one of the new unique on, we have to save the old filename if (!preg_match('#\w+-\w{13}\.#', $filename)) { //Save filename to attachment field @@ -312,7 +322,7 @@ class AttachmentSubmitHandler //Save info to attachment entity $new_path = $this->pathResolver->realPathToPlaceholder($new_path); - $attachment->setPath($new_path); + $attachment->setInternalPath($new_path); return $attachment; } @@ -320,27 +330,46 @@ class AttachmentSubmitHandler /** * Download the URL set in the attachment and save it on the server. * - * @param array $options The options from the handleFormSubmit function + * @param bool $secureAttachment True if the file should be moved to the secure attachment storage * - * @return Attachment The attachment with the new filepath + * @return Attachment The attachment with the downloaded copy */ - protected function downloadURL(Attachment $attachment, array $options): Attachment + protected function downloadURL(Attachment $attachment, bool $secureAttachment): Attachment { //Check if we are allowed to download files if (!$this->allow_attachments_downloads) { throw new RuntimeException('Download of attachments is not allowed!'); } - $url = $attachment->getURL(); + $url = $attachment->getExternalPath(); $fs = new Filesystem(); - $attachment_folder = $this->generateAttachmentPath($attachment, $options['secure_attachment']); + $attachment_folder = $this->generateAttachmentPath($attachment, $secureAttachment); $tmp_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'tmp'); try { - $response = $this->httpClient->request('GET', $url, [ + $opts = [ 'buffer' => false, - ]); + //Use user-agent and other headers to make the server think we are a browser + 'headers' => [ + "sec-ch-ua" => "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"", + "sec-ch-ua-mobile" => "?0", + "sec-ch-ua-platform" => "\"Windows\"", + "user-agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "sec-fetch-site" => "none", + "sec-fetch-mode" => "navigate", + ], + + ]; + $response = $this->httpClient->request('GET', $url, $opts); + //Digikey wants TLSv1.3, so try again with that if we get a 403 + if ($response->getStatusCode() === 403) { + $opts['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT; + $response = $this->httpClient->request('GET', $url, $opts); + } + # if you have these changes and downloads still fail, check if it's due to an unknown certificate. Curl by + # default uses the systems ca store and that doesn't contain all the intermediate certificates needed to + # verify the leafs if (200 !== $response->getStatusCode()) { throw new AttachmentDownloadException('Status code: '.$response->getStatusCode()); @@ -357,27 +386,29 @@ class AttachmentSubmitHandler //File download should be finished here, so determine the new filename and extension $headers = $response->getHeaders(); - //Try to determine an filename + //Try to determine a filename $filename = ''; - //If an content disposition header was set try to extract the filename out of it + //If a content disposition header was set try to extract the filename out of it if (isset($headers['content-disposition'])) { $tmp = []; - preg_match('/[^;\\n=]*=([\'\"])*(.*)(?(1)\1|)/', $headers['content-disposition'][0], $tmp); - $filename = $tmp[2]; + //Only use the filename if the regex matches properly + if (preg_match('/[^;\\n=]*=([\'\"])*(.*)(?(1)\1|)/', $headers['content-disposition'][0], $tmp)) { + $filename = $tmp[2]; + } } - //If we dont know filename yet, try to determine it out of url + //If we don't know filename yet, try to determine it out of url if ('' === $filename) { - $filename = basename(parse_url($url, PHP_URL_PATH)); + $filename = basename(parse_url((string) $url, PHP_URL_PATH)); } //Set original file $attachment->setFilename($filename); - //Check if we have a extension given + //Check if we have an extension given $pathinfo = pathinfo($filename); - if (!empty($pathinfo['extension'])) { + if (isset($pathinfo['extension']) && $pathinfo['extension'] !== '') { $new_ext = $pathinfo['extension']; } else { //Otherwise we have to guess the extension for the new file, based on its content $new_ext = $this->mimeTypes->getExtensions($this->mimeTypes->guessMimeType($tmp_path))[0] ?? 'tmp'; @@ -390,8 +421,8 @@ class AttachmentSubmitHandler //Make our file path relative to %BASE% $new_path = $this->pathResolver->realPathToPlaceholder($new_path); //Save the path to the attachment - $attachment->setPath($new_path); - } catch (TransportExceptionInterface $transportExceptionInterface) { + $attachment->setInternalPath($new_path); + } catch (TransportExceptionInterface) { throw new AttachmentDownloadException('Transport error!'); } @@ -403,22 +434,24 @@ class AttachmentSubmitHandler * * @param Attachment $attachment The attachment in which the file should be saved * @param UploadedFile $file The file which was uploaded - * @param array $options The options from the handleFormSubmit function + * @param bool $secureAttachment True if the file should be moved to the secure attachment storage * * @return Attachment The attachment with the new filepath */ - protected function upload(Attachment $attachment, UploadedFile $file, array $options): Attachment + protected function upload(Attachment $attachment, UploadedFile $file, bool $secureAttachment): Attachment { - //Move our temporay attachment to its final location + //Move our temporary attachment to its final location $file_path = $file->move( - $this->generateAttachmentPath($attachment, $options['secure_attachment']), + $this->generateAttachmentPath($attachment, $secureAttachment), $this->generateAttachmentFilename($attachment, $file->getClientOriginalExtension()) )->getRealPath(); //Make our file path relative to %BASE% $file_path = $this->pathResolver->realPathToPlaceholder($file_path); //Save the path to the attachment - $attachment->setPath($file_path); + $attachment->setInternalPath($file_path); + //reset any external paths the attachment might have had + $attachment->setExternalPath(null); //And save original filename $attachment->setFilename($file->getClientOriginalName()); @@ -428,8 +461,6 @@ class AttachmentSubmitHandler /** * Parses the given file size string and returns the size in bytes. * Taken from https://github.com/symfony/symfony/blob/6.2/src/Symfony/Component/Validator/Constraints/File.php - * @param string $maxSize - * @return int */ private function parseFileSizeString(string $maxSize): int { @@ -441,13 +472,15 @@ class AttachmentSubmitHandler 'g' => 1000 * 1000 * 1000, 'gi' => 1 << 30, ]; - if (ctype_digit((string) $maxSize)) { + if (ctype_digit($maxSize)) { return (int) $maxSize; - } elseif (preg_match('/^(\d++)('.implode('|', array_keys($factors)).')$/i', $maxSize, $matches)) { - return (((int) $matches[1]) * $factors[strtolower($matches[2])]); - } else { - throw new RuntimeException(sprintf('"%s" is not a valid maximum size.', $maxSize)); } + + if (preg_match('/^(\d++)('.implode('|', array_keys($factors)).')$/i', $maxSize, $matches)) { + return (((int) $matches[1]) * $factors[strtolower($matches[2])]); + } + + throw new RuntimeException(sprintf('"%s" is not a valid maximum size.', $maxSize)); } /* @@ -468,4 +501,32 @@ class AttachmentSubmitHandler return $this->max_upload_size_bytes; } + + /** + * Sanitizes the given SVG file, if the attachment is an internal SVG file. + * @param Attachment $attachment + * @return Attachment + */ + public function sanitizeSVGAttachment(Attachment $attachment): Attachment + { + //We can not do anything on builtins or external ressources + if ($attachment->isBuiltIn() || !$attachment->hasInternal()) { + return $attachment; + } + + //Resolve the path to the file + $path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); + + //Check if the file exists + if (!file_exists($path)) { + return $attachment; + } + + //Check if the file is an SVG + if ($attachment->getExtension() === "svg") { + $this->SVGSanitizer->sanitizeFile($path); + } + + return $attachment; + } } diff --git a/src/Services/Attachments/AttachmentURLGenerator.php b/src/Services/Attachments/AttachmentURLGenerator.php index 1aa37904..c22cefe4 100644 --- a/src/Services/Attachments/AttachmentURLGenerator.php +++ b/src/Services/Attachments/AttachmentURLGenerator.php @@ -22,47 +22,32 @@ declare(strict_types=1); namespace App\Services\Attachments; +use Imagine\Exception\RuntimeException; use App\Entity\Attachments\Attachment; use InvalidArgumentException; use Liip\ImagineBundle\Imagine\Cache\CacheManager; -use Liip\ImagineBundle\Service\FilterService; use Psr\Log\LoggerInterface; -use RuntimeException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\UrlHelper; -use Symfony\Component\Routing\Generator\UrlGenerator; use function strlen; use Symfony\Component\Asset\Packages; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +/** + * @see \App\Tests\Services\Attachments\AttachmentURLGeneratorTest + */ class AttachmentURLGenerator { - protected Packages $assets; protected string $public_path; - protected AttachmentPathResolver $pathResolver; - protected UrlGeneratorInterface $urlGenerator; - protected AttachmentManager $attachmentHelper; - protected CacheManager $thumbnailManager; - protected LoggerInterface $logger; - - public function __construct(Packages $assets, AttachmentPathResolver $pathResolver, - UrlGeneratorInterface $urlGenerator, AttachmentManager $attachmentHelper, - CacheManager $thumbnailManager, LoggerInterface $logger) + public function __construct(protected Packages $assets, protected AttachmentPathResolver $pathResolver, + protected UrlGeneratorInterface $urlGenerator, protected AttachmentManager $attachmentHelper, + protected CacheManager $thumbnailManager, protected LoggerInterface $logger) { - $this->assets = $assets; - $this->pathResolver = $pathResolver; - $this->urlGenerator = $urlGenerator; - $this->attachmentHelper = $attachmentHelper; - $this->thumbnailManager = $thumbnailManager; - $this->logger = $logger; - //Determine a normalized path to the public folder (assets are relative to this folder) $this->public_path = $this->pathResolver->parameterToAbsolutePath('public'); } /** - * Converts the absolute file path to a version relative to the public folder, that can be passed to asset + * Converts the absolute file path to a version relative to the public folder, that can be passed to the * Asset Component functions. * * @param string $absolute_path the absolute path that should be converted @@ -82,8 +67,8 @@ class AttachmentURLGenerator $public_path = $this->public_path; } - //Our absolute path must begin with public path or we can not use it for asset pathes. - if (0 !== strpos($absolute_path, $public_path)) { + //Our absolute path must begin with public path, or we can not use it for asset pathes. + if (!str_starts_with($absolute_path, $public_path)) { return null; } @@ -92,7 +77,7 @@ class AttachmentURLGenerator } /** - * Converts a placeholder path to a path to a image path. + * Converts a placeholder path to a path to an image path. * * @param string $placeholder_path the placeholder path that should be converted */ @@ -107,9 +92,9 @@ class AttachmentURLGenerator * Returns a URL under which the attachment file can be viewed. * @return string|null The URL or null if the attachment file is not existing */ - public function getViewURL(Attachment $attachment): ?string + public function getInternalViewURL(Attachment $attachment): ?string { - $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); + $absolute_path = $this->attachmentHelper->toAbsoluteInternalFilePath($attachment); if (null === $absolute_path) { return null; } @@ -125,7 +110,8 @@ class AttachmentURLGenerator } /** - * Returns a URL to an thumbnail of the attachment file. + * Returns a URL to a thumbnail of the attachment file. + * For external files the original URL is returned. * @return string|null The URL or null if the attachment file is not existing */ public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string @@ -134,11 +120,14 @@ class AttachmentURLGenerator throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!'); } - if ($attachment->isExternal() && !empty($attachment->getURL())) { - return $attachment->getURL(); + if (!$attachment->hasInternal()){ + if($attachment->hasExternal()) { + return $attachment->getExternalPath(); + } + return null; } - $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); + $absolute_path = $this->attachmentHelper->toAbsoluteInternalFilePath($attachment); if (null === $absolute_path) { return null; } @@ -150,7 +139,10 @@ class AttachmentURLGenerator } //GD can not work with SVG, so serve it directly... - if ('svg' === $attachment->getExtension()) { + //We can not use getExtension here, because it uses the original filename and not the real extension + //Instead we use the logic, which is also used to determine if the attachment is a picture + $extension = pathinfo(parse_url($attachment->getInternalPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION); + if ('svg' === $extension) { return $this->assets->getUrl($asset_path); } @@ -159,8 +151,8 @@ class AttachmentURLGenerator $tmp = $this->thumbnailManager->getBrowserPath($asset_path, $filter_name, [], null, UrlGeneratorInterface::NETWORK_PATH); //So we remove the schema manually return preg_replace('/^https?:/', '', $tmp); - } catch (\Imagine\Exception\RuntimeException $e) { - //If the filter fails, we can not serve the thumbnail and fall back to the original image and log an warning + } catch (RuntimeException $e) { + //If the filter fails, we can not serve the thumbnail and fall back to the original image and log a warning $this->logger->warning('Could not open thumbnail for attachment with ID ' . $attachment->getID() . ': ' . $e->getMessage()); return $this->assets->getUrl($asset_path); } @@ -169,7 +161,7 @@ class AttachmentURLGenerator /** * Returns a download link to the file associated with the attachment. */ - public function getDownloadURL(Attachment $attachment): string + public function getInternalDownloadURL(Attachment $attachment): string { //Redirect always to download controller, which sets the correct headers for downloading: return $this->urlGenerator->generate('attachment_download', ['id' => $attachment->getID()]); diff --git a/src/Services/Attachments/BuiltinAttachmentsFinder.php b/src/Services/Attachments/BuiltinAttachmentsFinder.php index 7c3c8f4b..b009cf60 100644 --- a/src/Services/Attachments/BuiltinAttachmentsFinder.php +++ b/src/Services/Attachments/BuiltinAttachmentsFinder.php @@ -30,16 +30,12 @@ use Symfony\Contracts\Cache\CacheInterface; /** * This service is used to find builtin attachment ressources. + * @see \App\Tests\Services\Attachments\BuiltinAttachmentsFinderTest */ class BuiltinAttachmentsFinder { - protected AttachmentPathResolver $pathResolver; - protected CacheInterface $cache; - - public function __construct(CacheInterface $cache, AttachmentPathResolver $pathResolver) + public function __construct(protected CacheInterface $cache, protected AttachmentPathResolver $pathResolver) { - $this->pathResolver = $pathResolver; - $this->cache = $cache; } /** @@ -49,7 +45,6 @@ class BuiltinAttachmentsFinder * '%FOOTPRINTS%/path/to/folder/file1.png', * '%FOOTPRINTS%/path/to/folder/file2.png', * ] - * @return array */ public function getListOfFootprintsGroupedByFolder(): array { @@ -63,7 +58,7 @@ class BuiltinAttachmentsFinder foreach($finder as $file) { $folder = $file->getRelativePath(); //Normalize path (replace \ with /) - $folder = str_replace('\\', '/', $folder); + $folder = str_replace('\\', '/', (string) $folder); if(!isset($output[$folder])) { $output[$folder] = []; @@ -109,7 +104,7 @@ class BuiltinAttachmentsFinder return $results; }); - } catch (InvalidArgumentException $invalidArgumentException) { + } catch (InvalidArgumentException) { return []; } } @@ -125,7 +120,7 @@ class BuiltinAttachmentsFinder */ public function find(string $keyword, array $options = [], ?array $base_list = []): array { - if (empty($base_list)) { + if ($base_list === null || $base_list === []) { $base_list = $this->getListOfRessources(); } diff --git a/src/Services/Attachments/FileTypeFilterTools.php b/src/Services/Attachments/FileTypeFilterTools.php index bf44cbe1..d689fda3 100644 --- a/src/Services/Attachments/FileTypeFilterTools.php +++ b/src/Services/Attachments/FileTypeFilterTools.php @@ -29,9 +29,10 @@ use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; /** - * An servive that helps working with filetype filters (based on the format accept uses. + * A service that helps to work with filetype filters (based on the format accept uses). * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers for * more details. + * @see \App\Tests\Services\Attachments\FileTypeFilterToolsTest */ class FileTypeFilterTools { @@ -43,13 +44,8 @@ class FileTypeFilterTools protected const AUDIO_EXTS = ['mp3', 'flac', 'ogg', 'oga', 'wav', 'm4a', 'opus']; protected const ALLOWED_MIME_PLACEHOLDERS = ['image/*', 'audio/*', 'video/*']; - protected MimeTypesInterface $mimeTypes; - protected CacheInterface $cache; - - public function __construct(MimeTypesInterface $mimeTypes, CacheInterface $cache) + public function __construct(protected MimeTypesInterface $mimeTypes, protected CacheInterface $cache) { - $this->mimeTypes = $mimeTypes; - $this->cache = $cache; } /** @@ -73,7 +69,7 @@ class FileTypeFilterTools $element = trim($element); if (!preg_match('#^\.\w+$#', $element) // .ext is allowed && !preg_match('#^[-\w.]+/[-\w.]+#', $element) //Explicit MIME type is allowed - && !in_array($element, static::ALLOWED_MIME_PLACEHOLDERS, false)) { //image/* is allowed + && !in_array($element, static::ALLOWED_MIME_PLACEHOLDERS, true)) { //image/* is allowed return false; } } @@ -108,7 +104,7 @@ class FileTypeFilterTools } //Convert *.jpg to .jpg - if (0 === strpos($element, '*.')) { + if (str_starts_with($element, '*.')) { $element = str_replace('*.', '.', $element); } @@ -119,11 +115,13 @@ class FileTypeFilterTools $element = 'video/*'; } elseif ('audio' === $element || 'audio/' === $element) { $element = 'audio/*'; - } elseif (!preg_match('#^[-\w.]+/[-\w.*]+#', $element) && 0 !== strpos($element, '.')) { + } elseif (!preg_match('#^[-\w.]+/[-\w.*]+#', $element) && !str_starts_with($element, '.')) { //Convert jpg to .jpg $element = '.'.$element; } } + //Prevent weird side effects + unset($element); $elements = array_unique($elements); @@ -147,7 +145,7 @@ class FileTypeFilterTools foreach ($elements as $element) { $element = trim($element); - if (0 === strpos($element, '.')) { + if (str_starts_with($element, '.')) { //We found an explicit specified file extension -> add it to list $extensions[] = substr($element, 1); } elseif ('image/*' === $element) { @@ -177,6 +175,6 @@ class FileTypeFilterTools { $extension = strtolower($extension); - return empty($filter) || in_array($extension, $this->resolveFileExtensions($filter), false); + return $filter === '' || in_array($extension, $this->resolveFileExtensions($filter), true); } } diff --git a/src/Services/Attachments/PartPreviewGenerator.php b/src/Services/Attachments/PartPreviewGenerator.php index 39d1c65c..ba6e5db0 100644 --- a/src/Services/Attachments/PartPreviewGenerator.php +++ b/src/Services/Attachments/PartPreviewGenerator.php @@ -22,16 +22,19 @@ declare(strict_types=1); namespace App\Services\Attachments; +use App\Entity\Parts\Footprint; +use App\Entity\ProjectSystem\Project; +use App\Entity\Parts\Category; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Manufacturer; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Part; class PartPreviewGenerator { - protected AttachmentManager $attachmentHelper; - - public function __construct(AttachmentManager $attachmentHelper) + public function __construct(protected AttachmentManager $attachmentHelper) { - $this->attachmentHelper = $attachmentHelper; } /** @@ -55,21 +58,29 @@ class PartPreviewGenerator $list[] = $attachment; } - if (null !== $part->getFootprint()) { + //Then comes the other images of the part + foreach ($part->getAttachments() as $attachment) { + //Dont show the master attachment twice + if ($this->isAttachmentValidPicture($attachment) && $attachment !== $part->getMasterPictureAttachment()) { + $list[] = $attachment; + } + } + + if ($part->getFootprint() instanceof Footprint) { $attachment = $part->getFootprint()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; } } - if (null !== $part->getBuiltProject()) { + if ($part->getBuiltProject() instanceof Project) { $attachment = $part->getBuiltProject()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; } } - if (null !== $part->getCategory()) { + if ($part->getCategory() instanceof Category) { $attachment = $part->getCategory()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; @@ -77,7 +88,7 @@ class PartPreviewGenerator } foreach ($part->getPartLots() as $lot) { - if (null !== $lot->getStorageLocation()) { + if ($lot->getStorageLocation() instanceof StorageLocation) { $attachment = $lot->getStorageLocation()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; @@ -85,14 +96,14 @@ class PartPreviewGenerator } } - if (null !== $part->getPartUnit()) { + if ($part->getPartUnit() instanceof MeasurementUnit) { $attachment = $part->getPartUnit()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; } } - if (null !== $part->getManufacturer()) { + if ($part->getManufacturer() instanceof Manufacturer) { $attachment = $part->getManufacturer()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { $list[] = $attachment; @@ -117,7 +128,7 @@ class PartPreviewGenerator } //Otherwise check if the part has a footprint with a valid master attachment - if (null !== $part->getFootprint()) { + if ($part->getFootprint() instanceof Footprint) { $attachment = $part->getFootprint()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { return $attachment; @@ -125,7 +136,7 @@ class PartPreviewGenerator } //With lowest priority use the master attachment of the project this part represents (when existing) - if (null !== $part->getBuiltProject()) { + if ($part->getBuiltProject() instanceof Project) { $attachment = $part->getBuiltProject()->getMasterPictureAttachment(); if ($this->isAttachmentValidPicture($attachment)) { return $attachment; @@ -145,7 +156,7 @@ class PartPreviewGenerator */ protected function isAttachmentValidPicture(?Attachment $attachment): bool { - return null !== $attachment + return $attachment instanceof Attachment && $attachment->isPicture() && $this->attachmentHelper->isFileExisting($attachment); } diff --git a/src/Services/Attachments/SVGSanitizer.php b/src/Services/Attachments/SVGSanitizer.php new file mode 100644 index 00000000..9ac5956b --- /dev/null +++ b/src/Services/Attachments/SVGSanitizer.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\Attachments; + +use Rhukster\DomSanitizer\DOMSanitizer; + +class SVGSanitizer +{ + + /** + * Sanitizes the given SVG string by removing any potentially harmful content (like inline scripts). + * @param string $input + * @return string + */ + public function sanitizeString(string $input): string + { + return (new DOMSanitizer(DOMSanitizer::SVG))->sanitize($input); + } + + /** + * Sanitizes the given SVG file by removing any potentially harmful content (like inline scripts). + * The sanitized content is written back to the file. + * @param string $filepath + */ + public function sanitizeFile(string $filepath): void + { + //Open the file and read the content + $content = file_get_contents($filepath); + if ($content === false) { + throw new \RuntimeException('Could not read file: ' . $filepath); + } + //Sanitize the content + $sanitizedContent = $this->sanitizeString($content); + //Write the sanitized content back to the file + file_put_contents($filepath, $sanitizedContent); + } +} \ No newline at end of file diff --git a/src/Services/Cache/ElementCacheTagGenerator.php b/src/Services/Cache/ElementCacheTagGenerator.php new file mode 100644 index 00000000..88fca09f --- /dev/null +++ b/src/Services/Cache/ElementCacheTagGenerator.php @@ -0,0 +1,69 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\Cache; + +use Doctrine\Persistence\Proxy; + +/** + * The purpose of this class is to generate cache tags for elements. + * E.g. to easily invalidate all caches for a given element type. + */ +class ElementCacheTagGenerator +{ + private array $cache = []; + + public function __construct() + { + } + + /** + * Returns a cache tag for the given element type, which can be used to invalidate all caches for this element type. + * @param string|object $element + * @return string + */ + public function getElementTypeCacheTag(string|object $element): string + { + //Ensure that the given element is a class name + if (is_object($element)) { + $element = $element::class; + } elseif (!class_exists($element)) { + //And that the class exists + throw new \InvalidArgumentException("The given class '$element' does not exist!"); + } + + //Check if the tag is already cached + if (isset($this->cache[$element])) { + return $this->cache[$element]; + } + + //If the element is a proxy, then get the real class name of the underlying object + if (is_a($element, Proxy::class, true) || str_starts_with($element, 'Proxies\\')) { + $element = get_parent_class($element); + } + + //Replace all backslashes with underscores to prevent problems with the cache and save the result + $this->cache[$element] = str_replace('\\', '_', $element); + return $this->cache[$element]; + } +} \ No newline at end of file diff --git a/src/Services/UserSystem/UserCacheKeyGenerator.php b/src/Services/Cache/UserCacheKeyGenerator.php similarity index 78% rename from src/Services/UserSystem/UserCacheKeyGenerator.php rename to src/Services/Cache/UserCacheKeyGenerator.php index c7c9e737..ac5487a5 100644 --- a/src/Services/UserSystem/UserCacheKeyGenerator.php +++ b/src/Services/Cache/UserCacheKeyGenerator.php @@ -20,25 +20,21 @@ declare(strict_types=1); -namespace App\Services\UserSystem; +namespace App\Services\Cache; use App\Entity\UserSystem\User; use Locale; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Security\Core\Security; /** * Purpose of this service is to generate a key unique for a user, to use in Cache keys and tags. */ class UserCacheKeyGenerator { - protected Security $security; - protected RequestStack $requestStack; - - public function __construct(Security $security, RequestStack $requestStack) + public function __construct(protected Security $security, protected RequestStack $requestStack) { - $this->security = $security; - $this->requestStack = $requestStack; } /** @@ -51,10 +47,10 @@ class UserCacheKeyGenerator { $request = $this->requestStack->getCurrentRequest(); //Retrieve the locale from the request, if possible, otherwise use the default locale - $locale = $request ? $request->getLocale() : Locale::getDefault(); + $locale = $request instanceof Request ? $request->getLocale() : Locale::getDefault(); //If no user was specified, use the currently used one. - if (null === $user) { + if (!$user instanceof User) { $user = $this->security->getUser(); } @@ -64,7 +60,7 @@ class UserCacheKeyGenerator return 'user$_'.User::ID_ANONYMOUS; } - //In the most cases we can just use the username (its unique) - return 'user_'.$user->getUsername().'_'.$locale; + //Use the unique user id and the locale to generate the key + return 'user_'.$user->getID().'_'.$locale; } } diff --git a/src/Services/CustomEnvVarProcessor.php b/src/Services/CustomEnvVarProcessor.php index 8969b765..f269cc7d 100644 --- a/src/Services/CustomEnvVarProcessor.php +++ b/src/Services/CustomEnvVarProcessor.php @@ -35,7 +35,7 @@ final class CustomEnvVarProcessor implements EnvVarProcessorInterface $env = $getEnv($name); return !empty($env) && 'null://null' !== $env; - } catch (EnvNotFoundException $envNotFoundException) { + } catch (EnvNotFoundException) { return false; } } diff --git a/src/Services/Misc/DBInfoHelper.php b/src/Services/Doctrine/DBInfoHelper.php similarity index 56% rename from src/Services/Misc/DBInfoHelper.php rename to src/Services/Doctrine/DBInfoHelper.php index 896c0637..160e2d89 100644 --- a/src/Services/Misc/DBInfoHelper.php +++ b/src/Services/Doctrine/DBInfoHelper.php @@ -1,4 +1,25 @@ . + */ + +declare(strict_types=1); + /* * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). * @@ -17,12 +38,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - -namespace App\Services\Misc; +namespace App\Services\Doctrine; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; -use Doctrine\DBAL\Platforms\SqlitePlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\ORM\EntityManagerInterface; /** @@ -31,11 +53,9 @@ use Doctrine\ORM\EntityManagerInterface; class DBInfoHelper { protected Connection $connection; - protected EntityManagerInterface $entityManager; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(protected EntityManagerInterface $entityManager) { - $this->entityManager = $entityManager; $this->connection = $entityManager->getConnection(); } @@ -49,17 +69,20 @@ class DBInfoHelper return 'mysql'; } - if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { + if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) { return 'sqlite'; } + if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) { + return 'postgresql'; + } + return null; } /** * Returns the database version of the used database. - * @return string|null - * @throws \Doctrine\DBAL\Exception + * @throws Exception */ public function getDatabaseVersion(): ?string { @@ -67,32 +90,44 @@ class DBInfoHelper return $this->connection->fetchOne('SELECT VERSION()'); } - if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { + if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) { return $this->connection->fetchOne('SELECT sqlite_version()'); } + if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) { + return $this->connection->fetchOne('SELECT version()'); + } + return null; } /** * Returns the database size in bytes. * @return int|null The database size in bytes or null if unknown - * @throws \Doctrine\DBAL\Exception + * @throws Exception */ public function getDatabaseSize(): ?int { if ($this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { try { - return $this->connection->fetchOne('SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = DATABASE()'); - } catch (\Doctrine\DBAL\Exception $e) { + return (int) $this->connection->fetchOne('SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = DATABASE()'); + } catch (Exception) { return null; } } - if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { + if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) { try { - return $this->connection->fetchOne('SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();'); - } catch (\Doctrine\DBAL\Exception $e) { + return (int) $this->connection->fetchOne('SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();'); + } catch (Exception) { + return null; + } + } + + if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) { + try { + return (int) $this->connection->fetchOne('SELECT pg_database_size(current_database())'); + } catch (Exception) { return null; } } @@ -102,30 +137,36 @@ class DBInfoHelper /** * Returns the name of the database. - * @return string|null */ public function getDatabaseName(): ?string { - return $this->connection->getDatabase() ?? null; + return $this->connection->getDatabase(); } /** * Returns the name of the database user. - * @return string|null */ public function getDatabaseUsername(): ?string { if ($this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { try { return $this->connection->fetchOne('SELECT USER()'); - } catch (\Doctrine\DBAL\Exception $e) { + } catch (Exception) { return null; } } - if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { + if ($this->connection->getDatabasePlatform() instanceof SQLitePlatform) { return 'sqlite'; } + + if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) { + try { + return $this->connection->fetchOne('SELECT current_user'); + } catch (Exception) { + return null; + } + } return null; } diff --git a/src/Services/Doctrine/NatsortDebugHelper.php b/src/Services/Doctrine/NatsortDebugHelper.php new file mode 100644 index 00000000..fe5b77aa --- /dev/null +++ b/src/Services/Doctrine/NatsortDebugHelper.php @@ -0,0 +1,86 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\Doctrine; + +use App\Doctrine\Functions\Natsort; +use App\Entity\Parts\Part; +use Doctrine\ORM\EntityManagerInterface; + +/** + * This service allows to debug the natsort function by showing various information about the current state of + * the natsort function. + */ +class NatsortDebugHelper +{ + public function __construct(private readonly EntityManagerInterface $entityManager) + { + // This is a dummy constructor + } + + /** + * Check if the slow natural sort is allowed on the Natsort function. + * If it is not, then the request handler might need to be adjusted. + * @return bool + */ + public function isSlowNaturalSortAllowed(): bool + { + return Natsort::isSlowNaturalSortAllowed(); + } + + public function getNaturalSortMethod(): string + { + //Construct a dummy query which uses the Natsort function + $query = $this->entityManager->createQuery('SELECT natsort(1) FROM ' . Part::class . ' p'); + $sql = $query->getSQL(); + //Remove the leading SELECT and the trailing semicolon + $sql = substr($sql, 7, -1); + + //Remove AS and everything afterwards + $sql = preg_replace('/\s+AS\s+.*/', '', $sql); + + //If just 1 is returned, then we use normal (non-natural sorting) + if ($sql === '1') { + return 'Disabled'; + } + + if (str_contains( $sql, 'COLLATE numeric')) { + return 'Native (PostgreSQL)'; + } + + if (str_contains($sql, 'NATURAL_SORT_KEY')) { + return 'Native (MariaDB)'; + } + + if (str_contains($sql, 'COLLATE NATURAL_CMP')) { + return 'Emulation via PHP (SQLite)'; + } + + if (str_contains($sql, 'NatSortKey')) { + return 'Emulation via custom function (MySQL)'; + } + + + return 'Unknown ('. $sql . ')'; + } +} \ No newline at end of file diff --git a/src/Services/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php new file mode 100644 index 00000000..d4cbab34 --- /dev/null +++ b/src/Services/EDA/KiCadHelper.php @@ -0,0 +1,347 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EDA; + +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Part; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\EntityURLGenerator; +use App\Services\Trees\NodesListBuilder; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +class KiCadHelper +{ + + public function __construct( + private readonly NodesListBuilder $nodesListBuilder, + private readonly TagAwareCacheInterface $kicadCache, + private readonly EntityManagerInterface $em, + private readonly ElementCacheTagGenerator $tagGenerator, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly EntityURLGenerator $entityURLGenerator, + private readonly TranslatorInterface $translator, + /** The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */ + private readonly int $category_depth, + ) { + } + + /** + * Returns an array of objects containing all categories in the database in the format required by KiCAD. + * The categories are flattened and sorted by their full path. + * Categories, which contain no parts, are filtered out. + * The result is cached for performance and invalidated on category changes. + * @return array + */ + public function getCategories(): array + { + return $this->kicadCache->get('kicad_categories_' . $this->category_depth, function (ItemInterface $item) { + //Invalidate the cache on category changes + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class); + $item->tag($secure_class_name); + + //Invalidate the cache on part changes (as the visibility depends on parts, and the parts can change) + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Part::class); + $item->tag($secure_class_name); + + //If the category depth is smaller than 0, create only one dummy category + if ($this->category_depth < 0) { + return [ + [ + 'id' => '0', + 'name' => 'Part-DB', + ] + ]; + } + + //Otherwise just get the categories and filter them + + $categories = $this->nodesListBuilder->typeToNodesList(Category::class); + $repo = $this->em->getRepository(Category::class); + $result = []; + foreach ($categories as $category) { + //Skip invisible categories + if ($category->getEdaInfo()->getVisibility() === false) { + continue; + } + + //Skip categories with a depth greater than the configured one + if ($category->getLevel() > $this->category_depth) { + continue; + } + + //Ensure that the category contains parts + //For the last level, we need to use a recursive query, otherwise we can use a simple query + /** @var Category $category */ + $parts_count = $category->getLevel() >= $this->category_depth ? $repo->getPartsCountRecursive($category) : $repo->getPartsCount($category); + + if ($parts_count < 1) { + continue; + } + + //Check if the category should be visible + if (!$this->shouldCategoryBeVisible($category)) { + continue; + } + + //Format the category for KiCAD + $result[] = [ + 'id' => (string)$category->getId(), + 'name' => $category->getFullPath('/'), + //Show the category link as the category description, this also fixes an segfault in KiCad see issue #878 + 'description' => $this->entityURLGenerator->listPartsURL($category), + ]; + } + + return $result; + }); + } + + /** + * Returns an array of objects containing all parts for the given category in the format required by KiCAD. + * The result is cached for performance and invalidated on category or part changes. + * @param Category|null $category + * @return array + */ + public function getCategoryParts(?Category $category): array + { + return $this->kicadCache->get('kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth, + function (ItemInterface $item) use ($category) { + $item->tag([ + $this->tagGenerator->getElementTypeCacheTag(Category::class), + $this->tagGenerator->getElementTypeCacheTag(Part::class), + //Visibility can change based on the footprint + $this->tagGenerator->getElementTypeCacheTag(Footprint::class) + ]); + + if ($this->category_depth >= 0) { + //Ensure that the category is set + if ($category === null) { + throw new NotFoundHttpException('Category must be set, if category_depth is greater than 1!'); + } + + $category_repo = $this->em->getRepository(Category::class); + if ($category->getLevel() >= $this->category_depth) { + //Get all parts for the category and its children + $parts = $category_repo->getPartsRecursive($category); + } else { + //Get only direct parts for the category (without children), as the category is not collapsed + $parts = $category_repo->getParts($category); + } + } else { + //Get all parts + $parts = $this->em->getRepository(Part::class)->findAll(); + } + + $result = []; + foreach ($parts as $part) { + //If the part is invisible, then skip it + if (!$this->shouldPartBeVisible($part)) { + continue; + } + + $result[] = [ + 'id' => (string)$part->getId(), + 'name' => $part->getName(), + 'description' => $part->getDescription(), + ]; + } + + return $result; + }); + } + + public function getKiCADPart(Part $part): array + { + $result = [ + 'id' => (string)$part->getId(), + 'name' => $part->getName(), + "symbolIdStr" => $part->getEdaInfo()->getKicadSymbol() ?? $part->getCategory()?->getEdaInfo()->getKicadSymbol() ?? "", + "exclude_from_bom" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBom() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBom() ?? false), + "exclude_from_board" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBoard() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBoard() ?? false), + "exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? true), + "fields" => [] + ]; + + $result["fields"]["footprint"] = $this->createField($part->getEdaInfo()->getKicadFootprint() ?? $part->getFootprint()?->getEdaInfo()->getKicadFootprint() ?? ""); + $result["fields"]["reference"] = $this->createField($part->getEdaInfo()->getReferencePrefix() ?? $part->getCategory()?->getEdaInfo()->getReferencePrefix() ?? 'U', true); + $result["fields"]["value"] = $this->createField($part->getEdaInfo()->getValue() ?? $part->getName(), true); + $result["fields"]["keywords"] = $this->createField($part->getTags()); + + //Use the part info page as datasheet link. It must be an absolute URL. + $result["fields"]["datasheet"] = $this->createField( + $this->urlGenerator->generate( + 'part_info', + ['id' => $part->getId()], + UrlGeneratorInterface::ABSOLUTE_URL) + ); + + //Add basic fields + $result["fields"]["description"] = $this->createField($part->getDescription()); + if ($part->getCategory() !== null) { + $result["fields"]["Category"] = $this->createField($part->getCategory()->getFullPath('/')); + } + if ($part->getManufacturer() !== null) { + $result["fields"]["Manufacturer"] = $this->createField($part->getManufacturer()->getName()); + } + if ($part->getManufacturerProductNumber() !== "") { + $result['fields']["MPN"] = $this->createField($part->getManufacturerProductNumber()); + } + if ($part->getManufacturingStatus() !== null) { + $result["fields"]["Manufacturing Status"] = $this->createField( + //Always use the english translation + $this->translator->trans($part->getManufacturingStatus()->toTranslationKey(), locale: 'en') + ); + } + if ($part->getFootprint() !== null) { + $result["fields"]["Part-DB Footprint"] = $this->createField($part->getFootprint()->getName()); + } + if ($part->getPartUnit() !== null) { + $unit = $part->getPartUnit()->getName(); + if ($part->getPartUnit()->getUnit() !== "") { + $unit .= ' ('.$part->getPartUnit()->getUnit().')'; + } + $result["fields"]["Part-DB Unit"] = $this->createField($unit); + } + if ($part->getMass()) { + $result["fields"]["Mass"] = $this->createField($part->getMass() . ' g'); + } + $result["fields"]["Part-DB ID"] = $this->createField($part->getId()); + if ($part->getIpn() !== null && $part->getIpn() !== '' && $part->getIpn() !== '0') { + $result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn()); + } + + + return $result; + } + + /** + * Determine if the given part should be visible for the EDA. + * @param Category $category + * @return bool + */ + private function shouldCategoryBeVisible(Category $category): bool + { + $eda_info = $category->getEdaInfo(); + + //If the category visibility is explicitly set, then use it + if ($eda_info->getVisibility() !== null) { + return $eda_info->getVisibility(); + } + + //try to check if the fields were set + if ($eda_info->getKicadSymbol() !== null + || $eda_info->getReferencePrefix() !== null) { + return true; + } + + //Check if there is any part in this category, which should be visible + $category_repo = $this->em->getRepository(Category::class); + if ($category->getLevel() >= $this->category_depth) { + //Get all parts for the category and its children + $parts = $category_repo->getPartsRecursive($category); + } else { + //Get only direct parts for the category (without children), as the category is not collapsed + $parts = $category_repo->getParts($category); + } + + foreach ($parts as $part) { + if ($this->shouldPartBeVisible($part)) { + return true; + } + } + + //Otherwise the category should be not visible + return false; + } + + /** + * Determine if the given part should be visible for the EDA. + * @param Part $part + * @return bool + */ + private function shouldPartBeVisible(Part $part): bool + { + $eda_info = $part->getEdaInfo(); + $category = $part->getCategory(); + + //If the user set a visibility, then use it + if ($eda_info->getVisibility() !== null) { + return $part->getEdaInfo()->getVisibility(); + } + + //If the part has a category, then use the category visibility if possible + if ($category && $category->getEdaInfo()->getVisibility() !== null) { + return $category->getEdaInfo()->getVisibility(); + } + + //If both are null, then we try to determine the visibility based on if fields are set + if ($eda_info->getKicadSymbol() !== null + || $eda_info->getKicadFootprint() !== null + || $eda_info->getReferencePrefix() !== null + || $eda_info->getValue() !== null) { + return true; + } + + //Check also if the fields are set for the category (if it exists) + if ($category && ( + $category->getEdaInfo()->getKicadSymbol() !== null + || $category->getEdaInfo()->getReferencePrefix() !== null + )) { + return true; + } + //And on the footprint + //Otherwise the part should be not visible + return $part->getFootprint() && $part->getFootprint()->getEdaInfo()->getKicadFootprint() !== null; + } + + /** + * Converts a boolean value to the format required by KiCAD. + * @param bool $value + * @return string + */ + private function boolToKicadBool(bool $value): string + { + return $value ? 'True' : 'False'; + } + + /** + * Creates a field array for KiCAD + * @param string|int|float $value + * @param bool $visible + * @return array + */ + private function createField(string|int|float $value, bool $visible = false): array + { + return [ + 'value' => (string)$value, + 'visible' => $this->boolToKicadBool($visible), + ]; + } +} \ No newline at end of file diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index be325320..14247145 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -22,9 +22,12 @@ declare(strict_types=1); namespace App\Services; +use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; +use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\NamedElementInterface; +use App\Entity\Parts\PartAssociation; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; @@ -34,7 +37,7 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Orderdetail; @@ -43,18 +46,17 @@ use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Exceptions\EntityNotSupportedException; -use function get_class; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @see \App\Tests\Services\ElementTypeNameGeneratorTest + */ class ElementTypeNameGenerator { - protected TranslatorInterface $translator; protected array $mapping; - public function __construct(TranslatorInterface $translator) + public function __construct(protected TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator) { - $this->translator = $translator; - //Child classes has to become before parent classes $this->mapping = [ Attachment::class => $this->translator->trans('attachment.label'), @@ -67,7 +69,7 @@ class ElementTypeNameGenerator MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), Part::class => $this->translator->trans('part.label'), PartLot::class => $this->translator->trans('part_lot.label'), - Storelocation::class => $this->translator->trans('storelocation.label'), + StorageLocation::class => $this->translator->trans('storelocation.label'), Supplier::class => $this->translator->trans('supplier.label'), Currency::class => $this->translator->trans('currency.label'), Orderdetail::class => $this->translator->trans('orderdetail.label'), @@ -76,11 +78,12 @@ class ElementTypeNameGenerator User::class => $this->translator->trans('user.label'), AbstractParameter::class => $this->translator->trans('parameter.label'), LabelProfile::class => $this->translator->trans('label_profile.label'), + PartAssociation::class => $this->translator->trans('part_association.label'), ]; } /** - * Gets an localized label for the type of the entity. + * Gets a localized label for the type of the entity. * A part element becomes "Part" ("Bauteil" in german) and a category object becomes "Category". * Useful when the type should be shown to user. * Throws an exception if the class is not supported. @@ -91,24 +94,24 @@ class ElementTypeNameGenerator * * @throws EntityNotSupportedException when the passed entity is not supported */ - public function getLocalizedTypeLabel($entity): string + public function getLocalizedTypeLabel(object|string $entity): string { - $class = is_string($entity) ? $entity : get_class($entity); + $class = is_string($entity) ? $entity : $entity::class; - //Check if we have an direct array entry for our entity class, then we can use it + //Check if we have a direct array entry for our entity class, then we can use it if (isset($this->mapping[$class])) { return $this->mapping[$class]; } //Otherwise iterate over array and check for inheritance (needed when the proxy element from doctrine are passed) - foreach ($this->mapping as $class => $translation) { - if (is_a($entity, $class, true)) { + foreach ($this->mapping as $class_to_check => $translation) { + if (is_a($entity, $class_to_check, true)) { return $translation; } } //When nothing was found throw an exception - throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', is_object($entity) ? get_class($entity) : (string) $entity)); + throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', is_object($entity) ? $entity::class : (string) $entity)); } /** @@ -132,4 +135,77 @@ class ElementTypeNameGenerator return $type.': '.$entity->getName(); } + + + /** + * Returns a HTML formatted label for the given enitity in the format "Type: Name" (on elements with a name) and + * "Type: ID" (on elements without a name). If possible the value is given as a link to the element. + * @param AbstractDBElement $entity The entity for which the label should be generated + * @param bool $include_associated If set to true, the associated entity (like the part belonging to a part lot) is included in the label to give further information + */ + public function formatLabelHTMLForEntity(AbstractDBElement $entity, bool $include_associated = false): string + { + //The element is existing + if ($entity instanceof NamedElementInterface && $entity->getName() !== '') { + try { + $tmp = sprintf( + '%s', + $this->entityURLGenerator->infoURL($entity), + $this->getTypeNameCombination($entity, true) + ); + } catch (EntityNotSupportedException) { + $tmp = $this->getTypeNameCombination($entity, true); + } + } else { //Target does not have a name + $tmp = sprintf( + '%s: %s', + $this->getLocalizedTypeLabel($entity), + $entity->getID() + ); + } + + //Add a hint to the associated element if possible + if ($include_associated) { + if ($entity instanceof Attachment && $entity->getElement() instanceof AttachmentContainingDBElement) { + $on = $entity->getElement(); + } elseif ($entity instanceof AbstractParameter && $entity->getElement() instanceof AbstractDBElement) { + $on = $entity->getElement(); + } elseif ($entity instanceof PartLot && $entity->getPart() instanceof Part) { + $on = $entity->getPart(); + } elseif ($entity instanceof Orderdetail && $entity->getPart() instanceof Part) { + $on = $entity->getPart(); + } elseif ($entity instanceof Pricedetail && $entity->getOrderdetail() instanceof Orderdetail && $entity->getOrderdetail()->getPart() instanceof Part) { + $on = $entity->getOrderdetail()->getPart(); + } elseif ($entity instanceof ProjectBOMEntry && $entity->getProject() instanceof Project) { + $on = $entity->getProject(); + } + + if (isset($on) && $on instanceof NamedElementInterface) { + try { + $tmp .= sprintf( + ' (%s)', + $this->entityURLGenerator->infoURL($on), + $this->getTypeNameCombination($on, true) + ); + } catch (EntityNotSupportedException) { + } + } + } + + return $tmp; + } + + /** + * Create a HTML formatted label for a deleted element of which we only know the class and the ID. + * Please note that it is not checked if the element really not exists anymore, so you have to do this yourself. + */ + public function formatElementDeletedHTML(string $class, int $id): string + { + return sprintf( + '%s: %s [%s]', + $this->getLocalizedTypeLabel($class), + $id, + $this->translator->trans('log.target_deleted') + ); + } } diff --git a/src/Services/EntityMergers/EntityMerger.php b/src/Services/EntityMergers/EntityMerger.php new file mode 100644 index 00000000..c0be84ee --- /dev/null +++ b/src/Services/EntityMergers/EntityMerger.php @@ -0,0 +1,76 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers; + +use App\Services\EntityMergers\Mergers\EntityMergerInterface; +use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; + +/** + * This service is used to merge two entities together. + * It automatically finds the correct merger (implementing EntityMergerInterface) for the two entities if one exists. + */ +class EntityMerger +{ + public function __construct(#[TaggedIterator('app.entity_merger')] protected iterable $mergers) + { + } + + /** + * This function finds the first merger that supports merging the other entity into the target entity. + * @param object $target + * @param object $other + * @param array $context + * @return EntityMergerInterface|null + */ + public function findMergerForObject(object $target, object $other, array $context = []): ?EntityMergerInterface + { + foreach ($this->mergers as $merger) { + if ($merger->supports($target, $other, $context)) { + return $merger; + } + } + return null; + } + + /** + * This function merges the other entity into the target entity. If no merger is found an exception is thrown. + * The target entity will be modified and returned. + * @param object $target + * @param object $other + * @param array $context + * @template T of object + * @phpstan-param T $target + * @phpstan-param T $other + * @phpstan-return T + * @return object + */ + public function merge(object $target, object $other, array $context = []): object + { + $merger = $this->findMergerForObject($target, $other, $context); + if ($merger === null) { + throw new \RuntimeException('No merger found for merging '.$other::class.' into '.$target::class); + } + return $merger->merge($target, $other, $context); + } +} \ No newline at end of file diff --git a/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php b/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php new file mode 100644 index 00000000..64c952a9 --- /dev/null +++ b/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php @@ -0,0 +1,358 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers\Mergers; + +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentContainingDBElement; +use App\Entity\Base\AbstractNamedDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parts\Part; +use Doctrine\Common\Collections\Collection; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Service\Attribute\Required; + +use function Symfony\Component\String\u; + +/** + * This trait provides helper methods for entity mergers. + * By default, it uses the value from the target entity, unless it not fullfills a condition. + */ +trait EntityMergerHelperTrait +{ + protected PropertyAccessorInterface $property_accessor; + + #[Required] + public function setPropertyAccessor(PropertyAccessorInterface $property_accessor): void + { + $this->property_accessor = $property_accessor; + } + + /** + * Choice the value to use from the target or the other entity by using a callback function. + * + * @param callable $callback The callback to use. The signature is: function($target_value, $other_value, $target, $other, $field). The callback should return the value to use. + * @param object $target The target entity + * @param object $other The other entity + * @param string $field The field to use + * @return object The target entity with the value set + */ + protected function useCallback(callable $callback, object $target, object $other, string $field): object + { + //Get the values from the entities + $target_value = $this->property_accessor->getValue($target, $field); + $other_value = $this->property_accessor->getValue($other, $field); + + //Call the callback, with the signature: function($target_value, $other_value, $target, $other, $field) + //The callback should return the value to use + $value = $callback($target_value, $other_value, $target, $other, $field); + + //Set the value + $this->property_accessor->setValue($target, $field, $value); + + return $target; + } + + /** + * Use the value from the other entity, if the value from the target entity is null. + * + * @param object $target The target entity + * @param object $other The other entity + * @param string $field The field to use + * @return object The target entity with the value set + */ + protected function useOtherValueIfNotNull(object $target, object $other, string $field): object + { + return $this->useCallback( + fn($target_value, $other_value) => $target_value ?? $other_value, + $target, + $other, + $field + ); + + } + + /** + * Use the value from the other entity, if the value from the target entity is empty. + * + * @param object $target The target entity + * @param object $other The other entity + * @param string $field The field to use + * @return object The target entity with the value set + */ + protected function useOtherValueIfNotEmtpy(object $target, object $other, string $field): object + { + return $this->useCallback( + fn($target_value, $other_value) => empty($target_value) ? $other_value : $target_value, + $target, + $other, + $field + ); + } + + /** + * Use the larger value from the target and the other entity for the given field. + * + * @param object $target + * @param object $other + * @param string $field + * @return object + */ + protected function useLargerValue(object $target, object $other, string $field): object + { + return $this->useCallback( + fn($target_value, $other_value) => max($target_value, $other_value), + $target, + $other, + $field + ); + } + + /** + * Use the smaller value from the target and the other entity for the given field. + * + * @param object $target + * @param object $other + * @param string $field + * @return object + */ + protected function useSmallerValue(object $target, object $other, string $field): object + { + return $this->useCallback( + fn($target_value, $other_value) => min($target_value, $other_value), + $target, + $other, + $field + ); + } + + /** + * Perform an OR operation on the boolean values from the target and the other entity for the given field. + * This effectively means that the value is true, if it is true in at least one of the entities. + * @param object $target + * @param object $other + * @param string $field + * @return object + */ + protected function useTrueValue(object $target, object $other, string $field): object + { + return $this->useCallback( + fn(bool $target_value, bool $other_value): bool => $target_value || $other_value, + $target, + $other, + $field + ); + } + + /** + * Perform a merge of comma separated lists from the target and the other entity for the given field. + * The values are merged and duplicates are removed. + * @param object $target + * @param object $other + * @param string $field + * @return object + */ + protected function mergeTags(object $target, object $other, string $field, string $separator = ','): object + { + return $this->useCallback( + function (string|null $t, string|null $o) use ($separator): string { + //Explode the strings into arrays + $t_array = explode($separator, $t ?? ''); + $o_array = explode($separator, $o ?? ''); + + //Merge the arrays and remove duplicates + $tmp = array_unique(array_merge($t_array, $o_array)); + + //Implode the array back to a string + return implode($separator, $tmp); + }, + $target, + $other, + $field + ); + } + + /** + * Merge the collections from the target and the other entity for the given field and put all items into the target collection. + * @param object $target + * @param object $other + * @param string $field + * @param callable|null $equal_fn A function, which checks if two items are equal. The signature is: function(object $target, object other): bool. + * Return true if the items are equal, false otherwise. If two items are equal, the item from the other collection is not added to the target collection. + * If null, the items are compared by (instance) identity. + * @return object + */ + protected function mergeCollections(object $target, object $other, string $field, ?callable $equal_fn = null): object + { + $target_collection = $this->property_accessor->getValue($target, $field); + $other_collection = $this->property_accessor->getValue($other, $field); + + if (!$target_collection instanceof Collection) { + throw new \InvalidArgumentException("The target field $field is not a collection"); + } + + //Clone the items from the other collection + $clones = []; + foreach ($other_collection as $item) { + //Check if the item is already in the target collection + if ($equal_fn !== null) { + foreach ($target_collection as $target_item) { + if ($equal_fn($target_item, $item)) { + continue 2; + } + } + } elseif ($target_collection->contains($item)) { + continue; + } + + $clones[] = clone $item; + } + + $tmp = array_merge($target_collection->toArray(), $clones); + + //Create a new collection with the clones and merge it into the target collection + $this->property_accessor->setValue($target, $field, $tmp); + + return $target; + } + + /** + * Merge the attachments from the target and the other entity. + * @param AttachmentContainingDBElement $target + * @param AttachmentContainingDBElement $other + * @return object + */ + protected function mergeAttachments(AttachmentContainingDBElement $target, AttachmentContainingDBElement $other): object + { + return $this->mergeCollections($target, $other, 'attachments', fn(Attachment $t, Attachment $o): bool => $t->getName() === $o->getName() + && $t->getAttachmentType() === $o->getAttachmentType() + && $t->getExternalPath() === $o->getExternalPath() + && $t->getInternalPath() === $o->getInternalPath()); + } + + /** + * Merge the parameters from the target and the other entity. + * @param AbstractStructuralDBElement|Part $target + * @param AbstractStructuralDBElement|Part $other + * @return object + */ + protected function mergeParameters(AbstractStructuralDBElement|Part $target, AbstractStructuralDBElement|Part $other): object + { + return $this->mergeCollections($target, $other, 'parameters', fn(AbstractParameter $t, AbstractParameter $o): bool => $t->getName() === $o->getName() + && $t->getSymbol() === $o->getSymbol() + && $t->getUnit() === $o->getUnit() + && $t->getValueMax() === $o->getValueMax() + && $t->getValueMin() === $o->getValueMin() + && $t->getValueTypical() === $o->getValueTypical() + && $t->getValueText() === $o->getValueText() + && $t->getGroup() === $o->getGroup()); + } + + /** + * Check if the two strings have equal content. + * This method is case-insensitive and ignores whitespace. + * @param string|\Stringable $t + * @param string|\Stringable $o + * @return bool + */ + protected function areStringsEqual(string|\Stringable $t, string|\Stringable $o): bool + { + $t_str = u($t)->trim()->folded(); + $o_str = u($o)->trim()->folded(); + + return $t_str->equalsTo($o_str); + } + + /** + * Merge the text from the target and the other entity for the given field by attaching the other text to the target text via the given separator. + * For example, if the target text is "Hello" and the other text is "World", the result is "Hello / World". + * If the text is the same in both entities, the target text is returned. + * @param object $target + * @param object $other + * @param string $field + * @param string $separator + * @return object + */ + protected function mergeTextWithSeparator(object $target, object $other, string $field, string $separator = ' / '): object + { + return $this->useCallback( + function (string $t, string $o) use ($separator): string { + //Check if the strings are equal + if ($this->areStringsEqual($t, $o)) { + return $t; + } + + //Skip empty strings + if (trim($t) === '') { + return trim($o); + } + if (trim($o) === '') { + return trim($t); + } + + return trim($t) . $separator . trim($o); + }, + $target, + $other, + $field + ); + } + + /** + * Merge two comments from the target and the other entity for the given field. + * The comments of the both entities get concated, while the second part get a headline with the name of the old part. + * @param AbstractNamedDBElement $target + * @param AbstractNamedDBElement $other + * @param string $field + * @return object + */ + protected function mergeComment(AbstractNamedDBElement $target, AbstractNamedDBElement $other, string $field = 'comment'): object + { + return $this->useCallback( + function (string $t, string $o) use ($other): string { + //Check if the strings are equal + if ($this->areStringsEqual($t, $o)) { + return $t; + } + + //Skip empty strings + if (trim($t) === '') { + return trim($o); + } + if (trim($o) === '') { + return trim($t); + } + + return sprintf("%s\n\n%s:\n%s", + trim($t), + $other->getName(), + trim($o) + ); + }, + $target, + $other, + $field + ); + } +} \ No newline at end of file diff --git a/src/Services/EntityMergers/Mergers/EntityMergerInterface.php b/src/Services/EntityMergers/Mergers/EntityMergerInterface.php new file mode 100644 index 00000000..046fc0ea --- /dev/null +++ b/src/Services/EntityMergers/Mergers/EntityMergerInterface.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers\Mergers; + + +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + +/** + * @template T of object + */ +#[AutoconfigureTag('app.entity_merger')] +interface EntityMergerInterface +{ + /** + * Determines if this merger supports merging the other entity into the target entity. + * @param object $target + * @phpstan-param T $target + * @param object $other + * @phpstan-param T $other + * @param array $context + * @return bool True if this merger supports merging the other entity into the target entity, false otherwise + */ + public function supports(object $target, object $other, array $context = []): bool; + + /** + * Merge the other entity into the target entity. + * The target entity will be modified and returned. + * @param object $target + * @phpstan-param T $target + * @param object $other + * @phpstan-param T $other + * @param array $context + * @phpstan-return T + * @return object + */ + public function merge(object $target, object $other, array $context = []): object; +} \ No newline at end of file diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php new file mode 100644 index 00000000..4ce779e8 --- /dev/null +++ b/src/Services/EntityMergers/Mergers/PartMerger.php @@ -0,0 +1,186 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers\Mergers; + +use App\Entity\Parts\InfoProviderReference; +use App\Entity\Parts\ManufacturingStatus; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartAssociation; +use App\Entity\PriceInformations\Orderdetail; + +/** + * This class merges two parts together. + * + * @implements EntityMergerInterface + * @see \App\Tests\Services\EntityMergers\Mergers\PartMergerTest + */ +class PartMerger implements EntityMergerInterface +{ + + use EntityMergerHelperTrait; + + public function supports(object $target, object $other, array $context = []): bool + { + return $target instanceof Part && $other instanceof Part; + } + + public function merge(object $target, object $other, array $context = []): Part + { + if (!$target instanceof Part || !$other instanceof Part) { + throw new \InvalidArgumentException('The target and the other entity must be instances of Part'); + } + + //Merge basic fields + $this->mergeTextWithSeparator($target, $other, 'name'); + $this->mergeTextWithSeparator($target, $other, 'description'); + $this->mergeComment($target, $other); + $this->useOtherValueIfNotEmtpy($target, $other, 'manufacturer_product_url'); + $this->useOtherValueIfNotEmtpy($target, $other, 'manufacturer_product_number'); + $this->useOtherValueIfNotEmtpy($target, $other, 'mass'); + $this->useOtherValueIfNotEmtpy($target, $other, 'ipn'); + + //Merge relations to other entities + $this->useOtherValueIfNotNull($target, $other, 'manufacturer'); + $this->useOtherValueIfNotNull($target, $other, 'footprint'); + $this->useOtherValueIfNotNull($target, $other, 'category'); + $this->useOtherValueIfNotNull($target, $other, 'partUnit'); + + //We assume that the higher value is the correct one for minimum instock + $this->useLargerValue($target, $other, 'minamount'); + + //We assume that a part needs review and is a favorite if one of the parts is + $this->useTrueValue($target, $other, 'needs_review'); + $this->useTrueValue($target, $other, 'favorite'); + + //Merge the tags using the tag merger + $this->mergeTags($target, $other, 'tags'); + + //Merge manufacturing status + $this->useCallback(function (?ManufacturingStatus $t, ?ManufacturingStatus $o): ManufacturingStatus { + //Use the other value, if the target value is not set + if ($t === ManufacturingStatus::NOT_SET || $t === null) { + return $o ?? ManufacturingStatus::NOT_SET; + } + + return $t; + }, $target, $other, 'manufacturing_status'); + + //Merge provider reference + $this->useCallback(function (InfoProviderReference $t, InfoProviderReference $o): InfoProviderReference { + if (!$t->isProviderCreated() && $o->isProviderCreated()) { + return $o; + } + return $t; + }, $target, $other, 'providerReference'); + + //Merge the collections + $this->mergeCollectionFields($target, $other, $context); + + return $target; + } + + private function comparePartAssociations(PartAssociation $t, PartAssociation $o): bool { + //We compare the translation keys, as it contains info about the type and other type info + return $t->getOther() === $o->getOther() + && $t->getTypeTranslationKey() === $o->getTypeTranslationKey(); + } + + private function mergeCollectionFields(Part $target, Part $other, array $context): void + { + /******************************************************************************** + * Merge collections + ********************************************************************************/ + + //Lots from different parts are never considered equal, so we just merge them together + $this->mergeCollections($target, $other, 'partLots'); + $this->mergeAttachments($target, $other); + $this->mergeParameters($target, $other); + + //Merge the associations + $this->mergeCollections($target, $other, 'associated_parts_as_owner', $this->comparePartAssociations(...)); + + //We have to recreate the associations towards the other part, as they are not created by the merger + foreach ($other->getAssociatedPartsAsOther() as $association) { + //Clone the association + $clone = clone $association; + //Set the target part as the other part + $clone->setOther($target); + $owner = $clone->getOwner(); + if (!$owner) { + continue; + } + //Ensure that the association is not already present + foreach ($owner->getAssociatedPartsAsOwner() as $existing_association) { + if ($this->comparePartAssociations($existing_association, $clone)) { + continue 2; + } + } + + //Add the association to the owner + $owner->addAssociatedPartsAsOwner($clone); + } + + $this->mergeCollections($target, $other, 'orderdetails', function (Orderdetail $t, Orderdetail $o) { + //First check that the orderdetails infos are equal + $tmp = $t->getSupplier() === $o->getSupplier() + && $t->getSupplierPartNr() === $o->getSupplierPartNr() + && $t->getSupplierProductUrl(false) === $o->getSupplierProductUrl(false); + + if (!$tmp) { + return false; + } + + //Check if the pricedetails are equal + $t_pricedetails = $t->getPricedetails(); + $o_pricedetails = $o->getPricedetails(); + //Ensure that both pricedetails have the same length + if (count($t_pricedetails) !== count($o_pricedetails)) { + return false; + } + + //Check if all pricedetails are equal + for ($n=0, $nMax = count($t_pricedetails); $n< $nMax; $n++) { + $t_price = $t_pricedetails->get($n); + $o_price = $o_pricedetails->get($n); + + if (!$t_price->getPrice()->isEqualTo($o_price->getPrice()) + || $t_price->getCurrency() !== $o_price->getCurrency() + || $t_price->getPriceRelatedQuantity() !== $o_price->getPriceRelatedQuantity() + || $t_price->getMinDiscountQuantity() !== $o_price->getMinDiscountQuantity() + ) { + return false; + } + } + + //If all pricedetails are equal, the orderdetails are equal + return true; + }); + //The pricedetails are not correctly assigned to the new orderdetails, so fix that + foreach ($target->getOrderdetails() as $orderdetail) { + foreach ($orderdetail->getPricedetails() as $pricedetail) { + $pricedetail->setOrderdetail($orderdetail); + } + } + } +} \ No newline at end of file diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 591abcc2..78db06f0 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -26,6 +26,7 @@ use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; +use App\Entity\Parameters\PartParameter; use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; @@ -34,7 +35,7 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Orderdetail; @@ -43,9 +44,7 @@ use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Exceptions\EntityNotSupportedException; use App\Services\Attachments\AttachmentURLGenerator; -use DateTime; use function array_key_exists; -use function get_class; use InvalidArgumentException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -56,13 +55,8 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; */ class EntityURLGenerator { - protected UrlGeneratorInterface $urlGenerator; - protected AttachmentURLGenerator $attachmentURLGenerator; - - public function __construct(UrlGeneratorInterface $urlGenerator, AttachmentURLGenerator $attachmentURLGenerator) + public function __construct(protected UrlGeneratorInterface $urlGenerator, protected AttachmentURLGenerator $attachmentURLGenerator) { - $this->urlGenerator = $urlGenerator; - $this->attachmentURLGenerator = $attachmentURLGenerator; } /** @@ -78,35 +72,25 @@ class EntityURLGenerator * @throws EntityNotSupportedException thrown if the entity is not supported for the given type * @throws InvalidArgumentException thrown if the givent type is not existing */ - public function getURL($entity, string $type): string + public function getURL(mixed $entity, string $type): string { - switch ($type) { - case 'info': - return $this->infoURL($entity); - case 'edit': - return $this->editURL($entity); - case 'create': - return $this->createURL($entity); - case 'clone': - return $this->cloneURL($entity); - case 'list': - case 'list_parts': - return $this->listPartsURL($entity); - case 'delete': - return $this->deleteURL($entity); - case 'file_download': - return $this->downloadURL($entity); - case 'file_view': - return $this->viewURL($entity); - } - - throw new InvalidArgumentException('Method is not supported!'); + return match ($type) { + 'info' => $this->infoURL($entity), + 'edit' => $this->editURL($entity), + 'create' => $this->createURL($entity), + 'clone' => $this->cloneURL($entity), + 'list', 'list_parts' => $this->listPartsURL($entity), + 'delete' => $this->deleteURL($entity), + 'file_download' => $this->downloadURL($entity), + 'file_view' => $this->viewURL($entity), + default => throw new InvalidArgumentException('Method is not supported!'), + }; } /** * Gets the URL to view the given element at a given timestamp. */ - public function timeTravelURL(AbstractDBElement $entity, DateTime $dateTime): string + public function timeTravelURL(AbstractDBElement $entity, \DateTimeInterface $dateTime): string { $map = [ Part::class => 'part_info', @@ -116,7 +100,7 @@ class EntityURLGenerator Project::class => 'project_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', - Storelocation::class => 'store_location_edit', + StorageLocation::class => 'store_location_edit', Footprint::class => 'footprint_edit', User::class => 'user_edit', Currency::class => 'currency_edit', @@ -133,7 +117,7 @@ class EntityURLGenerator 'timestamp' => $dateTime->getTimestamp(), ] ); - } catch (EntityNotSupportedException $exception) { + } catch (EntityNotSupportedException) { if ($entity instanceof PartLot) { return $this->urlGenerator->generate('part_info', [ 'id' => $entity->getPart()->getID(), @@ -158,33 +142,48 @@ class EntityURLGenerator 'timestamp' => $dateTime->getTimestamp(), ]); } + if ($entity instanceof PartParameter) { + return $this->urlGenerator->generate('part_info', [ + 'id' => $entity->getElement()->getID(), + 'timestamp' => $dateTime->getTimestamp(), + ]); + } } //Otherwise throw an error - throw new EntityNotSupportedException('The given entity is not supported yet!'); + throw new EntityNotSupportedException('The given entity is not supported yet! Passed class type: '.$entity::class); } public function viewURL(Attachment $entity): string { - if ($entity->isExternal()) { //For external attachments, return the link to external path - return $entity->getURL(); + //If the underlying file path is invalid, null gets returned, which is not allowed here. + //We still have the chance to use an external path, if it is set. + if ($entity->hasInternal() && ($url = $this->attachmentURLGenerator->getInternalViewURL($entity)) !== null) { + return $url; } - //return $this->urlGenerator->generate('attachment_view', ['id' => $entity->getID()]); - return $this->attachmentURLGenerator->getViewURL($entity) ?? ''; + + if($entity->hasExternal()) { + return $entity->getExternalPath(); + } + + throw new \RuntimeException('Attachment has no internal nor external path!'); } public function downloadURL($entity): string { - if ($entity instanceof Attachment) { - if ($entity->isExternal()) { //For external attachments, return the link to external path - return $entity->getURL(); - } - - return $this->attachmentURLGenerator->getDownloadURL($entity) ?? ''; + if (!($entity instanceof Attachment)) { + throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', $entity::class)); } - //Otherwise throw an error - throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', get_class($entity))); + if ($entity->hasInternal()) { + return $this->attachmentURLGenerator->getInternalDownloadURL($entity); + } + + if($entity->hasExternal()) { + return $entity->getExternalPath(); + } + + throw new \RuntimeException('Attachment has not internal or external path!'); } /** @@ -207,7 +206,7 @@ class EntityURLGenerator Project::class => 'project_info', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', - Storelocation::class => 'store_location_edit', + StorageLocation::class => 'store_location_edit', Footprint::class => 'footprint_edit', User::class => 'user_edit', Currency::class => 'currency_edit', @@ -222,13 +221,13 @@ class EntityURLGenerator /** * Generates an URL to a page, where this entity can be edited. * - * @param mixed $entity The entity for which the edit link should be generated + * @param AbstractDBElement $entity The entity for which the edit link should be generated * * @return string the URL to the edit page * * @throws EntityNotSupportedException If the method is not supported for the given Entity */ - public function editURL($entity): string + public function editURL(AbstractDBElement $entity): string { $map = [ Part::class => 'part_edit', @@ -237,7 +236,7 @@ class EntityURLGenerator Project::class => 'project_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', - Storelocation::class => 'store_location_edit', + StorageLocation::class => 'store_location_edit', Footprint::class => 'footprint_edit', User::class => 'user_edit', Currency::class => 'currency_edit', @@ -252,13 +251,14 @@ class EntityURLGenerator /** * Generates an URL to a page, where a entity of this type can be created. * - * @param mixed $entity The entity for which the link should be generated + * @param AbstractDBElement|string $entity The entity (or the entity class) for which the link should be generated + * @phpstan-param AbstractDBElement|class-string $entity * * @return string the URL to the page * * @throws EntityNotSupportedException If the method is not supported for the given Entity */ - public function createURL($entity): string + public function createURL(AbstractDBElement|string $entity): string { $map = [ Part::class => 'part_new', @@ -267,7 +267,7 @@ class EntityURLGenerator Project::class => 'project_new', Supplier::class => 'supplier_new', Manufacturer::class => 'manufacturer_new', - Storelocation::class => 'store_location_new', + StorageLocation::class => 'store_location_new', Footprint::class => 'footprint_new', User::class => 'user_new', Currency::class => 'currency_new', @@ -298,7 +298,7 @@ class EntityURLGenerator Project::class => 'device_clone', Supplier::class => 'supplier_clone', Manufacturer::class => 'manufacturer_clone', - Storelocation::class => 'store_location_clone', + StorageLocation::class => 'store_location_clone', Footprint::class => 'footprint_clone', User::class => 'user_clone', Currency::class => 'currency_clone', @@ -328,7 +328,7 @@ class EntityURLGenerator Footprint::class => 'part_list_footprint', Manufacturer::class => 'part_list_manufacturer', Supplier::class => 'part_list_supplier', - Storelocation::class => 'part_list_store_location', + StorageLocation::class => 'part_list_store_location', ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -343,7 +343,7 @@ class EntityURLGenerator Project::class => 'project_delete', Supplier::class => 'supplier_delete', Manufacturer::class => 'manufacturer_delete', - Storelocation::class => 'store_location_delete', + StorageLocation::class => 'store_location_delete', Footprint::class => 'footprint_delete', User::class => 'user_delete', Currency::class => 'currency_delete', @@ -360,26 +360,27 @@ class EntityURLGenerator * Throws an exception if the entity class is not known to the map. * * @param array $map The map that should be used for determing the controller - * @param mixed $entity The entity for which the controller name should be determined + * @param AbstractDBElement|string $entity The entity for which the controller name should be determined + * @phpstan-param AbstractDBElement|class-string $entity * * @return string The name of the controller fitting the entity class * * @throws EntityNotSupportedException */ - protected function mapToController(array $map, $entity): string + protected function mapToController(array $map, string|AbstractDBElement $entity): string { - $class = get_class($entity); + $class = is_string($entity) ? $entity : $entity::class; //Check if we have an direct mapping for the given class if (!array_key_exists($class, $map)) { //Check if we need to check inheritance by looping through our map foreach (array_keys($map) as $key) { - if (is_a($entity, $key)) { + if (is_a($entity, $key, true)) { return $map[$key]; } } - throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', get_class($entity))); + throw new EntityNotSupportedException(sprintf('The given entity is not supported yet! Passed class type: %s', $entity::class)); } return $map[$class]; diff --git a/src/Services/Formatters/AmountFormatter.php b/src/Services/Formatters/AmountFormatter.php index 18d52a44..73d59113 100644 --- a/src/Services/Formatters/AmountFormatter.php +++ b/src/Services/Formatters/AmountFormatter.php @@ -23,35 +23,30 @@ declare(strict_types=1); namespace App\Services\Formatters; use App\Entity\Parts\MeasurementUnit; -use App\Services\Formatters\SIFormatter; use InvalidArgumentException; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; /** - * This service formats an part amout using a Measurement Unit. + * This service formats a part amount using a Measurement Unit. + * @see \App\Tests\Services\Formatters\AmountFormatterTest */ class AmountFormatter { - protected SIFormatter $siFormatter; - - public function __construct(SIFormatter $siFormatter) + public function __construct(protected SIFormatter $siFormatter) { - $this->siFormatter = $siFormatter; } /** * Formats the given value using the measurement unit and options. * - * @param float|string|int $value * @param MeasurementUnit|null $unit The measurement unit, whose unit symbol should be used for formatting. * If set to null, it is assumed that the part amount is measured in pieces. * * @return string The formatted string - * * @throws InvalidArgumentException thrown if $value is not numeric */ - public function format($value, ?MeasurementUnit $unit = null, array $options = []): string + public function format(float|string|int $value, ?MeasurementUnit $unit = null, array $options = []): string { if (!is_numeric($value)) { throw new InvalidArgumentException('$value must be an numeric value!'); @@ -77,7 +72,7 @@ class AmountFormatter //Otherwise just output it if (!empty($options['unit'])) { $format_string = '%.'.$options['decimals'].'f '.$options['unit']; - } else { //Dont add space after number if no unit was specified + } else { //Don't add space after number if no unit was specified $format_string = '%.'.$options['decimals'].'f'; } @@ -127,7 +122,7 @@ class AmountFormatter $resolver->setAllowedTypes('decimals', 'int'); $resolver->setNormalizer('decimals', static function (Options $options, $value) { - // If the unit is integer based, then dont show any decimals + // If the unit is integer based, then don't show any decimals if ($options['is_integer']) { return 0; } diff --git a/src/Services/Formatters/MarkdownParser.php b/src/Services/Formatters/MarkdownParser.php index 805fd4bf..f3ef07df 100644 --- a/src/Services/Formatters/MarkdownParser.php +++ b/src/Services/Formatters/MarkdownParser.php @@ -25,22 +25,19 @@ namespace App\Services\Formatters; use Symfony\Contracts\Translation\TranslatorInterface; /** - * This class allows you to convert markdown text to HTML. + * This class allows you to convert Markdown text to HTML. */ class MarkdownParser { - protected TranslatorInterface $translator; - - public function __construct(TranslatorInterface $translator) + public function __construct(protected TranslatorInterface $translator) { - $this->translator = $translator; } /** * Mark the markdown for rendering. * The rendering of markdown is done on client side. * - * @param string $markdown the markdown text that should be parsed to html + * @param string $markdown the Markdown text that should be parsed to html * @param bool $inline_mode When true, p blocks will have no margins behind them * * @return string the markdown in a version that can be parsed on client side diff --git a/src/Services/Formatters/MoneyFormatter.php b/src/Services/Formatters/MoneyFormatter.php index ee8a189a..44a49cb5 100644 --- a/src/Services/Formatters/MoneyFormatter.php +++ b/src/Services/Formatters/MoneyFormatter.php @@ -28,27 +28,25 @@ use NumberFormatter; class MoneyFormatter { - protected string $base_currency; protected string $locale; - public function __construct(string $base_currency) + public function __construct(protected string $base_currency) { - $this->base_currency = $base_currency; $this->locale = Locale::getDefault(); } /** - * Format the the given value in the given currency. + * Format the given value in the given currency. * * @param string|float $value the value that should be formatted * @param Currency|null $currency The currency that should be used for formatting. If null the global one is used * @param int $decimals the number of decimals that should be shown * @param bool $show_all_digits if set to true, all digits are shown, even if they are null */ - public function format($value, ?Currency $currency = null, int $decimals = 5, bool $show_all_digits = false): string + public function format(string|float $value, ?Currency $currency = null, int $decimals = 5, bool $show_all_digits = false): string { $iso_code = $this->base_currency; - if (null !== $currency && !empty($currency->getIsoCode())) { + if ($currency instanceof Currency && ($currency->getIsoCode() !== '')) { $iso_code = $currency->getIsoCode(); } diff --git a/src/Services/Formatters/SIFormatter.php b/src/Services/Formatters/SIFormatter.php index 288641a3..a6325987 100644 --- a/src/Services/Formatters/SIFormatter.php +++ b/src/Services/Formatters/SIFormatter.php @@ -24,6 +24,7 @@ namespace App\Services\Formatters; /** * A service that helps you to format values using the SI prefixes. + * @see \App\Tests\Services\Formatters\SIFormatterTest */ class SIFormatter { @@ -45,7 +46,7 @@ class SIFormatter * * @param int $magnitude the magnitude for which the prefix should be determined * - * @return array A array, containing the divisor in first element, and the prefix symbol in second. For example, [1000, "k"]. + * @return array An array, containing the divisor in first element, and the prefix symbol in second. For example, [1000, "k"]. */ public function getPrefixByMagnitude(int $magnitude): array { @@ -93,11 +94,7 @@ class SIFormatter [$divisor, $symbol] = $this->getPrefixByMagnitude($this->getMagnitude($value)); $value /= $divisor; //Build the format string, e.g.: %.2d km - if ('' !== $unit || '' !== $symbol) { - $format_string = '%.'.$decimals.'f '.$symbol.$unit; - } else { - $format_string = '%.'.$decimals.'f'; - } + $format_string = '' !== $unit || '' !== $symbol ? '%.'.$decimals.'f '.$symbol.$unit : '%.'.$decimals.'f'; return sprintf($format_string, $value); } diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index d3fa4b9c..d4876445 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ImportExportSystem; use App\Entity\ProjectSystem\Project; @@ -26,19 +28,20 @@ use InvalidArgumentException; use League\Csv\Reader; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Validator\Validator\ValidatorInterface; +/** + * @see \App\Tests\Services\ImportExportSystem\BOMImporterTest + */ class BOMImporter { private const MAP_KICAD_PCB_FIELDS = [ - 'ID' => 'Id', - 'Bezeichner' => 'Designator', - 'Footprint' => 'Package', - 'Stückzahl' => 'Quantity', - 'Bezeichnung' => 'Designation', - 'Anbieter und Referenz' => 'Supplier and ref', + 0 => 'Id', + 1 => 'Designator', + 2 => 'Package', + 3 => 'Quantity', + 4 => 'Designation', + 5 => 'Supplier and ref', ]; public function __construct() @@ -56,9 +59,6 @@ class BOMImporter /** * Converts the given file into an array of BOM entries using the given options and save them into the given project. * The changes are not saved into the database yet. - * @param File $file - * @param array $options - * @param Project $project * @return ProjectBOMEntry[] */ public function importFileIntoProject(File $file, Project $project, array $options): array @@ -75,8 +75,6 @@ class BOMImporter /** * Converts the given file into an array of BOM entries using the given options. - * @param File $file - * @param array $options * @return ProjectBOMEntry[] */ public function fileToBOMEntries(File $file, array $options): array @@ -96,12 +94,10 @@ class BOMImporter $resolver = $this->configureOptions($resolver); $options = $resolver->resolve($options); - switch ($options['type']) { - case 'kicad_pcbnew': - return $this->parseKiCADPCB($data, $options); - default: - throw new InvalidArgumentException('Invalid import type!'); - } + return match ($options['type']) { + 'kicad_pcbnew' => $this->parseKiCADPCB($data, $options), + default => throw new InvalidArgumentException('Invalid import type!'), + }; } private function parseKiCADPCB(string $data, array $options = []): array @@ -114,9 +110,7 @@ class BOMImporter foreach ($csv->getRecords() as $offset => $entry) { //Translate the german field names to english - $entry = array_combine(array_map(function ($key) { - return self::MAP_KICAD_PCB_FIELDS[$key] ?? $key; - }, array_keys($entry)), $entry); + $entry = $this->normalizeColumnNames($entry); //Ensure that the entry has all required fields if (!isset ($entry['Designator'])) { @@ -143,4 +137,27 @@ class BOMImporter return $bom_entries; } -} \ No newline at end of file + + /** + * This function uses the order of the fields in the CSV files to make them locale independent. + * @param array $entry + * @return array + */ + private function normalizeColumnNames(array $entry): array + { + $out = []; + + //Map the entry order to the correct column names + foreach (array_values($entry) as $index => $field) { + if ($index > 5) { + break; + } + + //@phpstan-ignore-next-line We want to keep this check just to be safe when something changes + $new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!'); + $out[$new_index] = $field; + } + + return $out; + } +} diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 2b84b115..c37db50c 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -23,9 +23,13 @@ declare(strict_types=1); namespace App\Services\ImportExportSystem; use App\Entity\Base\AbstractNamedDBElement; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Helpers\FilenameSanatizer; +use App\Serializer\APIPlatform\SkippableItemNormalizer; use Symfony\Component\OptionsResolver\OptionsResolver; -use function in_array; use InvalidArgumentException; +use Symfony\Component\Serializer\Exception\CircularReferenceException; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use function is_array; use ReflectionClass; use ReflectionException; @@ -37,14 +41,12 @@ use function Symfony\Component\String\u; /** * Use this class to export an entity to multiple file formats. + * @see \App\Tests\Services\ImportExportSystem\EntityExporterTest */ class EntityExporter { - protected SerializerInterface $serializer; - - public function __construct(SerializerInterface $serializer) + public function __construct(protected SerializerInterface $serializer) { - $this->serializer = $serializer; } protected function configureOptions(OptionsResolver $resolver): void @@ -68,14 +70,13 @@ class EntityExporter * @param array $options The options to use for exporting * @return string The serialized data */ - public function exportEntities($entities, array $options): string + public function exportEntities(AbstractNamedDBElement|array $entities, array $options): string { if (!is_array($entities)) { $entities = [$entities]; } //Ensure that all entities are of type AbstractNamedDBElement - $entity_type = null; foreach ($entities as $entity) { if (!$entity instanceof AbstractNamedDBElement) { throw new InvalidArgumentException('All entities must be of type AbstractNamedDBElement!'); @@ -99,10 +100,28 @@ class EntityExporter 'as_collection' => true, 'csv_delimiter' => $options['csv_delimiter'], 'xml_root_node_name' => 'PartDBExport', + 'partdb_export' => true, + //Skip the item normalizer, so that we dont get IRIs in the output + SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true, + //Handle circular references + AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => $this->handleCircularReference(...), ] ); } + private function handleCircularReference(object $object, string $format, array $context): string + { + if ($object instanceof AbstractStructuralDBElement) { + return $object->getFullPath("->"); + } elseif ($object instanceof AbstractNamedDBElement) { + return $object->getName(); + } elseif ($object instanceof \Stringable) { + return $object->__toString(); + } + + throw new CircularReferenceException('Circular reference detected for object of type '.get_class($object)); + } + /** * Exports an Entity or an array of entities to multiple file formats. * @@ -113,7 +132,7 @@ class EntityExporter * * @throws ReflectionException */ - public function exportEntityFromRequest($entities, Request $request): Response + public function exportEntityFromRequest(AbstractNamedDBElement|array $entities, Request $request): Response { $options = [ 'format' => $request->get('format') ?? 'json', @@ -169,6 +188,9 @@ class EntityExporter $filename = 'export_'.$entity_name.'_'.$level.'.'.$format; + //Sanitize the filename + $filename = FilenameSanatizer::sanitizeFilename($filename); + // Create the disposition of the file $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index c798f9f5..cecab12d 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -26,7 +26,10 @@ use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parts\Category; use App\Entity\Parts\Part; -use Symplify\EasyCodingStandard\ValueObject\Option; +use App\Repository\StructuralDBElementRepository; +use App\Serializer\APIPlatform\SkippableItemNormalizer; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\ConstraintViolationListInterface; use function count; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; @@ -36,42 +39,58 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +/** + * @see \App\Tests\Services\ImportExportSystem\EntityImporterTest + */ class EntityImporter { - protected SerializerInterface $serializer; - protected EntityManagerInterface $em; - protected ValidatorInterface $validator; - public function __construct(SerializerInterface $serializer, EntityManagerInterface $em, ValidatorInterface $validator) + /** + * The encodings that are supported by the importer, and that should be autodeceted. + */ + private const ENCODINGS = ["ASCII", "UTF-8", "ISO-8859-1", "ISO-8859-15", "Windows-1252", "UTF-16", "UTF-32"]; + + public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator) { - $this->serializer = $serializer; - $this->em = $em; - $this->validator = $validator; } /** * Creates many entries at once, based on a (text) list of name. * The created entities are not persisted to database yet, so you have to do it yourself. * + * @template T of AbstractNamedDBElement * @param string $lines The list of names seperated by \n * @param string $class_name The name of the class for which the entities should be created + * @phpstan-param class-string $class_name * @param AbstractStructuralDBElement|null $parent the element which will be used as parent element for new elements * @param array $errors an associative array containing all validation errors + * @param-out list $errors * - * @return AbstractStructuralDBElement[] An array containing all valid imported entities (with the type $class_name) + * @return AbstractNamedDBElement[] An array containing all valid imported entities (with the type $class_name) + * @return T[] */ public function massCreation(string $lines, string $class_name, ?AbstractStructuralDBElement $parent = null, array &$errors = []): array { + //Try to detect the text encoding of the data and convert it to UTF-8 + $lines = mb_convert_encoding($lines, 'UTF-8', mb_detect_encoding($lines, self::ENCODINGS)); + //Expand every line to a single entry: $names = explode("\n", $lines); if (!is_a($class_name, AbstractNamedDBElement::class, true)) { throw new InvalidArgumentException('$class_name must be a StructuralDBElement type!'); } - if (null !== $parent && !is_a($parent, $class_name)) { + if ($parent instanceof AbstractStructuralDBElement && !$parent instanceof $class_name) { throw new InvalidArgumentException('$parent must have the same type as specified in $class_name!'); } + //Ensure that parent is already persisted. Otherwise the getNewEntityFromPath function will not work. + if ($parent !== null && $parent->getID() === null) { + throw new InvalidArgumentException('The parent must persisted to database!'); + } + + $repo = $this->em->getRepository($class_name); + $errors = []; $valid_entities = []; @@ -81,10 +100,10 @@ class EntityImporter $indentations = [0]; foreach ($names as $name) { - //Count intendation level (whitespace characters at the beginning of the line) + //Count indentation level (whitespace characters at the beginning of the line) $identSize = strlen($name)-strlen(ltrim($name)); - //If the line is intendet more than the last line, we have a new parent element + //If the line is intended more than the last line, we have a new parent element if ($identSize > end($indentations)) { $current_parent = $last_element; //Add the new indentation level to the stack @@ -92,11 +111,7 @@ class EntityImporter } while ($identSize < end($indentations)) { //If the line is intendet less than the last line, we have to go up in the tree - if ($current_parent instanceof AbstractStructuralDBElement) { - $current_parent = $current_parent->getParent(); - } else { - $current_parent = null; - } + $current_parent = $current_parent instanceof AbstractStructuralDBElement ? $current_parent->getParent() : null; array_pop($indentations); } @@ -105,14 +120,27 @@ class EntityImporter //Skip empty lines (StrucuralDBElements must have a name) continue; } + /** @var AbstractStructuralDBElement $entity */ - //Create new element with given name - $entity = new $class_name(); - $entity->setName($name); - //Only set the parent if the entity is a StructuralDBElement - if ($entity instanceof AbstractStructuralDBElement) { - $entity->setParent($current_parent); + //Create new element with given name. Using the function from the repository, to correctly reuse existing elements + + if ($current_parent instanceof AbstractStructuralDBElement) { + $new_path = $current_parent->getFullPath("->") . '->' . $name; + } else { + $new_path = $name; } + //We can only use the getNewEntityFromPath function, if the repository is a StructuralDBElementRepository + if ($repo instanceof StructuralDBElementRepository) { + $entities = $repo->getNewEntityFromPath($new_path); + $entity = end($entities); + if ($entity === false) { + throw new InvalidArgumentException('getNewEntityFromPath returned an empty array!'); + } + } else { //Otherwise just create a new entity + $entity = new $class_name; + $entity->setName($name); + } + //Validate entity $tmp = $this->validator->validate($entity); @@ -137,10 +165,14 @@ class EntityImporter * @param string $data The serialized data which should be imported * @param array $options The options for the import process * @param array $errors An array which will be filled with the validation errors, if any occurs during import + * @param-out array $errors * @return array An array containing all valid imported entities */ public function importString(string $data, array $options = [], array &$errors = []): array { + //Try to detect the text encoding of the data and convert it to UTF-8 + $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data, self::ENCODINGS)); + $resolver = new OptionsResolver(); $this->configureOptions($resolver); $options = $resolver->resolve($options); @@ -162,6 +194,9 @@ class EntityImporter 'csv_delimiter' => $options['csv_delimiter'], 'create_unknown_datastructures' => $options['create_unknown_datastructures'], 'path_delimiter' => $options['path_delimiter'], + 'partdb_import' => true, + //Disable API Platform normalizer, as we don't want to use it here + SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true, ]); //Ensure we have an array of entity elements. @@ -194,12 +229,21 @@ class EntityImporter //Iterate over each $entity write it to DB. foreach ($entities as $key => $entity) { + //Ensure that entity is a NamedDBElement + if (!$entity instanceof AbstractNamedDBElement) { + throw new \RuntimeException("Encountered an entity that is not a NamedDBElement!"); + } + //Validate entity $tmp = $this->validator->validate($entity); if (count($tmp) > 0) { //Log validation errors to global log. $name = $entity instanceof AbstractStructuralDBElement ? $entity->getFullPath() : $entity->getName(); + if (trim($name) === '') { + $name = 'Row ' . (string) $key; + } + $errors[$name] = [ 'violations' => $tmp, 'entity' => $entity, @@ -244,9 +288,9 @@ class EntityImporter * * @param File $file the file that should be used for importing * @param array $options options for the import process - * @param AbstractNamedDBElement[] $entities The imported entities are returned in this array + * @param-out AbstractNamedDBElement[] $entities The imported entities are returned in this array * - * @return array An associative array containing an ConstraintViolationList and the entity name as key are returned, + * @return array An associative array containing an ConstraintViolationList and the entity name as key are returned, * if an error happened during validation. When everything was successfully, the array should be empty. */ public function importFileAndPersistToDB(File $file, array $options = [], array &$entities = []): array @@ -278,8 +322,9 @@ class EntityImporter * * @param File $file the file that should be used for importing * @param array $options options for the import process + * @param-out array $errors * - * @return array an array containing the deserialized elements + * @return AbstractNamedDBElement[] an array containing the deserialized elements */ public function importFile(File $file, array $options = [], array &$errors = []): array { @@ -297,20 +342,13 @@ class EntityImporter //Convert the extension to lower case $extension = strtolower($extension); - switch ($extension) { - case 'json': - return 'json'; - case 'xml': - return 'xml'; - case 'csv': - case 'tsv': - return 'csv'; - case 'yaml': - case 'yml': - return 'yaml'; - default: - return null; - } + return match ($extension) { + 'json' => 'json', + 'xml' => 'xml', + 'csv', 'tsv' => 'csv', + 'yaml', 'yml' => 'yaml', + default => null, + }; } /** @@ -319,7 +357,7 @@ class EntityImporter * @param iterable $entities the list of entities that should be fixed * @param AbstractStructuralDBElement|null $parent the parent, to which the entity should be set */ - protected function correctParentEntites(iterable $entities, AbstractStructuralDBElement $parent = null): void + protected function correctParentEntites(iterable $entities, ?AbstractStructuralDBElement $parent = null): void { foreach ($entities as $entity) { /** @var AbstractStructuralDBElement $entity */ diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/MySQLDumpXMLConverter.php b/src/Services/ImportExportSystem/PartKeeprImporter/MySQLDumpXMLConverter.php index 9d583d2a..f221ee89 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/MySQLDumpXMLConverter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/MySQLDumpXMLConverter.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ImportExportSystem\PartKeeprImporter; class MySQLDumpXMLConverter @@ -69,7 +71,10 @@ class MySQLDumpXMLConverter //Iterate over all nodes and convert them to arrays foreach ($tables as $table) { - $table_data[$table->getAttribute('name')] = $this->convertTableToArray($table); + //Normalize the table name to lowercase. On Linux filesystems the tables sometimes contain uppercase letters + //However we expect the table names to be lowercase in the further steps + $table_name = strtolower($table->getAttribute('name')); + $table_data[$table_name] = $this->convertTableToArray($table); } return $table_data; @@ -104,4 +109,4 @@ class MySQLDumpXMLConverter return $row_data; } -} \ No newline at end of file +} diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php index 52732a44..1f842c23 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKDatastructureImporter.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ImportExportSystem\PartKeeprImporter; -use App\Doctrine\Purger\ResetAutoIncrementORMPurger; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\ManufacturerAttachment; -use App\Entity\Attachments\StorelocationAttachment; -use App\Entity\Base\AbstractDBElement; -use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\Contracts\TimeStampableInterface; -use App\Entity\Parameters\PartParameter; +use App\Entity\Attachments\StorageLocationAttachment; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Part; -use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadataInfo; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use function \count; +use function count; /** * This service is used to import the datastructures (categories, manufacturers, etc.) from a PartKeepr export. @@ -87,7 +81,7 @@ class PKDatastructureImporter $this->em->flush(); - return count($distributor_data); + return is_countable($distributor_data) ? count($distributor_data) : 0; } public function importManufacturers(array $data): int @@ -130,7 +124,7 @@ class PKDatastructureImporter $this->importAttachments($data, 'manufacturericlogo', Manufacturer::class, 'manufacturer_id', ManufacturerAttachment::class); - return count($manufacturer_data); + return is_countable($manufacturer_data) ? count($manufacturer_data) : 0; } public function importPartUnits(array $data): int @@ -151,7 +145,7 @@ class PKDatastructureImporter $this->em->flush(); - return count($partunit_data); + return is_countable($partunit_data) ? count($partunit_data) : 0; } public function importCategories(array $data): int @@ -180,15 +174,11 @@ class PKDatastructureImporter } $this->em->flush(); - return count($partcategory_data); + return is_countable($partcategory_data) ? count($partcategory_data) : 0; } /** * The common import functions for footprints and storeloactions - * @param array $data - * @param string $target_class - * @param string $data_prefix - * @return int */ private function importElementsWithCategory(array $data, string $target_class, string $data_prefix): int { @@ -249,7 +239,7 @@ class PKDatastructureImporter $this->em->flush(); - return count($footprint_data) + count($footprintcategory_data); + return (is_countable($footprint_data) ? count($footprint_data) : 0) + (is_countable($footprintcategory_data) ? count($footprintcategory_data) : 0); } public function importFootprints(array $data): int @@ -265,11 +255,10 @@ class PKDatastructureImporter public function importStorelocations(array $data): int { - $count = $this->importElementsWithCategory($data, Storelocation::class, 'storagelocation'); + $count = $this->importElementsWithCategory($data, StorageLocation::class, 'storagelocation'); - //Footprints have both attachments and images - $this->importAttachments($data, 'storagelocationimage', Storelocation::class, 'footprint_id', StorelocationAttachment::class); + $this->importAttachments($data, 'storagelocationimage', StorageLocation::class, 'storageLocation_id', StorageLocationAttachment::class); return $count; } -} \ No newline at end of file +} diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php index 3a1ae3c4..f36e48ce 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelper.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ImportExportSystem\PartKeeprImporter; use App\Doctrine\Purger\ResetAutoIncrementORMPurger; @@ -28,30 +30,24 @@ use Doctrine\ORM\EntityManagerInterface; */ class PKImportHelper { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $em) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $em; } /** * Purges the database tables for the import, so that all data can be created from scratch. * Existing users and groups are not purged. * This is needed to avoid ID collisions. - * @return void */ public function purgeDatabaseForImport(): void { - //Versions with "" are needed !! - $purger = new ResetAutoIncrementORMPurger($this->em, ['users', '"users"', 'groups', '"groups"', 'u2f_keys', 'internal', 'migration_versions']); + //We use the ResetAutoIncrementORMPurger to reset the auto increment values of the tables. Also it normalizes table names before checking for exclusion. + $purger = new ResetAutoIncrementORMPurger($this->em, ['users', 'groups', 'u2f_keys', 'internal', 'migration_versions']); $purger->purge(); } /** * Extracts the current database schema version from the PartKeepr XML dump. - * @param array $data - * @return string */ public function getDatabaseSchemaVersion(array $data): string { @@ -64,11 +60,10 @@ class PKImportHelper /** * Checks that the database schema of the PartKeepr XML dump is compatible with the importer - * @param array $data * @return bool True if the schema is compatible, false otherwise */ public function checkVersion(array $data): bool { return $this->getDatabaseSchemaVersion($data) === '20170601175559'; } -} \ No newline at end of file +} diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php index 8941502f..1e4cd3ba 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKImportHelperTrait.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ImportExportSystem\PartKeeprImporter; +use Doctrine\ORM\Id\AssignedGenerator; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentType; @@ -27,7 +30,7 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Contracts\TimeStampableInterface; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** @@ -45,12 +48,11 @@ trait PKImportHelperTrait * @param array $attachment_row The attachment row from the PartKeepr database * @param string $target_class The target class for the attachment * @param string $type The type of the attachment (attachment or image) - * @return Attachment * @throws \Exception */ protected function convertAttachmentDataToEntity(array $attachment_row, string $target_class, string $type): Attachment { - //By default we use the cached version + //By default, we use the cached version if (!$this->import_attachment_type) { //Get the import attachment type $this->import_attachment_type = $this->em->getRepository(AttachmentType::class)->findOneBy([ @@ -84,7 +86,15 @@ trait PKImportHelperTrait //Determine file extension (if the extension is empty, we use the original extension) if (empty($attachment_row['extension'])) { - $attachment_row['extension'] = pathinfo($attachment_row['originalname'], PATHINFO_EXTENSION); + //Use mime type to determine the extension like PartKeepr does in legacy implementation (just use the second part of the mime type) + //See UploadedFile.php:291 in PartKeepr (https://github.com/partkeepr/PartKeepr/blob/f6176c3354b24fa39ac8bc4328ee0df91de3d5b6/src/PartKeepr/UploadedFileBundle/Entity/UploadedFile.php#L291) + if (!empty ($attachment_row['mimetype'])) { + $attachment_row['extension'] = explode('/', (string) $attachment_row['mimetype'])[1]; + } else { + //If the mime type is empty, we use the original extension + $attachment_row['extension'] = pathinfo((string) $attachment_row['originalname'], PATHINFO_EXTENSION); + } + } //Determine file path @@ -95,7 +105,7 @@ trait PKImportHelperTrait //Next comes the filename plus extension $path .= '/'.$attachment_row['filename'].'.'.$attachment_row['extension']; - $attachment->setPath($path); + $attachment->setInternalPath($path); return $attachment; } @@ -103,11 +113,10 @@ trait PKImportHelperTrait /** * Imports the attachments from the given data * @param array $data The PartKeepr database - * @param string $table_name The table name for the attachments (if it contain "image", it will be treated as an image) + * @param string $table_name The table name for the attachments (if it contains "image", it will be treated as an image) * @param string $target_class The target class (e.g. Part) * @param string $target_id_field The field name where the target ID is stored * @param string $attachment_class The attachment class (e.g. PartAttachment) - * @return void */ protected function importAttachments(array $data, string $table_name, string $target_class, string $target_id_field, string $attachment_class): void { @@ -148,12 +157,9 @@ trait PKImportHelperTrait /** * Assigns the parent to the given entity, using the numerical IDs from the imported data. - * @param string $class - * @param int|string $element_id - * @param int|string $parent_id * @return AbstractStructuralDBElement The structural element that was modified (with $element_id) */ - protected function setParent(string $class, $element_id, $parent_id): AbstractStructuralDBElement + protected function setParent(string $class, int|string $element_id, int|string $parent_id): AbstractStructuralDBElement { $element = $this->em->find($class, (int) $element_id); if (!$element) { @@ -176,7 +182,6 @@ trait PKImportHelperTrait /** * Sets the given field of the given entity to the entity with the given ID. - * @return AbstractDBElement */ protected function setAssociationField(AbstractDBElement $element, string $field, string $other_class, $other_id): AbstractDBElement { @@ -197,21 +202,14 @@ trait PKImportHelperTrait /** * Set the ID of an entity to a specific value. Must be called before persisting the entity, but before flushing. - * @param AbstractDBElement $element - * @param int|string $id - * @return void */ - protected function setIDOfEntity(AbstractDBElement $element, $id): void + protected function setIDOfEntity(AbstractDBElement $element, int|string $id): void { - if (!is_int($id) && !is_string($id)) { - throw new \InvalidArgumentException('ID must be an integer or string'); - } - $id = (int) $id; - $metadata = $this->em->getClassMetadata(get_class($element)); - $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); - $metadata->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator()); + $metadata = $this->em->getClassMetadata($element::class); + $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE); + $metadata->setIdGenerator(new AssignedGenerator()); $metadata->setIdentifierValues($element, ['id' => $id]); } @@ -220,10 +218,10 @@ trait PKImportHelperTrait * @return void * @throws \Exception */ - protected function setCreationDate(TimeStampableInterface $entity, ?string $datetime_str) + protected function setCreationDate(TimeStampableInterface $entity, ?string $datetime_str): void { - if ($datetime_str) { - $date = new \DateTime($datetime_str); + if ($datetime_str !== null && $datetime_str !== '' && $datetime_str !== '0000-00-00 00:00:00') { + $date = new \DateTimeImmutable($datetime_str); } else { $date = null; //Null means "now" at persist time } @@ -233,4 +231,27 @@ trait PKImportHelperTrait $property->setAccessible(true); $property->setValue($entity, $date); } -} \ No newline at end of file + + /** + * Gets the SI prefix factor for the given prefix ID. + * Used to convert a value from the PartKeepr database to the PartDB database. + * @param array $data + * @param int $prefix_id + * @return float + */ + protected function getSIPrefixFactor(array $data, int $prefix_id): float + { + if ($prefix_id === 0) { + return 1.0; + } + + $prefixes = $data['siprefix']; + foreach ($prefixes as $prefix) { + if ((int) $prefix['id'] === $prefix_id) { + return (int)$prefix['base'] ** (int)$prefix['exponent']; + } + } + + throw new \RuntimeException(sprintf('Could not find SI prefix with ID %s', $prefix_id)); + } +} diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php index a19422e1..fafde29a 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKOptionalImporter.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ImportExportSystem\PartKeeprImporter; use App\Entity\Attachments\ProjectAttachment; @@ -30,7 +32,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** - * This service is used to other non mandatory data from a PartKeepr export. + * This service is used to other non-mandatory data from a PartKeepr export. * You have to import the datastructures and parts first to use project import! */ class PKOptionalImporter @@ -45,7 +47,6 @@ class PKOptionalImporter /** * Import the projects from the given data. - * @param array $data * @return int The number of imported projects */ public function importProjects(array $data): int @@ -99,12 +100,11 @@ class PKOptionalImporter $this->importAttachments($data, 'projectattachment', Project::class, 'project_id', ProjectAttachment::class); - return count($projects_data); + return is_countable($projects_data) ? count($projects_data) : 0; } /** * Import the users from the given data. - * @param array $data * @return int The number of imported users */ public function importUsers(array $data): int @@ -116,7 +116,7 @@ class PKOptionalImporter //All imported users get assigned to the "PartKeepr Users" group $group_users = $this->em->find(Group::class, 3); $group = $this->em->getRepository(Group::class)->findOneBy(['name' => 'PartKeepr Users', 'parent' => $group_users]); - if (!$group) { + if ($group === null) { $group = new Group(); $group->setName('PartKeepr Users'); $group->setParent($group_users); @@ -144,6 +144,6 @@ class PKOptionalImporter $this->em->flush(); - return count($users_data); + return is_countable($users_data) ? count($users_data) : 0; } -} \ No newline at end of file +} diff --git a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php index 5065d04c..9dd67233 100644 --- a/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php +++ b/src/Services/ImportExportSystem/PartKeeprImporter/PKPartImporter.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ImportExportSystem\PartKeeprImporter; use App\Entity\Attachments\PartAttachment; @@ -28,12 +30,14 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use Brick\Math\BigDecimal; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Intl\Currencies; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** @@ -43,7 +47,7 @@ class PKPartImporter { use PKImportHelperTrait; - public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor) + public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $propertyAccessor, private readonly string $base_currency) { $this->em = $em; $this->propertyAccessor = $propertyAccessor; @@ -71,7 +75,7 @@ class PKPartImporter $entity->setComment('This part represents a former metapart in PartKeepr. It is not supported in Part-DB yet. And you can most likely delete it.'); $entity->setTags('partkeepr-imported,partkeepr-metapart'); } else { - $entity->setMinAmount($part['minStockLevel'] ?? 0); + $entity->setMinAmount((float) ($part['minStockLevel'] ?? 0)); if (!empty($part['internalPartNumber'])) { $entity->setIpn($part['internalPartNumber']); } @@ -88,8 +92,8 @@ class PKPartImporter //Create a part lot to store the stock level and location $lot = new PartLot(); - $lot->setAmount($part['stockLevel'] ?? 0); - $this->setAssociationField($lot, 'storage_location', Storelocation::class, $part['storageLocation_id']); + $lot->setAmount((float) ($part['stockLevel'] ?? 0)); + $this->setAssociationField($lot, 'storage_location', StorageLocation::class, $part['storageLocation_id']); $entity->addPartLot($lot); //For partCondition, productionsRemarks and Status, create a custom parameter @@ -123,7 +127,7 @@ class PKPartImporter //Import attachments $this->importAttachments($data, 'partattachment', Part::class, 'part_id', PartAttachment::class); - return count($part_data); + return is_countable($part_data) ? count($part_data) : 0; } protected function importPartManufacturers(array $data): void @@ -141,7 +145,8 @@ class PKPartImporter throw new \RuntimeException(sprintf('Could not find part with ID %s', $partmanufacturer['part_id'])); } $manufacturer = $this->em->find(Manufacturer::class, (int) $partmanufacturer['manufacturer_id']); - if (!$manufacturer) { + //The manufacturer is optional + if (!$manufacturer instanceof Manufacturer && !empty($partmanufacturer['manufacturer_id'])) { throw new \RuntimeException(sprintf('Could not find manufacturer with ID %s', $partmanufacturer['manufacturer_id'])); } $part->setManufacturer($manufacturer); @@ -168,14 +173,24 @@ class PKPartImporter $entity->setName($name); $entity->setValueText($partparameter['stringValue'] ?? ''); - $entity->setUnit($this->getUnitSymbol($data, (int) $partparameter['unit_id'])); + if ($partparameter['unit_id'] !== null && (int) $partparameter['unit_id'] !== 0) { + $entity->setUnit($this->getUnitSymbol($data, (int)$partparameter['unit_id'])); + } else { + $entity->setUnit(""); + } - $entity->setValueMin($partparameter['normalizedMinValue'] ?? null); - $entity->setValueTypical($partparameter['normalizedValue'] ?? null); - $entity->setValueMax($partparameter['normalizedMaxValue'] ?? null); + if ($partparameter['value'] !== null) { + $entity->setValueTypical((float) $partparameter['value'] * $this->getSIPrefixFactor($data, (int) $partparameter['siPrefix_id'])); + } + if ($partparameter['minimumValue'] !== null) { + $entity->setValueMin((float) $partparameter['minimumValue'] * $this->getSIPrefixFactor($data, (int) $partparameter['minSiPrefix_id'])); + } + if ($partparameter['maximumValue'] !== null) { + $entity->setValueMax((float) $partparameter['maximumValue'] * $this->getSIPrefixFactor($data, (int) $partparameter['maxSiPrefix_id'])); + } $part = $this->em->find(Part::class, (int) $partparameter['part_id']); - if (!$part) { + if (!$part instanceof Part) { throw new \RuntimeException(sprintf('Could not find part with ID %s', $partparameter['part_id'])); } @@ -185,6 +200,35 @@ class PKPartImporter $this->em->flush(); } + /** + * Returns the currency for the given ISO code. If the currency does not exist, it is created. + * This function returns null if the ISO code is the base currency. + */ + protected function getOrCreateCurrency(string $currency_iso_code): ?Currency + { + //Normalize ISO code + $currency_iso_code = strtoupper($currency_iso_code); + + //We do not have a currency for the base currency to be consistent with prices without currencies + if ($currency_iso_code === $this->base_currency) { + return null; + } + + $currency = $this->em->getRepository(Currency::class)->findOneBy([ + 'iso_code' => $currency_iso_code, + ]); + + if ($currency === null) { + $currency = new Currency(); + $currency->setIsoCode($currency_iso_code); + $currency->setName(Currencies::getName($currency_iso_code)); + $this->em->persist($currency); + $this->em->flush(); + } + + return $currency; + } + protected function importOrderdetails(array $data): void { if (!isset($data['partdistributor'])) { @@ -194,12 +238,12 @@ class PKPartImporter foreach ($data['partdistributor'] as $partdistributor) { //Retrieve the part $part = $this->em->find(Part::class, (int) $partdistributor['part_id']); - if (!$part) { + if (!$part instanceof Part) { throw new \RuntimeException(sprintf('Could not find part with ID %s', $partdistributor['part_id'])); } //Retrieve the distributor $supplier = $this->em->find(Supplier::class, (int) $partdistributor['distributor_id']); - if (!$supplier) { + if (!$supplier instanceof Supplier) { throw new \RuntimeException(sprintf('Could not find supplier with ID %s', $partdistributor['distributor_id'])); } @@ -211,7 +255,7 @@ class PKPartImporter } elseif (!empty($partdistributor['orderNumber']) && !empty($partdistributor['sku'])) { $spn = $partdistributor['orderNumber'] . ' (' . $partdistributor['sku'] . ')'; } else { - $spn = 'PartKeepr Import'; + $spn = ''; } $orderdetail = $this->em->getRepository(Orderdetail::class)->findOneBy([ @@ -221,33 +265,44 @@ class PKPartImporter ]); //When no orderdetail exists, create one - if (!$orderdetail) { + if ($orderdetail === null) { $orderdetail = new Orderdetail(); $orderdetail->setSupplier($supplier); $orderdetail->setSupplierpartnr($spn); $part->addOrderdetail($orderdetail); + $this->em->persist($orderdetail); } - //Add the price information to the orderdetail - if (!empty($partdistributor['price'])) { + //Add the price information to the orderdetail (only if the price is not zero, as this was a placeholder in PartKeepr) + if (!empty($partdistributor['price']) && !BigDecimal::of($partdistributor['price'])->isZero()) { $pricedetail = new Pricedetail(); $orderdetail->addPricedetail($pricedetail); //Partkeepr stores the price per item, we need to convert it to the price per packaging unit $price_per_item = BigDecimal::of($partdistributor['price']); - $pricedetail->setPrice($price_per_item->multipliedBy($partdistributor['packagingUnit'])); - $pricedetail->setPriceRelatedQuantity($partdistributor['packagingUnit'] ?? 1); + $packaging_unit = (float) ($partdistributor['packagingUnit'] ?? 1); + $pricedetail->setPrice($price_per_item->multipliedBy($packaging_unit)); + $pricedetail->setPriceRelatedQuantity($packaging_unit); + //We have to set the minimum discount quantity to the packaging unit (PartKeepr does not know this concept) + //But in Part-DB the minimum discount qty have to be unique across a orderdetail + $pricedetail->setMinDiscountQuantity($packaging_unit); + + //Set the currency of the price + if (!empty($partdistributor['currency'])) { + $currency = $this->getOrCreateCurrency($partdistributor['currency']); + $pricedetail->setCurrency($currency); + } + + $this->em->persist($pricedetail); } - //We have to flush the changes in every loop, so the find function can find newly created entities $this->em->flush(); + //Clear the entity manager to improve performance + $this->em->clear(); } } /** * Returns the (parameter) unit symbol for the given ID. - * @param array $data - * @param int $id - * @return string */ protected function getUnitSymbol(array $data, int $id): string { @@ -259,4 +314,4 @@ class PKPartImporter throw new \RuntimeException(sprintf('Could not find unit with ID %s', $id)); } -} \ No newline at end of file +} diff --git a/src/Services/InfoProviderSystem/DTOs/FileDTO.php b/src/Services/InfoProviderSystem/DTOs/FileDTO.php new file mode 100644 index 00000000..0d1db76a --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/FileDTO.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\DTOs; + +/** + * This DTO represents a file that can be downloaded from a URL. + * This could be a datasheet, a 3D model, a picture or similar. + * @see \App\Tests\Services\InfoProviderSystem\DTOs\FileDTOTest + */ +class FileDTO +{ + /** + * @var string The URL where to get this file + */ + public readonly string $url; + + /** + * @param string $url The URL where to get this file + * @param string|null $name Optionally the name of this file + */ + public function __construct( + string $url, + public readonly ?string $name = null, + ) { + //Find all occurrences of non URL safe characters and replace them with their URL encoded version. + //We only want to replace characters which can not have a valid meaning in a URL (what would break the URL). + //Digikey provided some wrong URLs with a ^ in them, which is not a valid URL character. (https://github.com/Part-DB/Part-DB-server/issues/521) + $this->url = preg_replace_callback('/[^a-zA-Z0-9_\-.$+!*();\/?:@=&#%]/', static fn($matches) => rawurlencode($matches[0]), $url); + } + + +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php b/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php new file mode 100644 index 00000000..0b54d1a9 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php @@ -0,0 +1,165 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\DTOs; + +/** + * This DTO represents a parameter of a part (similar to the AbstractParameter entity). + * This could be a voltage, a current, a temperature or similar. + * @see \App\Tests\Services\InfoProviderSystem\DTOs\ParameterDTOTest + */ +class ParameterDTO +{ + public function __construct( + public readonly string $name, + public readonly ?string $value_text = null, + public readonly ?float $value_typ = null, + public readonly ?float $value_min = null, + public readonly ?float $value_max = null, + public readonly ?string $unit = null, + public readonly ?string $symbol = null, + public readonly ?string $group = null, + ) { + + } + + /** + * This function tries to decide on the value, if it is a numerical value (which is then stored in one of the value_*) fields) or a text value (which is stored in value_text). + * It is possible to give ranges like 1...2 (or 1~2) here, which will be parsed as value_min: 1.0, value_max: 2.0. + * + * For certain expressions (like ranges) the unit is automatically extracted from the value, if no unit is given + * @TODO Rework that, so that the difference between parseValueField and parseValueIncludingUnit is clearer or merge them + * @param string $name + * @param string|float $value + * @param string|null $unit + * @param string|null $symbol + * @param string|null $group + * @return self + */ + public static function parseValueField(string $name, string|float $value, ?string $unit = null, ?string $symbol = null, ?string $group = null): self + { + //If we encounter something like 2.5@text, then put the "@text" into text_value and continue with the number parsing + if (is_string($value) && preg_match('/^(.+)(@.+)$/', $value, $matches) === 1) { + $value = $matches[1]; + $value_text = $matches[2]; + } else { + $value_text = null; + } + + //If the value is just a number, we assume thats the typical value + if (is_float($value) || is_numeric($value)) { + return new self($name, value_text: $value_text, value_typ: (float) $value, unit: $unit, symbol: $symbol, + group: $group); + } + + //If the attribute contains ".." or "..." or a tilde we assume it is a range + if (preg_match('/(\.{2,3}|~)/', $value) === 1) { + $parts = preg_split('/\s*(\.{2,3}|~)\s*/', $value); + if (count($parts) === 2) { + //Try to extract number and unit from value (allow leading +) + if ($unit === null || trim($unit) === '') { + [$number, $unit] = self::splitIntoValueAndUnit(ltrim($parts[0], " +")) ?? [$parts[0], null]; + } else { + $number = $parts[0]; + } + + // If the second part has some extra info, we'll save that into value_text + if (!empty($unit) && preg_match('/^(.+' . preg_quote($unit, '/') . ')\s*(.+)$/', $parts[1], $matches) > 0) { + $parts[1] = $matches[1]; + $value_text2 = $matches[2]; + } else { + $value_text2 = null; + } + [$number2, $unit2] = self::splitIntoValueAndUnit(ltrim($parts[1], " +")) ?? [$parts[1], $unit]; + + //If both parts have the same unit and both values are numerical, we'll save it as range + if ($unit === $unit2 && is_numeric($number) && is_numeric($number2)) { + return new self(name: $name, value_text: $value_text2, value_min: (float) $number, + value_max: (float) $number2, unit: $unit, symbol: $symbol, group: $group); + } + } + //If it's a plus/minus value, we'll also treat it as a range + } elseif (str_starts_with($value, '±')) { + [$number, $unit] = self::splitIntoValueAndUnit(ltrim($value, " ±")) ?? [ltrim($value, ' ±'), $unit]; + if (is_numeric($number)) { + return new self(name: $name, value_min: -abs((float) $number), value_max: abs((float) $number), unit: $unit, symbol: $symbol, group: $group); + } + } + + //If no unit was passed to us, try to extract it from the value + if (empty($unit)) { + [$value, $unit] = self::splitIntoValueAndUnit($value) ?? [$value, null]; + } + + //Were we successful in trying to reduce the value to a number? + if ($value_text !== null && is_numeric($value)) { + return new self($name, value_text: $value_text, value_typ: (float) $value, unit: $unit, symbol: $symbol, + group: $group); + } + + return new self($name, value_text: $value.$value_text, unit: $unit, symbol: $symbol, group: $group); + } + + /** + * This function tries to decide on the value, if it is a numerical value (which is then stored in one of the value_*) fields) or a text value (which is stored in value_text). + * It also tries to extract the unit from the value field (so 3kg will be parsed as value_typ: 3.0, unit: kg). + * Ranges like 1...2 will be parsed as value_min: 1.0, value_max: 2.0. + * @param string $name + * @param string|float $value + * @param string|null $symbol + * @param string|null $group + * @return self + */ + public static function parseValueIncludingUnit(string $name, string|float $value, ?string $symbol = null, ?string $group = null): self + { + //Try to extract unit from value + $unit = null; + if (is_string($value)) { + [$number, $unit] = self::splitIntoValueAndUnit($value) ?? [$value, null]; + + return self::parseValueField(name: $name, value: $number, unit: $unit, symbol: $symbol, group: $group); + } + + //Otherwise we assume that no unit is given + return self::parseValueField(name: $name, value: $value, unit: null, symbol: $symbol, group: $group); + } + + /** + * Splits the given value into a value and a unit part if possible. + * If the value is not in the expected format, null is returned. + * @param string $value The value to split + * @return array|null An array with the value and the unit part or null if the value is not in the expected format + * @phpstan-return array{0: string, 1: string}|null + */ + public static function splitIntoValueAndUnit(string $value): ?array + { + if (preg_match('/^(?-?[0-9\.]+)\s*(?[%Ωµ°℃a-z_\/]+\s?\w{0,4})$/iu', $value, $matches)) { + $value = $matches['value']; + $unit = $matches['unit']; + + return [$value, $unit]; + } + + return null; + } +} diff --git a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php new file mode 100644 index 00000000..9f365f1e --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\DTOs; + +use App\Entity\Parts\ManufacturingStatus; + +/** + * This DTO represents a part with all its details. + */ +class PartDetailDTO extends SearchResultDTO +{ + public function __construct( + string $provider_key, + string $provider_id, + string $name, + string $description, + ?string $category = null, + ?string $manufacturer = null, + ?string $mpn = null, + ?string $preview_image_url = null, + ?ManufacturingStatus $manufacturing_status = null, + ?string $provider_url = null, + ?string $footprint = null, + public readonly ?string $notes = null, + /** @var FileDTO[]|null */ + public readonly ?array $datasheets = null, + /** @var FileDTO[]|null */ + public readonly ?array $images = null, + /** @var ParameterDTO[]|null */ + public readonly ?array $parameters = null, + /** @var PurchaseInfoDTO[]|null */ + public readonly ?array $vendor_infos = null, + /** The mass of the product in grams */ + public readonly ?float $mass = null, + /** The URL to the product on the website of the manufacturer */ + public readonly ?string $manufacturer_product_url = null, + ) { + parent::__construct( + provider_key: $provider_key, + provider_id: $provider_id, + name: $name, + description: $description, + category: $category, + manufacturer: $manufacturer, + mpn: $mpn, + preview_image_url: $preview_image_url, + manufacturing_status: $manufacturing_status, + provider_url: $provider_url, + footprint: $footprint, + ); + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php new file mode 100644 index 00000000..f1eb28f7 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php @@ -0,0 +1,59 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\DTOs; + +use Brick\Math\BigDecimal; + +/** + * This DTO represents a price for a single unit in a certain discount range + */ +class PriceDTO +{ + private readonly BigDecimal $price_as_big_decimal; + + public function __construct( + /** @var float The minimum amount that needs to get ordered for this price to be valid */ + public readonly float $minimum_discount_amount, + /** @var string The price as string (with .) */ + public readonly string $price, + /** @var string The currency of the used ISO code of this price detail */ + public readonly ?string $currency_iso_code, + /** @var bool If the price includes tax */ + public readonly ?bool $includes_tax = true, + /** @var float the price related quantity */ + public readonly ?float $price_related_quantity = 1.0, + ) + { + $this->price_as_big_decimal = BigDecimal::of($this->price); + } + + /** + * Gets the price as BigDecimal + * @return BigDecimal + */ + public function getPriceAsBigDecimal(): BigDecimal + { + return $this->price_as_big_decimal; + } +} diff --git a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php new file mode 100644 index 00000000..bcd8be43 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\DTOs; + +/** + * This DTO represents a purchase information for a part (supplier name, order number and prices). + * @see \App\Tests\Services\InfoProviderSystem\DTOs\PurchaseInfoDTOTest + */ +class PurchaseInfoDTO +{ + public function __construct( + public readonly string $distributor_name, + public readonly string $order_number, + /** @var PriceDTO[] */ + public readonly array $prices, + /** @var string|null An url to the product page of the vendor */ + public readonly ?string $product_url = null, + ) + { + //Ensure that the prices are PriceDTO instances + foreach ($this->prices as $price) { + if (!$price instanceof PriceDTO) { + throw new \InvalidArgumentException('The prices array must only contain PriceDTO instances'); + } + } + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php new file mode 100644 index 00000000..28943702 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php @@ -0,0 +1,74 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\DTOs; + +use App\Entity\Parts\ManufacturingStatus; + +/** + * This DTO represents a search result for a part. + * @see \App\Tests\Services\InfoProviderSystem\DTOs\SearchResultDTOTest + */ +class SearchResultDTO +{ + /** @var string|null An URL to a preview image */ + public readonly ?string $preview_image_url; + /** @var FileDTO|null The preview image as FileDTO object */ + public readonly ?FileDTO $preview_image_file; + + public function __construct( + /** @var string The provider key (e.g. "digikey") */ + public readonly string $provider_key, + /** @var string The ID which identifies the part in the provider system */ + public readonly string $provider_id, + /** @var string The name of the part */ + public readonly string $name, + /** @var string A short description of the part */ + public readonly string $description, + /** @var string|null The category the distributor assumes for the part */ + public readonly ?string $category = null, + /** @var string|null The manufacturer of the part */ + public readonly ?string $manufacturer = null, + /** @var string|null The manufacturer part number */ + public readonly ?string $mpn = null, + /** @var string|null An URL to a preview image */ + ?string $preview_image_url = null, + /** @var ManufacturingStatus|null The manufacturing status of the part */ + public readonly ?ManufacturingStatus $manufacturing_status = null, + /** @var string|null A link to the part on the providers page */ + public readonly ?string $provider_url = null, + /** @var string|null A footprint representation of the providers page */ + public readonly ?string $footprint = null, + ) { + + if ($preview_image_url !== null) { + //Utilize the escaping mechanism of FileDTO to ensure that the preview image URL is correctly encoded + //See issue #521: https://github.com/Part-DB/Part-DB-server/issues/521 + $this->preview_image_file = new FileDTO($preview_image_url); + $this->preview_image_url = $this->preview_image_file->url; + } else { + $this->preview_image_file = null; + $this->preview_image_url = null; + } + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php new file mode 100644 index 00000000..40f69498 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php @@ -0,0 +1,356 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem; + +use App\Entity\Attachments\AttachmentType; +use App\Entity\Attachments\PartAttachment; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\PartParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\InfoProviderReference; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\ManufacturingStatus; +use App\Entity\Parts\Part; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; +use App\Repository\Parts\CategoryRepository; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use Doctrine\ORM\EntityManagerInterface; + +/** + * This class converts DTOs to entities which can be persisted in the DB + * @see \App\Tests\Services\InfoProviderSystem\DTOtoEntityConverterTest + */ +final class DTOtoEntityConverter +{ + private const TYPE_DATASHEETS_NAME = 'Datasheet'; + private const TYPE_IMAGE_NAME = 'Image'; + + public function __construct(private readonly EntityManagerInterface $em, private readonly string $base_currency) + { + } + + /** + * Converts the given DTO to a PartParameter entity. + * @param ParameterDTO $dto + * @param PartParameter $entity The entity to apply the DTO on. If null a new entity will be created + * @return PartParameter + */ + public function convertParameter(ParameterDTO $dto, PartParameter $entity = new PartParameter()): PartParameter + { + $entity->setName($dto->name); + $entity->setValueText($dto->value_text ?? ''); + $entity->setValueTypical($dto->value_typ); + $entity->setValueMin($dto->value_min); + $entity->setValueMax($dto->value_max); + $entity->setUnit($dto->unit ?? ''); + $entity->setSymbol($dto->symbol ?? ''); + $entity->setGroup($dto->group ?? ''); + + return $entity; + } + + /** + * Converts the given DTO to a Pricedetail entity. + * @param PriceDTO $dto + * @param Pricedetail $entity + * @return Pricedetail + */ + public function convertPrice(PriceDTO $dto, Pricedetail $entity = new Pricedetail()): Pricedetail + { + $entity->setMinDiscountQuantity($dto->minimum_discount_amount); + $entity->setPrice($dto->getPriceAsBigDecimal()); + $entity->setPriceRelatedQuantity($dto->price_related_quantity); + + //Currency TODO + if ($dto->currency_iso_code !== null) { + $entity->setCurrency($this->getCurrency($dto->currency_iso_code)); + } else { + $entity->setCurrency(null); + } + + return $entity; + } + + /** + * Converts the given DTO to an orderdetail entity. + */ + public function convertPurchaseInfo(PurchaseInfoDTO $dto, Orderdetail $entity = new Orderdetail()): Orderdetail + { + $entity->setSupplierpartnr($dto->order_number); + $entity->setSupplierProductUrl($dto->product_url ?? ''); + + $entity->setSupplier($this->getOrCreateEntityNonNull(Supplier::class, $dto->distributor_name)); + foreach ($dto->prices as $price) { + $entity->addPricedetail($this->convertPrice($price)); + } + + return $entity; + } + + /** + * Converts the given DTO to an Attachment entity. + * @param FileDTO $dto + * @param AttachmentType $type The type which should be used for the attachment + * @param PartAttachment $entity + * @return PartAttachment + */ + public function convertFile(FileDTO $dto, AttachmentType $type, PartAttachment $entity = new PartAttachment()): PartAttachment + { + $entity->setURL($dto->url); + + $entity->setAttachmentType($type); + + //If no name is given, try to extract the name from the URL + if ($dto->name === null || $dto->name === '' || $dto->name === '0') { + $entity->setName($this->getAttachmentNameFromURL($dto->url)); + } else { + $entity->setName($dto->name); + } + + return $entity; + } + + private function getAttachmentNameFromURL(string $url): string + { + return basename(parse_url($url, PHP_URL_PATH)); + } + + /** + * Converts a PartDetailDTO to a Part entity + * @param PartDetailDTO $dto + * @param Part $entity The part entity to fill + * @return Part + */ + public function convertPart(PartDetailDTO $dto, Part $entity = new Part()): Part + { + $entity->setName($dto->name); + $entity->setDescription($dto->description ?? ''); + $entity->setComment($dto->notes ?? ''); + + $entity->setMass($dto->mass); + + //Try to map the category to an existing entity (but never create a new one) + if ($dto->category) { + //@phpstan-ignore-next-line For some reason php does not recognize the repo returns a category + $entity->setCategory($this->em->getRepository(Category::class)->findForInfoProvider($dto->category)); + } + + $entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer)); + $entity->setFootprint($this->getOrCreateEntity(Footprint::class, $dto->footprint)); + + $entity->setManufacturerProductNumber($dto->mpn ?? ''); + $entity->setManufacturingStatus($dto->manufacturing_status ?? ManufacturingStatus::NOT_SET); + $entity->setManufacturerProductURL($dto->manufacturer_product_url ?? ''); + + //Set the provider reference on the part + $entity->setProviderReference(InfoProviderReference::fromPartDTO($dto)); + + $param_groups = []; + + //Add parameters + foreach ($dto->parameters ?? [] as $parameter) { + $new_param = $this->convertParameter($parameter); + + $key = $new_param->getName() . '##' . $new_param->getGroup(); + //If there is already an parameter with the same name and group, rename the new parameter, by suffixing a number + if (count($param_groups[$key] ?? []) > 0) { + $new_param->setName($new_param->getName() . ' (' . (count($param_groups[$key]) + 1) . ')'); + } + + $param_groups[$key][] = $new_param; + + $entity->addParameter($new_param); + } + + //Add preview image + $image_type = $this->getImageType(); + + if ($dto->preview_image_url) { + $preview_image = new PartAttachment(); + $preview_image->setURL($dto->preview_image_url); + $preview_image->setName('Main image'); + $preview_image->setAttachmentType($image_type); + + $entity->addAttachment($preview_image); + $entity->setMasterPictureAttachment($preview_image); + } + + $attachments_grouped = []; + + //Add other images + $images = $this->files_unique($dto->images ?? []); + foreach ($images as $image) { + //Ensure that the image is not the same as the preview image + if ($image->url === $dto->preview_image_url) { + continue; + } + + $attachment = $this->convertFile($image, $image_type); + + $attachments_grouped[$attachment->getName()][] = $attachment; + if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) { + $attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()]) + 1) . ')'); + } + + + $entity->addAttachment($attachment); + } + + //Add datasheets + $datasheet_type = $this->getDatasheetType(); + $datasheets = $this->files_unique($dto->datasheets ?? []); + foreach ($datasheets as $datasheet) { + $attachment = $this->convertFile($datasheet, $datasheet_type); + + $attachments_grouped[$attachment->getName()][] = $attachment; + if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) { + $attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()])) . ')'); + } + + $entity->addAttachment($attachment); + } + + //Add orderdetails and prices + foreach ($dto->vendor_infos ?? [] as $vendor_info) { + $entity->addOrderdetail($this->convertPurchaseInfo($vendor_info)); + } + + return $entity; + } + + /** + * Returns the given array of files with all duplicates removed. + * @param FileDTO[] $files + * @return FileDTO[] + */ + private function files_unique(array $files): array + { + $unique = []; + //We use the URL and name as unique identifier. If two file DTO have the same URL and name, they are considered equal + //and get filtered out, if it already exists in the array + foreach ($files as $file) { + //Skip already existing files, to preserve the order. The second condition ensure that we keep the version with a name over the one without a name + if (isset($unique[$file->url]) && $unique[$file->url]->name !== null) { + continue; + } + $unique[$file->url] = $file; + } + + return array_values($unique); + } + + /** + * Get the existing entity of the given class with the given name or create it if it does not exist. + * If the name is null, null is returned. + * @template T of AbstractStructuralDBElement + * @param string $class + * @phpstan-param class-string $class + * @param string|null $name + * @return AbstractStructuralDBElement|null + * @phpstan-return T|null + */ + private function getOrCreateEntity(string $class, ?string $name): ?AbstractStructuralDBElement + { + //Fall through to make converting easier + if ($name === null) { + return null; + } + + return $this->getOrCreateEntityNonNull($class, $name); + } + + /** + * Get the existing entity of the given class with the given name or create it if it does not exist. + * @template T of AbstractStructuralDBElement + * @param string $class The class of the entity to create + * @phpstan-param class-string $class + * @param string $name The name of the entity to create + * @return AbstractStructuralDBElement + * @phpstan-return T + */ + private function getOrCreateEntityNonNull(string $class, string $name): AbstractStructuralDBElement + { + return $this->em->getRepository($class)->findOrCreateForInfoProvider($name); + } + + /** + * Returns the currency entity for the given ISO code or create it if it does not exist + * @param string $iso_code + * @return Currency|null + */ + private function getCurrency(string $iso_code): ?Currency + { + //Check if the currency is the base currency (then we can just return null) + if ($iso_code === $this->base_currency) { + return null; + } + + return $this->em->getRepository(Currency::class)->findOrCreateByISOCode($iso_code); + } + + /** + * Returns the attachment type used for datasheets or creates it if it does not exist + * @return AttachmentType + */ + private function getDatasheetType(): AttachmentType + { + /** @var AttachmentType $tmp */ + $tmp = $this->em->getRepository(AttachmentType::class)->findOrCreateForInfoProvider(self::TYPE_DATASHEETS_NAME); + + //If the entity was newly created, set the file filter + if ($tmp->getID() === null) { + $tmp->setFiletypeFilter('application/pdf'); + $tmp->setAlternativeNames(self::TYPE_DATASHEETS_NAME); + } + + return $tmp; + } + + /** + * Returns the attachment type used for datasheets or creates it if it does not exist + * @return AttachmentType + */ + private function getImageType(): AttachmentType + { + /** @var AttachmentType $tmp */ + $tmp = $this->em->getRepository(AttachmentType::class)->findOrCreateForInfoProvider(self::TYPE_IMAGE_NAME); + + //If the entity was newly created, set the file filter + if ($tmp->getID() === null) { + $tmp->setFiletypeFilter('image/*'); + $tmp->setAlternativeNames(self::TYPE_IMAGE_NAME); + } + + return $tmp; + } + +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/ExistingPartFinder.php b/src/Services/InfoProviderSystem/ExistingPartFinder.php new file mode 100644 index 00000000..762c1517 --- /dev/null +++ b/src/Services/InfoProviderSystem/ExistingPartFinder.php @@ -0,0 +1,77 @@ +findAllExisting($dto); + return count($results) > 0 ? $results[0] : null; + } + + /** + * Returns all existing local parts that match the search result. + * If no part is found, return an empty array. + * @param SearchResultDTO $dto + * @return Part[] + */ + public function findAllExisting(SearchResultDTO $dto): array + { + $qb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); + $qb->select('part') + ->leftJoin('part.manufacturer', 'manufacturer') + ->Orwhere($qb->expr()->andX( + 'part.providerReference.provider_key = :providerKey', + 'part.providerReference.provider_id = :providerId', + )) + + //Or the manufacturer (allowing for alternative names) and the MPN (or part name) must match + ->OrWhere( + $qb->expr()->andX( + $qb->expr()->orX( + "ILIKE(manufacturer.name, :manufacturerName) = TRUE", + "ILIKE(manufacturer.alternative_names, :manufacturerAltNames) = TRUE", + ), + $qb->expr()->orX( + "ILIKE(part.manufacturer_product_number, :mpn) = TRUE", + "ILIKE(part.name, :mpn) = TRUE", + ) + ) + ) + ; + + $qb->setParameter('providerKey', $dto->provider_key); + $qb->setParameter('providerId', $dto->provider_id); + + $qb->setParameter('manufacturerName', $dto->manufacturer); + $qb->setParameter('manufacturerAltNames', '%'.$dto->manufacturer.'%'); + $qb->setParameter('mpn', $dto->mpn); + + return $qb->getQuery()->getResult(); + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php new file mode 100644 index 00000000..0eb74642 --- /dev/null +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -0,0 +1,148 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem; + +use App\Entity\Parts\Part; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; + +final class PartInfoRetriever +{ + + private const CACHE_DETAIL_EXPIRATION = 60 * 60 * 24 * 4; // 4 days + private const CACHE_RESULT_EXPIRATION = 60 * 60 * 24 * 4; // 7 days + + public function __construct(private readonly ProviderRegistry $provider_registry, + private readonly DTOtoEntityConverter $dto_to_entity_converter, private readonly CacheInterface $partInfoCache, + #[Autowire(param: "kernel.debug")] + private readonly bool $debugMode = false) + { + } + + /** + * Search for a keyword in the given providers. The results can be cached + * @param string[]|InfoProviderInterface[] $providers A list of providers to search in, either as provider keys or as provider instances + * @param string $keyword The keyword to search for + * @return SearchResultDTO[] The search results + */ + public function searchByKeyword(string $keyword, array $providers): array + { + $results = []; + + foreach ($providers as $provider) { + if (is_string($provider)) { + $provider = $this->provider_registry->getProviderByKey($provider); + } + + //Ensure that the provider is active + if (!$provider->isActive()) { + throw new \RuntimeException("The provider with key {$provider->getProviderKey()} is not active!"); + } + + if (!$provider instanceof InfoProviderInterface) { + throw new \InvalidArgumentException("The provider must be either a provider key or a provider instance!"); + } + + /** @noinspection SlowArrayOperationsInLoopInspection */ + $results = array_merge($results, $this->searchInProvider($provider, $keyword)); + } + + return $results; + } + + /** + * Search for a keyword in the given provider. The result is cached for 7 days. + * @return SearchResultDTO[] + */ + protected function searchInProvider(InfoProviderInterface $provider, string $keyword): array + { + //Generate key and escape reserved characters from the provider id + $escaped_keyword = urlencode($keyword); + return $this->partInfoCache->get("search_{$provider->getProviderKey()}_{$escaped_keyword}", function (ItemInterface $item) use ($provider, $keyword) { + //Set the expiration time + $item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 1); + + return $provider->searchByKeyword($keyword); + }); + } + + /** + * Retrieves the details for a part from the given provider with the given (provider) part id. + * The result is cached for 4 days. + * @param string $provider_key + * @param string $part_id + * @return PartDetailDTO + */ + public function getDetails(string $provider_key, string $part_id): PartDetailDTO + { + $provider = $this->provider_registry->getProviderByKey($provider_key); + + //Ensure that the provider is active + if (!$provider->isActive()) { + throw new \RuntimeException("The provider with key $provider_key is not active!"); + } + + //Generate key and escape reserved characters from the provider id + $escaped_part_id = urlencode($part_id); + return $this->partInfoCache->get("details_{$provider_key}_{$escaped_part_id}", function (ItemInterface $item) use ($provider, $part_id) { + //Set the expiration time + $item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 1); + + return $provider->getDetails($part_id); + }); + } + + /** + * Retrieves the details for a part, based on the given search result. + * @param SearchResultDTO $search_result + * @return PartDetailDTO + */ + public function getDetailsForSearchResult(SearchResultDTO $search_result): PartDetailDTO + { + return $this->getDetails($search_result->provider_key, $search_result->provider_id); + } + + /** + * Converts the given DTO to a part entity + * @return Part + */ + public function dtoToPart(PartDetailDTO $search_result): Part + { + return $this->createPart($search_result->provider_key, $search_result->provider_id); + } + + /** + * Use the given details to create a part entity + */ + public function createPart(string $provider_key, string $part_id): Part + { + $details = $this->getDetails($provider_key, $part_id); + + return $this->dto_to_entity_converter->convertPart($details); + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/ProviderRegistry.php b/src/Services/InfoProviderSystem/ProviderRegistry.php new file mode 100644 index 00000000..f6c398d2 --- /dev/null +++ b/src/Services/InfoProviderSystem/ProviderRegistry.php @@ -0,0 +1,142 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem; + +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; + +/** + * This class keeps track of all registered info providers and allows to find them by their key + * @see \App\Tests\Services\InfoProviderSystem\ProviderRegistryTest + */ +final class ProviderRegistry +{ + /** + * @var InfoProviderInterface[] The info providers index by their keys + * @phpstan-var array + */ + private array $providers_by_name = []; + + /** + * @var InfoProviderInterface[] The enabled providers indexed by their keys + */ + private array $providers_active = []; + + /** + * @var InfoProviderInterface[] The disabled providers indexed by their keys + */ + private array $providers_disabled = []; + + /** + * @var bool Whether the registry has been initialized + */ + private bool $initialized = false; + + /** + * @param iterable $providers + */ + public function __construct(private readonly iterable $providers) + { + //We do not initialize the structures here, because we do not want to do unnecessary work + //We do this lazy on the first call to getProviders() + } + + /** + * Initializes the registry, we do this lazy to avoid unnecessary work, on construction, which is always called + * even if the registry is not used + * @return void + */ + private function initStructures(): void + { + foreach ($this->providers as $provider) { + $key = $provider->getProviderKey(); + + if (isset($this->providers_by_name[$key])) { + throw new \LogicException("Provider with key $key already registered"); + } + + $this->providers_by_name[$key] = $provider; + if ($provider->isActive()) { + $this->providers_active[$key] = $provider; + } else { + $this->providers_disabled[$key] = $provider; + } + } + + $this->initialized = true; + } + + /** + * Returns an array of all registered providers (enabled and disabled) + * @return InfoProviderInterface[] + */ + public function getProviders(): array + { + if (!$this->initialized) { + $this->initStructures(); + } + + return $this->providers_by_name; + } + + /** + * Returns the provider identified by the given key + * @param string $key + * @return InfoProviderInterface + * @throws \InvalidArgumentException If the provider with the given key does not exist + */ + public function getProviderByKey(string $key): InfoProviderInterface + { + if (!$this->initialized) { + $this->initStructures(); + } + + return $this->providers_by_name[$key] ?? throw new \InvalidArgumentException("Provider with key $key not found"); + } + + /** + * Returns an array of all active providers + * @return InfoProviderInterface[] + */ + public function getActiveProviders(): array + { + if (!$this->initialized) { + $this->initStructures(); + } + + return $this->providers_active; + } + + /** + * Returns an array of all disabled providers + * @return InfoProviderInterface[] + */ + public function getDisabledProviders(): array + { + if (!$this->initialized) { + $this->initStructures(); + } + + return $this->providers_disabled; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php new file mode 100644 index 00000000..b20368ce --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -0,0 +1,314 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Services\OAuth\OAuthTokenManager; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class DigikeyProvider implements InfoProviderInterface +{ + + private const OAUTH_APP_NAME = 'ip_digikey_oauth'; + + //Sandbox:'https://sandbox-api.digikey.com'; (you need to change it in knpu/oauth2-client-bundle config too) + private const BASE_URI = 'https://api.digikey.com'; + + private const VENDOR_NAME = 'DigiKey'; + + private readonly HttpClientInterface $digikeyClient; + + /** + * A list of parameter IDs, that are always assumed as text only and will never be converted to a numerical value. + * This allows to fix issues like #682, where the "Supplier Device Package" was parsed as a numerical value. + */ + private const TEXT_ONLY_PARAMETERS = [ + 1291, //Supplier Device Package + 39246, //Package / Case + ]; + + public function __construct(HttpClientInterface $httpClient, private readonly OAuthTokenManager $authTokenManager, + private readonly string $currency, private readonly string $clientId, + private readonly string $language, private readonly string $country) + { + //Create the HTTP client with some default options + $this->digikeyClient = $httpClient->withOptions([ + "base_uri" => self::BASE_URI, + "headers" => [ + "X-DIGIKEY-Client-Id" => $clientId, + "X-DIGIKEY-Locale-Site" => $this->country, + "X-DIGIKEY-Locale-Language" => $this->language, + "X-DIGIKEY-Locale-Currency" => $this->currency, + "X-DIGIKEY-Customer-Id" => 0, + ] + ]); + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'DigiKey', + 'description' => 'This provider uses the DigiKey API to search for parts.', + 'url' => 'https://www.digikey.com/', + 'oauth_app_name' => self::OAUTH_APP_NAME, + 'disabled_help' => 'Set the PROVIDER_DIGIKEY_CLIENT_ID and PROVIDER_DIGIKEY_SECRET env option and connect OAuth to enable.' + ]; + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ]; + } + + public function getProviderKey(): string + { + return 'digikey'; + } + + public function isActive(): bool + { + //The client ID has to be set and a token has to be available (user clicked connect) + return $this->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME); + } + + public function searchByKeyword(string $keyword): array + { + $request = [ + 'Keywords' => $keyword, + 'Limit' => 50, + 'Offset' => 0, + 'FilterOptionsRequest' => [ + 'MarketPlaceFilter' => 'ExcludeMarketPlace', + ], + ]; + + //$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [ + $response = $this->digikeyClient->request('POST', '/products/v4/search/keyword', [ + 'json' => $request, + 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) + ]); + + $response_array = $response->toArray(); + + + $result = []; + $products = $response_array['Products']; + foreach ($products as $product) { + foreach ($product['ProductVariations'] as $variation) { + $result[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $variation['DigiKeyProductNumber'], + name: $product['ManufacturerProductNumber'], + description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], + category: $this->getCategoryString($product), + manufacturer: $product['Manufacturer']['Name'] ?? null, + mpn: $product['ManufacturerProductNumber'], + preview_image_url: $product['PhotoUrl'] ?? null, + manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']), + provider_url: $product['ProductUrl'], + footprint: $variation['PackageType']['Name'], //Use the footprint field, to show the user the package type (Tape & Reel, etc., as digikey has many different package types) + ); + } + } + + return $result; + } + + public function getDetails(string $id): PartDetailDTO + { + $response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [ + 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) + ]); + + $response_array = $response->toArray(); + $product = $response_array['Product']; + + $footprint = null; + $parameters = $this->parametersToDTOs($product['Parameters'] ?? [], $footprint); + $media = $this->mediaToDTOs($id); + + // Get the price_breaks of the selected variation + $price_breaks = []; + foreach ($product['ProductVariations'] as $variation) { + if ($variation['DigiKeyProductNumber'] == $id) { + $price_breaks = $variation['StandardPricing'] ?? []; + break; + } + } + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $id, + name: $product['ManufacturerProductNumber'], + description: $product['Description']['DetailedDescription'] ?? $product['Description']['ProductDescription'], + category: $this->getCategoryString($product), + manufacturer: $product['Manufacturer']['Name'] ?? null, + mpn: $product['ManufacturerProductNumber'], + preview_image_url: $product['PhotoUrl'] ?? null, + manufacturing_status: $this->productStatusToManufacturingStatus($product['ProductStatus']['Id']), + provider_url: $product['ProductUrl'], + footprint: $footprint, + datasheets: $media['datasheets'], + images: $media['images'], + parameters: $parameters, + vendor_infos: $this->pricingToDTOs($price_breaks, $id, $product['ProductUrl']), + ); + } + + /** + * Converts the product status from the Digikey API to the manufacturing status used in Part-DB + * @param int|null $dk_status + * @return ManufacturingStatus|null + */ + private function productStatusToManufacturingStatus(?int $dk_status): ?ManufacturingStatus + { + // The V4 can use strings to get the status, but if you have changed the PROVIDER_DIGIKEY_LANGUAGE it will not match. + // Using the Id instead which should be fixed. + // + // The API is not well documented and the ID are not there yet, so were extracted using "trial and error". + // The 'Preliminary' id was not found in several categories so I was unable to extract it. Disabled for now. + return match ($dk_status) { + null => null, + 0 => ManufacturingStatus::ACTIVE, + 1 => ManufacturingStatus::DISCONTINUED, + 2, 4 => ManufacturingStatus::EOL, + 7 => ManufacturingStatus::NRFND, + //'Preliminary' => ManufacturingStatus::ANNOUNCED, + default => ManufacturingStatus::NOT_SET, + }; + } + + private function getCategoryString(array $product): string + { + $category = $product['Category']['Name']; + $sub_category = current($product['Category']['ChildCategories']); + + if ($sub_category) { + //Replace the ' - ' category separator with ' -> ' + $category = $category . ' -> ' . str_replace(' - ', ' -> ', $sub_category["Name"]); + } + + return $category; + } + + /** + * This function converts the "Parameters" part of the Digikey API response to an array of ParameterDTOs + * @param array $parameters + * @param string|null $footprint_name You can pass a variable by reference, where the name of the footprint will be stored + * @return ParameterDTO[] + */ + private function parametersToDTOs(array $parameters, string|null &$footprint_name = null): array + { + $results = []; + + $footprint_name = null; + + foreach ($parameters as $parameter) { + if ($parameter['ParameterId'] === 1291) { //Meaning "Manufacturer given footprint" + $footprint_name = $parameter['ValueText']; + } + + if (in_array(trim((string) $parameter['ValueText']), ['', '-'], true)) { + continue; + } + + //If the parameter was marked as text only, then we do not try to parse it as a numerical value + if (in_array($parameter['ParameterId'], self::TEXT_ONLY_PARAMETERS, true)) { + $results[] = new ParameterDTO(name: $parameter['ParameterText'], value_text: $parameter['ValueText']); + } else { //Otherwise try to parse it as a numerical value + $results[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterText'], $parameter['ValueText']); + } + } + + return $results; + } + + /** + * Converts the pricing (StandardPricing field) from the Digikey API to an array of PurchaseInfoDTOs + * @param array $price_breaks + * @param string $order_number + * @param string $product_url + * @return PurchaseInfoDTO[] + */ + private function pricingToDTOs(array $price_breaks, string $order_number, string $product_url): array + { + $prices = []; + + foreach ($price_breaks as $price_break) { + $prices[] = new PriceDTO(minimum_discount_amount: $price_break['BreakQuantity'], price: (string) $price_break['UnitPrice'], currency_iso_code: $this->currency); + } + + return [ + new PurchaseInfoDTO(distributor_name: self::VENDOR_NAME, order_number: $order_number, prices: $prices, product_url: $product_url) + ]; + } + + /** + * @param string $id The Digikey product number, to get the media for + * @return FileDTO[][] + * @phpstan-return array + */ + private function mediaToDTOs(string $id): array + { + $datasheets = []; + $images = []; + + $response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/media', [ + 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) + ]); + + $media_array = $response->toArray(); + + foreach ($media_array['MediaLinks'] as $media_link) { + $file = new FileDTO(url: $media_link['Url'], name: $media_link['Title']); + + switch ($media_link['MediaType']) { + case 'Datasheets': + $datasheets[] = $file; + break; + case 'Product Photos': + $images[] = $file; + break; + } + } + + return [ + 'datasheets' => $datasheets, + 'images' => $images, + ]; + } + +} diff --git a/src/Services/InfoProviderSystem/Providers/Element14Provider.php b/src/Services/InfoProviderSystem/Providers/Element14Provider.php new file mode 100644 index 00000000..b942b929 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php @@ -0,0 +1,310 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use Composer\CaBundle\CaBundle; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class Element14Provider implements InfoProviderInterface +{ + + private const ENDPOINT_URL = 'https://api.element14.com/catalog/products'; + private const API_VERSION_NUMBER = '1.4'; + private const NUMBER_OF_RESULTS = 20; + + public const DISTRIBUTOR_NAME = 'Farnell'; + + private const COMPLIANCE_ATTRIBUTES = ['euEccn', 'hazardous', 'MSL', 'productTraceability', 'rohsCompliant', + 'rohsPhthalatesCompliant', 'SVHC', 'tariffCode', 'usEccn', 'hazardCode']; + + private readonly HttpClientInterface $element14Client; + + public function __construct(HttpClientInterface $element14Client, private readonly string $api_key, private readonly string $store_id) + { + /* We use the mozilla CA from the composer ca bundle directly, as some debian systems seems to have problems + * with the SSL.COM CA, element14 uses. See https://github.com/Part-DB/Part-DB-server/issues/866 + * + * This is a workaround until the issue is resolved in debian (or never). + * As this only affects this provider, this should have no negative impact and the CA bundle is still secure. + */ + $this->element14Client = $element14Client->withOptions([ + 'cafile' => CaBundle::getBundledCaBundlePath(), + ]); + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Farnell element14', + 'description' => 'This provider uses the Farnell element14 API to search for parts.', + 'url' => 'https://www.element14.com/', + 'disabled_help' => 'Configure the API key in the PROVIDER_ELEMENT14_KEY environment variable to enable.' + ]; + } + + public function getProviderKey(): string + { + return 'element14'; + } + + public function isActive(): bool + { + return $this->api_key !== ''; + } + + /** + * @param string $term + * @return PartDetailDTO[] + */ + private function queryByTerm(string $term): array + { + $response = $this->element14Client->request('GET', self::ENDPOINT_URL, [ + 'query' => [ + 'term' => $term, + 'storeInfo.id' => $this->store_id, + 'resultsSettings.offset' => 0, + 'resultsSettings.numberOfResults' => self::NUMBER_OF_RESULTS, + 'resultsSettings.responseGroup' => 'large', + 'callInfo.apiKey' => $this->api_key, + 'callInfo.responseDataFormat' => 'json', + 'versionNumber' => self::API_VERSION_NUMBER, + ], + ]); + + $arr = $response->toArray(); + if (isset($arr['keywordSearchReturn'])) { + $products = $arr['keywordSearchReturn']['products'] ?? []; + } elseif (isset($arr['premierFarnellPartNumberReturn'])) { + $products = $arr['premierFarnellPartNumberReturn']['products'] ?? []; + } else { + throw new \RuntimeException('Unknown response format'); + } + + $result = []; + + foreach ($products as $product) { + $result[] = new PartDetailDTO( + provider_key: $this->getProviderKey(), provider_id: $product['sku'], + name: $product['translatedManufacturerPartNumber'], + description: $this->displayNameToDescription($product['displayName'], $product['translatedManufacturerPartNumber']), + manufacturer: $product['vendorName'] ?? $product['brandName'] ?? null, + mpn: $product['translatedManufacturerPartNumber'], + preview_image_url: $this->toImageUrl($product['image'] ?? null), + manufacturing_status: $this->releaseStatusCodeToManufacturingStatus($product['releaseStatusCode'] ?? null), + provider_url: $product['productURL'], + notes: $product['productOverview']['description'] ?? null, + datasheets: $this->parseDataSheets($product['datasheets'] ?? null), + parameters: $this->attributesToParameters($product['attributes'] ?? null), + vendor_infos: $this->pricesToVendorInfo($product['sku'], $product['prices'] ?? [], $product['productURL']), + + ); + } + + return $result; + } + + /** + * @param array|null $datasheets + * @return FileDTO[]|null Array of FileDTOs + */ + private function parseDataSheets(?array $datasheets): ?array + { + if ($datasheets === null || count($datasheets) === 0) { + return null; + } + + $result = []; + foreach ($datasheets as $datasheet) { + $result[] = new FileDTO(url: $datasheet['url'], name: $datasheet['description']); + } + + return $result; + } + + private function toImageUrl(?array $image): ?string + { + if ($image === null || count($image) === 0) { + return null; + } + + //See Constructing an Image URL: https://partner.element14.com/docs/Product_Search_API_REST__Description + $locale = 'en_GB'; + if ($image['vrntPath'] === 'nio/') { + $locale = 'en_US'; + } + + return 'https://' . $this->store_id . '/productimages/standard/' . $locale . $image['baseName']; + } + + /** + * Converts the price array to a VendorInfoDTO array to be used in the PartDetailDTO + * @param string $sku + * @param array $prices + * @return array + */ + private function pricesToVendorInfo(string $sku, array $prices, string $product_url): array + { + $price_dtos = []; + + foreach ($prices as $price) { + $price_dtos[] = new PriceDTO( + minimum_discount_amount: $price['from'], + price: (string) $price['cost'], + currency_iso_code: $this->getUsedCurrency(), + includes_tax: false, + ); + } + + return [ + new PurchaseInfoDTO( + distributor_name: self::DISTRIBUTOR_NAME, + order_number: $sku, + prices: $price_dtos, + product_url: $product_url + ) + ]; + } + + public function getUsedCurrency(): string + { + //Decide based on the shop ID + return match ($this->store_id) { + 'bg.farnell.com', 'at.farnell.com', 'si.farnell.com', 'sk.farnell.com', 'ro.farnell.com', 'pt.farnell.com', 'nl.farnell.com', 'be.farnell.com', 'lv.farnell.com', 'lt.farnell.com', 'it.farnell.com', 'fr.farnell.com', 'fi.farnell.com', 'ee.farnell.com', 'es.farnell.com', 'ie.farnell.com', 'cpcireland.farnell.com', 'de.farnell.com' => 'EUR', + 'cz.farnell.com' => 'CZK', + 'dk.farnell.com' => 'DKK', + 'ch.farnell.com' => 'CHF', + 'cpc.farnell.com', 'uk.farnell.com', 'onecall.farnell.com', 'export.farnell.com' => 'GBP', + 'il.farnell.com', 'www.newark.com' => 'USD', + 'hu.farnell.com' => 'HUF', + 'no.farnell.com' => 'NOK', + 'pl.farnell.com' => 'PLN', + 'ru.farnell.com' => 'RUB', + 'se.farnell.com' => 'SEK', + 'tr.farnell.com' => 'TRY', + 'canada.newark.com' => 'CAD', + 'mexico.newark.com' => 'MXN', + 'cn.element14.com' => 'CNY', + 'au.element14.com' => 'AUD', + 'nz.element14.com' => 'NZD', + 'hk.element14.com' => 'HKD', + 'sg.element14.com' => 'SGD', + 'my.element14.com' => 'MYR', + 'ph.element14.com' => 'PHP', + 'th.element14.com' => 'THB', + 'in.element14.com' => 'INR', + 'tw.element14.com' => 'TWD', + 'kr.element14.com' => 'KRW', + 'vn.element14.com' => 'VND', + default => throw new \RuntimeException('Unknown store ID: ' . $this->store_id) + }; + } + + /** + * @param array|null $attributes + * @return ParameterDTO[] + */ + private function attributesToParameters(?array $attributes): array + { + $result = []; + + foreach ($attributes as $attribute) { + $group = null; + + //Check if the attribute is a compliance attribute, they get assigned to the compliance group + if (in_array($attribute['attributeLabel'], self::COMPLIANCE_ATTRIBUTES, true)) { + $group = 'Compliance'; + } + + //tariffCode is a special case, we prepend a # to prevent conversion to float + if (in_array($attribute['attributeLabel'], ['tariffCode', 'hazardCode'], true)) { + $attribute['attributeValue'] = '#' . $attribute['attributeValue']; + } + + $result[] = ParameterDTO::parseValueField(name: $attribute['attributeLabel'], value: $attribute['attributeValue'], unit: $attribute['attributeUnit'] ?? null, group: $group); + } + + return $result; + } + + private function displayNameToDescription(string $display_name, string $mpn): string + { + //Try to find the position of the '-' after the MPN + $pos = strpos($display_name, $mpn . ' - '); + if ($pos === false) { + return $display_name; + } + + //Remove the MPN and the '-' from the display name + return substr($display_name, $pos + strlen($mpn) + 3); + } + + private function releaseStatusCodeToManufacturingStatus(?int $releaseStatusCode): ?ManufacturingStatus + { + if ($releaseStatusCode === null) { + return null; + } + + return match ($releaseStatusCode) { + 1 => ManufacturingStatus::ANNOUNCED, + 2,4 => ManufacturingStatus::ACTIVE, + 6 => ManufacturingStatus::EOL, + 7 => ManufacturingStatus::DISCONTINUED, + default => ManufacturingStatus::NOT_SET + }; + } + + public function searchByKeyword(string $keyword): array + { + return $this->queryByTerm('any:' . $keyword); + } + + public function getDetails(string $id): PartDetailDTO + { + $tmp = $this->queryByTerm('id:' . $id); + if (count($tmp) === 0) { + throw new \RuntimeException('No part found with ID ' . $id); + } + + if (count($tmp) > 1) { + throw new \RuntimeException('Multiple parts found with ID ' . $id); + } + + return $tmp[0]; + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ]; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php new file mode 100644 index 00000000..30821bad --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php @@ -0,0 +1,81 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; + +interface InfoProviderInterface +{ + + /** + * Get information about this provider + * + * @return array An associative array with the following keys (? means optional): + * - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated + * - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated + * - logo?: The logo of the provider (e.g. "digikey.png") + * - url?: The url of the provider (e.g. "https://www.digikey.com") + * - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it + * - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown + * + * @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string } + */ + public function getProviderInfo(): array; + + /** + * Returns a unique key for this provider, which will be saved into the database + * and used to identify the provider + * @return string A unique key for this provider (e.g. "digikey") + */ + public function getProviderKey(): string; + + /** + * Checks if this provider is enabled or not (meaning that it can be used for searching) + * @return bool True if the provider is enabled, false otherwise + */ + public function isActive(): bool; + + /** + * Searches for a keyword and returns a list of search results + * @param string $keyword The keyword to search for + * @return SearchResultDTO[] A list of search results + */ + public function searchByKeyword(string $keyword): array; + + /** + * Returns detailed information about the part with the given id + * @param string $id + * @return PartDetailDTO + */ + public function getDetails(string $id): PartDetailDTO; + + /** + * A list of capabilities this provider supports (which kind of data it can provide). + * Not every part have to contain all of these data, but the provider should be able to provide them in general. + * Currently, this list is purely informational and not used in functional checks. + * @return ProviderCapabilities[] + */ + public function getCapabilities(): array; +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php new file mode 100755 index 00000000..d903a8dd --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -0,0 +1,366 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class LCSCProvider implements InfoProviderInterface +{ + + private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm'; + + public const DISTRIBUTOR_NAME = 'LCSC'; + + public function __construct(private readonly HttpClientInterface $lcscClient, private readonly string $currency, private readonly bool $enabled = true) + { + + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'LCSC', + 'description' => 'This provider uses the (unofficial) LCSC API to search for parts.', + 'url' => 'https://www.lcsc.com/', + 'disabled_help' => 'Set PROVIDER_LCSC_ENABLED to 1 (or true) in your environment variable config.' + ]; + } + + public function getProviderKey(): string + { + return 'lcsc'; + } + + // This provider is always active + public function isActive(): bool + { + return $this->enabled; + } + + /** + * @param string $id + * @return PartDetailDTO + */ + private function queryDetail(string $id): PartDetailDTO + { + $response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/product/detail", [ + 'headers' => [ + 'Cookie' => new Cookie('currencyCode', $this->currency) + ], + 'query' => [ + 'productCode' => $id, + ], + ]); + + $arr = $response->toArray(); + $product = $arr['result'] ?? null; + + if ($product === null) { + throw new \RuntimeException('Could not find product code: ' . $id); + } + + return $this->getPartDetail($product); + } + + /** + * @param string $url + * @return String + */ + private function getRealDatasheetUrl(?string $url): string + { + if ($url !== null && trim($url) !== '' && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) { + if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) { + $url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1]; + } + $response = $this->lcscClient->request('GET', $url, [ + 'headers' => [ + 'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html' + ], + ]); + if (preg_match('/(previewPdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) { + //HACKY: The URL string contains escaped characters like \u002F, etc. To decode it, the JSON decoding is reused + //See https://github.com/Part-DB/Part-DB-server/pull/582#issuecomment-2033125934 + $jsonObj = json_decode('{"' . $matches[1] . '": ' . $matches[2] . '}'); + $url = $jsonObj->previewPdfUrl; + } + } + return $url; + } + + /** + * @param string $term + * @return PartDetailDTO[] + */ + private function queryByTerm(string $term): array + { + $response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/search/global", [ + 'headers' => [ + 'Cookie' => new Cookie('currencyCode', $this->currency) + ], + 'query' => [ + 'keyword' => $term, + ], + ]); + + $arr = $response->toArray(); + + // Get products list + $products = $arr['result']['productSearchResultVO']['productList'] ?? []; + // Get product tip + $tipProductCode = $arr['result']['tipProductDetailUrlVO']['productCode'] ?? null; + + $result = []; + + // LCSC does not display LCSC codes in the search, instead taking you directly to the + // detailed product listing. It does so utilizing a product tip field. + // If product tip exists and there are no products in the product list try a detail query + if (count($products) === 0 && $tipProductCode !== null) { + $result[] = $this->queryDetail($tipProductCode); + } + + foreach ($products as $product) { + $result[] = $this->getPartDetail($product); + } + + return $result; + } + + /** + * Sanitizes a field by removing any HTML tags and other unwanted characters + * @param string|null $field + * @return string|null + */ + private function sanitizeField(?string $field): ?string + { + if ($field === null) { + return null; + } + + return strip_tags($field); + } + + + /** + * Takes a deserialized json object of the product and returns a PartDetailDTO + * @param array $product + * @return PartDetailDTO + */ + private function getPartDetail(array $product): PartDetailDTO + { + // Get product images in advance + $product_images = $this->getProductImages($product['productImages'] ?? null); + $product['productImageUrl'] ??= null; + + // If the product does not have a product image but otherwise has attached images, use the first one. + if (count($product_images) > 0) { + $product['productImageUrl'] ??= $product_images[0]->url; + } + + // LCSC puts HTML in footprints and descriptions sometimes randomly + $footprint = $product["encapStandard"] ?? null; + //If the footprint just consists of a dash, we'll assume it's empty + if ($footprint === '-') { + $footprint = null; + } + + //Build category by concatenating the catalogName and parentCatalogName + $category = $product['parentCatalogName'] ?? null; + if (isset($product['catalogName'])) { + $category = ($category ?? '') . ' -> ' . $product['catalogName']; + + // Replace the / with a -> for better readability + $category = str_replace('/', ' -> ', $category); + } + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $product['productCode'], + name: $product['productModel'], + description: $this->sanitizeField($product['productIntroEn']), + category: $this->sanitizeField($category ?? null), + manufacturer: $this->sanitizeField($product['brandNameEn'] ?? null), + mpn: $this->sanitizeField($product['productModel'] ?? null), + preview_image_url: $product['productImageUrl'], + manufacturing_status: null, + provider_url: $this->getProductShortURL($product['productCode']), + footprint: $this->sanitizeField($footprint), + datasheets: $this->getProductDatasheets($product['pdfUrl'] ?? null), + images: $product_images, + parameters: $this->attributesToParameters($product['paramVOList'] ?? []), + vendor_infos: $this->pricesToVendorInfo($product['productCode'], $this->getProductShortURL($product['productCode']), $product['productPriceList'] ?? []), + mass: $product['weight'] ?? null, + ); + } + + /** + * Converts the price array to a VendorInfoDTO array to be used in the PartDetailDTO + * @param string $sku + * @param string $url + * @param array $prices + * @return array + */ + private function pricesToVendorInfo(string $sku, string $url, array $prices): array + { + $price_dtos = []; + + foreach ($prices as $price) { + $price_dtos[] = new PriceDTO( + minimum_discount_amount: $price['ladder'], + price: $price['productPrice'], + currency_iso_code: $this->getUsedCurrency($price['currencySymbol']), + includes_tax: false, + ); + } + + return [ + new PurchaseInfoDTO( + distributor_name: self::DISTRIBUTOR_NAME, + order_number: $sku, + prices: $price_dtos, + product_url: $url, + ) + ]; + } + + /** + * Converts LCSC currency symbol to an ISO code. + * @param string $currency + * @return string + */ + private function getUsedCurrency(string $currency): string + { + //Decide based on the currency symbol + return match ($currency) { + 'US$', '$' => 'USD', + '€' => 'EUR', + 'A$' => 'AUD', + 'C$' => 'CAD', + '£' => 'GBP', + 'HK$' => 'HKD', + 'JP¥' => 'JPY', + 'RM' => 'MYR', + 'S$' => 'SGD', + '₽' => 'RUB', + 'kr' => 'SEK', + 'kr.' => 'DKK', + '₹' => 'INR', + //Fallback to the configured currency + default => $this->currency, + }; + } + + /** + * Returns a valid LCSC product short URL from product code + * @param string $product_code + * @return string + */ + private function getProductShortURL(string $product_code): string + { + return 'https://www.lcsc.com/product-detail/' . $product_code .'.html'; + } + + /** + * Returns a product datasheet FileDTO array from a single pdf url + * @param string $url + * @return FileDTO[] + */ + private function getProductDatasheets(?string $url): array + { + if ($url === null) { + return []; + } + + $realUrl = $this->getRealDatasheetUrl($url); + + return [new FileDTO($realUrl, null)]; + } + + /** + * Returns a FileDTO array with a list of product images + * @param array|null $images + * @return FileDTO[] + */ + private function getProductImages(?array $images): array + { + return array_map(static fn($image) => new FileDTO($image), $images ?? []); + } + + /** + * @param array|null $attributes + * @return ParameterDTO[] + */ + private function attributesToParameters(?array $attributes): array + { + $result = []; + + foreach ($attributes as $attribute) { + + //Skip this attribute if it's empty + if (in_array(trim((string) $attribute['paramValueEn']), ['', '-'], true)) { + continue; + } + + $result[] = ParameterDTO::parseValueIncludingUnit(name: $attribute['paramNameEn'], value: $attribute['paramValueEn'], group: null); + } + + return $result; + } + + public function searchByKeyword(string $keyword): array + { + return $this->queryByTerm($keyword); + } + + public function getDetails(string $id): PartDetailDTO + { + $tmp = $this->queryByTerm($id); + if (count($tmp) === 0) { + throw new \RuntimeException('No part found with ID ' . $id); + } + + if (count($tmp) > 1) { + throw new \RuntimeException('Multiple parts found with ID ' . $id); + } + + return $tmp[0]; + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::FOOTPRINT, + ]; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/MouserProvider.php b/src/Services/InfoProviderSystem/Providers/MouserProvider.php new file mode 100644 index 00000000..90bad263 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php @@ -0,0 +1,350 @@ +. + */ + +/* +* This file provide an interface with the Mouser API V2 (also compatible with the V1) +* +* Copyright (C) 2023 Pasquale D'Orsi (https://github.com/pdo59) +* +* TODO: Obtain an API keys with an US Mouser user (currency $) and test the result of prices +* +*/ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + + +class MouserProvider implements InfoProviderInterface +{ + + private const ENDPOINT_URL = 'https://api.mouser.com/api/v2/search'; + + public const DISTRIBUTOR_NAME = 'Mouser'; + + public function __construct( + private readonly HttpClientInterface $mouserClient, + private readonly string $api_key, + private readonly string $language, + private readonly string $options, + private readonly int $search_limit + ) { + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Mouser', + 'description' => 'This provider uses the Mouser API to search for parts.', + 'url' => 'https://www.mouser.com/', + 'disabled_help' => 'Configure the API key in the PROVIDER_MOUSER_KEY environment variable to enable.' + ]; + } + + public function getProviderKey(): string + { + return 'mouser'; + } + + public function isActive(): bool + { + return $this->api_key !== ''; + } + + public function searchByKeyword(string $keyword): array + { + /* + SearchByKeywordRequest description: + Search parts by keyword and return a maximum of 50 parts. + + keyword* string + Used for keyword part search. + + records integer($int32) + Used to specify how many records the method should return. + + startingRecord integer($int32) + Indicates where in the total recordset the return set should begin. + From the startingRecord, the number of records specified will be returned up to the end of the recordset. + This is useful for paging through the complete recordset of parts matching keyword. + + + searchOptions string + Optional. + If not provided, the default is None. + Refers to options supported by the search engine. + Only one value at a time is supported. + Available options: None | Rohs | InStock | RohsAndInStock - can use string representations or integer IDs: 1[None] | 2[Rohs] | 4[InStock] | 8[RohsAndInStock]. + + searchWithYourSignUpLanguage string + Optional. + If not provided, the default is false. + Used when searching for keywords in the language specified when you signed up for Search API. + Can use string representation: true. + { + "SearchByKeywordRequest": { + "keyword": "BC557", + "records": 0, + "startingRecord": 0, + "searchOptions": "", + "searchWithYourSignUpLanguage": "" + } + } + */ + + $response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/keyword", [ + 'query' => [ + 'apiKey' => $this->api_key, + ], + 'json' => [ + 'SearchByKeywordRequest' => [ + 'keyword' => $keyword, + 'records' => $this->search_limit, //self::NUMBER_OF_RESULTS, + 'startingRecord' => 0, + 'searchOptions' => $this->options, + 'searchWithYourSignUpLanguage' => $this->language, + ] + ], + ]); + + return $this->responseToDTOArray($response); + } + + public function getDetails(string $id): PartDetailDTO + { + /* + SearchByPartRequest description: + Search parts by part number and return a maximum of 50 parts. + + mouserPartNumber string + Used to search parts by the specific Mouser part number with a maximum input of 10 part numbers, separated by a pipe symbol for the search. + Each part number must be a minimum of 3 characters and a maximum of 40 characters. For example: 494-JANTX2N2222A|610-2N2222-TL|637-2N2222A + + partSearchOptions string + Optional. + If not provided, the default is None. Refers to options supported by the search engine. Only one value at a time is supported. + The following values are valid: None | Exact - can use string representations or integer IDs: 1[None] | 2[Exact] + + { + "SearchByPartRequest": { + "mouserPartNumber": "string", + "partSearchOptions": "string" + } + } + */ + + $response = $this->mouserClient->request('POST', self::ENDPOINT_URL."/partnumber", [ + 'query' => [ + 'apiKey' => $this->api_key, + ], + 'json' => [ + 'SearchByPartRequest' => [ + 'mouserPartNumber' => $id, + 'partSearchOptions' => 2 + ] + ], + ]); + $tmp = $this->responseToDTOArray($response); + + //Ensure that we have exactly one result + if (count($tmp) === 0) { + throw new \RuntimeException('No part found with ID '.$id); + } + + //Manually filter out the part with the correct ID + $tmp = array_filter($tmp, fn(PartDetailDTO $part) => $part->provider_id === $id); + if (count($tmp) === 0) { + throw new \RuntimeException('No part found with ID '.$id); + } + if (count($tmp) > 1) { + throw new \RuntimeException('Multiple parts found with ID '.$id); + } + + return reset($tmp); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ]; + } + + + /** + * @param ResponseInterface $response + * @return PartDetailDTO[] + */ + private function responseToDTOArray(ResponseInterface $response): array + { + $arr = $response->toArray(); + + if (isset($arr['SearchResults'])) { + $products = $arr['SearchResults']['Parts'] ?? []; + } else { + throw new \RuntimeException('Unknown response format: ' .json_encode($arr, JSON_THROW_ON_ERROR)); + } + + $result = []; + foreach ($products as $product) { + + //Check if we have a valid product number. We assume that a product number, must have at least 4 characters + //Otherwise filter it out + if (strlen($product['MouserPartNumber']) < 4) { + continue; + } + + //Check if we have a mass field available + $mass = null; + if (isset($product['UnitWeightKg']['UnitWeight'])) { + $mass = (float) $product['UnitWeightKg']['UnitWeight']; + //The mass is given in kg, we want it in g + $mass *= 1000; + } + + + $result[] = new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $product['MouserPartNumber'], + name: $product['ManufacturerPartNumber'], + description: $product['Description'], + category: $product['Category'], + manufacturer: $product['Manufacturer'], + mpn: $product['ManufacturerPartNumber'], + preview_image_url: $product['ImagePath'], + manufacturing_status: $this->releaseStatusCodeToManufacturingStatus( + $product['LifecycleStatus'] ?? null, + (int) ($product['AvailabilityInStock'] ?? 0) + ), + provider_url: $product['ProductDetailUrl'], + datasheets: $this->parseDataSheets($product['DataSheetUrl'] ?? null, + $product['MouserPartNumber'] ?? null), + vendor_infos: $this->pricingToDTOs($product['PriceBreaks'] ?? [], $product['MouserPartNumber'], + $product['ProductDetailUrl']), + mass: $mass, + ); + } + return $result; + } + + + private function parseDataSheets(?string $sheetUrl, ?string $sheetName): ?array + { + if ($sheetUrl === null || $sheetUrl === '' || $sheetUrl === '0') { + return null; + } + $result = []; + $result[] = new FileDTO(url: $sheetUrl, name: $sheetName); + return $result; + } + + /* + * Mouser API price is a string in the form "n[.,]nnn[.,] currency" + * then this convert it to a number + * Austria has a format like "€ 2,10" + */ + private function priceStrToFloat($val): float + { + //Remove any character that is not a number, dot or comma (like currency symbols) + $val = preg_replace('/[^0-9.,]/', '', $val); + + //Trim the string + $val = trim($val); + + //Convert commas to dots + $val = str_replace(",", ".", $val); + //Remove any dot that is not the last one (to avoid problems with thousands separators) + $val = preg_replace('/\.(?=.*\.)/', '', $val); + return (float)$val; + } + + /** + * Converts the pricing (StandardPricing field) from the Mouser API to an array of PurchaseInfoDTOs + * @param array $price_breaks + * @param string $order_number + * @param string $product_url + * @return PurchaseInfoDTO[] + */ + private function pricingToDTOs(array $price_breaks, string $order_number, string $product_url): array + { + $prices = []; + + foreach ($price_breaks as $price_break) { + $number = $this->priceStrToFloat($price_break['Price']); + $prices[] = new PriceDTO( + minimum_discount_amount: $price_break['Quantity'], + price: (string)$number, + currency_iso_code: $price_break['Currency'] + ); + } + + return [ + new PurchaseInfoDTO(distributor_name: self::DISTRIBUTOR_NAME, order_number: $order_number, prices: $prices, + product_url: $product_url) + ]; + } + + + /* Converts the product status from the MOUSER API to the manufacturing status used in Part-DB: + Factory Special Order - Ordine speciale in fabbrica + Not Recommended for New Designs - Non raccomandato per nuovi progetti + New Product - Nuovo prodotto + End of Life - Fine vita + -vuoto- - Attivo + + TODO: Probably need to review the values of field Lifecyclestatus + */ + /** + * Converts the lifecycle status from the Mouser API to a ManufacturingStatus + * @param string|null $productStatus The lifecycle status from the Mouser API + * @param int $availableInStock The number of parts available in stock + * @return ManufacturingStatus|null + */ + private function releaseStatusCodeToManufacturingStatus(?string $productStatus, int $availableInStock = 0): ?ManufacturingStatus + { + $tmp = match ($productStatus) { + null => null, + "New Product" => ManufacturingStatus::ANNOUNCED, + "Not Recommended for New Designs" => ManufacturingStatus::NRFND, + "Factory Special Order", "Obsolete" => ManufacturingStatus::DISCONTINUED, + "End of Life" => ManufacturingStatus::EOL, + default => ManufacturingStatus::ACTIVE, + }; + + //If the part would be assumed to be announced, check if it is in stock, then it is active + if ($tmp === ManufacturingStatus::ANNOUNCED && $availableInStock > 0) { + $tmp = ManufacturingStatus::ACTIVE; + } + + return $tmp; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php new file mode 100644 index 00000000..ccf800f8 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php @@ -0,0 +1,1471 @@ +. + */ + +/** + * OEMSecretsProvider Class + * + * This class is responsible for interfacing with the OEMSecrets API (version 3.0.1) to retrieve and manage information + * about electronic components. Since the API does not provide a unique identifier for each part, the class aggregates + * results based on "part_number" and "manufacturer_id". It also transforms unstructured descriptions into structured + * parameters and aggregates datasheets and images provided by multiple distributors. + * The OEMSecrets API returns results by matching the provided part number not only with the original part number + * but also with the distributor-assigned part number and/or the part description. + * + * Key functionalities: + * - Aggregation of results based on part_number and manufacturer_id to ensure unique identification of parts. + * - Conversion of component descriptions into structured parameters (ParameterDTO) for better clarity and searchability. + * - Aggregation of datasheets and images from multiple distributors, ensuring that all available resources are collected. + * - Price handling, including filtering of distributors that offer zero prices, controlled by the `zero_price` configuration variable. + * - A sorting algorithm that first prioritizes exact matches with the keyword, followed by alphabetical sorting of items + * with the same prefix (e.g., "BC546", "BC546A", "BC546B"), and finally, sorts by either manufacturer or completeness + * based on the specified criteria. + * - Sorting the distributors: + * 1. Environment's country_code first. + * 2. Region matching environment's country_code, prioritizing "Global" ('XX'). + * 3. Distributors with null country_code/region are placed last. + * 4. Final fallback is alphabetical sorting by region and country_code. + * + * Configuration: + * - The ZERO_PRICE variable must be set in the `.env.local` file. If is set to 0, the class will skip distributors + * that do not offer valid prices for the components. + * - Currency and country settings can also be specified for localized pricing and distributor filtering. + * - Generation of parameters: if SET_PARAM is set to 1 the parameters for the part are generated from the description + * transforming unstructured descriptions into structured parameters; each parameter in description should have the form: + * "...;name1:value1;name2:value2" + * - Sorting is guided by SORT_CRITERIA variable. The sorting process first arranges items based on the provided keyword. + * Then, if set to 'C', it further sorts by completeness (prioritizing items with the most detailed information). + * If set to 'M', it further sorts by manufacturer name. If unset or set to any other value, no sorting is performed. + * Distributors within each item are further sorted based on country_code and region, following the rules explained + * in the previous comment. + * + * Data Handling: + * - The class divides and stores component information across multiple session arrays: + * - `basic_info_results`: Stores basic information like name, description, manufacturer, and category. + * - `datasheets_results`: Aggregates datasheets provided by distributors, ensuring no duplicates. + * - `images_results`: Collects images of components from various sources, preventing duplication. + * - `parameters_results`: Extracts and stores key parameters parsed from component descriptions. + * - `purchase_info_results`: Contains detailed purchasing information like pricing and distributor details. + * + * - By splitting the data into separate session arrays, the class optimizes memory usage and simplifies retrieval + * of specific details without loading the entire dataset at once. + * + * Technical Details: + * - Uses OEMSecrets API (version 3.0.1) to retrieve component data. + * - Data processing includes sanitizing input, avoiding duplicates, and dynamically adjusting information as new distributor + * data becomes available (e.g., adding missing datasheets or parameters from subsequent API responses). + * + * @package App\Services\InfoProviderSystem\Providers + * @author Pasquale D'Orsi (https://github.com/pdo59) + * @version 1.2.0 + * @since 2024 August + */ + + +declare(strict_types=1); + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Psr\Cache\CacheItemPoolInterface; + + +class OEMSecretsProvider implements InfoProviderInterface +{ + + private const ENDPOINT_URL = 'https://oemsecretsapi.com/partsearch'; + + public function __construct( + private readonly HttpClientInterface $oemsecretsClient, + private readonly string $api_key, + private readonly string $country_code, + private readonly string $currency, + private readonly string $zero_price, + private readonly string $set_param, + private readonly string $sort_criteria, + private readonly CacheItemPoolInterface $partInfoCache + ) + { + } + + private array $countryNameToCodeMap = [ + 'Andorra' => 'AD', + 'United Arab Emirates' => 'AE', + 'Antarctica' => 'AQ', + 'Argentina' => 'AR', + 'Austria' => 'AT', + 'Australia' => 'AU', + 'Belgium' => 'BE', + 'Bolivia' => 'BO', + 'Brazil' => 'BR', + 'Bouvet Island' => 'BV', + 'Belarus' => 'BY', + 'Canada' => 'CA', + 'Switzerland' => 'CH', + 'Chile' => 'CL', + 'China' => 'CN', + 'Colombia' => 'CO', + 'Czech Republic' => 'CZ', + 'Germany' => 'DE', + 'Denmark' => 'DK', + 'Ecuador' => 'EC', + 'Estonia' => 'EE', + 'Western Sahara' => 'EH', + 'Spain' => 'ES', + 'Finland' => 'FI', + 'Falkland Islands' => 'FK', + 'Faroe Islands' => 'FO', + 'France' => 'FR', + 'United Kingdom' => 'GB', + 'Georgia' => 'GE', + 'French Guiana' => 'GF', + 'Guernsey' => 'GG', + 'Gibraltar' => 'GI', + 'Greenland' => 'GL', + 'Greece' => 'GR', + 'South Georgia and the South Sandwich Islands' => 'GS', + 'Guyana' => 'GY', + 'Hong Kong' => 'HK', + 'Heard Island and McDonald Islands' => 'HM', + 'Croatia' => 'HR', + 'Hungary' => 'HU', + 'Ireland' => 'IE', + 'Isle of Man' => 'IM', + 'India' => 'IN', + 'Iceland' => 'IS', + 'Italy' => 'IT', + 'Jamaica' => 'JM', + 'Japan' => 'JP', + 'North Korea' => 'KP', + 'South Korea' => 'KR', + 'Kazakhstan' => 'KZ', + 'Liechtenstein' => 'LI', + 'Sri Lanka' => 'LK', + 'Lithuania' => 'LT', + 'Luxembourg' => 'LU', + 'Monaco' => 'MC', + 'Moldova' => 'MD', + 'Montenegro' => 'ME', + 'North Macedonia' => 'MK', + 'Malta' => 'MT', + 'Netherlands' => 'NL', + 'Norway' => 'NO', + 'New Zealand' => 'NZ', + 'Peru' => 'PE', + 'Philippines' => 'PH', + 'Poland' => 'PL', + 'Portugal' => 'PT', + 'Paraguay' => 'PY', + 'Romania' => 'RO', + 'Serbia' => 'RS', + 'Russia' => 'RU', + 'Solomon Islands' => 'SB', + 'Sudan' => 'SD', + 'Sweden' => 'SE', + 'Singapore' => 'SG', + 'Slovenia' => 'SI', + 'Svalbard and Jan Mayen' => 'SJ', + 'Slovakia' => 'SK', + 'San Marino' => 'SM', + 'Somalia' => 'SO', + 'Suriname' => 'SR', + 'Syria' => 'SY', + 'Eswatini' => 'SZ', + 'Turks and Caicos Islands' => 'TC', + 'French Southern Territories' => 'TF', + 'Togo' => 'TG', + 'Thailand' => 'TH', + 'Tajikistan' => 'TJ', + 'Tokelau' => 'TK', + 'Turkmenistan' => 'TM', + 'Tunisia' => 'TN', + 'Tonga' => 'TO', + 'Turkey' => 'TR', + 'Trinidad and Tobago' => 'TT', + 'Tuvalu' => 'TV', + 'Taiwan' => 'TW', + 'Tanzania' => 'TZ', + 'Ukraine' => 'UA', + 'Uganda' => 'UG', + 'United States Minor Outlying Islands' => 'UM', + 'United States' => 'US', + 'Uruguay' => 'UY', + 'Uzbekistan' => 'UZ', + 'Vatican City' => 'VA', + 'Venezuela' => 'VE', + 'British Virgin Islands' => 'VG', + 'U.S. Virgin Islands' => 'VI', + 'Vietnam' => 'VN', + 'Vanuatu' => 'VU', + 'Wallis and Futuna' => 'WF', + 'Yemen' => 'YE', + 'South Africa' => 'ZA', + 'Zambia' => 'ZM', + 'Zimbabwe' => 'ZW', + 'Global' => 'XX' + ]; + + private array $distributorCountryCodes = []; + private array $countryCodeToRegionMap = []; + + /** + * Get information about this provider + * + * @return array An associative array with the following keys (? means optional): + * - name: The (user friendly) name of the provider (e.g. "Digikey"), will be translated + * - description?: A short description of the provider (e.g. "Digikey is a ..."), will be translated + * - logo?: The logo of the provider (e.g. "digikey.png") + * - url?: The url of the provider (e.g. "https://www.digikey.com") + * - disabled_help?: A help text which is shown when the provider is disabled, explaining how to enable it + * - oauth_app_name?: The name of the OAuth app which is used for authentication (e.g. "ip_digikey_oauth"). If this is set a connect button will be shown + * + * @phpstan-return array{ name: string, description?: string, logo?: string, url?: string, disabled_help?: string, oauth_app_name?: string } + */ + public function getProviderInfo(): array + { + return [ + 'name' => 'OEMSecrets', + 'description' => 'This provider uses the OEMSecrets API to search for parts.', + 'url' => 'https://www.oemsecrets.com/', + 'disabled_help' => 'Configure the API key in the PROVIDER_OEMSECRETS_KEY environment variable to enable.' + ]; + } + /** + * Returns a unique key for this provider, which will be saved into the database + * and used to identify the provider + * @return string A unique key for this provider (e.g. "digikey") + */ + public function getProviderKey(): string + { + return 'oemsecrets'; + } + + /** + * Checks if this provider is enabled or not (meaning that it can be used for searching) + * @return bool True if the provider is enabled, false otherwise + */ + public function isActive(): bool + { + return $this->api_key !== ''; + } + + + /** + * Searches for products based on a given keyword using the OEMsecrets Part Search API. + * + * This method queries the OEMsecrets API to retrieve distributor data for the provided part number, + * including details such as pricing, compliance, and inventory. It supports both direct API queries + * and debugging with local JSON files. The results are processed, cached, and then sorted based + * on the keyword and specified criteria. + * + * @param string $keyword The part number to search for + * @return array An array of processed product details, sorted by relevance and additional criteria. + * + * @throws \Exception If the JSON file used for debugging is not found or contains errors. + */ + public function searchByKeyword(string $keyword): array + { + /* + oemsecrets Part Search API 3.0.1 + + "https://oemsecretsapi.com/partsearch? + searchTerm=BC547 + &apiKey=icawpb0bspoo2c6s64uv4vpdfp2vgr7e27bxw0yct2bzh87mpl027x353uelpq2x + ¤cy=EUR + &countryCode=IT" + + partsearch description: + Use the Part Search API to find distributor data for a full or partial manufacturer + part number including part details, pricing, compliance and inventory. + + Required Parameter Format Description + searchTerm string Part number you are searching for + apiKey string Your unique API key provided to you by OEMsecrets + + Additional Parameter Format Description + countryCode string The country you want to output for + currency string / array The currency you want the prices to be displayed as + + To display the output for GB and to view prices in USD, add [ countryCode=GB ] and [ currency=USD ] + as seen below: + oemsecretsapi.com/partsearch?apiKey=abcexampleapikey123&searchTerm=bd04&countryCode=GB¤cy=USD + + To view prices in both USD and GBP add [ currency[]=USD¤cy[]=GBP ] + oemsecretsapi.com/partsearch?searchTerm=bd04&apiKey=abcexampleapikey123¤cy[]=USD¤cy[]=GBP + + */ + + + // Activate this block when querying the real APIs + //------------------ + + $response = $this->oemsecretsClient->request('GET', self::ENDPOINT_URL, [ + 'query' => [ + 'searchTerm' => $keyword, + 'apiKey' => $this->api_key, + 'currency' => $this->currency, + 'countryCode' => $this->country_code, + ], + ]); + + $response_array = $response->toArray(); + //------------------*/ + + // Or activate this block when we use json file for debugging + /*/------------------ + $jsonFilePath = ''; + if (!file_exists($jsonFilePath)) { + throw new \Exception("JSON file not found."); + } + $jsonContent = file_get_contents($jsonFilePath); + $response_array = json_decode($jsonContent, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception("JSON file decode failed: " . json_last_error_msg()); + } + //------------------*/ + + $products = $response_array['stock'] ?? []; + + $results = []; + $basicInfoResults = []; + $datasheetsResults = []; + $imagesResults = []; + $parametersResults = []; + $purchaseInfoResults = []; + + foreach ($products as $product) { + if (!isset($product['part_number'], $product['manufacturer'])) { + continue; // Skip invalid product entries + } + $provider_id = $this->generateProviderId($product['part_number'], $product['manufacturer']); + + $partDetailDTO = $this->processBatch( + $product, + $provider_id, + $basicInfoResults, + $datasheetsResults, + $imagesResults, + $parametersResults, + $purchaseInfoResults + ); + + if ($partDetailDTO !== null) { + $results[$provider_id] = $partDetailDTO; + $cacheKey = $this->getCacheKey($provider_id); + $cacheItem = $this->partInfoCache->getItem($cacheKey); + $cacheItem->set($partDetailDTO); + $cacheItem->expiresAfter(3600 * 24); + $this->partInfoCache->save($cacheItem); + } + } + + //Force garbage collection to free up memory + gc_collect_cycles(); + + // Sort of the results + $this->sortResultsData($results, $keyword); + + //Force garbage collection to free up memory + gc_collect_cycles(); + + return $results; + + } + + /** + * Generates a cache key for storing part details based on the provided provider ID. + * + * This method creates a unique cache key by prefixing the provider ID with 'part_details_' + * and hashing the provider ID using MD5 to ensure a consistent and compact key format. + * + * @param string $provider_id The unique identifier of the provider or part. + * @return string The generated cache key. + */ + private function getCacheKey(string $provider_id): string { + return 'oemsecrets_part_' . md5($provider_id); + } + + + /** + * Retrieves detailed information about the part with the given provider ID from the cache. + * + * This method checks the cache for the details of the specified part. If the details are + * found in the cache, they are returned. If not, an exception is thrown indicating that + * the details could not be found. + * + * @param string $id The unique identifier of the provider or part. + * @return PartDetailDTO The detailed information about the part. + * + * @throws \Exception If no details are found for the given provider ID. + */ + public function getDetails(string $id): PartDetailDTO + { + $cacheKey = $this->getCacheKey($id); + $cacheItem = $this->partInfoCache->getItem($cacheKey); + + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + //If we have no cached result yet, we extract the part number (first part of our ID) and search for it + $partNumber = explode('|', $id)[0]; + + //The searchByKeyword method will write the results to cache, so we can just try it again afterwards + $this->searchByKeyword($partNumber); + + $cacheItem = $this->partInfoCache->getItem($cacheKey); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + // If the details still are not found in the cache, throw an exception + throw new \RuntimeException("Details not found for provider_id $id"); + } + + + /** + * A list of capabilities this provider supports (which kind of data it can provide). + * Not every part have to contain all of these data, but the provider should be able to provide them in general. + * Currently, this list is purely informational and not used in functional checks. + * @return ProviderCapabilities[] + */ + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ]; + } + + + /** + * Processes a single product and updates arrays for basic information, datasheets, images, parameters, + * and purchase information. Aggregates and organizes data received for a specific `part_number` and `manufacturer_id`. + * Distributors within the product are also sorted based on country_code and region. + * + * @param array $product The product data received from the OEMSecrets API. + * @param string $provider_id A string that contains the unique key created for the part + * @param array &$basicInfoResults Array containing the basic product information (e.g., name, description, category). + * @param array &$datasheetsResults Array containing datasheets collected from various distributors for the product. + * @param array &$imagesResults Array containing images of the product collected from various distributors. + * @param array &$parametersResults Array containing technical parameters extracted from the product descriptions. + * @param array &$purchaseInfoResults Array containing purchase information, including distributors and pricing details. + * + * @return PartDetailDTO|null Returns a PartDetailDTO object if the product is processed successfully, otherwise null. + * + * @throws \Exception If a required key in the product data is missing or if there is an issue creating the DTO. + * + * @see createOrUpdateBasicInfo() Creates or updates the basic product information. + * @see getPrices() Extracts the pricing information for the product. + * @see parseDataSheets() Parses and prevents duplication of datasheets. + * @see getImages() Extracts and avoids duplication of images. + * @see getParameters() Extracts technical parameters from the product description. + * @see createPurchaseInfoDTO() Creates a PurchaseInfoDTO containing distributor and price information. + * + * @note Distributors within the product are sorted by country_code and region: + * 1. Distributors with the environment's country_code come first. + * 2. Distributors in the same region as the environment's country_code are next, + * with "Global" ('XX') prioritized within this region. + * 3. Distributors with null country_code or region are placed last. + * 4. Remaining distributors are sorted alphabetically by region and country_code. + + */ + private function processBatch( + array $product, + string $provider_id, + array &$basicInfoResults, + array &$datasheetsResults, + array &$imagesResults, + array &$parametersResults, + array &$purchaseInfoResults + ): ?PartDetailDTO + { + if (!isset($product['manufacturer'], $product['part_number'])) { + throw new \InvalidArgumentException("Missing required product data: 'manufacturer' or 'part_number'"); + } + + // Retrieve the country_code associated with the distributor and store it in the $distributorCountryCodes array. + $distributorCountry = $product['distributor']['distributor_country'] ?? null; + $distributorName = $product['distributor']['distributor_name'] ?? null; + $distributorRegion = $product['distributor']['distributor_region'] ?? null; + + if ($distributorCountry && $distributorName) { + $countryCode = $this->mapCountryNameToCode($distributorCountry); + if ($countryCode) { + $this->distributorCountryCodes[$distributorName] = $countryCode; + } + if ($distributorRegion) { + $this->countryCodeToRegionMap[$countryCode] = $distributorRegion; + } + } + + // Truncate the description and handle notes + $thenotes = ''; + $description = $product['description'] ?? ''; + if (strlen($description) > 100) { + $thenotes = $description; // Save the complete description + $description = substr($description, 0, 100) . '...'; // Truncate the description + } + + // Extract prices + $priceDTOs = $this->getPrices($product); + if (empty($priceDTOs) && (int)$this->zero_price === 0) { + return null; // Skip products without valid prices + } + + $existingBasicInfo = isset($basicInfoResults[$provider_id]) && is_array($basicInfoResults[$provider_id]) + ? $basicInfoResults[$provider_id] + : []; + + $basicInfoResults[$provider_id] = $this->createOrUpdateBasicInfo( + $provider_id, + $product, + $description, + $thenotes, + $existingBasicInfo + ); + + // Update images, datasheets, and parameters + + $newDatasheets = $this->parseDataSheets($product['datasheet_url'] ?? null, null, $datasheetsResults[$provider_id] ?? []); + if ($newDatasheets !== null) { + $datasheetsResults[$provider_id] = array_merge($datasheetsResults[$provider_id] ?? [], $newDatasheets); + } + + $imagesResults[$provider_id] = $this->getImages($product, $imagesResults[$provider_id] ?? []); + if ($this->set_param == 1) { + $parametersResults[$provider_id] = $this->getParameters($product, $parametersResults[$provider_id] ?? []); + } else { + $parametersResults[$provider_id] = []; + } + + // Handle purchase information + $currentDistributor = $this->createPurchaseInfoDTO($product, $priceDTOs, $purchaseInfoResults[$provider_id] ?? []); + if ($currentDistributor !== null) { + $purchaseInfoResults[$provider_id][] = $currentDistributor; + } + + // If there is data in $purchaseInfoResults, sort it before creating the PartDetailDTO + if (!empty($purchaseInfoResults[$provider_id])) { + usort($purchaseInfoResults[$provider_id], function ($a, $b) { + $nameA = $a->distributor_name; + $nameB = $b->distributor_name; + + $countryCodeA = $this->distributorCountryCodes[$nameA] ?? null; + $countryCodeB = $this->distributorCountryCodes[$nameB] ?? null; + + $regionA = $this->countryCodeToRegionMap[$countryCodeA] ?? ''; + $regionB = $this->countryCodeToRegionMap[$countryCodeB] ?? ''; + + // If the map is empty or doesn't contain the key for $this->country_code, assign a placeholder region. + $regionForEnvCountry = $this->countryCodeToRegionMap[$this->country_code] ?? ''; + + // Convert to string before comparison to avoid mixed types + $countryCodeA = (string) $countryCodeA; + $countryCodeB = (string) $countryCodeB; + $regionA = (string) $regionA; + $regionB = (string) $regionB; + + + // Step 0: If either country code is null, place it at the end + if ($countryCodeA === '' || $regionA === '') { + return 1; // Metti A dopo B + } elseif ($countryCodeB === '' || $regionB === '') { + return -1; // Metti B dopo A + } + + // Step 1: country_code from the environment + if ($countryCodeA === $this->country_code && $countryCodeB !== $this->country_code) { + return -1; + } elseif ($countryCodeA !== $this->country_code && $countryCodeB === $this->country_code) { + return 1; + } + + // Step 2: Sort by environment's region, prioritizing "Global" (XX) + if ($regionA === $regionForEnvCountry && $regionB !== $regionForEnvCountry) { + return -1; + } elseif ($regionA !== $regionForEnvCountry && $regionB === $regionForEnvCountry) { + return 1; + } + + // Step 3: If regions are the same, prioritize "Global" (XX) + if ($regionA === $regionB) { + if ($countryCodeA === 'XX' && $countryCodeB !== 'XX') { + return -1; + } elseif ($countryCodeA !== 'XX' && $countryCodeB === 'XX') { + return 1; + } + } + + // Step 4: Alphabetical sorting by region and country_code + $regionComparison = strcasecmp($regionA , $regionB); + if ($regionComparison !== 0) { + return $regionComparison; + } + + // Alphabetical sorting as a fallback + return strcasecmp($countryCodeA, $countryCodeB); + }); + } + // Convert the gathered data into a PartDetailDTO + + $partDetailDTO = new PartDetailDTO( + provider_key: $basicInfoResults[$provider_id]['provider_key'], + provider_id: $provider_id, + name: $basicInfoResults[$provider_id]['name'], + description: $basicInfoResults[$provider_id]['description'], + category: $basicInfoResults[$provider_id]['category'], + manufacturer: $basicInfoResults[$provider_id]['manufacturer'], + mpn: $basicInfoResults[$provider_id]['mpn'], + preview_image_url: $basicInfoResults[$provider_id]['preview_image_url'], + manufacturing_status: $basicInfoResults[$provider_id]['manufacturing_status'], + provider_url: $basicInfoResults[$provider_id]['provider_url'], + footprint: $basicInfoResults[$provider_id]['footprint'] ?? null, + notes: $basicInfoResults[$provider_id]['notes'] ?? null, + datasheets: $datasheetsResults[$provider_id] ?? [], + images: $imagesResults[$provider_id] ?? [], + parameters: $parametersResults[$provider_id] ?? [], + vendor_infos: $purchaseInfoResults[$provider_id] ?? [] + ); + + return $partDetailDTO; + } + + + /** + * Extracts pricing information from the product data, converts it to PriceDTO objects, + * and returns them as an array. + * + * @param array{ + * prices?: array>, + * source_currency?: string + * } $product The product data from the OEMSecrets API containing price details. + * + * @return PriceDTO[] Array of PriceDTO objects representing different price tiers for the product. + */ + private function getPrices(array $product): array + { + $prices = $product['prices'] ?? []; + $sourceCurrency = $product['source_currency'] ?? null; + $priceDTOs = []; + + // Flag to check if we have added prices in the preferred currency + $foundPreferredCurrency = false; + + if (is_array($prices)) { + // Step 1: Check if prices exist in the preferred currency + if (isset($prices[$this->currency]) && is_array($prices[$this->currency])) { + $priceDetails = $prices[$this->currency]; + foreach ($priceDetails as $priceDetail) { + if ( + is_array($priceDetail) && + isset($priceDetail['unit_break'], $priceDetail['unit_price']) && + is_numeric($priceDetail['unit_break']) && + is_string($priceDetail['unit_price']) && + $priceDetail['unit_price'] !== "0.0000" + ) { + $priceDTOs[] = new PriceDTO( + minimum_discount_amount: (float)$priceDetail['unit_break'], + price: (string)$priceDetail['unit_price'], + currency_iso_code: $this->currency, + includes_tax: false, + price_related_quantity: 1.0 + ); + $foundPreferredCurrency = true; + } + } + } + + // Step 2: If no prices in the preferred currency, use source currency + if (!$foundPreferredCurrency && $sourceCurrency && isset($prices[$sourceCurrency]) && is_array($prices[$sourceCurrency])) { + $priceDetails = $prices[$sourceCurrency]; + foreach ($priceDetails as $priceDetail) { + if ( + is_array($priceDetail) && + isset($priceDetail['unit_break'], $priceDetail['unit_price']) && + is_numeric($priceDetail['unit_break']) && + is_string($priceDetail['unit_price']) && + $priceDetail['unit_price'] !== "0.0000" + ) { + $priceDTOs[] = new PriceDTO( + minimum_discount_amount: (float)$priceDetail['unit_break'], + price: (string)$priceDetail['unit_price'], + currency_iso_code: $sourceCurrency, + includes_tax: false, + price_related_quantity: 1.0 + ); + } + } + } + } + + return $priceDTOs; + } + + + /** + * Retrieves product images provided by the distributor. Prevents duplicates based on the image name. + * @param array{ + * image_url?: string + * } $product The product data from the OEMSecrets API containing image URLs. + * @param FileDTO[] $existingImages Optional. Existing images for the product to avoid duplicates. + * + * @return FileDTO[] Array of FileDTO objects representing the product images. + */ + private function getImages(array $product, array $existingImages = []): array + { + $images = $existingImages; + $imageUrl = $product['image_url'] ?? null; + + if ($imageUrl) { + $imageName = basename(parse_url($imageUrl, PHP_URL_PATH)); + if (!in_array($imageName, array_column($images, 'name'), true)) { + $images[] = new FileDTO(url: $imageUrl, name: $imageName); + } + } + return $images; + } + + /** + * Extracts technical parameters from the product description, ensures no duplicates, and returns them as an array. + * + * @param array{ + * description?: string + * } $product The product data from the OEMSecrets API containing product descriptions. + * @param ParameterDTO[] $existingParameters Optional. Existing parameters for the product to avoid duplicates. + * + * @return ParameterDTO[] Array of ParameterDTO objects representing technical parameters extracted from the product description. + */ + private function getParameters(array $product, array $existingParameters = []): array + { + $parameters = $existingParameters; + $description = $product['description'] ?? ''; + + // Logic to extract parameters from the description + $extractedParameters = $this->parseDescriptionToParameters($description) ?? []; + + foreach ($extractedParameters as $newParam) { + $isDuplicate = false; + foreach ($parameters as $existingParam) { + if ($existingParam->name === $newParam->name) { + $isDuplicate = true; + break; + } + } + if (!$isDuplicate) { + $parameters[] = $newParam; + } + } + + return $parameters; + } + + /** + * Creates a PurchaseInfoDTO object containing distributor and pricing information for a product. + * Ensures that the distributor name is valid and prices are available. + * + * @param array{ + * distributor?: array{ + * distributor_name?: string + * }, + * sku?: string, + * source_part_number: string, + * buy_now_url?: string, + * lead_time_weeks?: mixed + * } $product The product data from the OEMSecrets API. + * @param PriceDTO[] $priceDTOs Array of PriceDTO objects representing pricing tiers. + * @param PurchaseInfoDTO[] $existingPurchaseInfos Optional. Existing purchase information for the product to avoid duplicates. + * + * @return PurchaseInfoDTO|null A PurchaseInfoDTO object containing the distributor information, or null if invalid. + */ + private function createPurchaseInfoDTO(array $product, array $priceDTOs, array $existingPurchaseInfos = []): ?PurchaseInfoDTO + { + $distributor_name = $product['distributor']['distributor_name'] ?? null; + if ($distributor_name && !empty($priceDTOs)) { + $sku = isset($product['sku']) ? (string)$product['sku'] : null; + $order_number_base = $sku ?: (string)$product['source_part_number']; + $order_number = $order_number_base; + + // Remove duplicates from the quantity/price tiers + $uniquePriceDTOs = []; + foreach ($priceDTOs as $priceDTO) { + $key = $priceDTO->minimum_discount_amount . '-' . $priceDTO->price; + $uniquePriceDTOs[$key] = $priceDTO; + } + $priceDTOs = array_values($uniquePriceDTOs); + + // Differentiate $order_number if duplicated + if ($this->isDuplicateOrderNumber($order_number, $distributor_name, $existingPurchaseInfos)) { + $lead_time_weeks = isset($product['lead_time_weeks']) ? (string)$product['lead_time_weeks'] : ''; + $order_number = $order_number_base . '-' . $lead_time_weeks; + + // If there is still a duplicate after adding lead_time_weeks + $counter = 1; + while ($this->isDuplicateOrderNumber($order_number, $distributor_name, $existingPurchaseInfos)) { + $order_number = $order_number_base . '-' . $lead_time_weeks . '-' . $counter; + $counter++; + } + } + + return new PurchaseInfoDTO( + distributor_name: $distributor_name, + order_number: $order_number, + prices: $priceDTOs, + product_url: $this->unwrapURL($product['buy_now_url'] ?? null) + ); + } + return null; // Return null if no valid distributor exists + } + + /** + * Checks if an order number already exists for a given distributor in the existing purchase infos. + * + * @param string $order_number The order number to check. + * @param string $distributor_name The name of the distributor. + * @param PurchaseInfoDTO[] $existingPurchaseInfos The existing purchase information to check against. + * @return bool True if a duplicate order number is found, otherwise false. + */ + private function isDuplicateOrderNumber(string $order_number, string $distributor_name, array $existingPurchaseInfos): bool + { + foreach ($existingPurchaseInfos as $purchaseInfo) { + if ($purchaseInfo->distributor_name === $distributor_name && $purchaseInfo->order_number === $order_number) { + return true; + } + } + return false; + } + + /** + * Creates or updates the basic information of a product, including the description, category, manufacturer, + * and other metadata. This function manages the PartDetailDTO creation or update. + * + * @param string $provider_id The unique identifier for the product based on part_number and manufacturer. + * * @param array{ + * part_number: string, + * category: string, + * manufacturer: string, + * source_part_number: string, + * image_url?: string, + * life_cycle?: string, + * quantity_in_stock?: int + * } $product The product data from the OEMSecrets API. + * @param string $description The truncated description for the product. + * @param string $thenotes The full description saved as notes for the product. + * + * @return array The updated or newly created PartDetailDTO containing basic product information. + */ + private function createOrUpdateBasicInfo( + string $provider_id, + array $product, + string $description, + string $thenotes, + ?array $existingBasicInfo + ): array { + // If there is no existing basic info array, we create a new one + if (is_null($existingBasicInfo)) { + return [ + 'provider_key' => $this->getProviderKey(), + 'provider_id' => $provider_id, + 'name' => $product['part_number'], + 'description' => $description, + 'category' => $product['category'], + 'manufacturer' => $product['manufacturer'], + 'mpn' => $product['source_part_number'], + 'preview_image_url' => $product['image_url'] ?? null, + 'manufacturing_status' => $this->releaseStatusCodeToManufacturingStatus( + $product['life_cycle'] ?? null, + (int)($product['quantity_in_stock'] ?? 0) + ), + 'provider_url' => $this->generateInquiryUrl($product['part_number']), + 'notes' => $thenotes, + 'footprint' => null + ]; + } + + // Update fields only if empty or undefined, with additional check for preview_image_url + return [ + 'provider_key' => $existingBasicInfo['provider_key'] ?? $this->getProviderKey(), + 'provider_id' => $existingBasicInfo['provider_id'] ?? $provider_id, + 'name' => $existingBasicInfo['name'] ?? $product['part_number'], + // Update description if it's null/empty + 'description' => !empty($existingBasicInfo['description']) + ? $existingBasicInfo['description'] + : $description, + // Update category if it's null/empty + 'category' => !empty($existingBasicInfo['category']) + ? $existingBasicInfo['category'] + : $product['category'], + 'manufacturer' => $existingBasicInfo['manufacturer'] ?? $product['manufacturer'], + 'mpn' => $existingBasicInfo['mpn'] ?? $product['source_part_number'], + 'preview_image_url' => !empty($existingBasicInfo['preview_image_url']) + ? $existingBasicInfo['preview_image_url'] + : ($product['image_url'] ?? null), + 'manufacturing_status' => !empty($existingBasicInfo['manufacturing_status']) + ? $existingBasicInfo['manufacturing_status'] + : $this->releaseStatusCodeToManufacturingStatus( + $product['life_cycle'] ?? null, + (int)($product['quantity_in_stock'] ?? 0) + ), + 'provider_url' => $existingBasicInfo['provider_url'] ?? $this->generateInquiryUrl($product['part_number']), // ?? $product['buy_now_url'], + 'notes' => $existingBasicInfo['notes'] ?? $thenotes, + 'footprint' => null + ]; + } + + /** + * Parses the datasheet URL and returns an array of FileDTO objects representing the datasheets. + * If the datasheet name is not provided, it attempts to extract the file name from the URL. + * If multiple datasheets with the same default name are encountered, the function appends a + * numeric suffix to ensure uniqueness. + * The query parameter used to extract the event link can be customized. + * + * URL Requirements: + * - The URL should be a valid URL string. + * - The URL can include a query parameter named "event_link", which contains a sub-URL where the + * actual datasheet file name is located (e.g., a link to a PDF file). + * - If "event_link" is not present, the function attempts to extract the file name directly from + * the URL path. + * - The URL path should ideally end with a valid file extension (e.g., .pdf, .doc, .xls, etc.). + * + * Example 1: + * Given URL: `https://example.com/datasheet.php?event_link=https%3A%2F%2Ffiles.example.com%2Fdatasheet.pdf` + * Extracted name: `datasheet.pdf` + * + * Example 2: + * Given URL: `https://example.com/files/datasheet.pdf` + * Extracted name: `datasheet.pdf` + * + * Example 3 (default name fallback): + * Given URL: `https://example.com/files/noextensionfile` + * Extracted name: `datasheet.pdf` + * + * @param string|null $sheetUrl The URL of the datasheet. + * @param string|null $sheetName The optional name of the datasheet. If null, the name is extracted from the URL. + * @param array $existingDatasheets The array of existing datasheets to check for duplicates. + * + * @return FileDTO[]|null Returns an array containing the new datasheet if unique, or null if the datasheet is a duplicate or invalid. + * + * @see FileDTO Used to create datasheet objects with a URL and name. + */ + private function parseDataSheets(?string $sheetUrl, ?string $sheetName, array $existingDatasheets = []): ?array + { + if ($sheetUrl === null || $sheetUrl === '' || $sheetUrl === '0') { + return null; + } + + //Unwrap the URL (remove analytics part) + $sheetUrl = $this->unwrapURL($sheetUrl); + + // If the datasheet name is not provided, extract it from the URL + if ($sheetName === null) { + $urlPath = parse_url($sheetUrl, PHP_URL_PATH); + if ($urlPath === false) { + throw new \RuntimeException("Invalid URL path: $sheetUrl"); + } + + // If "event_link" does not exist, try to extract the name from the main URL path + $sheetName = basename($urlPath); + if (!str_contains($sheetName, '.') || !preg_match('/\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$/i', $sheetName)) { + // If the name does not have a valid extension, assign a default name + $sheetName = 'datasheet_' . uniqid('', true) . '.pdf'; + } + } + + // Create an array of existing file names + $existingNames = array_map(static function ($existingDatasheet) { + return $existingDatasheet->name; + }, $existingDatasheets); + + // Check if the name already exists + if (in_array($sheetName, $existingNames, true)) { + // The name already exists, so do not add the datasheet + return null; + } + + // Create an array with the datasheet data if it does not already exist + $result = []; + $result[] = new FileDTO(url: $sheetUrl, name: $sheetName); + return $result; + } + + /** + * Converts the lifecycle status from the API to a ManufacturingStatus + * - "Factory Special Order" / "Ordine speciale in fabbrica" + * - "Not Recommended for New Designs" / "Non raccomandato per nuovi progetti" + * - "New Product" / "Nuovo prodotto" (if availableInStock > 0 else ANNOUNCED) + * - "End of Life" / "Fine vita" + * - vuoto / "Attivo" + * + * @param string|null $productStatus The lifecycle status from the Mouser API. Expected values are: + * - "Factory Special Order" + * - "Not Recommended for New Designs" + * - "New Product" + * - "End of Life" + * - "Obsolete" + * @param int $availableInStock The number of parts available in stock. + * @return ManufacturingStatus|null Returns the corresponding ManufacturingStatus or null if the status is unknown. + * + * @todo Probably need to review the values of field Lifecyclestatus. + */ + private function releaseStatusCodeToManufacturingStatus(?string $productStatus, int $availableInStock = 0): ?ManufacturingStatus + { + $tmp = match ($productStatus) { + null => null, + "New Product" => ManufacturingStatus::ANNOUNCED, + "Not Recommended for New Designs" => ManufacturingStatus::NRFND, + "Factory Special Order", "Obsolete" => ManufacturingStatus::DISCONTINUED, + "End of Life" => ManufacturingStatus::EOL, + default => null, //ManufacturingStatus::ACTIVE, + }; + + //If the part would be assumed to be announced, check if it is in stock, then it is active + if ($tmp === ManufacturingStatus::ANNOUNCED && $availableInStock > 0) { + $tmp = ManufacturingStatus::ACTIVE; + } + + return $tmp; + } + + /** + * Parses the given product description to extract parameters and convert them into `ParameterDTO` objects. + * If the description contains only a single `:`, it is considered unstructured and ignored. + * The function processes the description by searching for key-value pairs in the format `name: value`, + * ignoring any parts of the description that do not follow this format. Parameters are split using either + * `;` or `,` as separators. + * + * The extraction logic handles typical values, ranges, units, and textual information from the description. + * If the description is empty or cannot be processed into valid parameters, the function returns null. + * + * @param string|null $description The description text from which parameters are to be extracted. + * + * @return ParameterDTO[]|null Returns an array of `ParameterDTO` objects if parameters are successfully extracted, + * or null if no valid parameters can be extracted from the description. + */ + private function parseDescriptionToParameters(?string $description): ?array + { + // If the description is null or empty, return null + if ($description === null || trim($description) === '') { + return null; + } + + // If the description contains only a single ':', return null + if (substr_count($description, ':') === 1) { + return null; + } + + // Array to store parsed parameters + $parameters = []; + + // Split the description using the ';' separator + $parts = preg_split('/[;,]/', $description); //explode(';', $description); + + // Process each part of the description + foreach ($parts as $part) { + $part = trim($part); + + // Check if the part contains a key-value structure + if (str_contains($part, ':')) { + [$name, $value] = explode(':', $part, 2); + $name = trim($name); + $value = trim($value); + + // Attempt to parse the value, handling ranges, units, and additional information + $parsedValue = $this->customParseValueIncludingUnit($name, $value); + + // If the value was successfully parsed, create a ParameterDTO + if ($parsedValue) { + // Convert numeric values to float + $value_typ = isset($parsedValue['value_typ']) ? (float)$parsedValue['value_typ'] : null; + $value_min = isset($parsedValue['value_min']) ? (float)$parsedValue['value_min'] : null; + $value_max = isset($parsedValue['value_max']) ? (float)$parsedValue['value_max'] : null; + + $parameters[] = new ParameterDTO( + name: $parsedValue['name'], + value_text: $parsedValue['value_text'] ?? null, + value_typ: $value_typ, + value_min: $value_min, + value_max: $value_max, + unit: $parsedValue['unit'] ?? null, // Add extracted unit + symbol: $parsedValue['symbol'] ?? null // Add extracted symbol + ); + } + } + } + + return !empty($parameters) ? $parameters : null; + } + + /** + * Parses a value that may contain both a numerical value and its corresponding unit. + * This function splits the value into its numerical part and its unit, handling cases + * where the value includes symbols, ranges, or additional text. It also detects and + * processes plus/minus ranges, typical values, and other special formats. + * + * Example formats that can be handled: + * - "2.5V" + * - "±5%" + * - "1-10A" + * - "2.5 @text" + * - "~100 Ohm" + * + * @param string $value The value string to be parsed, which may contain a number, unit, or both. + * + * @return array An associative array with parsed components: + * - 'name' => string (the name of the parameter) + * - 'value_typ' => float|null (the typical or parsed value) + * - 'range_min' => float|null (the minimum value if it's a range) + * - 'range_max' => float|null (the maximum value if it's a range) + * - 'value_text' => string|null (any additional text or symbol) + * - 'unit' => string|null (the detected or default unit) + * - 'symbol' => string|null (any special symbol or additional text) + */ + private function customParseValueIncludingUnit(string $name, string $value): array + { + // Parse using logic for units, ranges, and other elements + $result = [ + 'name' => $name, + 'value_typ' => null, + 'value_min' => null, + 'value_max' => null, + 'value_text' => null, + 'unit' => null, + 'symbol' => null, + ]; + + // Trim any whitespace from the value + $value = trim($value); + + // Handle ranges and plus/minus signs + if (str_contains($value, '...') || str_contains($value, '~') || str_contains($value, '±')) { + // Handle ranges + $value = str_replace(['...', '~'], '...', $value); // Normalize range separators + $rangeParts = preg_split('/\s*[\.\~]\s*/', $value); + + if (count($rangeParts) === 2) { + // Splitting the values and units + $parsedMin = $this->customSplitIntoValueAndUnit($rangeParts[0]); + $parsedMax = $this->customSplitIntoValueAndUnit($rangeParts[1]); + + // Assigning the parsed values + $result['value_min'] = $parsedMin['value_typ']; + $result['value_max'] = $parsedMax['value_typ']; + + // Determine the unit + $result['unit'] = $parsedMax['unit'] ?? $parsedMin['unit']; + } + + } elseif (str_contains($value, '@')) { + // If we find "@", we treat it as additional textual information + [$numericValue, $textValue] = explode('@', $value); + $result['value_typ'] = (float) $numericValue; + $result['value_text'] = trim($textValue); + } else { + // Check if the value is numeric with a unit + if (preg_match('/^([\+\-]?\d+(\.\d+)?)([a-zA-Z%°]+)?$/u', $value, $matches)) { + // It is a number with or without a unit + $result['value_typ'] = (float) $matches[1]; + $result['unit'] = $matches[3] ?? null; + } else { + // It's not a number, so we treat it as text + $result['value_text'] = $value; + } + } + + return $result; + } + + /** + * Splits a string into a numerical value and its associated unit. The function attempts to separate + * a number from its unit, handling common formats where the unit follows the number (e.g., "50kHz", "10A"). + * The function assumes the unit is the non-numeric part of the string. + * + * Example formats that can be handled: + * - "100 Ohm" + * - "10 MHz" + * - "5kV" + * - "±5%" + * + * @param string $value1 The input string containing both a numerical value and a unit. + * @param string|null $value2 Optional. A second value string, typically used for ranges (e.g., "10-20A"). + * + * @return array An associative array with the following elements: + * - 'value_typ' => string|null The first numerical part of the string. + * - 'unit' => string|null The unit part of the string, or null if no unit is detected. + * - 'value_min' => string|null The minimum value in a range, if applicable. + * - 'value_max' => string|null The maximum value in a range, if applicable. + */ + private function customSplitIntoValueAndUnit(string $value1, ?string $value2 = null): array + { + // Separate numbers and units (basic parsing handling) + $unit = null; + $value_typ = null; + + // Search for the number + unit pattern + if (preg_match('/^([\+\-]?\d+(\.\d+)?)([a-zA-Z%°]+)?$/u', $value1, $matches)) { + $value_typ = $matches[1]; + $unit = $matches[3] ?? null; + } + + $result = [ + 'value_typ' => $value_typ, + 'unit' => $unit, + ]; + + if ($value2 !== null) { + if (preg_match('/^([\+\-]?\d+(\.\d+)?)([a-zA-Z%°]+)?$/u', $value2, $matches2)) { + $result['value_min'] = $value_typ; + $result['value_max'] = $matches2[1]; + $result['unit'] = $matches2[3] ?? $unit; // If both values have the same unit, we keep it + } + } + + return $result; + } + + /** + * Generates the API URL to fetch product information for the specified part number from OEMSecrets. + * Ensures that the base API URL and any query parameters are properly formatted. + * + * @param string $partNumber The part number to include in the URL. + * @param string $oemInquiry The inquiry path for the OEMSecrets API, with a default value of 'compare/'. + * This parameter represents the specific API endpoint to query. + * + * @return string The complete provider URL including the base provider URL, the inquiry path, and the part number. + * + * Example: + * If the base URL is "https://www.oemsecrets.com/", the inquiry path is "compare/", and the part number is "NE555", + * the resulting URL will be: "https://www.oemsecrets.com/compare/NE555" + */ + private function generateInquiryUrl(string $partNumber, string $oemInquiry = 'compare/'): string + { + $baseUrl = rtrim($this->getProviderInfo()['url'], '/') . '/'; + $inquiryPath = trim($oemInquiry, '/') . '/'; + $encodedPartNumber = urlencode(trim($partNumber)); + return $baseUrl . $inquiryPath . $encodedPartNumber; + } + + /** + * Sorts the results data array based on the specified search keyword and sorting criteria. + * The sorting process involves multiple phases: + * 1. Exact match with the search keyword. + * 2. Prefix match with the search keyword. + * 3. Alphabetical order of the suffix following the keyword. + * 4. Optional sorting by completeness or manufacturer based on the sort criteria. + * + * The sorting criteria (`sort_criteria`) is an environment variable configured in the `.env.local` file: + * PROVIDER_OEMSECRETS_SORT_CRITERIA + * It determines the final sorting phase: + * - 'C': Sort by completeness. + * - 'M': Sort by manufacturer. + * + * @param array $resultsData The array of result objects to be sorted. Each object should have 'name' and 'manufacturer' properties. + * @param string $searchKeyword The search keyword used for sorting the results. + * + * @return void + */ + private function sortResultsData(array &$resultsData, string $searchKeyword): void + { + // If the SORT_CRITERIA is not 'C' or 'M', do not sort + if ($this->sort_criteria !== 'C' && $this->sort_criteria !== 'M') { + return; + } + usort($resultsData, function ($a, $b) use ($searchKeyword) { + $nameA = trim($a->name); + $nameB = trim($b->name); + + // First phase: Sorting by exact match with the keyword + $exactMatchA = strcasecmp($nameA, $searchKeyword) === 0; + $exactMatchB = strcasecmp($nameB, $searchKeyword) === 0; + + if ($exactMatchA && !$exactMatchB) { + return -1; + } elseif (!$exactMatchA && $exactMatchB) { + return 1; + } + + // Second phase: Sorting by prefix (name starting with the keyword) + $startsWithKeywordA = stripos($nameA, $searchKeyword) === 0; + $startsWithKeywordB = stripos($nameB, $searchKeyword) === 0; + + if ($startsWithKeywordA && !$startsWithKeywordB) { + return -1; + } elseif (!$startsWithKeywordA && $startsWithKeywordB) { + return 1; + } + + if ($startsWithKeywordA && $startsWithKeywordB) { + // Alphabetical sorting of suffixes + $suffixA = substr($nameA, strlen($searchKeyword)); + $suffixB = substr($nameB, strlen($searchKeyword)); + $suffixComparison = strcasecmp($suffixA, $suffixB); + + if ($suffixComparison !== 0) { + return $suffixComparison; + } + } + + // Final sorting: by completeness or manufacturer, if necessary + if ($this->sort_criteria === 'C') { + return $this->compareByCompleteness($a, $b); + } elseif ($this->sort_criteria === 'M') { + return strcasecmp($a->manufacturer, $b->manufacturer); + } + + }); + } + + /** + * Compares two objects based on their "completeness" score. + * The completeness score is calculated by the `calculateCompleteness` method, which assigns a numeric score + * based on the amount of information (such as parameters, datasheets, images, etc.) available for each object. + * The comparison is done in descending order, giving priority to the objects with higher completeness. + * + * @param object $a The first object to compare. + * @param object $b The second object to compare. + * + * @return int A negative value if $b is more complete than $a, zero if they are equally complete, + * or a positive value if $a is more complete than $b. + */ + private function compareByCompleteness(object $a, object $b): int + { + // Calculate the completeness score for each object + $completenessA = $this->calculateCompleteness($a); + $completenessB = $this->calculateCompleteness($b); + + // Sort in descending order by completeness (higher score is better) + return $completenessB - $completenessA; + } + + + /** + * Calculates a "completeness" score for a given part object based on the presence and count of various attributes. + * The completeness score is used to prioritize parts that have more detailed information. + * + * The score is calculated as follows: + * - Counts the number of elements in the `parameters`, `datasheets`, `images`, and `vendor_infos` arrays. + * - Adds 1 point for the presence of `category`, `description`, `mpn`, `preview_image_url`, and `footprint`. + * - Adds 1 or 2 points based on the presence or absence of `manufacturing_status` (higher score if `null`). + * + * @param object $part The part object for which the completeness score is calculated. The object is expected + * to have properties like `parameters`, `datasheets`, `images`, `vendor_infos`, `category`, + * `description`, `mpn`, `preview_image_url`, `footprint`, and `manufacturing_status`. + * + * @return int The calculated completeness score, with a higher score indicating more complete information. + */ + private function calculateCompleteness(object $part): int + { + // Counts the number of elements in each field that can have multiple values + $paramsCount = is_array($part->parameters) ? count($part->parameters) : 0; + $datasheetsCount = is_array($part->datasheets) ? count($part->datasheets) : 0; + $imagesCount = is_array($part->images) ? count($part->images) : 0; + $vendorInfosCount = is_array($part->vendor_infos) ? count($part->vendor_infos) : 0; + + // Check for the presence of single fields and assign a score + $categoryScore = !empty($part->category) ? 1 : 0; + $descriptionScore = !empty($part->description) ? 1 : 0; + $mpnScore = !empty($part->mpn) ? 1 : 0; + $previewImageScore = !empty($part->preview_image_url) ? 1 : 0; + $footprintScore = !empty($part->footprint) ? 1 : 0; + + // Weight for manufacturing_status: higher if null + $manufacturingStatusScore = is_null($part->manufacturing_status) ? 2 : 1; + + // Sum the counts and scores to obtain a completeness score + return $paramsCount + + $datasheetsCount + + $imagesCount + + $vendorInfosCount + + $categoryScore + + $descriptionScore + + $mpnScore + + $previewImageScore + + $footprintScore + + $manufacturingStatusScore; + } + + + /** + * Generates a unique provider ID by concatenating the part number and manufacturer name, + * separated by a pipe (`|`). The generated ID is typically used to uniquely identify + * a specific part from a particular manufacturer. + * + * @param string $partNumber The part number of the product. + * @param string $manufacturer The name of the manufacturer. + * + * @return string The generated provider ID, in the format "partNumber|manufacturer". + */ + private function generateProviderId(string $partNumber, string $manufacturer): string + { + return trim($partNumber) . '|' . trim($manufacturer); + } + + /** + * Maps the name of a country to its corresponding ISO 3166-1 alpha-2 code. + * + * @param string|null $countryName The name of the country to map. + * @return string|null The ISO code for the country, or null if not found. + */ + private function mapCountryNameToCode(?string $countryName): ?string + { + return $this->countryNameToCodeMap[$countryName] ?? null; + } + + /** + * Removes the analytics tracking parts from the URLs returned by the API. + * + * @param string|null $url + * @return string|null + */ + private function unwrapURL(?string $url): ?string + { + if ($url === null) { + return null; + } + + //Check if the URL is a one redirected via analytics + if (str_contains($url, 'analytics.oemsecrets.com/main.php')) { + //Extract the URL from the analytics URL + $queryParams = []; + parse_str(parse_url($url, PHP_URL_QUERY), $queryParams); + + //The real URL is stored in the 'event_link' query parameter + if (isset($queryParams['event_link']) && trim($queryParams['event_link']) !== '') { + $url = $queryParams['event_link']; + + //Replace any spaces in the URL by %20 to avoid invalid URLs + return str_replace(' ', '%20', $url); + } + } + + //Otherwise return the URL as it is + return $url; + } + +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php new file mode 100644 index 00000000..e28162ba --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php @@ -0,0 +1,406 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\OAuth\OAuthTokenManager; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\HttpClient\HttpOptions; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * This class implements the Octopart/Nexar API as an InfoProvider + * + * As the limits for Octopart are quite limited, we use an additional layer of caching here, we get the full parts during a search + * and cache them, so we can use them for the detail view without having to query the API again. + */ +class OctopartProvider implements InfoProviderInterface +{ + private const OAUTH_APP_NAME = 'ip_octopart_oauth'; + + /** + * This defines what fields are returned in the answer from the Octopart API + */ + private const GRAPHQL_PART_SECTION = <<<'GRAPHQL' + { + id + mpn + octopartUrl + manufacturer { + name + } + shortDescription + category { + ancestors { + name + } + name + } + bestImage { + url + } + bestDatasheet { + url + name + } + manufacturerUrl + medianPrice1000 { + price + currency + quantity + } + sellers(authorizedOnly: $authorizedOnly) { + company { + name + } + isAuthorized + offers { + clickUrl + inventoryLevel + moq + sku + packaging + prices { + price + currency + quantity + } + } + }, + specs { + attribute { + name + shortname + group + id + } + displayValue + value + siValue + units + unitsName + unitsSymbol + valueType + } + } + GRAPHQL; + + + public function __construct(private readonly HttpClientInterface $httpClient, + private readonly OAuthTokenManager $authTokenManager, private readonly CacheItemPoolInterface $partInfoCache, + private readonly string $clientId, private readonly string $secret, + private readonly string $currency, private readonly string $country, + private readonly int $search_limit, private readonly bool $onlyAuthorizedSellers) + { + + } + + /** + * Gets the latest OAuth token for the Octopart API, or creates a new one if none is available + * @return string + */ + private function getToken(): string + { + //Check if we already have a token saved for this app, otherwise we have to retrieve one via OAuth + if (!$this->authTokenManager->hasToken(self::OAUTH_APP_NAME)) { + $this->authTokenManager->retrieveClientCredentialsToken(self::OAUTH_APP_NAME); + } + + $tmp = $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME); + if ($tmp === null) { + throw new \RuntimeException('Could not retrieve OAuth token for Octopart'); + } + + return $tmp; + } + + /** + * Make a GraphQL call to the Octopart API + * @return array + */ + private function makeGraphQLCall(string $query, ?array $variables = null): array + { + if ($variables === []) { + $variables = null; + } + + $options = (new HttpOptions()) + ->setJson(['query' => $query, 'variables' => $variables]) + ->setAuthBearer($this->getToken()) + ; + + $response = $this->httpClient->request( + 'POST', + 'https://api.nexar.com/graphql/', + $options->toArray(), + ); + + return $response->toArray(true); + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Octopart', + 'description' => 'This provider uses the Nexar/Octopart API to search for parts on Octopart.', + 'url' => 'https://www.octopart.com/', + 'disabled_help' => 'Set the PROVIDER_OCTOPART_CLIENT_ID and PROVIDER_OCTOPART_SECRET env option.' + ]; + } + + public function getProviderKey(): string + { + return 'octopart'; + } + + public function isActive(): bool + { + //The client ID has to be set and a token has to be available (user clicked connect) + //return /*!empty($this->clientId) && */ $this->authTokenManager->hasToken(self::OAUTH_APP_NAME); + return $this->clientId !== '' && $this->secret !== ''; + } + + private function mapLifeCycleStatus(?string $value): ?ManufacturingStatus + { + return match ($value) { + 'Production', 'New' => ManufacturingStatus::ACTIVE, + 'Obsolete' => ManufacturingStatus::DISCONTINUED, + 'NRND' => ManufacturingStatus::NRFND, + 'EOL' => ManufacturingStatus::EOL, + default => null, + }; + } + + /** + * Saves the given part to the cache. + * Everytime this function is called, the cache is overwritten. + * @param PartDetailDTO $part + * @return void + */ + private function saveToCache(PartDetailDTO $part): void + { + $key = 'octopart_part_'.$part->provider_id; + + $item = $this->partInfoCache->getItem($key); + $item->set($part); + $item->expiresAfter(3600 * 24); //Cache for 1 day + $this->partInfoCache->save($item); + } + + /** + * Retrieves a from the cache, or null if it was not cached yet. + * @param string $id + * @return PartDetailDTO|null + */ + private function getFromCache(string $id): ?PartDetailDTO + { + $key = 'octopart_part_'.$id; + + $item = $this->partInfoCache->getItem($key); + if ($item->isHit()) { + return $item->get(); + } + + return null; + } + + private function partResultToDTO(array $part): PartDetailDTO + { + //Parse the specifications + $parameters = []; + $mass = null; + $package = null; + $pinCount = null; + $mStatus = null; + foreach ($part['specs'] as $spec) { + + //If we encounter the mass spec, we save it for later + if ($spec['attribute']['shortname'] === "weight") { + $mass = (float) $spec['siValue']; + } elseif ($spec['attribute']['shortname'] === "case_package") { + //Package + $package = $spec['value']; + } elseif ($spec['attribute']['shortname'] === "numberofpins") { + //Pin Count + $pinCount = $spec['value']; + } elseif ($spec['attribute']['shortname'] === "lifecyclestatus") { + //LifeCycleStatus + $mStatus = $this->mapLifeCycleStatus($spec['value']); + } + + $parameters[] = new ParameterDTO( + name: $spec['attribute']['name'], + value_text: $spec['valueType'] === 'text' ? $spec['value'] : null, + value_typ: in_array($spec['valueType'], ['float', 'integer'], true) ? (float) $spec['value'] : null, + unit: $spec['valueType'] === 'text' ? null : $spec['units'], + group: $spec['attribute']['group'], + ); + } + + //Parse the offers + $orderinfos = []; + foreach ($part['sellers'] as $seller) { + foreach ($seller['offers'] as $offer) { + $prices = []; + foreach ($offer['prices'] as $price) { + $prices[] = new PriceDTO( + minimum_discount_amount: $price['quantity'], + price: (string) $price['price'], + currency_iso_code: $price['currency'], + ); + } + + $orderinfos[] = new PurchaseInfoDTO( + distributor_name: $seller['company']['name'], + order_number: $offer['sku'], + prices: $prices, + product_url: $offer['clickUrl'], + ); + } + } + + //Generate a footprint name from the package and pin count + $footprint = null; + if ($package !== null) { + $footprint = $package; + if ($pinCount !== null) { //Add pin count if available + $footprint .= '-' . $pinCount; + } + } + + //Built the category full path + $category = null; + if (!empty($part['category']['name'])) { + $category = implode(' -> ', array_map(static fn($c) => $c['name'], $part['category']['ancestors'] ?? [])); + if ($category !== '' && $category !== '0') { + $category .= ' -> '; + } + $category .= $part['category']['name']; + } + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $part['id'], + name: $part['mpn'], + description: $part['shortDescription'] ?? null, + category: $category , + manufacturer: $part['manufacturer']['name'] ?? null, + mpn: $part['mpn'], + preview_image_url: $part['bestImage']['url'] ?? null, + manufacturing_status: $mStatus, + provider_url: $part['octopartUrl'] ?? null, + footprint: $footprint, + datasheets: $part['bestDatasheet'] !== null ? [new FileDTO($part['bestDatasheet']['url'], $part['bestDatasheet']['name'])]: null, + parameters: $parameters, + vendor_infos: $orderinfos, + mass: $mass, + manufacturer_product_url: $part['manufacturerUrl'] ?? null, + ); + } + + public function searchByKeyword(string $keyword): array + { + $graphQL = sprintf(<<<'GRAPHQL' + query partSearch($keyword: String, $limit: Int, $currency: String!, $country: String!, $authorizedOnly: Boolean!) { + supSearch( + q: $keyword + inStockOnly: false + limit: $limit + currency: $currency + country: $country + ) { + hits + results { + part + %s + } + } + } + GRAPHQL, self::GRAPHQL_PART_SECTION); + + + $result = $this->makeGraphQLCall($graphQL, [ + 'keyword' => $keyword, + 'limit' => $this->search_limit, + 'currency' => $this->currency, + 'country' => $this->country, + 'authorizedOnly' => $this->onlyAuthorizedSellers, + ]); + + $tmp = []; + + foreach ($result['data']['supSearch']['results'] ?? [] as $p) { + $dto = $this->partResultToDTO($p['part']); + $tmp[] = $dto; + //Cache the part, so we can get the details later, without having to make another request + $this->saveToCache($dto); + } + + return $tmp; + } + + public function getDetails(string $id): PartDetailDTO + { + //Check if we have the part cached + $cached = $this->getFromCache($id); + if ($cached !== null) { + return $cached; + } + + //Otherwise we have to make a request + $graphql = sprintf(<<<'GRAPHQL' + query partSearch($ids: [String!]!, $currency: String!, $country: String!, $authorizedOnly: Boolean!) { + supParts(ids: $ids, currency: $currency, country: $country) + %s + } + GRAPHQL, self::GRAPHQL_PART_SECTION); + + $result = $this->makeGraphQLCall($graphql, [ + 'ids' => [$id], + 'currency' => $this->currency, + 'country' => $this->country, + 'authorizedOnly' => $this->onlyAuthorizedSellers, + ]); + + $tmp = $this->partResultToDTO($result['data']['supParts'][0]); + $this->saveToCache($tmp); + return $tmp; + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ]; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/PollinProvider.php b/src/Services/InfoProviderSystem/Providers/PollinProvider.php new file mode 100644 index 00000000..09ab8fd4 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/PollinProvider.php @@ -0,0 +1,249 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Entity\Parts\Part; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class PollinProvider implements InfoProviderInterface +{ + + public function __construct(private readonly HttpClientInterface $client, + #[Autowire(env: 'bool:PROVIDER_POLLIN_ENABLED')] + private readonly bool $enabled = true, + ) + { + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Pollin', + 'description' => 'Webscraping from pollin.de to get part information', + 'url' => 'https://www.pollin.de/', + 'disabled_help' => 'Set PROVIDER_POLLIN_ENABLED env to 1' + ]; + } + + public function getProviderKey(): string + { + return 'pollin'; + } + + public function isActive(): bool + { + return $this->enabled; + } + + public function searchByKeyword(string $keyword): array + { + $response = $this->client->request('GET', 'https://www.pollin.de/search', [ + 'query' => [ + 'search' => $keyword + ] + ]); + + $content = $response->getContent(); + + //If the response has us redirected to the product page, then just return the single item + if ($response->getInfo('redirect_count') > 0) { + return [$this->parseProductPage($content)]; + } + + $dom = new Crawler($content); + + $results = []; + + //Iterate over each div.product-box + $dom->filter('div.product-box')->each(function (Crawler $node) use (&$results) { + $results[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'), + name: $node->filter('a.product-name')->text(), + description: '', + preview_image_url: $node->filter('img.product-image')->attr('src'), + manufacturing_status: $this->mapAvailability($node->filter('link[itemprop="availability"]')->attr('href')), + provider_url: $node->filter('a.product-name')->attr('href') + ); + }); + + return $results; + } + + private function mapAvailability(string $availabilityURI): ManufacturingStatus + { + return match( $availabilityURI) { + 'http://schema.org/InStock' => ManufacturingStatus::ACTIVE, + 'http://schema.org/OutOfStock' => ManufacturingStatus::DISCONTINUED, + default => ManufacturingStatus::NOT_SET + }; + } + + public function getDetails(string $id): PartDetailDTO + { + //Ensure that $id is numeric + if (!is_numeric($id)) { + throw new \InvalidArgumentException("The id must be numeric!"); + } + + $response = $this->client->request('GET', 'https://www.pollin.de/search', [ + 'query' => [ + 'search' => $id + ] + ]); + + //The response must have us redirected to the product page + if ($response->getInfo('redirect_count') > 0) { + throw new \RuntimeException("Could not resolve the product page for the given id!"); + } + + $content = $response->getContent(); + + return $this->parseProductPage($content); + } + + private function parseProductPage(string $content): PartDetailDTO + { + $dom = new Crawler($content); + + $productPageUrl = $dom->filter('meta[property="product:product_link"]')->attr('content'); + $orderId = trim($dom->filter('span[itemprop="sku"]')->text()); //Text is important here + + //Calculate the mass + $massStr = $dom->filter('meta[itemprop="weight"]')->attr('content'); + //Remove the unit + $massStr = str_replace('kg', '', $massStr); + //Convert to float and convert to grams + $mass = (float) $massStr * 1000; + + //Parse purchase info + $purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl); + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $orderId, + name: trim($dom->filter('meta[property="og:title"]')->attr('content')), + description: $dom->filter('meta[property="og:description"]')->attr('content'), + category: $this->parseCategory($dom), + manufacturer: $dom->filter('meta[property="product:brand"]')->count() > 0 ? $dom->filter('meta[property="product:brand"]')->attr('content') : null, + preview_image_url: $dom->filter('meta[property="og:image"]')->attr('content'), + manufacturing_status: $this->mapAvailability($dom->filter('link[itemprop="availability"]')->attr('href')), + provider_url: $productPageUrl, + notes: $this->parseNotes($dom), + datasheets: $this->parseDatasheets($dom), + parameters: $this->parseParameters($dom), + vendor_infos: [$purchaseInfo], + mass: $mass, + ); + } + + private function parseDatasheets(Crawler $dom): array + { + //Iterate over each a element withing div.pol-product-detail-download-files + $datasheets = []; + $dom->filter('div.pol-product-detail-download-files a')->each(function (Crawler $node) use (&$datasheets) { + $datasheets[] = new FileDTO($node->attr('href'), $node->text()); + }); + + return $datasheets; + } + + private function parseParameters(Crawler $dom): array + { + $parameters = []; + + //Iterate over each tr.properties-row inside table.product-detail-properties-table + $dom->filter('table.product-detail-properties-table tr.properties-row')->each(function (Crawler $node) use (&$parameters) { + $parameters[] = ParameterDTO::parseValueIncludingUnit( + name: rtrim($node->filter('th.properties-label')->text(), ':'), + value: trim($node->filter('td.properties-value')->text()) + ); + }); + + return $parameters; + } + + private function parseCategory(Crawler $dom): string + { + $category = ''; + + //Iterate over each li.breadcrumb-item inside ol.breadcrumb + $dom->filter('ol.breadcrumb li.breadcrumb-item')->each(function (Crawler $node) use (&$category) { + //Skip if it has breadcrumb-item-home class + if (str_contains($node->attr('class'), 'breadcrumb-item-home')) { + return; + } + + + $category .= $node->text() . ' -> '; + }); + + //Remove the last ' -> ' + return substr($category, 0, -4); + } + + private function parseNotes(Crawler $dom): string + { + //Concat product highlights and product description + return $dom->filter('div.product-detail-top-features')->html('') . '

' . $dom->filter('div.product-detail-description-text')->html(''); + } + + private function parsePrices(Crawler $dom): array + { + //TODO: Properly handle multiple prices, for now we just look at the price for one piece + + //We assume the currency is always the same + $currency = $dom->filter('meta[property="product:price:currency"]')->attr('content'); + + //If there is meta[property=highPrice] then use this as the price + if ($dom->filter('meta[itemprop="highPrice"]')->count() > 0) { + $price = $dom->filter('meta[itemprop="highPrice"]')->attr('content'); + } else { + $price = $dom->filter('meta[property="product:price:amount"]')->attr('content'); + } + + return [ + new PriceDTO(1.0, $price, $currency) + ]; + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::PRICE, + ProviderCapabilities::DATASHEET + ]; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php new file mode 100644 index 00000000..fd67cd2c --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +/** + * This enum contains all capabilities (which data it can provide) a provider can have. + */ +enum ProviderCapabilities +{ + /** Basic information about a part, like the name, description, part number, manufacturer etc */ + case BASIC; + + /** Information about the footprint of a part */ + case FOOTPRINT; + + /** Provider can provide a picture for a part */ + case PICTURE; + + /** Provider can provide datasheets for a part */ + case DATASHEET; + + /** Provider can provide prices for a part */ + case PRICE; + + public function getTranslationKey(): string + { + return 'info_providers.capabilities.' . match($this) { + self::BASIC => 'basic', + self::FOOTPRINT => 'footprint', + self::PICTURE => 'picture', + self::DATASHEET => 'datasheet', + self::PRICE => 'price', + }; + } + + public function getFAIconClass(): string + { + return 'fa-solid ' . match($this) { + self::BASIC => 'fa-info-circle', + self::FOOTPRINT => 'fa-microchip', + self::PICTURE => 'fa-image', + self::DATASHEET => 'fa-file-alt', + self::PRICE => 'fa-money-bill-wave', + }; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php new file mode 100644 index 00000000..0c31c411 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php @@ -0,0 +1,285 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +class ReicheltProvider implements InfoProviderInterface +{ + + public const DISTRIBUTOR_NAME = "Reichelt"; + + public function __construct(private readonly HttpClientInterface $client, + #[Autowire(env: "bool:PROVIDER_REICHELT_ENABLED")] + private readonly bool $enabled = true, + #[Autowire(env: "PROVIDER_REICHELT_LANGUAGE")] + private readonly string $language = "en", + #[Autowire(env: "PROVIDER_REICHELT_COUNTRY")] + private readonly string $country = "DE", + #[Autowire(env: "PROVIDER_REICHELT_INCLUDE_VAT")] + private readonly bool $includeVAT = false, + #[Autowire(env: "PROVIDER_REICHELT_CURRENCY")] + private readonly string $currency = "EUR", + ) + { + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'Reichelt', + 'description' => 'Webscraping from reichelt.com to get part information', + 'url' => 'https://www.reichelt.com/', + 'disabled_help' => 'Set PROVIDER_REICHELT_ENABLED env to 1' + ]; + } + + public function getProviderKey(): string + { + return 'reichelt'; + } + + public function isActive(): bool + { + return $this->enabled; + } + + public function searchByKeyword(string $keyword): array + { + $response = $this->client->request('GET', sprintf($this->getBaseURL() . '/shop/search/%s', $keyword)); + $html = $response->getContent(); + + //Parse the HTML and return the results + $dom = new Crawler($html); + //Iterate over all div.al_gallery_article elements + $results = []; + $dom->filter('div.al_gallery_article')->each(function (Crawler $element) use (&$results) { + + //Extract product id from data-product attribute + $artId = json_decode($element->attr('data-product'), true, 2, JSON_THROW_ON_ERROR)['artid']; + + $productID = $element->filter('meta[itemprop="productID"]')->attr('content'); + $name = $element->filter('meta[itemprop="name"]')->attr('content'); + $sku = $element->filter('meta[itemprop="sku"]')->attr('content'); + + //Try to extract a picture URL: + $pictureURL = $element->filter("div.al_artlogo img")->attr('src'); + + $results[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $artId, + name: $productID, + description: $name, + category: null, + manufacturer: $sku, + preview_image_url: $pictureURL, + provider_url: $element->filter('a.al_artinfo_link')->attr('href') + ); + }); + + return $results; + } + + public function getDetails(string $id): PartDetailDTO + { + //Check that the ID is a number + if (!is_numeric($id)) { + throw new \InvalidArgumentException("Invalid ID"); + } + + //Use this endpoint to resolve the artID to a product page + $response = $this->client->request('GET', + sprintf( + 'https://www.reichelt.com/?ACTION=514&id=74&article=%s&LANGUAGE=%s&CCOUNTRY=%s', + $id, + strtoupper($this->language), + strtoupper($this->country) + ) + ); + $json = $response->toArray(); + + //Retrieve the product page from the response + $productPage = $this->getBaseURL() . '/shop/product' . $json[0]['article_path']; + + + $response = $this->client->request('GET', $productPage, [ + 'query' => [ + 'CCTYPE' => $this->includeVAT ? 'private' : 'business', + 'currency' => $this->currency, + ], + ]); + $html = $response->getContent(); + $dom = new Crawler($html); + + //Extract the product notes + $notes = $dom->filter('p[itemprop="description"]')->html(); + + //Extract datasheets + $datasheets = []; + $dom->filter('div.articleDatasheet a')->each(function (Crawler $element) use (&$datasheets) { + $datasheets[] = new FileDTO($element->attr('href'), $element->filter('span')->text()); + }); + + //Determine price for one unit + $priceString = $dom->filter('meta[itemprop="price"]')->attr('content'); + $currency = $dom->filter('meta[itemprop="priceCurrency"]')->attr('content', 'EUR'); + + //Create purchase info + $purchaseInfo = new PurchaseInfoDTO( + distributor_name: self::DISTRIBUTOR_NAME, + order_number: $json[0]['article_artnr'], + prices: array_merge( + [new PriceDTO(1.0, $priceString, $currency, $this->includeVAT)] + , $this->parseBatchPrices($dom, $currency)), + product_url: $productPage + ); + + //Create part object + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $id, + name: $json[0]['article_artnr'], + description: $json[0]['article_besch'], + category: $this->parseCategory($dom), + manufacturer: $json[0]['manufacturer_name'], + mpn: $this->parseMPN($dom), + preview_image_url: $json[0]['article_picture'], + provider_url: $productPage, + notes: $notes, + datasheets: $datasheets, + parameters: $this->parseParameters($dom), + vendor_infos: [$purchaseInfo] + ); + + } + + private function parseMPN(Crawler $dom): string + { + //Find the small element directly after meta[itemprop="url"] element + $element = $dom->filter('meta[itemprop="url"] + small'); + //If the text contains GTIN text, take the small element afterwards + if (str_contains($element->text(), 'GTIN')) { + $element = $dom->filter('meta[itemprop="url"] + small + small'); + } + + //The MPN is contained in the span inside the element + return $element->filter('span')->text(); + } + + private function parseBatchPrices(Crawler $dom, string $currency): array + { + //Iterate over each a.inline-block element in div.discountValue + $prices = []; + $dom->filter('div.discountValue a.inline-block')->each(function (Crawler $element) use (&$prices, $currency) { + //The minimum amount is the number in the span.block element + $minAmountText = $element->filter('span.block')->text(); + + //Extract a integer from the text + $matches = []; + if (!preg_match('/\d+/', $minAmountText, $matches)) { + return; + } + + $minAmount = (int) $matches[0]; + + //The price is the text of the p.productPrice element + $priceString = $element->filter('p.productPrice')->text(); + //Replace comma with dot + $priceString = str_replace(',', '.', $priceString); + //Strip any non-numeric characters + $priceString = preg_replace('/[^0-9.]/', '', $priceString); + + $prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->includeVAT); + }); + + return $prices; + } + + + private function parseCategory(Crawler $dom): string + { + // Look for ol.breadcrumb and iterate over the li elements + $category = ''; + $dom->filter('ol.breadcrumb li.triangle-left')->each(function (Crawler $element) use (&$category) { + //Do not include the .breadcrumb-showmore element + if ($element->attr('id') === 'breadcrumb-showmore') { + return; + } + + $category .= $element->text() . ' -> '; + }); + //Remove the trailing ' -> ' + $category = substr($category, 0, -4); + + return $category; + } + + /** + * @param Crawler $dom + * @return ParameterDTO[] + */ + private function parseParameters(Crawler $dom): array + { + $parameters = []; + //Iterate over each ul.articleTechnicalData which contains the specifications of each group + $dom->filter('ul.articleTechnicalData')->each(function (Crawler $groupElement) use (&$parameters) { + $groupName = $groupElement->filter('li.articleTechnicalHeadline')->text(); + + //Iterate over each second li in ul.articleAttribute, which contains the specifications + $groupElement->filter('ul.articleAttribute li:nth-child(2n)')->each(function (Crawler $specElement) use (&$parameters, $groupName) { + $parameters[] = ParameterDTO::parseValueIncludingUnit( + name: $specElement->previousAll()->text(), + value: $specElement->text(), + group: $groupName + ); + }); + }); + + return $parameters; + } + + private function getBaseURL(): string + { + //Without the trailing slash + return 'https://www.reichelt.com/' . strtolower($this->country) . '/' . strtolower($this->language); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ]; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/TMEClient.php b/src/Services/InfoProviderSystem/Providers/TMEClient.php new file mode 100644 index 00000000..d4df133e --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/TMEClient.php @@ -0,0 +1,96 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class TMEClient +{ + public const BASE_URI = 'https://api.tme.eu'; + + public function __construct(private readonly HttpClientInterface $tmeClient, private readonly string $token, private readonly string $secret) + { + + } + + public function makeRequest(string $action, array $parameters): ResponseInterface + { + $parameters['Token'] = $this->token; + $parameters['ApiSignature'] = $this->getSignature($action, $parameters, $this->secret); + + return $this->tmeClient->request('POST', $this->getUrlForAction($action), [ + 'body' => $parameters, + ]); + } + + public function isUsable(): bool + { + return $this->token !== '' && $this->secret !== ''; + } + + /** + * Returns true if the client is using a private (account related token) instead of a deprecated anonymous token + * to authenticate with TME. + * @return bool + */ + public function isUsingPrivateToken(): bool + { + //Private tokens are longer than anonymous ones (50 instead of 45 characters) + return strlen($this->token) > 45; + } + + /** + * Generates the signature for the given action and parameters. + * Taken from https://github.com/tme-dev/TME-API/blob/master/PHP/basic/using_curl.php + */ + public function getSignature(string $action, array $parameters, string $appSecret): string + { + $parameters = $this->sortSignatureParams($parameters); + + $queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986); + $signatureBase = strtoupper('POST') . + '&' . rawurlencode($this->getUrlForAction($action)) . '&' . rawurlencode($queryString); + + return base64_encode(hash_hmac('sha1', $signatureBase, $appSecret, true)); + } + + private function getUrlForAction(string $action): string + { + return self::BASE_URI . '/' . $action . '.json'; + } + + private function sortSignatureParams(array $params): array + { + ksort($params); + + foreach ($params as &$value) { + if (is_array($value)) { + $value = $this->sortSignatureParams($value); + } + } + + return $params; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/TMEProvider.php b/src/Services/InfoProviderSystem/Providers/TMEProvider.php new file mode 100644 index 00000000..32fc0c72 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php @@ -0,0 +1,301 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; + +class TMEProvider implements InfoProviderInterface +{ + + private const VENDOR_NAME = 'TME'; + + /** @var bool If true, the prices are gross prices. If false, the prices are net prices. */ + private readonly bool $get_gross_prices; + + public function __construct(private readonly TMEClient $tmeClient, private readonly string $country, + private readonly string $language, private readonly string $currency, + bool $get_gross_prices) + { + //If we have a private token, set get_gross_prices to false, as it is automatically determined by the account type then + if ($this->tmeClient->isUsingPrivateToken()) { + $this->get_gross_prices = false; + } else { + $this->get_gross_prices = $get_gross_prices; + } + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'TME', + 'description' => 'This provider uses the API of TME (Transfer Multipart).', + 'url' => 'https://tme.eu/', + 'disabled_help' => 'Configure the PROVIDER_TME_KEY and PROVIDER_TME_SECRET environment variables to use this provider.' + ]; + } + + public function getProviderKey(): string + { + return 'tme'; + } + + public function isActive(): bool + { + return $this->tmeClient->isUsable(); + } + + public function searchByKeyword(string $keyword): array + { + $response = $this->tmeClient->makeRequest('Products/Search', [ + 'Country' => $this->country, + 'Language' => $this->language, + 'SearchPlain' => $keyword, + ]); + + $data = $response->toArray()['Data']; + + $result = []; + + foreach($data['ProductList'] as $product) { + $result[] = new SearchResultDTO( + provider_key: $this->getProviderKey(), + provider_id: $product['Symbol'], + name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], + description: $product['Description'], + category: $product['Category'], + manufacturer: $product['Producer'], + mpn: $product['OriginalSymbol'] ?? null, + preview_image_url: $this->normalizeURL($product['Photo']), + manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['ProductStatusList']), + provider_url: $this->normalizeURL($product['ProductInformationPage']), + ); + } + + return $result; + } + + public function getDetails(string $id): PartDetailDTO + { + $response = $this->tmeClient->makeRequest('Products/GetProducts', [ + 'Country' => $this->country, + 'Language' => $this->language, + 'SymbolList' => [$id], + ]); + + $product = $response->toArray()['Data']['ProductList'][0]; + + //Add a explicit https:// to the url if it is missing + $productInfoPage = $this->normalizeURL($product['ProductInformationPage']); + + $files = $this->getFiles($id); + + $footprint = null; + + $parameters = $this->getParameters($id, $footprint); + + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $product['Symbol'], + name: empty($product['OriginalSymbol']) ? $product['Symbol'] : $product['OriginalSymbol'], + description: $product['Description'], + category: $product['Category'], + manufacturer: $product['Producer'], + mpn: $product['OriginalSymbol'] ?? null, + preview_image_url: $this->normalizeURL($product['Photo']), + manufacturing_status: $this->productStatusArrayToManufacturingStatus($product['ProductStatusList']), + provider_url: $productInfoPage, + footprint: $footprint, + datasheets: $files['datasheets'], + images: $files['images'], + parameters: $parameters, + vendor_infos: [$this->getVendorInfo($id, $productInfoPage)], + mass: $product['WeightUnit'] === 'g' ? $product['Weight'] : null, + ); + } + + /** + * Fetches all files for a given product id + * @param string $id + * @return array> An array with the keys 'datasheet' + * @phpstan-return array{datasheets: list, images: list} + */ + public function getFiles(string $id): array + { + $response = $this->tmeClient->makeRequest('Products/GetProductsFiles', [ + 'Country' => $this->country, + 'Language' => $this->language, + 'SymbolList' => [$id], + ]); + + $data = $response->toArray()['Data']; + $files = $data['ProductList'][0]['Files']; + + //Extract datasheets + $documentList = $files['DocumentList']; + $datasheets = []; + foreach($documentList as $document) { + $datasheets[] = new FileDTO( + url: $this->normalizeURL($document['DocumentUrl']), + ); + } + + //Extract images + $imageList = $files['AdditionalPhotoList']; + $images = []; + foreach($imageList as $image) { + $images[] = new FileDTO( + url: $this->normalizeURL($image['HighResolutionPhoto']), + ); + } + + + return [ + 'datasheets' => $datasheets, + 'images' => $images, + ]; + } + + /** + * Fetches the vendor/purchase information for a given product id. + * @param string $id + * @param string|null $productURL + * @return PurchaseInfoDTO + */ + public function getVendorInfo(string $id, ?string $productURL = null): PurchaseInfoDTO + { + $response = $this->tmeClient->makeRequest('Products/GetPricesAndStocks', [ + 'Country' => $this->country, + 'Language' => $this->language, + 'Currency' => $this->currency, + 'GrossPrices' => $this->get_gross_prices, + 'SymbolList' => [$id], + ]); + + $data = $response->toArray()['Data']; + $currency = $data['Currency']; + $include_tax = $data['PriceType'] === 'GROSS'; + + + $product = $response->toArray()['Data']['ProductList'][0]; + $vendor_order_number = $product['Symbol']; + $priceList = $product['PriceList']; + + $prices = []; + foreach ($priceList as $price) { + $prices[] = new PriceDTO( + minimum_discount_amount: $price['Amount'], + price: (string) $price['PriceValue'], + currency_iso_code: $currency, + includes_tax: $include_tax, + ); + } + + return new PurchaseInfoDTO( + distributor_name: self::VENDOR_NAME, + order_number: $vendor_order_number, + prices: $prices, + product_url: $productURL, + ); + } + + /** + * Fetches the parameters of a product + * @param string $id + * @param string|null $footprint_name You can pass a variable by reference, where the name of the footprint will be stored + * @return ParameterDTO[] + */ + public function getParameters(string $id, string|null &$footprint_name = null): array + { + $response = $this->tmeClient->makeRequest('Products/GetParameters', [ + 'Country' => $this->country, + 'Language' => $this->language, + 'SymbolList' => [$id], + ]); + + $data = $response->toArray()['Data']['ProductList'][0]; + + $result = []; + + $footprint_name = null; + + foreach($data['ParameterList'] as $parameter) { + $result[] = ParameterDTO::parseValueIncludingUnit($parameter['ParameterName'], $parameter['ParameterValue']); + + //Check if the parameter is the case/footprint + if ($parameter['ParameterId'] === 35) { + $footprint_name = $parameter['ParameterValue']; + } + } + + return $result; + } + + /** + * Convert the array of product statuses to a single manufacturing status + * @param array $statusArray + * @return ManufacturingStatus + */ + private function productStatusArrayToManufacturingStatus(array $statusArray): ManufacturingStatus + { + if (in_array('AVAILABLE_WHILE_STOCKS_LAST', $statusArray, true)) { + return ManufacturingStatus::EOL; + } + + if (in_array('INVALID', $statusArray, true)) { + return ManufacturingStatus::DISCONTINUED; + } + + //By default we assume that the part is active + return ManufacturingStatus::ACTIVE; + } + + + + private function normalizeURL(string $url): string + { + //If a URL starts with // we assume that it is a relative URL and we add the protocol + if (str_starts_with($url, '//')) { + return 'https:' . $url; + } + + return $url; + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ]; + } +} \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/Providers/TestProvider.php b/src/Services/InfoProviderSystem/Providers/TestProvider.php new file mode 100644 index 00000000..8b78c95a --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/TestProvider.php @@ -0,0 +1,95 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use Symfony\Component\DependencyInjection\Attribute\When; + +/** + * This is a provider, which is used during tests + */ +#[When(env: 'test')] +class TestProvider implements InfoProviderInterface +{ + + public function getProviderInfo(): array + { + return [ + 'name' => 'Test Provider', + 'description' => 'This is a test provider', + //'url' => 'https://example.com', + 'disabled_help' => 'This provider is disabled for testing purposes' + ]; + } + + public function getProviderKey(): string + { + return 'test'; + } + + public function isActive(): bool + { + return true; + } + + public function searchByKeyword(string $keyword): array + { + return [ + new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element1', name: 'Element 1', description: 'fd'), + new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element2', name: 'Element 2', description: 'fd'), + new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element3', name: 'Element 3', description: 'fd'), + ]; + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::FOOTPRINT, + ]; + } + + public function getDetails(string $id): PartDetailDTO + { + return new PartDetailDTO( + provider_key: $this->getProviderKey(), + provider_id: $id, + name: 'Test Element', + description: 'fd', + manufacturer: 'Test Manufacturer', + mpn: '1234', + provider_url: 'https://invalid.invalid', + footprint: 'Footprint', + notes: 'Notes', + datasheets: [ + new FileDTO('https://invalid.invalid/invalid.pdf', 'Datasheet') + ], + images: [ + new FileDTO('https://invalid.invalid/invalid.png', 'Image') + ] + ); + } +} \ No newline at end of file diff --git a/src/Services/LabelSystem/BarcodeGenerator.php b/src/Services/LabelSystem/BarcodeGenerator.php deleted file mode 100644 index a192abf2..00000000 --- a/src/Services/LabelSystem/BarcodeGenerator.php +++ /dev/null @@ -1,144 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * 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 . - */ - -namespace App\Services\LabelSystem; - -use App\Entity\LabelSystem\LabelOptions; -use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator; -use Com\Tecnick\Barcode\Barcode; -use InvalidArgumentException; -use PhpParser\Node\Stmt\Label; -use Symfony\Component\Mime\MimeTypes; -use Twig\Extra\Html\HtmlExtension; - -final class BarcodeGenerator -{ - private BarcodeContentGenerator $barcodeContentGenerator; - - - public function __construct(BarcodeContentGenerator $barcodeContentGenerator) - { - $this->barcodeContentGenerator = $barcodeContentGenerator; - } - - public function generateHTMLBarcode(LabelOptions $options, object $target): ?string - { - $svg = $this->generateSVG($options, $target); - $base64 = $this->dataUri($svg, 'image/svg+xml'); - return ''. $this->getContent($options, $target) . ''; - } - - /** - * Creates a data URI (RFC 2397). - * Based on the Twig implementaion from HTMLExtension - * - * Length validation is not performed on purpose, validation should - * be done before calling this filter. - * - * @return string The generated data URI - */ - private function dataUri(string $data, string $mime): string - { - $repr = 'data:'; - - $repr .= $mime; - if (0 === strpos($mime, 'text/')) { - $repr .= ','.rawurlencode($data); - } else { - $repr .= ';base64,'.base64_encode($data); - } - - return $repr; - } - - public function generateSVG(LabelOptions $options, object $target): ?string - { - $barcode = new Barcode(); - - switch ($options->getBarcodeType()) { - case 'qr': - $type = 'QRCODE'; - - break; - case 'datamatrix': - $type = 'DATAMATRIX'; - - break; - case 'code39': - $type = 'C39'; - - break; - case 'code93': - $type = 'C93'; - - break; - case 'code128': - $type = 'C128A'; - - break; - case 'none': - return null; - default: - throw new InvalidArgumentException('Unknown label type!'); - } - - $bobj = $barcode->getBarcodeObj($type, $this->getContent($options, $target)); - - return $bobj->getSvgCode(); - } - - public function getContent(LabelOptions $options, object $target): ?string - { - switch ($options->getBarcodeType()) { - case 'qr': - case 'datamatrix': - return $this->barcodeContentGenerator->getURLContent($target); - case 'code39': - case 'code93': - case 'code128': - return $this->barcodeContentGenerator->get1DBarcodeContent($target); - case 'none': - return null; - default: - throw new InvalidArgumentException('Unknown label type!'); - } - } -} diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php new file mode 100644 index 00000000..2de7c035 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php @@ -0,0 +1,166 @@ +. + */ + +declare(strict_types=1); + +/** + * 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 . + */ + +namespace App\Services\LabelSystem\BarcodeScanner; + +use App\Entity\LabelSystem\LabelSupportedElement; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityNotFoundException; +use InvalidArgumentException; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest + */ +final class BarcodeRedirector +{ + public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em) + { + } + + /** + * Determines the URL to which the user should be redirected, when scanning a QR code. + * + * @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan + * @return string the URL to which should be redirected + * + * @throws EntityNotFoundException + */ + public function getRedirectURL(BarcodeScanResultInterface $barcodeScan): string + { + if($barcodeScan instanceof LocalBarcodeScanResult) { + return $this->getURLLocalBarcode($barcodeScan); + } + + if ($barcodeScan instanceof EIGP114BarcodeScanResult) { + return $this->getURLVendorBarcode($barcodeScan); + } + + throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan)); + } + + private function getURLLocalBarcode(LocalBarcodeScanResult $barcodeScan): string + { + switch ($barcodeScan->target_type) { + case LabelSupportedElement::PART: + return $this->urlGenerator->generate('app_part_show', ['id' => $barcodeScan->target_id]); + case LabelSupportedElement::PART_LOT: + //Try to determine the part to the given lot + $lot = $this->em->find(PartLot::class, $barcodeScan->target_id); + if (!$lot instanceof PartLot) { + throw new EntityNotFoundException(); + } + + return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]); + + case LabelSupportedElement::STORELOCATION: + return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]); + + default: + throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name); + } + } + + /** + * Gets the URL to a part from a scan of a Vendor Barcode + */ + private function getURLVendorBarcode(EIGP114BarcodeScanResult $barcodeScan): string + { + $part = $this->getPartFromVendor($barcodeScan); + return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]); + } + + /** + * Gets a part from a scan of a Vendor Barcode by filtering for parts + * with the same Info Provider Id or, if that fails, by looking for parts with a + * matching manufacturer product number. Only returns the first matching part. + */ + private function getPartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : Part + { + // first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via + // the info provider system or if the part was bought from a different vendor than the data was retrieved + // from. + if($barcodeScan->digikeyPartNumber) { + $qb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); + //Lower() to be case insensitive + $qb->where($qb->expr()->like('LOWER(part.providerReference.provider_id)', 'LOWER(:vendor_id)')); + $qb->setParameter('vendor_id', $barcodeScan->digikeyPartNumber); + $results = $qb->getQuery()->getResult(); + if ($results) { + return $results[0]; + } + } + + if(!$barcodeScan->supplierPartNumber){ + throw new EntityNotFoundException(); + } + + //Fallback to the manufacturer part number. This may return false positives, since it is common for + //multiple manufacturers to use the same part number for their version of a common product + //We assume the user is able to realize when this returns the wrong part + //If the barcode specifies the manufacturer we try to use that as well + $mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); + $mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)')); + $mpnQb->setParameter('mpn', $barcodeScan->supplierPartNumber); + + if($barcodeScan->mouserManufacturer){ + $manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder("manufacturer"); + $manufacturerQb->where($manufacturerQb->expr()->like("LOWER(manufacturer.name)", "LOWER(:manufacturer_name)")); + $manufacturerQb->setParameter("manufacturer_name", $barcodeScan->mouserManufacturer); + $manufacturers = $manufacturerQb->getQuery()->getResult(); + + if($manufacturers) { + $mpnQb->andWhere($mpnQb->expr()->eq("part.manufacturer", ":manufacturer")); + $mpnQb->setParameter("manufacturer", $manufacturers); + } + + } + + $results = $mpnQb->getQuery()->getResult(); + if($results){ + return $results[0]; + } + throw new EntityNotFoundException(); + } +} diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php new file mode 100644 index 00000000..e5930b36 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php @@ -0,0 +1,243 @@ +. + */ + +declare(strict_types=1); + +/** + * 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 . + */ + +namespace App\Services\LabelSystem\BarcodeScanner; + +use App\Entity\LabelSystem\LabelSupportedElement; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use Doctrine\ORM\EntityManagerInterface; +use InvalidArgumentException; + +/** + * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeScanHelperTest + */ +final class BarcodeScanHelper +{ + private const PREFIX_TYPE_MAP = [ + 'L' => LabelSupportedElement::PART_LOT, + 'P' => LabelSupportedElement::PART, + 'S' => LabelSupportedElement::STORELOCATION, + ]; + + public const QR_TYPE_MAP = [ + 'lot' => LabelSupportedElement::PART_LOT, + 'part' => LabelSupportedElement::PART, + 'location' => LabelSupportedElement::STORELOCATION, + ]; + + public function __construct(private readonly EntityManagerInterface $entityManager) + { + } + + /** + * Parse the given barcode content and return the target type and ID. + * If the barcode could not be parsed, an exception is thrown. + * Using the $type parameter, you can specify how the barcode should be parsed. If set to null, the function + * will try to guess the type. + * @param string $input + * @param BarcodeSourceType|null $type + * @return BarcodeScanResultInterface + */ + public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): BarcodeScanResultInterface + { + //Do specific parsing + if ($type === BarcodeSourceType::INTERNAL) { + return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); + } + if ($type === BarcodeSourceType::USER_DEFINED) { + return $this->parseUserDefinedBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); + } + if ($type === BarcodeSourceType::IPN) { + return $this->parseIPNBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); + } + if ($type === BarcodeSourceType::EIGP114) { + return $this->parseEIGP114Barcode($input); + } + + //Null means auto and we try the different formats + $result = $this->parseInternalBarcode($input); + + if ($result !== null) { + return $result; + } + + //Try to parse as User defined barcode + $result = $this->parseUserDefinedBarcode($input); + if ($result !== null) { + return $result; + } + + //If the barcode is formatted as EIGP114, we can parse it directly + if (EIGP114BarcodeScanResult::isFormat06Code($input)) { + return $this->parseEIGP114Barcode($input); + } + + //Try to parse as IPN barcode + $result = $this->parseIPNBarcode($input); + if ($result !== null) { + return $result; + } + + throw new InvalidArgumentException('Unknown barcode'); + } + + private function parseEIGP114Barcode(string $input): EIGP114BarcodeScanResult + { + return EIGP114BarcodeScanResult::parseFormat06Code($input); + } + + private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult + { + $lot_repo = $this->entityManager->getRepository(PartLot::class); + //Find only the first result + $results = $lot_repo->findBy(['user_barcode' => $input], limit: 1); + + if (count($results) === 0) { + return null; + } + //We found a part, so use it to create the result + $lot = $results[0]; + + return new LocalBarcodeScanResult( + target_type: LabelSupportedElement::PART_LOT, + target_id: $lot->getID(), + source_type: BarcodeSourceType::USER_DEFINED + ); + } + + private function parseIPNBarcode(string $input): ?LocalBarcodeScanResult + { + $part_repo = $this->entityManager->getRepository(Part::class); + //Find only the first result + $results = $part_repo->findBy(['ipn' => $input], limit: 1); + + if (count($results) === 0) { + return null; + } + //We found a part, so use it to create the result + $part = $results[0]; + + return new LocalBarcodeScanResult( + target_type: LabelSupportedElement::PART, + target_id: $part->getID(), + source_type: BarcodeSourceType::IPN + ); + } + + /** + * This function tries to interpret the given barcode content as an internal barcode. + * If the barcode could not be parsed at all, null is returned. If the barcode is a valid format, but could + * not be found in the database, an exception is thrown. + * @param string $input + * @return LocalBarcodeScanResult|null + */ + private function parseInternalBarcode(string $input): ?LocalBarcodeScanResult + { + $input = trim($input); + $matches = []; + + //Some scanner output '-' as ß, so replace it (ß is never used, so we can replace it safely) + $input = str_replace('ß', '-', $input); + + //Extract parts from QR code's URL + if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) { + return new LocalBarcodeScanResult( + target_type: self::QR_TYPE_MAP[strtolower($matches[1])], + target_id: (int) $matches[2], + source_type: BarcodeSourceType::INTERNAL + ); + } + + //New Code39 barcode use L0001 format + if (preg_match('#^([A-Z])(\d{4,})$#', $input, $matches)) { + $prefix = $matches[1]; + $id = (int) $matches[2]; + + if (!isset(self::PREFIX_TYPE_MAP[$prefix])) { + throw new InvalidArgumentException('Unknown prefix '.$prefix); + } + + return new LocalBarcodeScanResult( + target_type: self::PREFIX_TYPE_MAP[$prefix], + target_id: $id, + source_type: BarcodeSourceType::INTERNAL + ); + } + + //During development the L-000001 format was used + if (preg_match('#^(\w)-(\d{6,})$#', $input, $matches)) { + $prefix = $matches[1]; + $id = (int) $matches[2]; + + if (!isset(self::PREFIX_TYPE_MAP[$prefix])) { + throw new InvalidArgumentException('Unknown prefix '.$prefix); + } + + return new LocalBarcodeScanResult( + target_type: self::PREFIX_TYPE_MAP[$prefix], + target_id: $id, + source_type: BarcodeSourceType::INTERNAL + ); + } + + //Legacy Part-DB location labels used $L00336 format + if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) { + return new LocalBarcodeScanResult( + target_type: LabelSupportedElement::STORELOCATION, + target_id: (int) $matches[1], + source_type: BarcodeSourceType::INTERNAL + ); + } + + //Legacy Part-DB used EAN8 barcodes for part labels. Format 0000001(2) (note the optional 8th digit => checksum) + if (preg_match('#^(\d{7})\d?$#', $input, $matches)) { + return new LocalBarcodeScanResult( + target_type: LabelSupportedElement::PART, + target_id: (int) $matches[1], + source_type: BarcodeSourceType::INTERNAL + ); + } + + //This function abstain from further parsing + return null; + } +} diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php new file mode 100644 index 00000000..88130351 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +interface BarcodeScanResultInterface +{ + /** + * Returns all data that was decoded from the barcode in a format, that can be shown in a table to the user. + * The return values of this function are not meant to be parsed by code again, but should just give a information + * to the user. + * The keys of the returned array are the first column of the table and the values are the second column. + * @return array + */ + public function getDecodedForInfoMode(): array; +} \ No newline at end of file diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php new file mode 100644 index 00000000..40f707de --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +/** + * This enum represents the different types, where a barcode/QR-code can be generated from + */ +enum BarcodeSourceType +{ + /** This Barcode was generated using Part-DB internal recommended barcode generator */ + case INTERNAL; + /** This barcode is containing an internal part number (IPN) */ + case IPN; + + /** + * This barcode is a user defined barcode defined on a part lot + */ + case USER_DEFINED; + + /** + * EIGP114 formatted barcodes like used by digikey, mouser, etc. + */ + case EIGP114; +} \ No newline at end of file diff --git a/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php new file mode 100644 index 00000000..0b4f4b56 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php @@ -0,0 +1,332 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +/** + * This class represents the content of a EIGP114 barcode. + * Based on PR 811, EIGP 114.2018 (https://www.ecianow.org/assets/docs/GIPC/EIGP-114.2018%20ECIA%20Labeling%20Specification%20for%20Product%20and%20Shipment%20Identification%20in%20the%20Electronics%20Industry%20-%202D%20Barcode.pdf), + * , https://forum.digikey.com/t/digikey-product-labels-decoding-digikey-barcodes/41097 + */ +class EIGP114BarcodeScanResult implements BarcodeScanResultInterface +{ + + /** + * @var string|null Ship date in format YYYYMMDD + */ + public readonly ?string $shipDate; + + /** + * @var string|null Customer assigned part number – Optional based on + * agreements between Distributor and Supplier + */ + public readonly ?string $customerPartNumber; + + /** + * @var string|null Supplier assigned part number + */ + public readonly ?string $supplierPartNumber; + + /** + * @var int|null Quantity of product + */ + public readonly ?int $quantity; + + /** + * @var string|null Customer assigned purchase order number + */ + public readonly ?string $customerPO; + + /** + * @var string|null Line item number from PO. Required on Logistic Label when + * used on back of Packing Slip. See Section 4.9 + */ + public readonly ?string $customerPOLine; + + /** + * 9D - YYWW (Year and Week of Manufacture). ) If no date code is used + * for a particular part, this field should be populated with N/T + * to indicate the product is Not Traceable by this data field. + * @var string|null + */ + public readonly ?string $dateCode; + + /** + * 10D - YYWW (Year and Week of Manufacture). ) If no date code is used + * for a particular part, this field should be populated with N/T + * to indicate the product is Not Traceable by this data field. + * @var string|null + */ + public readonly ?string $alternativeDateCode; + + /** + * Traceability number assigned to a batch or group of items. If + * no lot code is used for a particular part, this field should be + * populated with N/T to indicate the product is Not Traceable + * by this data field. + * @var string|null + */ + public readonly ?string $lotCode; + + /** + * Country where part was manufactured. Two-letter code from + * ISO 3166 country code list + * @var string|null + */ + public readonly ?string $countryOfOrigin; + + /** + * @var string|null Unique alphanumeric number assigned by supplier + * 3S - Package ID for Inner Pack when part of a mixed Logistic + * Carton. Always used in conjunction with a mixed logistic label + * with a 5S data identifier for Package ID. + */ + public readonly ?string $packageId1; + + /** + * @var string|null + * 4S - Package ID for Logistic Carton with like items + */ + public readonly ?string $packageId2; + + /** + * @var string|null + * 5S - Package ID for Logistic Carton with mixed items + */ + public readonly ?string $packageId3; + + /** + * @var string|null Unique alphanumeric number assigned by supplier. + */ + public readonly ?string $packingListNumber; + + /** + * @var string|null Ship date in format YYYYMMDD + */ + public readonly ?string $serialNumber; + + /** + * @var string|null Code for sorting and classifying LEDs. Use when applicable + */ + public readonly ?string $binCode; + + /** + * @var int|null Sequential carton count in format “#/#” or “# of #” + */ + public readonly ?int $packageCount; + + /** + * @var string|null Alphanumeric string assigned by the supplier to distinguish + * from one closely-related design variation to another. Use as + * required or when applicable + */ + public readonly ?string $revisionNumber; + + /** + * @var string|null Digikey Extension: This is not represented in the ECIA spec, but the field being used is found in the ANSI MH10.8.2-2016 spec on which the ECIA spec is based. In the ANSI spec it is called First Level (Supplier Assigned) Part Number. + */ + public readonly ?string $digikeyPartNumber; + + /** + * @var string|null Digikey Extension: This can be shared across multiple invoices and time periods and is generated as an order enters our system from any vector (web, API, phone order, etc.) + */ + public readonly ?string $digikeySalesOrderNumber; + + /** + * @var string|null Digikey extension: This is typically assigned per shipment as items are being released to be picked in the warehouse. A SO can have many Invoice numbers + */ + public readonly ?string $digikeyInvoiceNumber; + + /** + * @var string|null Digikey extension: This is for internal DigiKey purposes and defines the label type. + */ + public readonly ?string $digikeyLabelType; + + /** + * @var string|null You will also see this as the last part of a URL for a product detail page. Ex https://www.digikey.com/en/products/detail/w%C3%BCrth-elektronik/860010672008/5726907 + */ + public readonly ?string $digikeyPartID; + + /** + * @var string|null Digikey Extension: For internal use of Digikey. Probably not needed + */ + public readonly ?string $digikeyNA; + + /** + * @var string|null Digikey Extension: This is a field of varying length used to keep the barcode approximately the same size between labels. It is safe to ignore. + */ + public readonly ?string $digikeyPadding; + + public readonly ?string $mouserPositionInOrder; + + public readonly ?string $mouserManufacturer; + + + + /** + * + * @param array $data The fields of the EIGP114 barcode, where the key is the field name and the value is the field content + */ + public function __construct(public readonly array $data) + { + //IDs per EIGP 114.2018 + $this->shipDate = $data['6D'] ?? null; + $this->customerPartNumber = $data['P'] ?? null; + $this->supplierPartNumber = $data['1P'] ?? null; + $this->quantity = isset($data['Q']) ? (int)$data['Q'] : null; + $this->customerPO = $data['K'] ?? null; + $this->customerPOLine = $data['4K'] ?? null; + $this->dateCode = $data['9D'] ?? null; + $this->alternativeDateCode = $data['10D'] ?? null; + $this->lotCode = $data['1T'] ?? null; + $this->countryOfOrigin = $data['4L'] ?? null; + $this->packageId1 = $data['3S'] ?? null; + $this->packageId2 = $data['4S'] ?? null; + $this->packageId3 = $data['5S'] ?? null; + $this->packingListNumber = $data['11K'] ?? null; + $this->serialNumber = $data['S'] ?? null; + $this->binCode = $data['33P'] ?? null; + $this->packageCount = isset($data['13Q']) ? (int)$data['13Q'] : null; + $this->revisionNumber = $data['2P'] ?? null; + //IDs used by Digikey + $this->digikeyPartNumber = $data['30P'] ?? null; + $this->digikeySalesOrderNumber = $data['1K'] ?? null; + $this->digikeyInvoiceNumber = $data['10K'] ?? null; + $this->digikeyLabelType = $data['11Z'] ?? null; + $this->digikeyPartID = $data['12Z'] ?? null; + $this->digikeyNA = $data['13Z'] ?? null; + $this->digikeyPadding = $data['20Z'] ?? null; + //IDs used by Mouser + $this->mouserPositionInOrder = $data['14K'] ?? null; + $this->mouserManufacturer = $data['1V'] ?? null; + } + + /** + * Tries to guess the vendor of the barcode based on the supplied data field. + * This is experimental and should not be relied upon. + * @return string|null The guessed vendor as smallcase string (e.g. "digikey", "mouser", etc.), or null if the vendor could not be guessed + */ + public function guessBarcodeVendor(): ?string + { + //If the barcode data contains the digikey extensions, we assume it is a digikey barcode + if (isset($this->data['13Z']) || isset($this->data['20Z']) || isset($this->data['12Z']) || isset($this->data['11Z'])) { + return 'digikey'; + } + + //If the barcode data contains the mouser extensions, we assume it is a mouser barcode + if (isset($this->data['14K']) || isset($this->data['1V'])) { + return 'mouser'; + } + + //According to this thread (https://github.com/inventree/InvenTree/issues/853), Newark/element14 codes contains a "3P" field + if (isset($this->data['3P'])) { + return 'element14'; + } + + return null; + } + + /** + * Checks if the given input is a valid format06 formatted data. + * This just perform a simple check for the header, the content might be malformed still. + * @param string $input + * @return bool + */ + public static function isFormat06Code(string $input): bool + { + //Code must begin with [)>06 + if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){ + return false; + } + + //Digikey does not put a trailer onto the barcode, so we just check for the header + + return true; + } + + /** + * Parses a format06 code a returns a new instance of this class + * @param string $input + * @return self + */ + public static function parseFormat06Code(string $input): self + { + //Ensure that the input is a valid format06 code + if (!self::isFormat06Code($input)) { + throw new \InvalidArgumentException("The given input is not a valid format06 code"); + } + + //Remove the trailer, if present + if (str_ends_with($input, "\u{1E}\u{04}")){ + $input = substr($input, 5, -2); + } + + //Split the input into the different fields (using the separator) + $parts = explode("\u{1D}", $input); + + //The first field is the format identifier, which we do not need + array_shift($parts); + + //Split the fields into key-value pairs + $results = []; + + foreach($parts as $part) { + //^ 0* ([1-9]? \d* [A-Z]) + //Start of the string Leading zeros are discarded Not a zero Any number of digits single uppercase Letter + // 00 1 4 K + + if(!preg_match('/^0*([1-9]?\d*[A-Z])/', $part, $matches)) { + throw new \LogicException("Could not parse field: $part"); + } + //Extract the key + $key = $matches[0]; + //Extract the field value + $fieldValue = substr($part, strlen($matches[0])); + + $results[$key] = $fieldValue; + } + + return new self($results); + } + + public function getDecodedForInfoMode(): array + { + $tmp = [ + 'Barcode type' => 'EIGP114', + 'Guessed vendor from barcode' => $this->guessBarcodeVendor() ?? 'Unknown', + ]; + + //Iterate over all fields of this object and add them to the array if they are not null + foreach((array) $this as $key => $value) { + //Skip data key + if ($key === 'data') { + continue; + } + if($value !== null) { + $tmp[$key] = $value; + } + } + + return $tmp; + } +} \ No newline at end of file diff --git a/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php new file mode 100644 index 00000000..050aff6f --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +use App\Entity\LabelSystem\LabelSupportedElement; + +/** + * This class represents the result of a barcode scan of a barcode that uniquely identifies a local entity, + * like an internally generated barcode or a barcode that was added manually to the system by a user + */ +class LocalBarcodeScanResult implements BarcodeScanResultInterface +{ + public function __construct( + public readonly LabelSupportedElement $target_type, + public readonly int $target_id, + public readonly BarcodeSourceType $source_type, + ) { + } + + public function getDecodedForInfoMode(): array + { + return [ + 'Barcode type' => $this->source_type->name, + 'Target type' => $this->target_type->name, + 'Target ID' => $this->target_id, + ]; + } +} \ No newline at end of file diff --git a/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php b/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php index 588e34b4..7ceb30dd 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php +++ b/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php @@ -44,29 +44,29 @@ namespace App\Services\LabelSystem\Barcodes; use App\Entity\Base\AbstractDBElement; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use InvalidArgumentException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +/** + * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeContentGeneratorTest + */ final class BarcodeContentGenerator { public const PREFIX_MAP = [ Part::class => 'P', PartLot::class => 'L', - Storelocation::class => 'S', + StorageLocation::class => 'S', ]; private const URL_MAP = [ Part::class => 'part', PartLot::class => 'lot', - Storelocation::class => 'location', + StorageLocation::class => 'location', ]; - private UrlGeneratorInterface $urlGenerator; - - public function __construct(UrlGeneratorInterface $urlGenerator) + public function __construct(private readonly UrlGeneratorInterface $urlGenerator) { - $this->urlGenerator = $urlGenerator; } /** @@ -76,11 +76,11 @@ final class BarcodeContentGenerator { $type = $this->classToString(self::URL_MAP, $target); - return $this->urlGenerator->generate('scan_qr', [ - 'type' => $type, + return $this->urlGenerator->generate('scan_qr', [ + 'type' => $type, 'id' => $target->getID() ?? 0, '_locale' => null, -], UrlGeneratorInterface::ABSOLUTE_URL); + ], UrlGeneratorInterface::ABSOLUTE_URL); } /** @@ -97,17 +97,17 @@ final class BarcodeContentGenerator private function classToString(array $map, object $target): string { - $class = get_class($target); + $class = $target::class; if (isset($map[$class])) { return $map[$class]; } foreach ($map as $class => $string) { - if (is_a($target, $class)) { + if ($target instanceof $class) { return $string; } } - throw new InvalidArgumentException('Unknown object class '.get_class($target)); + throw new InvalidArgumentException('Unknown object class '.$target::class); } } diff --git a/src/Services/LabelSystem/Barcodes/BarcodeHelper.php b/src/Services/LabelSystem/Barcodes/BarcodeHelper.php new file mode 100644 index 00000000..c9fe64f3 --- /dev/null +++ b/src/Services/LabelSystem/Barcodes/BarcodeHelper.php @@ -0,0 +1,97 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\Barcodes; + +use App\Entity\LabelSystem\BarcodeType; +use Com\Tecnick\Barcode\Barcode; + +/** + * This function is used to generate barcodes of various types using arbitrary (text) content. + * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeHelperTest + */ +class BarcodeHelper +{ + + /** + * Generates a barcode with the given content and type and returns it as SVG string. + * @param string $content + * @param BarcodeType $type + * @return string + */ + public function barcodeAsSVG(string $content, BarcodeType $type): string + { + $barcode = new Barcode(); + + $type_str = match ($type) { + BarcodeType::NONE => throw new \InvalidArgumentException('Barcode type must not be NONE! This would make no sense...'), + BarcodeType::QR => 'QRCODE', + BarcodeType::DATAMATRIX => 'DATAMATRIX', + BarcodeType::CODE39 => 'C39', + BarcodeType::CODE93 => 'C93', + BarcodeType::CODE128 => 'C128A', + }; + + return $barcode->getBarcodeObj($type_str, $content)->getSvgCode(); + } + + /** + * Generates a barcode with the given content and type and returns it as HTML image tag. + * @param string $content + * @param BarcodeType $type + * @param string $width Width of the image tag + * @param string|null $alt_text The alt text of the image tag. If null, the content is used. + * @return string + */ + public function barcodeAsHTML(string $content, BarcodeType $type, string $width = '100%', ?string $alt_text = null): string + { + $svg = $this->barcodeAsSVG($content, $type); + $base64 = $this->dataUri($svg, 'image/svg+xml'); + $alt_text ??= $content; + + return ''.$alt_text.''; + } + + /** + * Creates a data URI (RFC 2397). + * Based on the Twig implementation from HTMLExtension + * + * Length validation is not performed on purpose, validation should + * be done before calling this filter. + * + * @return string The generated data URI + */ + private function dataUri(string $data, string $mime): string + { + $repr = 'data:'; + + $repr .= $mime; + if (str_starts_with($mime, 'text/')) { + $repr .= ','.rawurlencode($data); + } else { + $repr .= ';base64,'.base64_encode($data); + } + + return $repr; + } +} \ No newline at end of file diff --git a/src/Services/LabelSystem/Barcodes/BarcodeNormalizer.php b/src/Services/LabelSystem/Barcodes/BarcodeNormalizer.php deleted file mode 100644 index 4e6d8cbd..00000000 --- a/src/Services/LabelSystem/Barcodes/BarcodeNormalizer.php +++ /dev/null @@ -1,107 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * 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 . - */ - -namespace App\Services\LabelSystem\Barcodes; - -use InvalidArgumentException; - -final class BarcodeNormalizer -{ - private const PREFIX_TYPE_MAP = [ - 'L' => 'lot', - 'P' => 'part', - 'S' => 'location', - ]; - - /** - * Parses barcode content and normalizes it. - * Returns an array in the format ['part', 1]: First entry contains element type, second the ID of the element. - */ - public function normalizeBarcodeContent(string $input): array - { - $input = trim($input); - $matches = []; - - //Some scanner output '-' as ß, so replace it (ß is never used, so we can replace it safely) - $input = str_replace('ß', '-', $input); - - //Extract parts from QR code's URL - if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) { - return [$matches[1], (int) $matches[2]]; - } - - //New Code39 barcode use L0001 format - if (preg_match('#^([A-Z])(\d{4,})$#', $input, $matches)) { - $prefix = $matches[1]; - $id = (int) $matches[2]; - - if (!isset(self::PREFIX_TYPE_MAP[$prefix])) { - throw new InvalidArgumentException('Unknown prefix '.$prefix); - } - - return [self::PREFIX_TYPE_MAP[$prefix], $id]; - } - - //During development the L-000001 format was used - if (preg_match('#^(\w)-(\d{6,})$#', $input, $matches)) { - $prefix = $matches[1]; - $id = (int) $matches[2]; - - if (!isset(self::PREFIX_TYPE_MAP[$prefix])) { - throw new InvalidArgumentException('Unknown prefix '.$prefix); - } - - return [self::PREFIX_TYPE_MAP[$prefix], $id]; - } - - //Legacy Part-DB location labels used $L00336 format - if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) { - return ['location', (int) $matches[1]]; - } - - //Legacy Part-DB used EAN8 barcodes for part labels. Format 0000001(2) (note the optional 8th digit => checksum) - if (preg_match('#^(\d{7})\d?$#', $input, $matches)) { - return ['part', (int) $matches[1]]; - } - - throw new InvalidArgumentException('Unknown barcode format!'); - } -} diff --git a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php b/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php deleted file mode 100644 index 198cb43b..00000000 --- a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php +++ /dev/null @@ -1,92 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * 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 . - */ - -namespace App\Services\LabelSystem\Barcodes; - -use App\Entity\Parts\PartLot; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityNotFoundException; -use InvalidArgumentException; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -final class BarcodeRedirector -{ - private UrlGeneratorInterface $urlGenerator; - private EntityManagerInterface $em; - - public function __construct(UrlGeneratorInterface $urlGenerator, EntityManagerInterface $entityManager) - { - $this->urlGenerator = $urlGenerator; - $this->em = $entityManager; - } - - /** - * Determines the URL to which the user should be redirected, when scanning a QR code. - * - * @param string $type The type of the element that was scanned (e.g. 'part', 'lot', etc.) - * @param int $id The ID of the element that was scanned - * - * @return string the URL to which should be redirected - * - * @throws EntityNotFoundException - */ - public function getRedirectURL(string $type, int $id): string - { - switch ($type) { - case 'part': - return $this->urlGenerator->generate('app_part_show', ['id' => $id]); - case 'lot': - //Try to determine the part to the given lot - $lot = $this->em->find(PartLot::class, $id); - if (null === $lot) { - throw new EntityNotFoundException(); - } - - return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]); - - case 'location': - return $this->urlGenerator->generate('part_list_store_location', ['id' => $id]); - - default: - throw new InvalidArgumentException('Unknown $type: '.$type); - } - } -} diff --git a/src/Services/LabelSystem/DompdfFactory.php b/src/Services/LabelSystem/DompdfFactory.php new file mode 100644 index 00000000..a2c8c3cd --- /dev/null +++ b/src/Services/LabelSystem/DompdfFactory.php @@ -0,0 +1,54 @@ +. + */ +namespace App\Services\LabelSystem; + +use Dompdf\Dompdf; +use Jbtronics\DompdfFontLoaderBundle\Services\DompdfFactoryInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +#[AsDecorator(decorates: DompdfFactoryInterface::class)] +class DompdfFactory implements DompdfFactoryInterface +{ + public function __construct(private readonly string $fontDirectory, private readonly string $tmpDirectory) + { + //Create folder if it does not exist + $this->createDirectoryIfNotExisting($this->fontDirectory); + $this->createDirectoryIfNotExisting($this->tmpDirectory); + } + + private function createDirectoryIfNotExisting(string $path): void + { + if (!is_dir($path) && (!mkdir($concurrentDirectory = $path, 0777, true) && !is_dir($concurrentDirectory))) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); + } + } + + public function create(): Dompdf + { + return new Dompdf([ + 'fontDir' => $this->fontDirectory, + 'fontCache' => $this->fontDirectory, + 'tempDir' => $this->tmpDirectory, + ]); + } +} diff --git a/src/Services/LabelSystem/LabelBarcodeGenerator.php b/src/Services/LabelSystem/LabelBarcodeGenerator.php new file mode 100644 index 00000000..66f74e58 --- /dev/null +++ b/src/Services/LabelSystem/LabelBarcodeGenerator.php @@ -0,0 +1,100 @@ +. + */ + +declare(strict_types=1); + +/** + * 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 . + */ + +namespace App\Services\LabelSystem; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\LabelSystem\BarcodeType; +use App\Entity\LabelSystem\LabelOptions; +use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator; +use App\Services\LabelSystem\Barcodes\BarcodeHelper; +use InvalidArgumentException; + +/** + * @see \App\Tests\Services\LabelSystem\LabelBarcodeGeneratorTest + */ +final class LabelBarcodeGenerator +{ + public function __construct(private readonly BarcodeContentGenerator $barcodeContentGenerator, private readonly BarcodeHelper $barcodeHelper) + { + } + + /** + * Generate the barcode for the given label as HTML image tag. + * @param LabelOptions $options + * @param AbstractDBElement $target + * @return string|null + */ + public function generateHTMLBarcode(LabelOptions $options, AbstractDBElement $target): ?string + { + if ($options->getBarcodeType() === BarcodeType::NONE) { + return null; + } + + return $this->barcodeHelper->barcodeAsHTML($this->getContent($options, $target), $options->getBarcodeType()); + } + + /** + * Generate the barcode for the given label as SVG string. + * @param LabelOptions $options + * @param AbstractDBElement $target + * @return string|null + */ + public function generateSVG(LabelOptions $options, AbstractDBElement $target): ?string + { + if ($options->getBarcodeType() === BarcodeType::NONE) { + return null; + } + + return $this->barcodeHelper->barcodeAsSVG($this->getContent($options, $target), $options->getBarcodeType()); + } + + public function getContent(LabelOptions $options, AbstractDBElement $target): ?string + { + $barcode = $options->getBarcodeType(); + return match (true) { + $barcode->is2D() => $this->barcodeContentGenerator->getURLContent($target), + $barcode->is1D() => $this->barcodeContentGenerator->get1DBarcodeContent($target), + $barcode === BarcodeType::NONE => null, + default => throw new InvalidArgumentException('Unknown label type!'), + }; + } +} diff --git a/src/Services/LabelSystem/LabelExampleElementsGenerator.php b/src/Services/LabelSystem/LabelExampleElementsGenerator.php index 0bc3761a..d344c929 100644 --- a/src/Services/LabelSystem/LabelExampleElementsGenerator.php +++ b/src/Services/LabelSystem/LabelExampleElementsGenerator.php @@ -42,12 +42,14 @@ declare(strict_types=1); namespace App\Services\LabelSystem; use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\UserSystem\User; use DateTime; use InvalidArgumentException; @@ -55,18 +57,13 @@ use ReflectionClass; final class LabelExampleElementsGenerator { - public function getElement(string $type): object + public function getElement(LabelSupportedElement $type): object { - switch ($type) { - case 'part': - return $this->getExamplePart(); - case 'part_lot': - return $this->getExamplePartLot(); - case 'storelocation': - return $this->getStorelocation(); - default: - throw new InvalidArgumentException('Unknown $type.'); - } + return match ($type) { + LabelSupportedElement::PART => $this->getExamplePart(), + LabelSupportedElement::PART_LOT => $this->getExamplePartLot(), + LabelSupportedElement::STORELOCATION => $this->getStorelocation(), + }; } public function getExamplePart(): Part @@ -83,7 +80,7 @@ final class LabelExampleElementsGenerator $part->setMass(123.4); $part->setManufacturerProductNumber('CUSTOM MPN'); $part->setTags('Tag1, Tag2, Tag3'); - $part->setManufacturingStatus('active'); + $part->setManufacturingStatus(ManufacturingStatus::ACTIVE); $part->updateTimestamps(); $part->setFavorite(true); @@ -100,24 +97,24 @@ final class LabelExampleElementsGenerator $lot->setDescription('Example Lot'); $lot->setComment('Lot comment'); - $lot->setExpirationDate(new DateTime('+1 days')); - $lot->setStorageLocation($this->getStructuralData(Storelocation::class)); + $lot->setExpirationDate(new \DateTimeImmutable('+1 day')); + $lot->setStorageLocation($this->getStructuralData(StorageLocation::class)); $lot->setAmount(123); $lot->setOwner($this->getUser()); return $lot; } - private function getStorelocation(): Storelocation + private function getStorelocation(): StorageLocation { - $storelocation = new Storelocation(); + $storelocation = new StorageLocation(); $storelocation->setName('Location 1'); $storelocation->setComment('Example comment'); $storelocation->updateTimestamps(); $storelocation->setOwner($this->getUser()); - $parent = new Storelocation(); + $parent = new StorageLocation(); $parent->setName('Parent'); $storelocation->setParent($parent); @@ -135,17 +132,25 @@ final class LabelExampleElementsGenerator return $user; } + /** + * @template T of AbstractStructuralDBElement + * @param string $class + * @phpstan-param class-string $class + * @return AbstractStructuralDBElement + * @phpstan-return T + * @throws \ReflectionException + */ private function getStructuralData(string $class): AbstractStructuralDBElement { if (!is_a($class, AbstractStructuralDBElement::class, true)) { throw new InvalidArgumentException('$class must be an child of AbstractStructuralDBElement'); } - /** @var AbstractStructuralDBElement $parent */ + /** @var T $parent */ $parent = new $class(); $parent->setName('Example'); - /** @var AbstractStructuralDBElement $child */ + /** @var T $child */ $child = new $class(); $child->setName((new ReflectionClass($class))->getShortName()); $child->setParent($parent); diff --git a/src/Services/LabelSystem/LabelGenerator.php b/src/Services/LabelSystem/LabelGenerator.php index 3244875f..bfb8d27b 100644 --- a/src/Services/LabelSystem/LabelGenerator.php +++ b/src/Services/LabelSystem/LabelGenerator.php @@ -42,38 +42,26 @@ declare(strict_types=1); namespace App\Services\LabelSystem; use App\Entity\LabelSystem\LabelOptions; -use App\Entity\Parts\Part; -use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; -use Dompdf\Dompdf; use InvalidArgumentException; +use Jbtronics\DompdfFontLoaderBundle\Services\DompdfFactoryInterface; +/** + * @see \App\Tests\Services\LabelSystem\LabelGeneratorTest + */ final class LabelGenerator { - public const CLASS_SUPPORT_MAPPING = [ - 'part' => Part::class, - 'part_lot' => PartLot::class, - 'storelocation' => Storelocation::class, - ]; - public const MM_TO_POINTS_FACTOR = 2.83465; - private LabelHTMLGenerator $labelHTMLGenerator; - - public function __construct(LabelHTMLGenerator $labelHTMLGenerator) + public function __construct(private readonly LabelHTMLGenerator $labelHTMLGenerator, + private readonly DompdfFactoryInterface $dompdfFactory) { - $this->labelHTMLGenerator = $labelHTMLGenerator; } /** - * @param object|object[] $elements An element or an array of elements for which labels should be generated + * @param object|object[] $elements An element or an array of elements for which labels should be generated */ - public function generateLabel(LabelOptions $options, $elements): string + public function generateLabel(LabelOptions $options, object|array $elements): string { - if (!is_array($elements) && !is_object($elements)) { - throw new InvalidArgumentException('$element must be an object or an array of objects!'); - } - if (!is_array($elements)) { $elements = [$elements]; } @@ -84,12 +72,12 @@ final class LabelGenerator } } - $dompdf = new Dompdf(); + $dompdf = $this->dompdfFactory->create(); $dompdf->setPaper($this->mmToPointsArray($options->getWidth(), $options->getHeight())); $dompdf->loadHtml($this->labelHTMLGenerator->getLabelHTML($options, $elements)); $dompdf->render(); - return $dompdf->output(); + return $dompdf->output() ?? throw new \RuntimeException('Could not generate label!'); } /** @@ -98,15 +86,12 @@ final class LabelGenerator public function supports(LabelOptions $options, object $element): bool { $supported_type = $options->getSupportedElement(); - if (!isset(static::CLASS_SUPPORT_MAPPING[$supported_type])) { - throw new InvalidArgumentException('Supported type name of the Label options not known!'); - } - return is_a($element, static::CLASS_SUPPORT_MAPPING[$supported_type]); + return is_a($element, $supported_type->getEntityClass()); } /** - * Converts width and height given in mm to an size array, that can be used by DOMPDF for page size. + * Converts width and height given in mm to a size array, that can be used by DOMPDF for page size. * * @param float $width The width of the paper * @param float $height The height of the paper diff --git a/src/Services/LabelSystem/LabelHTMLGenerator.php b/src/Services/LabelSystem/LabelHTMLGenerator.php index f526ac9d..42aa1e72 100644 --- a/src/Services/LabelSystem/LabelHTMLGenerator.php +++ b/src/Services/LabelSystem/LabelHTMLGenerator.php @@ -41,61 +41,56 @@ declare(strict_types=1); namespace App\Services\LabelSystem; +use App\Entity\LabelSystem\LabelProcessMode; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Contracts\NamedElementInterface; use App\Entity\LabelSystem\LabelOptions; use App\Exceptions\TwigModeException; use App\Services\ElementTypeNameGenerator; use InvalidArgumentException; -use Symfony\Component\Security\Core\Security; use Twig\Environment; use Twig\Error\Error; final class LabelHTMLGenerator { - private Environment $twig; - private ElementTypeNameGenerator $elementTypeNameGenerator; - private LabelTextReplacer $replacer; - private BarcodeGenerator $barcodeGenerator; - private SandboxedTwigProvider $sandboxedTwigProvider; - private string $partdb_title; - private Security $security; - - public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator, LabelTextReplacer $replacer, Environment $twig, - BarcodeGenerator $barcodeGenerator, SandboxedTwigProvider $sandboxedTwigProvider, Security $security, string $partdb_title) + public function __construct( + private readonly ElementTypeNameGenerator $elementTypeNameGenerator, + private readonly LabelTextReplacer $replacer, + private readonly Environment $twig, + private readonly LabelBarcodeGenerator $barcodeGenerator, + private readonly SandboxedTwigFactory $sandboxedTwigProvider, + private readonly Security $security, + private readonly string $partdb_title) { - $this->twig = $twig; - $this->elementTypeNameGenerator = $elementTypeNameGenerator; - $this->replacer = $replacer; - $this->barcodeGenerator = $barcodeGenerator; - $this->sandboxedTwigProvider = $sandboxedTwigProvider; - $this->security = $security; - $this->partdb_title = $partdb_title; } public function getLabelHTML(LabelOptions $options, array $elements): string { - if (empty($elements)) { + if ($elements === []) { throw new InvalidArgumentException('$elements must not be empty'); } $twig_elements = []; - if ('twig' === $options->getLinesMode()) { - $sandboxed_twig = $this->sandboxedTwigProvider->getTwig($options); + if (LabelProcessMode::TWIG === $options->getProcessMode()) { + $sandboxed_twig = $this->sandboxedTwigProvider->createTwig($options); $current_user = $this->security->getUser(); } $page = 1; foreach ($elements as $element) { - if (isset($sandboxed_twig, $current_user) && 'twig' === $options->getLinesMode()) { + if (isset($sandboxed_twig, $current_user) && LabelProcessMode::TWIG === $options->getProcessMode()) { try { $lines = $sandboxed_twig->render( 'lines', [ 'element' => $element, 'page' => $page, + 'last_page' => count($elements), 'user' => $current_user, 'install_title' => $this->partdb_title, + 'paper_width' => $options->getWidth(), + 'paper_height' => $options->getHeight(), ] ); } catch (Error $exception) { diff --git a/src/Services/LabelSystem/LabelProfileDropdownHelper.php b/src/Services/LabelSystem/LabelProfileDropdownHelper.php index 662922f6..773923ab 100644 --- a/src/Services/LabelSystem/LabelProfileDropdownHelper.php +++ b/src/Services/LabelSystem/LabelProfileDropdownHelper.php @@ -42,35 +42,43 @@ declare(strict_types=1); namespace App\Services\LabelSystem; use App\Entity\LabelSystem\LabelProfile; +use App\Entity\LabelSystem\LabelSupportedElement; use App\Repository\LabelProfileRepository; -use App\Services\UserSystem\UserCacheKeyGenerator; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; final class LabelProfileDropdownHelper { - private TagAwareCacheInterface $cache; - private EntityManagerInterface $entityManager; - private UserCacheKeyGenerator $keyGenerator; - - public function __construct(TagAwareCacheInterface $treeCache, EntityManagerInterface $entityManager, UserCacheKeyGenerator $keyGenerator) - { - $this->cache = $treeCache; - $this->entityManager = $entityManager; - $this->keyGenerator = $keyGenerator; + public function __construct( + private readonly TagAwareCacheInterface $cache, + private readonly EntityManagerInterface $entityManager, + private readonly UserCacheKeyGenerator $keyGenerator, + private readonly ElementCacheTagGenerator $tagGenerator, + ) { } - public function getDropdownProfiles(string $type): array + /** + * Return all label profiles for the given supported element type + * @param LabelSupportedElement|string $type + * @return array + */ + public function getDropdownProfiles(LabelSupportedElement|string $type): array { - $secure_class_name = str_replace('\\', '_', LabelProfile::class); - $key = 'profile_dropdown_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.'_'.$type; + //Useful for the twig templates, where we use the string representation of the enum + if (is_string($type)) { + $type = LabelSupportedElement::from($type); + } - /** @var LabelProfileRepository $repo */ + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag(LabelProfile::class); + $key = 'profile_dropdown_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.'_'.$type->value; + $repo = $this->entityManager->getRepository(LabelProfile::class); return $this->cache->get($key, function (ItemInterface $item) use ($repo, $type, $secure_class_name) { - // Invalidate when groups, a element with the class or the user changes + // Invalidate when groups, an element with the class or the user changes $item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]); return $repo->getDropdownProfiles($type); diff --git a/src/Services/LabelSystem/LabelTextReplacer.php b/src/Services/LabelSystem/LabelTextReplacer.php index 5b94352b..6f0a9ee8 100644 --- a/src/Services/LabelSystem/LabelTextReplacer.php +++ b/src/Services/LabelSystem/LabelTextReplacer.php @@ -46,14 +46,12 @@ use App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface; /** * This service replaces the Placeholders of the user provided lines with the proper informations. * It uses the PlaceholderProviders provided by PlaceholderProviderInterface classes. + * @see \App\Tests\Services\LabelSystem\LabelTextReplacerTest */ final class LabelTextReplacer { - private iterable $providers; - - public function __construct(iterable $providers) + public function __construct(private readonly iterable $providers) { - $this->providers = $providers; } /** @@ -66,6 +64,17 @@ final class LabelTextReplacer * @return string If the placeholder was valid, the replaced info. Otherwise the passed string. */ public function handlePlaceholder(string $placeholder, object $target): string + { + return $this->handlePlaceholderOrReturnNull($placeholder, $target) ?? $placeholder; + } + + /** + * Similar to handlePlaceholder, but returns null if the placeholder is not known (instead of the original string) + * @param string $placeholder + * @param object $target + * @return string|null + */ + public function handlePlaceholderOrReturnNull(string $placeholder, object $target): ?string { foreach ($this->providers as $provider) { /** @var PlaceholderProviderInterface $provider */ @@ -75,25 +84,24 @@ final class LabelTextReplacer } } - return $placeholder; + return null; } /** - * Replaces all placeholders in the input lines. + * Replaces all placeholders in the input lines. * * @param string $lines The input lines that should be replaced - * @param object $target the object that should be used as source for the informations + * @param object $target the object that should be used as source for the information * - * @return string the Lines with replaced informations + * @return string the Lines with replaced information */ public function replace(string $lines, object $target): string { $patterns = [ - '/(\[\[[A-Z_0-9]+\]\])/' => function ($match) use ($target) { - return $this->handlePlaceholder($match[0], $target); - }, + '/(\[\[[A-Z_0-9]+\]\])/' => fn($match): string => $this->handlePlaceholder($match[0], $target), ]; - return preg_replace_callback_array($patterns, $lines); + return preg_replace_callback_array($patterns, $lines) ?? throw new \RuntimeException('Could not replace placeholders!'); + } } diff --git a/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php index f765cd0c..081b3e91 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/AbstractDBElementProvider.php @@ -46,11 +46,8 @@ use App\Services\ElementTypeNameGenerator; final class AbstractDBElementProvider implements PlaceholderProviderInterface { - private ElementTypeNameGenerator $elementTypeNameGenerator; - - public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator) + public function __construct(private readonly ElementTypeNameGenerator $elementTypeNameGenerator) { - $this->elementTypeNameGenerator = $elementTypeNameGenerator; } public function replace(string $placeholder, object $label_target, array $options = []): ?string diff --git a/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php b/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php index 9fbcd293..400fef35 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/BarcodeProvider.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\LabelSystem\PlaceholderProviders; +use App\Entity\LabelSystem\BarcodeType; use App\Entity\LabelSystem\LabelOptions; -use App\Entity\LabelSystem\LabelProfile; -use App\Services\LabelSystem\BarcodeGenerator; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Services\LabelSystem\Barcodes\BarcodeHelper; +use App\Services\LabelSystem\LabelBarcodeGenerator; use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator; +use Com\Tecnick\Barcode\Exception; final class BarcodeProvider implements PlaceholderProviderInterface { - private BarcodeGenerator $barcodeGenerator; - private BarcodeContentGenerator $barcodeContentGenerator; - - public function __construct(BarcodeGenerator $barcodeGenerator, BarcodeContentGenerator $barcodeContentGenerator) + public function __construct(private readonly LabelBarcodeGenerator $barcodeGenerator, + private readonly BarcodeContentGenerator $barcodeContentGenerator, + private readonly BarcodeHelper $barcodeHelper) { - $this->barcodeGenerator = $barcodeGenerator; - $this->barcodeContentGenerator = $barcodeContentGenerator; } public function replace(string $placeholder, object $label_target, array $options = []): ?string @@ -41,7 +44,7 @@ final class BarcodeProvider implements PlaceholderProviderInterface if ('[[1D_CONTENT]]' === $placeholder) { try { return $this->barcodeContentGenerator->get1DBarcodeContent($label_target); - } catch (\InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { return 'ERROR!'; } } @@ -49,29 +52,72 @@ final class BarcodeProvider implements PlaceholderProviderInterface if ('[[2D_CONTENT]]' === $placeholder) { try { return $this->barcodeContentGenerator->getURLContent($label_target); - } catch (\InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { return 'ERROR!'; } } if ('[[BARCODE_QR]]' === $placeholder) { $label_options = new LabelOptions(); - $label_options->setBarcodeType('qr'); + $label_options->setBarcodeType(BarcodeType::QR); + return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target); + } + + if ('[[BARCODE_DATAMATRIX]]' === $placeholder) { + $label_options = new LabelOptions(); + $label_options->setBarcodeType(BarcodeType::DATAMATRIX); return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target); } if ('[[BARCODE_C39]]' === $placeholder) { $label_options = new LabelOptions(); - $label_options->setBarcodeType('code39'); + $label_options->setBarcodeType(BarcodeType::CODE39); + return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target); + } + + if ('[[BARCODE_C93]]' === $placeholder) { + $label_options = new LabelOptions(); + $label_options->setBarcodeType(BarcodeType::CODE93); return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target); } if ('[[BARCODE_C128]]' === $placeholder) { $label_options = new LabelOptions(); - $label_options->setBarcodeType('code128'); + $label_options->setBarcodeType(BarcodeType::CODE128); return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target); } + if (($label_target instanceof Part || $label_target instanceof PartLot) + && str_starts_with($placeholder, '[[IPN_BARCODE_')) { + if ($label_target instanceof PartLot) { + $label_target = $label_target->getPart(); + } + + if ($label_target === null || $label_target->getIPN() === null || $label_target->getIPN() === '') { + //Replace with empty result, if no IPN is set + return ''; + } + + try { + //Add placeholders for the IPN barcode + if ('[[IPN_BARCODE_C39]]' === $placeholder) { + return $this->barcodeHelper->barcodeAsHTML($label_target->getIPN(), BarcodeType::CODE39); + } + if ('[[IPN_BARCODE_C128]]' === $placeholder) { + return $this->barcodeHelper->barcodeAsHTML($label_target->getIPN(), BarcodeType::CODE128); + } + if ('[[IPN_BARCODE_QR]]' === $placeholder) { + return $this->barcodeHelper->barcodeAsHTML($label_target->getIPN(), BarcodeType::QR); + } + } catch (Exception $e) { + //If an error occurs, output it + return 'IPN Barcode ERROR!: '.$e->getMessage(); + } + } + + + + return null; } -} \ No newline at end of file +} diff --git a/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php b/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php index 1dd7188a..ddd4dbf1 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php +++ b/src/Services/LabelSystem/PlaceholderProviders/GlobalProviders.php @@ -41,28 +41,21 @@ declare(strict_types=1); namespace App\Services\LabelSystem\PlaceholderProviders; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\UserSystem\User; use DateTime; use IntlDateFormatter; use Locale; -use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Security; /** * Provides Placeholders for infos about global infos like Installation name or datetimes. + * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\GlobalProvidersTest */ final class GlobalProviders implements PlaceholderProviderInterface { - private string $partdb_title; - private Security $security; - private UrlGeneratorInterface $url_generator; - - public function __construct(string $partdb_title, Security $security, UrlGeneratorInterface $url_generator) + public function __construct(private readonly string $partdb_title, private readonly Security $security, private readonly UrlGeneratorInterface $url_generator) { - $this->partdb_title = $partdb_title; - $this->security = $security; - $this->url_generator = $url_generator; } public function replace(string $placeholder, object $label_target, array $options = []): ?string @@ -88,7 +81,7 @@ final class GlobalProviders implements PlaceholderProviderInterface return 'anonymous'; } - $now = new DateTime(); + $now = new \DateTimeImmutable(); if ('[[DATETIME]]' === $placeholder) { $formatter = IntlDateFormatter::create( diff --git a/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php index fc5fedfe..d8d38120 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/NamedElementProvider.php @@ -43,6 +43,9 @@ namespace App\Services\LabelSystem\PlaceholderProviders; use App\Entity\Contracts\NamedElementInterface; +/** + * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\NamedElementProviderTest + */ final class NamedElementProvider implements PlaceholderProviderInterface { public function replace(string $placeholder, object $label_target, array $options = []): ?string diff --git a/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php b/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php index a4fdf32a..946b4892 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/PartLotProvider.php @@ -41,21 +41,21 @@ declare(strict_types=1); namespace App\Services\LabelSystem\PlaceholderProviders; +use App\Entity\Parts\StorageLocation; +use App\Entity\UserSystem\User; use App\Entity\Parts\PartLot; use App\Services\Formatters\AmountFormatter; use App\Services\LabelSystem\LabelTextReplacer; use IntlDateFormatter; use Locale; +/** + * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartLotProviderTest + */ final class PartLotProvider implements PlaceholderProviderInterface { - private LabelTextReplacer $labelTextReplacer; - private AmountFormatter $amountFormatter; - - public function __construct(LabelTextReplacer $labelTextReplacer, AmountFormatter $amountFormatter) + public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly AmountFormatter $amountFormatter) { - $this->labelTextReplacer = $labelTextReplacer; - $this->amountFormatter = $amountFormatter; } public function replace(string $placeholder, object $label_target, array $options = []): ?string @@ -74,7 +74,7 @@ final class PartLotProvider implements PlaceholderProviderInterface } if ('[[EXPIRATION_DATE]]' === $placeholder) { - if (null === $label_target->getExpirationDate()) { + if (!$label_target->getExpirationDate() instanceof \DateTimeInterface) { return ''; } $formatter = IntlDateFormatter::create( @@ -95,19 +95,19 @@ final class PartLotProvider implements PlaceholderProviderInterface } if ('[[LOCATION]]' === $placeholder) { - return $label_target->getStorageLocation() ? $label_target->getStorageLocation()->getName() : ''; + return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getName() : ''; } if ('[[LOCATION_FULL]]' === $placeholder) { - return $label_target->getStorageLocation() ? $label_target->getStorageLocation()->getFullPath() : ''; + return $label_target->getStorageLocation() instanceof StorageLocation ? $label_target->getStorageLocation()->getFullPath() : ''; } if ('[[OWNER]]' === $placeholder) { - return $label_target->getOwner() ? $label_target->getOwner()->getFullName() : ''; + return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : ''; } if ('[[OWNER_USERNAME]]' === $placeholder) { - return $label_target->getOwner() ? $label_target->getOwner()->getUsername() : ''; + return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : ''; } return $this->labelTextReplacer->handlePlaceholder($placeholder, $label_target->getPart()); diff --git a/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php b/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php index 48e547f6..0df4d3d7 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/PartProvider.php @@ -41,20 +41,21 @@ declare(strict_types=1); namespace App\Services\LabelSystem\PlaceholderProviders; +use App\Entity\Parts\Category; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\Footprint; use App\Entity\Parts\Part; use App\Services\Formatters\SIFormatter; use Parsedown; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\PartProviderTest + */ final class PartProvider implements PlaceholderProviderInterface { - private SIFormatter $siFormatter; - private TranslatorInterface $translator; - - public function __construct(SIFormatter $SIFormatter, TranslatorInterface $translator) + public function __construct(private readonly SIFormatter $siFormatter, private readonly TranslatorInterface $translator) { - $this->siFormatter = $SIFormatter; - $this->translator = $translator; } public function replace(string $placeholder, object $part, array $options = []): ?string @@ -64,27 +65,27 @@ final class PartProvider implements PlaceholderProviderInterface } if ('[[CATEGORY]]' === $placeholder) { - return $part->getCategory() ? $part->getCategory()->getName() : ''; + return $part->getCategory() instanceof Category ? $part->getCategory()->getName() : ''; } if ('[[CATEGORY_FULL]]' === $placeholder) { - return $part->getCategory() ? $part->getCategory()->getFullPath() : ''; + return $part->getCategory() instanceof Category ? $part->getCategory()->getFullPath() : ''; } if ('[[MANUFACTURER]]' === $placeholder) { - return $part->getManufacturer() ? $part->getManufacturer()->getName() : ''; + return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getName() : ''; } if ('[[MANUFACTURER_FULL]]' === $placeholder) { - return $part->getManufacturer() ? $part->getManufacturer()->getFullPath() : ''; + return $part->getManufacturer() instanceof Manufacturer ? $part->getManufacturer()->getFullPath() : ''; } if ('[[FOOTPRINT]]' === $placeholder) { - return $part->getFootprint() ? $part->getFootprint()->getName() : ''; + return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : ''; } if ('[[FOOTPRINT_FULL]]' === $placeholder) { - return $part->getFootprint() ? $part->getFootprint()->getFullPath() : ''; + return $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getFullPath() : ''; } if ('[[MASS]]' === $placeholder) { @@ -95,16 +96,20 @@ final class PartProvider implements PlaceholderProviderInterface return $part->getManufacturerProductNumber(); } + if ('[[IPN]]' === $placeholder) { + return $part->getIpn() ?? ''; + } + if ('[[TAGS]]' === $placeholder) { return $part->getTags(); } if ('[[M_STATUS]]' === $placeholder) { - if ('' === $part->getManufacturingStatus()) { + if (null === $part->getManufacturingStatus()) { return ''; } - return $this->translator->trans('m_status.'.$part->getManufacturingStatus()); + return $this->translator->trans($part->getManufacturingStatus()->toTranslationKey()); } $parsedown = new Parsedown(); @@ -114,7 +119,7 @@ final class PartProvider implements PlaceholderProviderInterface } if ('[[DESCRIPTION_T]]' === $placeholder) { - return strip_tags($parsedown->line($part->getDescription())); + return strip_tags((string) $parsedown->line($part->getDescription())); } if ('[[COMMENT]]' === $placeholder) { @@ -122,7 +127,7 @@ final class PartProvider implements PlaceholderProviderInterface } if ('[[COMMENT_T]]' === $placeholder) { - return strip_tags($parsedown->line($part->getComment())); + return strip_tags((string) $parsedown->line($part->getComment())); } return null; diff --git a/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php b/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php index aac7f985..4b4d8dcd 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/StorelocationProvider.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\LabelSystem\PlaceholderProviders; -use App\Entity\Parts\Storelocation; +use App\Entity\UserSystem\User; +use App\Entity\Parts\StorageLocation; class StorelocationProvider implements PlaceholderProviderInterface { public function replace(string $placeholder, object $label_target, array $options = []): ?string { - if ($label_target instanceof Storelocation) { + if ($label_target instanceof StorageLocation) { if ('[[OWNER]]' === $placeholder) { - return $label_target->getOwner() ? $label_target->getOwner()->getFullName() : ''; + return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getFullName() : ''; } if ('[[OWNER_USERNAME]]' === $placeholder) { - return $label_target->getOwner() ? $label_target->getOwner()->getUsername() : ''; + return $label_target->getOwner() instanceof User ? $label_target->getOwner()->getUsername() : ''; } } return null; } -} \ No newline at end of file +} diff --git a/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php index f4aebd8a..f37f5901 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/StructuralDBElementProvider.php @@ -52,16 +52,16 @@ final class StructuralDBElementProvider implements PlaceholderProviderInterface return $label_target->getComment(); } if ('[[COMMENT_T]]' === $placeholder) { - return strip_tags($label_target->getComment()); + return strip_tags((string) $label_target->getComment()); } if ('[[FULL_PATH]]' === $placeholder) { return $label_target->getFullPath(); } if ('[[PARENT]]' === $placeholder) { - return $label_target->getParent() ? $label_target->getParent()->getName() : ''; + return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getName() : ''; } if ('[[PARENT_FULL_PATH]]' === $placeholder) { - return $label_target->getParent() ? $label_target->getParent()->getFullPath() : ''; + return $label_target->getParent() instanceof AbstractStructuralDBElement ? $label_target->getParent()->getFullPath() : ''; } } diff --git a/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php b/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php index ef5967b2..b316abf2 100644 --- a/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php +++ b/src/Services/LabelSystem/PlaceholderProviders/TimestampableElementProvider.php @@ -42,10 +42,12 @@ declare(strict_types=1); namespace App\Services\LabelSystem\PlaceholderProviders; use App\Entity\Contracts\TimeStampableInterface; -use DateTime; use IntlDateFormatter; use Locale; +/** + * @see \App\Tests\Services\LabelSystem\PlaceholderProviders\TimestampableElementProviderTest + */ final class TimestampableElementProvider implements PlaceholderProviderInterface { public function replace(string $placeholder, object $label_target, array $options = []): ?string @@ -54,11 +56,11 @@ final class TimestampableElementProvider implements PlaceholderProviderInterface $formatter = new IntlDateFormatter(Locale::getDefault(), IntlDateFormatter::SHORT, IntlDateFormatter::SHORT); if ('[[LAST_MODIFIED]]' === $placeholder) { - return $formatter->format($label_target->getLastModified() ?? new DateTime()); + return $formatter->format($label_target->getLastModified() ?? new \DateTimeImmutable()); } if ('[[CREATION_DATE]]' === $placeholder) { - return $formatter->format($label_target->getAddedDate() ?? new DateTime()); + return $formatter->format($label_target->getAddedDate() ?? new \DateTimeImmutable()); } } diff --git a/src/Services/LabelSystem/SandboxedTwigProvider.php b/src/Services/LabelSystem/SandboxedTwigFactory.php similarity index 59% rename from src/Services/LabelSystem/SandboxedTwigProvider.php rename to src/Services/LabelSystem/SandboxedTwigFactory.php index 66488fca..d6ea6968 100644 --- a/src/Services/LabelSystem/SandboxedTwigProvider.php +++ b/src/Services/LabelSystem/SandboxedTwigFactory.php @@ -49,81 +49,125 @@ use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Contracts\TimeStampableInterface; use App\Entity\LabelSystem\LabelOptions; +use App\Entity\LabelSystem\LabelProcessMode; use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parts\InfoProviderReference; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; +use App\Entity\Parts\PartAssociation; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use App\Entity\UserSystem\User; +use App\Twig\BarcodeExtension; +use App\Twig\EntityExtension; use App\Twig\FormatExtension; use App\Twig\Sandbox\InheritanceSecurityPolicy; +use App\Twig\Sandbox\SandboxedLabelExtension; +use App\Twig\TwigCoreExtension; use InvalidArgumentException; use Twig\Environment; use Twig\Extension\SandboxExtension; +use Twig\Extra\Html\HtmlExtension; use Twig\Extra\Intl\IntlExtension; +use Twig\Extra\Markdown\MarkdownExtension; +use Twig\Extra\String\StringExtension; use Twig\Loader\ArrayLoader; use Twig\Sandbox\SecurityPolicyInterface; -final class SandboxedTwigProvider +/** + * This service creates a sandboxed twig environment for the label system. + * @see \App\Tests\Services\LabelSystem\SandboxedTwigFactoryTest + */ +final class SandboxedTwigFactory { private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'verbatim', 'with']; private const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name', - 'currency_name', 'currency_symbol', 'date', 'date_modify', 'default', 'escape', 'filter', 'first', 'format', - 'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'join', 'keys', - 'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'merge', 'nl2br', 'raw', 'number_format', - 'reduce', 'replace', 'reverse', 'slice', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title', - 'trim', 'upper', 'url_encode', - //Part-DB specific filters: - 'moneyFormat', 'siFormat', 'amountFormat', ]; + 'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'first', 'format', + 'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'html_to_markdown', 'join', 'keys', + 'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'raw', 'number_format', + 'reduce', 'replace', 'reverse', 'round', 'slice', 'slug', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title', + 'trim', 'u', 'upper', 'url_encode', - private const ALLOWED_FUNCTIONS = ['date', 'html_classes', 'max', 'min', 'random', 'range']; + //Part-DB specific filters: + + //FormatExtension: + 'format_money', 'format_si', 'format_amount', 'format_bytes', + + //SandboxedLabelExtension + 'placeholders', + ]; + + private const ALLOWED_FUNCTIONS = ['country_names', 'country_timezones', 'currency_names', 'cycle', + 'date', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names', + 'template_from_string', 'timezone_names', + + //Part-DB specific extensions: + //EntityExtension: + 'entity_type', 'entity_url', + //BarcodeExtension: + 'barcode_svg', + //SandboxedLabelExtension + 'placeholder', + ]; private const ALLOWED_METHODS = [ NamedElementInterface::class => ['getName'], AbstractDBElement::class => ['getID', '__toString'], TimeStampableInterface::class => ['getLastModified', 'getAddedDate'], AbstractStructuralDBElement::class => ['isChildOf', 'isRoot', 'getParent', 'getComment', 'getLevel', - 'getFullPath', 'getPathArray', 'getChildren', 'isNotSelectable', ], - AbstractCompany::class => ['getAddress', 'getPhoneNumber', 'getFaxNumber', 'getEmailAddress', 'getWebsite'], + 'getFullPath', 'getPathArray', 'getSubelements', 'getChildren', 'isNotSelectable', ], + AbstractCompany::class => ['getAddress', 'getPhoneNumber', 'getFaxNumber', 'getEmailAddress', 'getWebsite', 'getAutoProductUrl'], AttachmentContainingDBElement::class => ['getAttachments', 'getMasterPictureAttachment'], - Attachment::class => ['isPicture', 'is3DModel', 'isExternal', 'isSecure', 'isBuiltIn', 'getExtension', - 'getElement', 'getURL', 'getFilename', 'getAttachmentType', 'getShowInTable', ], + Attachment::class => ['isPicture', 'is3DModel', 'hasExternal', 'hasInternal', 'isSecure', 'isBuiltIn', 'getExtension', + 'getElement', 'getExternalPath', 'getHost', 'getFilename', 'getAttachmentType', 'getShowInTable'], AbstractParameter::class => ['getFormattedValue', 'getGroup', 'getSymbol', 'getValueMin', 'getValueMax', 'getValueTypical', 'getUnit', 'getValueText', ], MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'], PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation', - 'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill', ], - Storelocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'], + 'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill', 'getVendorBarcode'], + StorageLocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'], Supplier::class => ['getShippingCosts', 'getDefaultCurrency'], - Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getDescription', 'isFavorite', 'getCategory', - 'getFootprint', 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getAmountSum', + Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference', + 'getDescription', 'getComment', 'isFavorite', 'getCategory', 'getFootprint', + 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getAmountSum', 'isNotEnoughInstock', 'isAmountUnknown', 'getExpiredAmountSum', 'getManufacturerProductUrl', 'getCustomProductURL', 'getManufacturingStatus', 'getManufacturer', - 'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete', ], + 'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete', + 'getParameters', 'getGroupedParameters', + 'isProjectBuildPart', 'getBuiltProject', + 'getAssociatedPartsAsOwner', 'getAssociatedPartsAsOther', 'getAssociatedPartsAll', + 'getEdaInfo' + ], Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'], Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete', - 'getPricedetails', 'findPriceForQty', ], + 'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl'], Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity', - 'getMinDiscountQuantity', 'getCurrency', ], + 'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode'], + InfoProviderReference:: class => ['getProviderKey', 'getProviderId', 'getProviderUrl', 'getLastUpdated', 'isProviderCreated'], + PartAssociation::class => ['getType', 'getComment', 'getOwner', 'getOther', 'getOtherType'], + //Only allow very little information about users... User::class => ['isAnonymousUser', 'getUsername', 'getFullName', 'getFirstName', 'getLastName', 'getDepartment', 'getEmail', ], ]; private const ALLOWED_PROPERTIES = []; - private FormatExtension $appExtension; - - public function __construct(FormatExtension $appExtension) + public function __construct( + private readonly FormatExtension $formatExtension, + private readonly BarcodeExtension $barcodeExtension, + private readonly EntityExtension $entityExtension, + private readonly TwigCoreExtension $twigCoreExtension, + private readonly SandboxedLabelExtension $sandboxedLabelExtension, + ) { - $this->appExtension = $appExtension; } - public function getTwig(LabelOptions $options): Environment + public function createTwig(LabelOptions $options): Environment { - if ('twig' !== $options->getLinesMode()) { + if (LabelProcessMode::TWIG !== $options->getProcessMode()) { throw new InvalidArgumentException('The LabelOptions must explicitly allow twig via lines_mode = "twig"!'); } @@ -138,9 +182,16 @@ final class SandboxedTwigProvider //Add IntlExtension $twig->addExtension(new IntlExtension()); + $twig->addExtension(new MarkdownExtension()); + $twig->addExtension(new StringExtension()); + $twig->addExtension(new HtmlExtension()); //Add Part-DB specific extension - $twig->addExtension($this->appExtension); + $twig->addExtension($this->formatExtension); + $twig->addExtension($this->barcodeExtension); + $twig->addExtension($this->entityExtension); + $twig->addExtension($this->twigCoreExtension); + $twig->addExtension($this->sandboxedLabelExtension); return $twig; } diff --git a/src/Services/LogSystem/EventCommentHelper.php b/src/Services/LogSystem/EventCommentHelper.php index 4afcf04d..45e95b2c 100644 --- a/src/Services/LogSystem/EventCommentHelper.php +++ b/src/Services/LogSystem/EventCommentHelper.php @@ -41,15 +41,17 @@ declare(strict_types=1); namespace App\Services\LogSystem; +/** + * @see \App\Tests\Services\LogSystem\EventCommentHelperTest + */ class EventCommentHelper { protected const MAX_MESSAGE_LENGTH = 255; - protected ?string $message; + protected ?string $message = null; public function __construct() { - $this->message = null; } /** @@ -60,11 +62,7 @@ class EventCommentHelper public function setMessage(?string $message): void { //Restrict the length of the string - if ($message) { - $this->message = mb_strimwidth($message, 0, self::MAX_MESSAGE_LENGTH, '...'); - } else { - $this->message = null; - } + $this->message = $message ? mb_strimwidth($message, 0, self::MAX_MESSAGE_LENGTH, '...') : null; } /** diff --git a/src/Services/LogSystem/EventCommentNeededHelper.php b/src/Services/LogSystem/EventCommentNeededHelper.php index 7305b304..8440f199 100644 --- a/src/Services/LogSystem/EventCommentNeededHelper.php +++ b/src/Services/LogSystem/EventCommentNeededHelper.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\LogSystem; /** * This service is used to check if a log change comment is needed for a given operation type. * It is configured using the "enforce_change_comments_for" config parameter. + * @see \App\Tests\Services\LogSystem\EventCommentNeededHelperTest */ class EventCommentNeededHelper { - protected array $enforce_change_comments_for; - - public const VALID_OPERATION_TYPES = [ + final public const VALID_OPERATION_TYPES = [ 'part_edit', 'part_create', 'part_delete', @@ -38,15 +39,12 @@ class EventCommentNeededHelper 'datastructure_delete', ]; - public function __construct(array $enforce_change_comments_for) + public function __construct(protected array $enforce_change_comments_for) { - $this->enforce_change_comments_for = $enforce_change_comments_for; } /** * Checks if a log change comment is needed for the given operation type - * @param string $comment_type - * @return bool */ public function isCommentNeeded(string $comment_type): bool { @@ -57,4 +55,4 @@ class EventCommentNeededHelper return in_array($comment_type, $this->enforce_change_comments_for, true); } -} \ No newline at end of file +} diff --git a/src/Services/LogSystem/EventLogger.php b/src/Services/LogSystem/EventLogger.php index 8155819b..11147de6 100644 --- a/src/Services/LogSystem/EventLogger.php +++ b/src/Services/LogSystem/EventLogger.php @@ -22,30 +22,24 @@ declare(strict_types=1); namespace App\Services\LogSystem; +use App\Entity\LogSystem\LogLevel; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\User\UserInterface; use App\Entity\LogSystem\AbstractLogEntry; use App\Entity\UserSystem\User; use App\Services\Misc\ConsoleInfoHelper; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Security\Core\Security; +/** + * @see \App\Tests\Services\LogSystem\EventLoggerTest + */ class EventLogger { - protected int $minimum_log_level; - protected array $blacklist; - protected array $whitelist; - protected EntityManagerInterface $em; - protected Security $security; - protected ConsoleInfoHelper $console_info_helper; + protected LogLevel $minimum_log_level; - public function __construct(int $minimum_log_level, array $blacklist, array $whitelist, EntityManagerInterface $em, - Security $security, ConsoleInfoHelper $console_info_helper) + public function __construct(int $minimum_log_level, protected array $blacklist, protected array $whitelist, protected EntityManagerInterface $em, protected Security $security, protected ConsoleInfoHelper $console_info_helper) { - $this->minimum_log_level = $minimum_log_level; - $this->blacklist = $blacklist; - $this->whitelist = $whitelist; - $this->em = $em; - $this->security = $security; - $this->console_info_helper = $console_info_helper; + $this->minimum_log_level = LogLevel::tryFrom($minimum_log_level); } /** @@ -58,14 +52,14 @@ class EventLogger { $user = $this->security->getUser(); //If the user is not specified explicitly, set it to the current user - if ((null === $user || $user instanceof User) && null === $logEntry->getUser()) { - if (null === $user) { + if ((!$user instanceof UserInterface || $user instanceof User) && !$logEntry->getUser() instanceof User) { + if (!$user instanceof User) { $repo = $this->em->getRepository(User::class); $user = $repo->getAnonymousUser(); } //If no anonymous user is available skip the log (needed for data fixtures) - if (null === $user) { + if (!$user instanceof User) { return false; } $logEntry->setUser($user); @@ -88,25 +82,23 @@ class EventLogger /** * Same as log(), but this function can be safely called from within the onFlush() doctrine event, as it * updated the changesets of the unit of work. - * @param AbstractLogEntry $logEntry - * @return bool */ public function logFromOnFlush(AbstractLogEntry $logEntry): bool { if ($this->log($logEntry)) { $uow = $this->em->getUnitOfWork(); //As we call it from onFlush, we have to recompute the changeset here, according to https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/events.html#reference-events-on-flush - $uow->computeChangeSet($this->em->getClassMetadata(get_class($logEntry)), $logEntry); + $uow->computeChangeSet($this->em->getClassMetadata($logEntry::class), $logEntry); return true; } - //If the normal log function does not added the log entry, we just do nothing + //If the normal log function does not get added to the log entry, we just do nothing return false; } /** - * Adds the given log entry to the Log, if the entry fullfills the global configured criterias and flush afterwards. + * Adds the given log entry to the Log, if the entry fulfills the global configured criteria and flush afterward. * * @return bool returns true, if the event was added to log */ @@ -120,32 +112,27 @@ class EventLogger public function shouldBeAdded( AbstractLogEntry $logEntry, - ?int $minimum_log_level = null, + ?LogLevel $minimum_log_level = null, ?array $blacklist = null, ?array $whitelist = null ): bool { //Apply the global settings, if nothing was specified - $minimum_log_level = $minimum_log_level ?? $this->minimum_log_level; - $blacklist = $blacklist ?? $this->blacklist; - $whitelist = $whitelist ?? $this->whitelist; + $minimum_log_level ??= $this->minimum_log_level; + $blacklist ??= $this->blacklist; + $whitelist ??= $this->whitelist; - //Dont add the entry if it does not reach the minimum level - if ($logEntry->getLevel() > $minimum_log_level) { + //Don't add the entry if it does not reach the minimum level + if ($logEntry->getLevel()->lessImportThan($minimum_log_level)) { return false; } - //Check if the event type is black listed - if (!empty($blacklist) && $this->isObjectClassInArray($logEntry, $blacklist)) { + //Check if the event type is blacklisted + if ($blacklist !== [] && $this->isObjectClassInArray($logEntry, $blacklist)) { return false; } - //Check for whitelisting - if (!empty($whitelist) && !$this->isObjectClassInArray($logEntry, $whitelist)) { - return false; - } - - // By default all things should be added - return true; + // By default, all things should be added + return !($whitelist !== [] && !$this->isObjectClassInArray($logEntry, $whitelist)); } /** @@ -157,13 +144,13 @@ class EventLogger protected function isObjectClassInArray(object $object, array $classes): bool { //Check if the class is directly in the classes array - if (in_array(get_class($object), $classes, true)) { + if (in_array($object::class, $classes, true)) { return true; } //Iterate over all classes and check for inheritance foreach ($classes as $class) { - if (is_a($object, $class)) { + if ($object instanceof $class) { return true; } } diff --git a/src/Services/LogSystem/EventUndoHelper.php b/src/Services/LogSystem/EventUndoHelper.php index 3dd65eb1..c57f7724 100644 --- a/src/Services/LogSystem/EventUndoHelper.php +++ b/src/Services/LogSystem/EventUndoHelper.php @@ -42,33 +42,22 @@ declare(strict_types=1); namespace App\Services\LogSystem; use App\Entity\LogSystem\AbstractLogEntry; -use InvalidArgumentException; class EventUndoHelper { - public const MODE_UNDO = 'undo'; - public const MODE_REVERT = 'revert'; - - protected const ALLOWED_MODES = [self::MODE_REVERT, self::MODE_UNDO]; - - protected ?AbstractLogEntry $undone_event; - protected string $mode; + protected ?AbstractLogEntry $undone_event = null; + protected EventUndoMode $mode = EventUndoMode::UNDO; public function __construct() { - $this->undone_event = null; - $this->mode = self::MODE_UNDO; } - public function setMode(string $mode): void + public function setMode(EventUndoMode $mode): void { - if (!in_array($mode, self::ALLOWED_MODES, true)) { - throw new InvalidArgumentException('Invalid mode passed!'); - } $this->mode = $mode; } - public function getMode(): string + public function getMode(): EventUndoMode { return $this->mode; } @@ -91,7 +80,7 @@ class EventUndoHelper } /** - * Clear the currently the set undone event. + * Clear the currently set undone event. */ public function clearUndoneEvent(): void { @@ -99,7 +88,7 @@ class EventUndoHelper } /** - * Check if a event is undone. + * Check if an event is undone. */ public function isUndo(): bool { diff --git a/src/Services/LogSystem/EventUndoMode.php b/src/Services/LogSystem/EventUndoMode.php new file mode 100644 index 00000000..de30dcfd --- /dev/null +++ b/src/Services/LogSystem/EventUndoMode.php @@ -0,0 +1,48 @@ +. + */ +namespace App\Services\LogSystem; + +use InvalidArgumentException; + +enum EventUndoMode: string +{ + case UNDO = 'undo'; + case REVERT = 'revert'; + + public function toExtraInt(): int + { + return match ($this) { + self::UNDO => 1, + self::REVERT => 2, + }; + } + + public static function fromExtraInt(int $int): self + { + return match ($int) { + 1 => self::UNDO, + 2 => self::REVERT, + default => throw new InvalidArgumentException('Invalid int ' . (string) $int . ' for EventUndoMode'), + }; + } +} diff --git a/src/Services/LogSystem/HistoryHelper.php b/src/Services/LogSystem/HistoryHelper.php index e1638f41..3a31f127 100644 --- a/src/Services/LogSystem/HistoryHelper.php +++ b/src/Services/LogSystem/HistoryHelper.php @@ -44,7 +44,6 @@ namespace App\Services\LogSystem; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; @@ -55,10 +54,10 @@ class HistoryHelper } /** - * Returns an array containing all elements that are associated with the argument. - * The returned array contains the given element. + * Returns an array containing all elements that are associated with the argument. + * The returned array contains the given element. * - * @psalm-return array + * @return AbstractDBElement[] */ public function getAssociatedElements(AbstractDBElement $element): array { diff --git a/src/Services/LogSystem/LogDataFormatter.php b/src/Services/LogSystem/LogDataFormatter.php new file mode 100644 index 00000000..af54c60c --- /dev/null +++ b/src/Services/LogSystem/LogDataFormatter.php @@ -0,0 +1,153 @@ +. + */ +namespace App\Services\LogSystem; + +use App\Entity\LogSystem\AbstractLogEntry; +use App\Services\ElementTypeNameGenerator; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +class LogDataFormatter +{ + private const STRING_MAX_LENGTH = 1024; + + public function __construct(private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $entityManager, private readonly ElementTypeNameGenerator $elementTypeNameGenerator) + { + } + + /** + * Formats the given data of a log entry as HTML + */ + public function formatData(mixed $data, AbstractLogEntry $logEntry, string $fieldName): string + { + if (is_string($data)) { + $tmp = '"' . mb_strimwidth(htmlspecialchars($data), 0, self::STRING_MAX_LENGTH) . '"'; + + //Show special characters and line breaks + $tmp = preg_replace('/\n/', '\\n
', $tmp); + $tmp = preg_replace('/\r/', '\\r', $tmp); + + return preg_replace('/\t/', '\\t', $tmp); + } + + if (is_bool($data)) { + return $this->formatBool($data); + } + + if (is_int($data)) { + return (string) $data; + } + + if (is_float($data)) { + return (string) $data; + } + + if (is_null($data)) { + return 'null'; + } + + if (is_array($data)) { + //If the array contains only one element with the key @id, it is a reference to another entity (foreign key) + if (isset($data['@id'])) { + return $this->formatForeignKey($data, $logEntry, $fieldName); + } + + //If the array contains a "date", "timezone_type" and "timezone" key, it is a DateTime object + if (isset($data['date'], $data['timezone_type'], $data['timezone'])) { + return $this->formatDateTime($data); + } + + + return $this->formatJSON($data); + } + + + throw new \RuntimeException('Type of $data not supported (' . gettype($data) . ')'); + } + + private function formatJSON(array $data): string + { + $json = htmlspecialchars(json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), ENT_QUOTES | ENT_SUBSTITUTE); + + return sprintf( + '
', + $json + ); + } + + private function formatForeignKey(array $data, AbstractLogEntry $logEntry, string $fieldName): string + { + //Extract the id from the @id key + $id = $data['@id']; + + try { + //Retrieve the class type from the logEntry and retrieve the doctrine metadata + $classMetadata = $this->entityManager->getClassMetadata($logEntry->getTargetClass()); + $fkTargetClass = $classMetadata->getAssociationTargetClass($fieldName); + + //Try to retrieve the entity from the database + $entity = $this->entityManager->getRepository($fkTargetClass)->find($id); + + //If the entity was found, return a label for this entity + if ($entity) { + return $this->elementTypeNameGenerator->formatLabelHTMLForEntity($entity, true); + } else { //Otherwise the entity was deleted, so return the id + return $this->elementTypeNameGenerator->formatElementDeletedHTML($fkTargetClass, $id); + } + + + } catch (\InvalidArgumentException|\ReflectionException) { + return 'unknown target class: ' . $id; + } + } + + private function formatDateTime(array $data): string + { + if (!isset($data['date'], $data['timezone_type'], $data['timezone'])) { + return 'unknown DateTime format'; + } + + $date = $data['date']; + $timezoneType = $data['timezone_type']; + $timezone = $data['timezone']; + + if (!is_string($date) || !is_int($timezoneType) || !is_string($timezone)) { + return 'unknown DateTime format'; + } + + try { + $dateTime = new \DateTimeImmutable($date, new \DateTimeZone($timezone)); + } catch (\Exception) { + return 'unknown DateTime format'; + } + + //Format it to the users locale + $formatter = new \IntlDateFormatter(null, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM); + return $formatter->format($dateTime); + } + + private function formatBool(bool $data): string + { + return $data ? $this->translator->trans('true') : $this->translator->trans('false'); + } +} diff --git a/src/Services/LogSystem/LogDiffFormatter.php b/src/Services/LogSystem/LogDiffFormatter.php new file mode 100644 index 00000000..8b165d5a --- /dev/null +++ b/src/Services/LogSystem/LogDiffFormatter.php @@ -0,0 +1,81 @@ +. + */ +namespace App\Services\LogSystem; + +use Jfcherng\Diff\DiffHelper; + +class LogDiffFormatter +{ + /** + * Format the diff between the given data, depending on the type of the data. + * If the diff is not possible, an empty string is returned. + * @param $old_data + * @param $new_data + */ + public function formatDiff($old_data, $new_data): string + { + if (is_string($old_data) && is_string($new_data)) { + return $this->diffString($old_data, $new_data); + } + + if (is_numeric($old_data) && is_numeric($new_data)) { + return $this->diffNumeric($old_data, $new_data); + } + + return ''; + } + + private function diffString(string $old_data, string $new_data): string + { + return DiffHelper::calculate($old_data, $new_data, 'Combined', + [ //Diff options + 'context' => 2, + ], + [ //Render options + 'detailLevel' => 'char', + 'showHeader' => false, + ]); + } + + /** + * @param numeric $old_data + * @param numeric $new_data + */ + private function diffNumeric(int|float|string $old_data, int|float|string $new_data): string + { + if ((!is_numeric($old_data)) || (!is_numeric($new_data))) { + throw new \InvalidArgumentException('The given data is not numeric.'); + } + + $difference = $new_data - $old_data; + + //Positive difference + if ($difference > 0) { + return sprintf('+%s', $difference); + } elseif ($difference < 0) { + return sprintf('%s', $difference); + } else { + return sprintf('%s', $difference); + } + } +} diff --git a/src/Services/LogSystem/LogEntryExtraFormatter.php b/src/Services/LogSystem/LogEntryExtraFormatter.php index 74eded48..ae2a5eba 100644 --- a/src/Services/LogSystem/LogEntryExtraFormatter.php +++ b/src/Services/LogSystem/LogEntryExtraFormatter.php @@ -33,6 +33,7 @@ use App\Entity\LogSystem\ElementEditedLogEntry; use App\Entity\LogSystem\ExceptionLogEntry; use App\Entity\LogSystem\LegacyInstockChangedLogEntry; use App\Entity\LogSystem\PartStockChangedLogEntry; +use App\Entity\LogSystem\PartStockChangeType; use App\Entity\LogSystem\SecurityEventLogEntry; use App\Entity\LogSystem\UserLoginLogEntry; use App\Entity\LogSystem\UserLogoutLogEntry; @@ -48,17 +49,13 @@ class LogEntryExtraFormatter { protected const CONSOLE_SEARCH = ['', '', '', '', '']; protected const CONSOLE_REPLACE = ['→', '', '', '', '']; - protected TranslatorInterface $translator; - protected ElementTypeNameGenerator $elementTypeNameGenerator; - public function __construct(TranslatorInterface $translator, ElementTypeNameGenerator $elementTypeNameGenerator) + public function __construct(protected TranslatorInterface $translator, protected ElementTypeNameGenerator $elementTypeNameGenerator) { - $this->translator = $translator; - $this->elementTypeNameGenerator = $elementTypeNameGenerator; } /** - * Return an user viewable representation of the extra data in a log entry, styled for console output. + * Return a user viewable representation of the extra data in a log entry, styled for console output. */ public function formatConsole(AbstractLogEntry $logEntry): string { @@ -72,7 +69,7 @@ class LogEntryExtraFormatter $str .= ''.$this->translator->trans($key).': '; } $str .= $value; - if (!empty($str)) { + if ($str !== '') { $tmp[] = $str; } } @@ -81,7 +78,7 @@ class LogEntryExtraFormatter } /** - * Return a HTML formatted string containing a user viewable form of the Extra data. + * Return an HTML formatted string containing a user viewable form of the Extra data. */ public function format(AbstractLogEntry $context): string { @@ -95,7 +92,7 @@ class LogEntryExtraFormatter $str .= ''.$this->translator->trans($key).': '; } $str .= $value; - if (!empty($str)) { + if ($str !== '') { $tmp[] = $str; } } @@ -130,15 +127,15 @@ class LogEntryExtraFormatter } if (($context instanceof LogWithEventUndoInterface) && $context->isUndoEvent()) { - if ('undo' === $context->getUndoMode()) { - $array['log.undo_mode.undo'] = (string) $context->getUndoEventID(); - } elseif ('revert' === $context->getUndoMode()) { - $array['log.undo_mode.revert'] = (string) $context->getUndoEventID(); + if (EventUndoMode::UNDO === $context->getUndoMode()) { + $array['log.undo_mode.undo'] = '#' . $context->getUndoEventID(); + } elseif (EventUndoMode::REVERT === $context->getUndoMode()) { + $array['log.undo_mode.revert'] = '#' . $context->getUndoEventID(); } } if ($context instanceof LogWithCommentInterface && $context->hasComment()) { - $array[] = htmlspecialchars($context->getComment()); + $array[] = htmlspecialchars((string) $context->getComment()); } if ($context instanceof ElementCreatedLogEntry && $context->hasCreationInstockValue()) { @@ -163,7 +160,7 @@ class LogEntryExtraFormatter '%s %s (%s)', $context->getOldInstock(), $context->getNewInstock(), - (!$context->isWithdrawal() ? '+' : '-').$context->getDifference(true) + ($context->isWithdrawal() ? '-' : '+').$context->getDifference(true) ); $array['log.instock_changed.comment'] = htmlspecialchars($context->getComment()); } @@ -188,14 +185,18 @@ class LogEntryExtraFormatter $context->getNewStock(), ($context->getNewStock() > $context->getOldStock() ? '+' : '-'). $context->getChangeAmount(), ); - if (!empty($context->getComment())) { + if ($context->getComment() !== '') { $array['log.part_stock_changed.comment'] = htmlspecialchars($context->getComment()); } - if ($context->getInstockChangeType() === PartStockChangedLogEntry::TYPE_MOVE) { + if ($context->getInstockChangeType() === PartStockChangeType::MOVE) { $array['log.part_stock_changed.move_target'] = htmlspecialchars($this->elementTypeNameGenerator->getLocalizedTypeLabel(PartLot::class)) .' ' . $context->getMoveToTargetID(); } + if ($context->getActionTimestamp() !== null) { + $formatter = new \IntlDateFormatter($this->translator->getLocale(), \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT); + $array['log.part_stock_changed.timestamp'] = $formatter->format($context->getActionTimestamp()); + } } return $array; diff --git a/src/Services/LogSystem/LogLevelHelper.php b/src/Services/LogSystem/LogLevelHelper.php new file mode 100644 index 00000000..67e87392 --- /dev/null +++ b/src/Services/LogSystem/LogLevelHelper.php @@ -0,0 +1,64 @@ +. + */ +namespace App\Services\LogSystem; + +use Psr\Log\LogLevel; + +class LogLevelHelper +{ + /** + * Returns the FontAwesome icon class for the given log level. + * This returns just the specific icon class (so 'fa-info' for example). + * @param string $logLevel The string representation of the log level (one of the LogLevel::* constants) + */ + public function logLevelToIconClass(string $logLevel): string + { + return match ($logLevel) { + LogLevel::DEBUG => 'fa-bug', + LogLevel::INFO => 'fa-info', + LogLevel::NOTICE => 'fa-flag', + LogLevel::WARNING => 'fa-exclamation-circle', + LogLevel::ERROR => 'fa-exclamation-triangle', + LogLevel::CRITICAL => 'fa-bolt', + LogLevel::ALERT => 'fa-radiation', + LogLevel::EMERGENCY => 'fa-skull-crossbones', + default => 'fa-question-circle', + }; + } + + /** + * Returns the Bootstrap table color class for the given log level. + * @param string $logLevel The string representation of the log level (one of the LogLevel::* constants) + * @return string The table color class (one of the 'table-*' classes) + */ + public function logLevelToTableColorClass(string $logLevel): string + { + + return match ($logLevel) { + LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR => 'table-danger', + LogLevel::WARNING => 'table-warning', + LogLevel::NOTICE => 'table-info', + default => '', + }; + } +} diff --git a/src/Services/LogSystem/LogTargetHelper.php b/src/Services/LogSystem/LogTargetHelper.php new file mode 100644 index 00000000..5dd649f1 --- /dev/null +++ b/src/Services/LogSystem/LogTargetHelper.php @@ -0,0 +1,79 @@ +. + */ +namespace App\Services\LogSystem; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\UserNotAllowedLogEntry; +use App\Repository\LogEntryRepository; +use App\Services\ElementTypeNameGenerator; +use App\Services\EntityURLGenerator; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +class LogTargetHelper +{ + protected LogEntryRepository $entryRepository; + + public function __construct(protected EntityManagerInterface $em, protected EntityURLGenerator $entityURLGenerator, + protected ElementTypeNameGenerator $elementTypeNameGenerator, protected TranslatorInterface $translator) + { + $this->entryRepository = $em->getRepository(AbstractLogEntry::class); + } + + private function configureOptions(OptionsResolver $resolver): self + { + $resolver->setDefault('show_associated', true); + $resolver->setDefault('showAccessDeniedPath', true); + + return $this; + } + + public function formatTarget(AbstractLogEntry $context, array $options = []): string + { + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $options = $optionsResolver->resolve($options); + + if ($context instanceof UserNotAllowedLogEntry && $options['showAccessDeniedPath']) { + return htmlspecialchars($context->getPath()); + } + + /** @var AbstractLogEntry $context */ + $target = $this->entryRepository->getTargetElement($context); + + //If the target is null and the context has a target, that means that the target was deleted. Show it that way. + if (!$target instanceof AbstractDBElement) { + if ($context->hasTarget()) { + return $this->elementTypeNameGenerator->formatElementDeletedHTML($context->getTargetClass(), + $context->getTargetID()); + } + //If no target is set, we can't do anything + return ''; + } + + //Otherwise we can return a label for the target + return $this->elementTypeNameGenerator->formatLabelHTMLForEntity($target, $options['show_associated']); + } +} diff --git a/src/Services/LogSystem/TimeTravel.php b/src/Services/LogSystem/TimeTravel.php index 9933d235..68d962bb 100644 --- a/src/Services/LogSystem/TimeTravel.php +++ b/src/Services/LogSystem/TimeTravel.php @@ -34,23 +34,21 @@ use App\Repository\LogEntryRepository; use Brick\Math\BigDecimal; use DateTime; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException; -use DoctrineExtensions\Query\Mysql\Date; use Exception; use InvalidArgumentException; use ReflectionClass; +use Symfony\Component\PropertyAccess\PropertyAccessor; class TimeTravel { - protected EntityManagerInterface $em; protected LogEntryRepository $repo; - public function __construct(EntityManagerInterface $em) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $em; $this->repo = $em->getRepository(AbstractLogEntry::class); } @@ -58,8 +56,11 @@ class TimeTravel /** * Undeletes the element with the given ID. * + * @template T of AbstractDBElement * @param string $class The class name of the element that should be undeleted + * @phpstan-param class-string $class * @param int $id the ID of the element that should be undeleted + * @phpstan-return T */ public function undeleteEntity(string $class, int $id): AbstractDBElement { @@ -83,7 +84,7 @@ class TimeTravel * * @throws Exception */ - public function revertEntityToTimestamp(AbstractDBElement $element, DateTime $timestamp, array $reverted_elements = []): void + public function revertEntityToTimestamp(AbstractDBElement $element, \DateTimeInterface $timestamp, array $reverted_elements = []): void { if (!$element instanceof TimeStampableInterface) { throw new InvalidArgumentException('$element must have a Timestamp!'); @@ -128,7 +129,7 @@ class TimeTravel } // Revert any of the associated elements - $metadata = $this->em->getClassMetadata(get_class($element)); + $metadata = $this->em->getClassMetadata($element::class); $associations = $metadata->getAssociationMappings(); foreach ($associations as $field => $mapping) { if ( @@ -138,27 +139,27 @@ class TimeTravel continue; } - //Revert many to one association (one element in property) + //Revert many-to-one association (one element in property) if ( - ClassMetadataInfo::MANY_TO_ONE === $mapping['type'] - || ClassMetadataInfo::ONE_TO_ONE === $mapping['type'] + ClassMetadata::MANY_TO_ONE === $mapping['type'] + || ClassMetadata::ONE_TO_ONE === $mapping['type'] ) { $target_element = $this->getField($element, $field); if (null !== $target_element && $element->getLastModified() > $timestamp) { $this->revertEntityToTimestamp($target_element, $timestamp, $reverted_elements); } } elseif ( //Revert *_TO_MANY associations (collection properties) - (ClassMetadataInfo::MANY_TO_MANY === $mapping['type'] - || ClassMetadataInfo::ONE_TO_MANY === $mapping['type']) - && false === $mapping['isOwningSide'] + (ClassMetadata::MANY_TO_MANY === $mapping['type'] + || ClassMetadata::ONE_TO_MANY === $mapping['type']) + && !$mapping['isOwningSide'] ) { $target_elements = $this->getField($element, $field); - if (null === $target_elements || count($target_elements) > 10) { + if (null === $target_elements || (is_countable($target_elements) ? count($target_elements) : 0) > 10) { continue; } foreach ($target_elements as $target_element) { if (null !== $target_element && $element->getLastModified() >= $timestamp) { - //Remove the element from collection, if it did not existed at $timestamp + //Remove the element from collection, if it did not exist at $timestamp if (!$this->repo->getElementExistedAtTimestamp( $target_element, $timestamp @@ -175,17 +176,26 @@ class TimeTravel /** * This function decodes the array which is created during the json_encode of a datetime object and returns a DateTime object. * @param array $input - * @return DateTime + * @return \DateTimeInterface * @throws Exception */ - private function dateTimeDecode(?array $input): ?\DateTime + private function dateTimeDecode(?array $input, string $doctrineType): ?\DateTimeInterface { //Allow null values if ($input === null) { return null; } - return new \DateTime($input['date'], new \DateTimeZone($input['timezone'])); + //Mutable types + if (in_array($doctrineType, [Types::DATETIME_MUTABLE, Types::DATE_MUTABLE], true)) { + return new \DateTime($input['date'], new \DateTimeZone($input['timezone'])); + } + //Immutable types + if (in_array($doctrineType, [Types::DATETIME_IMMUTABLE, Types::DATE_IMMUTABLE], true)) { + return new \DateTimeImmutable($input['date'], new \DateTimeZone($input['timezone'])); + } + + throw new InvalidArgumentException('The given doctrine type is not a datetime type!'); } /** @@ -196,24 +206,34 @@ class TimeTravel public function applyEntry(AbstractDBElement $element, TimeTravelInterface $logEntry): void { //Skip if this does not provide any info... - if (!$logEntry->hasOldDataInformations()) { + if (!$logEntry->hasOldDataInformation()) { return; } if (!$element instanceof TimeStampableInterface) { return; } - $metadata = $this->em->getClassMetadata(get_class($element)); + $metadata = $this->em->getClassMetadata($element::class); $old_data = $logEntry->getOldData(); foreach ($old_data as $field => $data) { - if ($metadata->hasField($field)) { + + //We use the fieldMappings property directly instead of the hasField method, as we do not want to match the embedded field itself + //The sub fields are handled in the setField method + if (isset($metadata->fieldMappings[$field])) { //We need to convert the string to a BigDecimal first - if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)['type'])) { + if (!$data instanceof BigDecimal && ('big_decimal' === $metadata->getFieldMapping($field)->type)) { $data = BigDecimal::of($data); } - if (!$data instanceof DateTime && ('datetime' === $metadata->getFieldMapping($field)['type'])) { - $data = $this->dateTimeDecode($data); + if (!$data instanceof \DateTimeInterface + && (in_array($metadata->getFieldMapping($field)->type, + [ + Types::DATETIME_IMMUTABLE, + Types::DATETIME_IMMUTABLE, + Types::DATE_MUTABLE, + Types::DATETIME_IMMUTABLE + ], true))) { + $data = $this->dateTimeDecode($data, $metadata->getFieldMapping($field)->type); } $this->setField($element, $field, $data); @@ -223,7 +243,7 @@ class TimeTravel $target_class = $mapping['targetEntity']; //Try to extract the old ID: if (is_array($data) && isset($data['@id'])) { - $entity = $this->em->getPartialReference($target_class, $data['@id']); + $entity = $this->em->getReference($target_class, $data['@id']); $this->setField($element, $field, $entity); } } @@ -232,24 +252,45 @@ class TimeTravel $this->setField($element, 'lastModified', $logEntry->getTimestamp()); } - protected function getField(AbstractDBElement $element, string $field) + protected function getField(AbstractDBElement $element, string $field): mixed { - $reflection = new ReflectionClass(get_class($element)); + $reflection = new ReflectionClass($element::class); $property = $reflection->getProperty($field); - $property->setAccessible(true); return $property->getValue($element); } /** - * @param DateTime|int|null $new_value + * @param int|null|object $new_value */ - protected function setField(AbstractDBElement $element, string $field, $new_value): void + protected function setField(AbstractDBElement $element, string $field, mixed $new_value): void { - $reflection = new ReflectionClass(get_class($element)); - $property = $reflection->getProperty($field); - $property->setAccessible(true); + //If the field name contains a dot, it is a embeddedable object and we need to split the field name + if (str_contains($field, '.')) { + [$embedded, $embedded_field] = explode('.', $field); - $property->setValue($element, $new_value); + $elementClass = new ReflectionClass($element::class); + $property = $elementClass->getProperty($embedded); + $embeddedClass = $property->getValue($element); + + $embeddedReflection = new ReflectionClass($embeddedClass::class); + $property = $embeddedReflection->getProperty($embedded_field); + $target_element = $embeddedClass; + } else { + $reflection = new ReflectionClass($element::class); + $property = $reflection->getProperty($field); + $target_element = $element; + } + + //Check if the property is an BackedEnum, then convert the int or float value to an enum instance + if ((is_string($new_value) || is_int($new_value)) + && $property->getType() instanceof \ReflectionNamedType + && is_a($property->getType()->getName(), \BackedEnum::class, true)) { + /** @phpstan-var class-string<\BackedEnum> $enum_class */ + $enum_class = $property->getType()->getName(); + $new_value = $enum_class::from($new_value); + } + + $property->setValue($target_element, $new_value); } } diff --git a/src/Services/Misc/ConsoleInfoHelper.php b/src/Services/Misc/ConsoleInfoHelper.php index 8aea004e..98de5e07 100644 --- a/src/Services/Misc/ConsoleInfoHelper.php +++ b/src/Services/Misc/ConsoleInfoHelper.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\Misc; class ConsoleInfoHelper @@ -34,6 +36,7 @@ class ConsoleInfoHelper /** * Returns the username of the user who started the current script if possible. * @return string|null the username of the user who started the current script if possible, null otherwise + * @noinspection PhpUndefinedFunctionInspection */ public function getCLIUser(): ?string { @@ -47,17 +50,7 @@ class ConsoleInfoHelper return $user['name']; } - //Try to retrieve the name via the environment variable Username (Windows) - if (isset($_SERVER['USERNAME'])) { - return $_SERVER['USERNAME']; - } - - //Try to retrieve the name via the environment variable USER (Linux) - if (isset($_SERVER['USER'])) { - return $_SERVER['USER']; - } - //Otherwise we can't determine the username - return null; + return $_SERVER['USERNAME'] ?? $_SERVER['USER'] ?? null; } -} \ No newline at end of file +} diff --git a/src/Services/Misc/FAIconGenerator.php b/src/Services/Misc/FAIconGenerator.php index b8ee4481..2ea727af 100644 --- a/src/Services/Misc/FAIconGenerator.php +++ b/src/Services/Misc/FAIconGenerator.php @@ -24,8 +24,10 @@ namespace App\Services\Misc; use App\Entity\Attachments\Attachment; use function in_array; -use InvalidArgumentException; +/** + * @see \App\Tests\Services\Misc\FAIconGeneratorTest + */ class FAIconGenerator { protected const EXT_MAPPING = [ @@ -53,9 +55,6 @@ class FAIconGenerator */ public function fileExtensionToFAType(string $extension): string { - if ('' === $extension) { - throw new InvalidArgumentException('You must specify an extension!'); - } //Normalize file extension $extension = strtolower($extension); foreach (self::EXT_MAPPING as $fa => $exts) { diff --git a/src/Services/Misc/RangeParser.php b/src/Services/Misc/RangeParser.php index ab6e9aba..f1a5db5b 100644 --- a/src/Services/Misc/RangeParser.php +++ b/src/Services/Misc/RangeParser.php @@ -45,6 +45,7 @@ use InvalidArgumentException; /** * This Parser allows to parse number ranges like 1-3, 4, 5. + * @see \App\Tests\Services\Misc\RangeParserTest */ class RangeParser { @@ -70,7 +71,7 @@ class RangeParser $ranges[] = $this->generateMinMaxRange($matches[1], $matches[2]); } elseif (is_numeric($number)) { $ranges[] = [(int) $number]; - } elseif (empty($number)) { //Allow empty tokens + } elseif ($number === '') { //Allow empty tokens continue; } else { throw new InvalidArgumentException('Invalid range encoutered: '.$number); @@ -94,7 +95,7 @@ class RangeParser $this->parse($range_str); return true; - } catch (InvalidArgumentException $exception) { + } catch (InvalidArgumentException) { return false; } } diff --git a/src/Services/OAuth/OAuthTokenManager.php b/src/Services/OAuth/OAuthTokenManager.php new file mode 100644 index 00000000..9c22503b --- /dev/null +++ b/src/Services/OAuth/OAuthTokenManager.php @@ -0,0 +1,162 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\OAuth; + +use App\Entity\OAuthToken; +use Doctrine\ORM\EntityManagerInterface; +use KnpU\OAuth2ClientBundle\Client\ClientRegistry; +use League\OAuth2\Client\Token\AccessTokenInterface; + +final class OAuthTokenManager +{ + public function __construct(private readonly ClientRegistry $clientRegistry, private readonly EntityManagerInterface $entityManager) + { + + } + + /** + * Saves the given token to the database, so it can be retrieved later + * @param string $app_name + * @param AccessTokenInterface $token + * @return OAuthToken The saved token as database entity + */ + public function saveToken(string $app_name, AccessTokenInterface $token): OAuthToken + { + //Check if we already have a token for this app + $tokenEntity = $this->entityManager->getRepository(OAuthToken::class)->findOneBy(['name' => $app_name]); + + //If the token was already existing, we just replace it with the new one + if ($tokenEntity !== null) { + $tokenEntity->replaceWithNewToken($token); + + $this->entityManager->flush(); + + //We are done + return $tokenEntity; + } + + //If the token was not existing, we create a new one + $tokenEntity = OAuthToken::fromAccessToken($token, $app_name); + $this->entityManager->persist($tokenEntity); + + $this->entityManager->flush(); + + return $tokenEntity; + } + + /** + * Returns the token for the given app name + * @param string $app_name + * @return OAuthToken|null + */ + public function getToken(string $app_name): ?OAuthToken + { + return $this->entityManager->getRepository(OAuthToken::class)->findOneBy(['name' => $app_name]); + } + + /** + * Checks if a token for the given app name is existing + * @param string $app_name + * @return bool + */ + public function hasToken(string $app_name): bool + { + return $this->getToken($app_name) !== null; + } + + /** + * This function refreshes the token for the given app name. The new token is saved to the database + * The app_name must be registered in the knpu_oauth2_client.yaml + * @param string $app_name + * @return OAuthToken + * @throws \Exception + */ + public function refreshToken(string $app_name): OAuthToken + { + $token = $this->getToken($app_name); + + if ($token === null) { + throw new \RuntimeException('No token was saved yet for '.$app_name); + } + + $client = $this->clientRegistry->getClient($app_name); + + //Check if the token is refreshable or if it is an client credentials token + if ($token->isClientCredentialsGrant()) { + $new_token = $client->getOAuth2Provider()->getAccessToken('client_credentials'); + } else { + //Otherwise we can use the refresh token to get a new access token + $new_token = $client->refreshAccessToken($token->getRefreshToken()); + } + + //Persist the token + $token->replaceWithNewToken($new_token); + $this->entityManager->flush(); + + return $token; + } + + /** + * This function returns the token of the given app name + * @param string $app_name + * @return string|null + */ + public function getAlwaysValidTokenString(string $app_name): ?string + { + //Get the token for the application + $token = $this->getToken($app_name); + + //If the token is not existing, we return null + if ($token === null) { + return null; + } + + //If the token is still valid, we return it + if (!$token->hasExpired()) { + return $token->getToken(); + } + + //If the token is expired, we refresh it + $this->refreshToken($app_name); + + //And return the new token + return $token->getToken(); + } + + /** + * Retrieves an access token for the given app name using the client credentials grant (so no user flow is needed) + * The app_name must be registered in the knpu_oauth2_client.yaml + * The token is saved to the database, and afterward can be used as usual + * @param string $app_name + * @return OAuthToken + */ + public function retrieveClientCredentialsToken(string $app_name): OAuthToken + { + $client = $this->clientRegistry->getClient($app_name); + $access_token = $client->getOAuth2Provider()->getAccessToken('client_credentials'); + + + return $this->saveToken($app_name, $access_token); + } +} \ No newline at end of file diff --git a/src/Services/Parameters/ParameterExtractor.php b/src/Services/Parameters/ParameterExtractor.php index de5ecb51..a133b282 100644 --- a/src/Services/Parameters/ParameterExtractor.php +++ b/src/Services/Parameters/ParameterExtractor.php @@ -47,6 +47,9 @@ use InvalidArgumentException; use function preg_match; +/** + * @see \App\Tests\Services\Parameters\ParameterExtractorTest + */ class ParameterExtractor { protected const ALLOWED_PARAM_SEPARATORS = [', ', "\n"]; @@ -74,7 +77,7 @@ class ParameterExtractor $split = $this->splitString($input); foreach ($split as $param_string) { $tmp = $this->stringToParam($param_string, $class); - if (null !== $tmp) { + if ($tmp instanceof AbstractParameter) { $parameters[] = $tmp; } } @@ -85,16 +88,16 @@ class ParameterExtractor protected function stringToParam(string $input, string $class): ?AbstractParameter { $input = trim($input); - $regex = '/^(.*) *(?:=|:) *(.+)/u'; + $regex = '/^(.*) *(?:=|:)(?!\/) *(.+)/u'; $matches = []; preg_match($regex, $input, $matches); - if (!empty($matches)) { + if ($matches !== []) { [, $name, $value] = $matches; $value = trim($value); - //Dont allow empty names or values (these are a sign of an invalid extracted string) - if (empty($name) || empty($value)) { + //Don't allow empty names or values (these are a sign of an invalid extracted string) + if ($name === '' || $value === '') { return null; } diff --git a/src/Services/Parts/PartLotWithdrawAddHelper.php b/src/Services/Parts/PartLotWithdrawAddHelper.php index 80403dd4..34ec4c1d 100644 --- a/src/Services/Parts/PartLotWithdrawAddHelper.php +++ b/src/Services/Parts/PartLotWithdrawAddHelper.php @@ -1,29 +1,28 @@ eventLogger = $eventLogger; - $this->eventCommentHelper = $eventCommentHelper; } /** * Checks whether the given part can - * @param PartLot $partLot - * @return bool */ public function canAdd(PartLot $partLot): bool { @@ -33,16 +32,11 @@ final class PartLotWithdrawAddHelper } //So far all other restrictions are defined at the storelocation level - if($partLot->getStorageLocation() === null) { + if(!$partLot->getStorageLocation() instanceof StorageLocation) { return true; } - //We can not add parts if the storage location of the lot is marked as full - if($partLot->getStorageLocation()->isFull()) { - return false; - } - - return true; + return !$partLot->getStorageLocation()->isFull(); } public function canWithdraw(PartLot $partLot): bool @@ -51,13 +45,8 @@ final class PartLotWithdrawAddHelper if ($partLot->isInstockUnknown()) { return false; } - //Part must contain more than 0 parts - if ($partLot->getAmount() <= 0) { - return false; - } - - return true; + return $partLot->getAmount() > 0; } /** @@ -66,9 +55,10 @@ final class PartLotWithdrawAddHelper * @param PartLot $partLot The partLot from which the instock should be taken (which value should be decreased) * @param float $amount The amount of parts that should be taken from the part lot * @param string|null $comment The optional comment describing the reason for the withdrawal - * @return PartLot The modified part lot + * @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards. + * @param bool $delete_lot_if_empty If true, the part lot will be deleted if the amount is 0 after the withdrawal. */ - public function withdraw(PartLot $partLot, float $amount, ?string $comment = null): PartLot + public function withdraw(PartLot $partLot, float $amount, ?string $comment = null, ?\DateTimeInterface $action_timestamp = null, bool $delete_lot_if_empty = false): void { //Ensure that amount is positive if ($amount <= 0) { @@ -96,15 +86,17 @@ final class PartLotWithdrawAddHelper $oldAmount = $partLot->getAmount(); $partLot->setAmount($oldAmount - $amount); - $event = PartStockChangedLogEntry::withdraw($partLot, $oldAmount, $partLot->getAmount(), $part->getAmountSum() , $comment); + $event = PartStockChangedLogEntry::withdraw($partLot, $oldAmount, $partLot->getAmount(), $part->getAmountSum() , $comment, $action_timestamp); $this->eventLogger->log($event); //Apply the comment also to global events, so it gets associated with the elementChanged log entry - if (!$this->eventCommentHelper->isMessageSet() && !empty($comment)) { + if (!$this->eventCommentHelper->isMessageSet() && ($comment !== null && $comment !== '')) { $this->eventCommentHelper->setMessage($comment); } - return $partLot; + if ($delete_lot_if_empty && $partLot->getAmount() === 0.0) { + $this->entityManager->remove($partLot); + } } /** @@ -113,9 +105,10 @@ final class PartLotWithdrawAddHelper * @param PartLot $partLot The partLot from which the instock should be taken (which value should be decreased) * @param float $amount The amount of parts that should be taken from the part lot * @param string|null $comment The optional comment describing the reason for the withdrawal + * @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards. * @return PartLot The modified part lot */ - public function add(PartLot $partLot, float $amount, ?string $comment = null): PartLot + public function add(PartLot $partLot, float $amount, ?string $comment = null, ?\DateTimeInterface $action_timestamp = null): PartLot { if ($amount <= 0) { throw new \InvalidArgumentException('Amount must be positive'); @@ -136,11 +129,11 @@ final class PartLotWithdrawAddHelper $oldAmount = $partLot->getAmount(); $partLot->setAmount($oldAmount + $amount); - $event = PartStockChangedLogEntry::add($partLot, $oldAmount, $partLot->getAmount(), $part->getAmountSum() , $comment); + $event = PartStockChangedLogEntry::add($partLot, $oldAmount, $partLot->getAmount(), $part->getAmountSum() , $comment, $action_timestamp); $this->eventLogger->log($event); //Apply the comment also to global events, so it gets associated with the elementChanged log entry - if (!$this->eventCommentHelper->isMessageSet() && !empty($comment)) { + if (!$this->eventCommentHelper->isMessageSet() && ($comment !== null && $comment !== '')) { $this->eventCommentHelper->setMessage($comment); } @@ -154,9 +147,10 @@ final class PartLotWithdrawAddHelper * @param PartLot $target The part lot to which the parts should be added * @param float $amount The amount of parts that should be moved * @param string|null $comment A comment describing the reason for the move - * @return void + * @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards. + * @param bool $delete_lot_if_empty If true, the part lot will be deleted if the amount is 0 after the withdrawal. */ - public function move(PartLot $origin, PartLot $target, float $amount, ?string $comment = null): void + public function move(PartLot $origin, PartLot $target, float $amount, ?string $comment = null, ?\DateTimeInterface $action_timestamp = null, bool $delete_lot_if_empty = false): void { if ($amount <= 0) { throw new \InvalidArgumentException('Amount must be positive'); @@ -191,12 +185,16 @@ final class PartLotWithdrawAddHelper //And add it to the target $target->setAmount($target->getAmount() + $amount); - $event = PartStockChangedLogEntry::move($origin, $oldOriginAmount, $origin->getAmount(), $part->getAmountSum() , $comment, $target); + $event = PartStockChangedLogEntry::move($origin, $oldOriginAmount, $origin->getAmount(), $part->getAmountSum() , $comment, $target, $action_timestamp); $this->eventLogger->log($event); //Apply the comment also to global events, so it gets associated with the elementChanged log entry - if (!$this->eventCommentHelper->isMessageSet() && !empty($comment)) { + if (!$this->eventCommentHelper->isMessageSet() && ($comment !== null && $comment !== '')) { $this->eventCommentHelper->setMessage($comment); } + + if ($delete_lot_if_empty && $origin->getAmount() === 0.0) { + $this->entityManager->remove($origin); + } } -} \ No newline at end of file +} diff --git a/src/Services/Parts/PartsTableActionHandler.php b/src/Services/Parts/PartsTableActionHandler.php index 3061d2f3..616df229 100644 --- a/src/Services/Parts/PartsTableActionHandler.php +++ b/src/Services/Parts/PartsTableActionHandler.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\Parts; +use App\Entity\Parts\StorageLocation; +use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; -use App\Repository\DBElementRepository; use App\Repository\PartRepository; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Security\Core\Security; +use Symfony\Contracts\Translation\TranslatableInterface; + +use function Symfony\Component\Translation\t; final class PartsTableActionHandler { - private EntityManagerInterface $entityManager; - private Security $security; - private UrlGeneratorInterface $urlGenerator; - - public function __construct(EntityManagerInterface $entityManager, Security $security, UrlGeneratorInterface $urlGenerator) + public function __construct(private readonly EntityManagerInterface $entityManager, private readonly Security $security, private readonly UrlGeneratorInterface $urlGenerator) { - $this->entityManager = $entityManager; - $this->security = $security; - $this->urlGenerator = $urlGenerator; } /** @@ -59,7 +57,6 @@ final class PartsTableActionHandler { $id_array = explode(',', $ids); - /** @var PartRepository $repo */ $repo = $this->entityManager->getRepository(Part::class); return $repo->getElementsFromIDArray($id_array); @@ -68,8 +65,9 @@ final class PartsTableActionHandler /** * @param Part[] $selected_parts * @return RedirectResponse|null Returns a redirect response if the user should be redirected to another page, otherwise null + * //@param-out list|array $errors */ - public function handleAction(string $action, array $selected_parts, ?int $target_id, ?string $redirect_url = null): ?RedirectResponse + public function handleAction(string $action, array $selected_parts, ?int $target_id, ?string $redirect_url = null, array &$errors = []): ?RedirectResponse { if ($action === 'add_to_project') { return new RedirectResponse( @@ -86,10 +84,8 @@ final class PartsTableActionHandler if ($action === 'generate_label') { $targets = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts)); } else { //For lots we have to extract the part lots - $targets = implode(',', array_map(static function (Part $part) { - //We concat the lot IDs of every part with a comma (which are later concated with a comma too per part) - return implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPartLots()->toArray())); - }, $selected_parts)); + $targets = implode(',', array_map(static fn(Part $part): string => //We concat the lot IDs of every part with a comma (which are later concated with a comma too per part) +implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPartLots()->toArray())), $selected_parts)); } return new RedirectResponse( @@ -106,18 +102,11 @@ final class PartsTableActionHandler $matches = []; if (preg_match('/^export_(json|yaml|xml|csv)$/', $action, $matches)) { $ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts)); - switch ($target_id) { - case 1: - default: - $level = 'simple'; - break; - case 2: - $level = 'extended'; - break; - case 3: - $level = 'full'; - break; - } + $level = match ($target_id) { + 2 => 'extended', + 3 => 'full', + default => 'simple', + }; return new RedirectResponse( @@ -177,6 +166,29 @@ final class PartsTableActionHandler $this->denyAccessUnlessGranted('@measurement_units.read'); $part->setPartUnit(null === $target_id ? null : $this->entityManager->find(MeasurementUnit::class, $target_id)); break; + case 'change_location': + $this->denyAccessUnlessGranted('@storelocations.read'); + //Retrieve the first part lot and set the location for it + $part_lots = $part->getPartLots(); + if ($part_lots->count() > 0) { + if ($part_lots->count() > 1) { + $errors[] = [ + 'part' => $part, + 'message' => t('parts.table.action_handler.error.part_lots_multiple'), + ]; + break; + } + + $part_lot = $part_lots->first(); + $part_lot->setStorageLocation(null === $target_id ? null : $this->entityManager->find(StorageLocation::class, $target_id)); + } else { //Create a new part lot if there are none + $part_lot = new PartLot(); + $part_lot->setPart($part); + $part_lot->setInstockUnknown(true); //We do not know how many parts are in stock, so we set it to true + $part_lot->setStorageLocation(null === $target_id ? null : $this->entityManager->find(StorageLocation::class, $target_id)); + $this->entityManager->persist($part_lot); + } + break; default: throw new InvalidArgumentException('The given action is unknown! ('.$action.')'); @@ -192,7 +204,7 @@ final class PartsTableActionHandler * * @throws AccessDeniedException */ - private function denyAccessUnlessGranted($attributes, $subject = null, string $message = 'Access Denied.'): void + private function denyAccessUnlessGranted(mixed $attributes, mixed $subject = null, string $message = 'Access Denied.'): void { if (!$this->security->isGranted($attributes, $subject)) { $exception = new AccessDeniedException($message); diff --git a/src/Services/Parts/PricedetailHelper.php b/src/Services/Parts/PricedetailHelper.php index f9902a98..092cc278 100644 --- a/src/Services/Parts/PricedetailHelper.php +++ b/src/Services/Parts/PricedetailHelper.php @@ -32,14 +32,15 @@ use Locale; use function count; +/** + * @see \App\Tests\Services\Parts\PricedetailHelperTest + */ class PricedetailHelper { - protected string $base_currency; protected string $locale; - public function __construct(string $base_currency) + public function __construct(protected string $base_currency) { - $this->base_currency = $base_currency; $this->locale = Locale::getDefault(); } @@ -56,7 +57,7 @@ class PricedetailHelper foreach ($orderdetails as $orderdetail) { $pricedetails = $orderdetail->getPricedetails(); //The orderdetail must have pricedetails, otherwise this will not work! - if (0 === count($pricedetails)) { + if (0 === (is_countable($pricedetails) ? count($pricedetails) : 0)) { continue; } @@ -67,9 +68,7 @@ class PricedetailHelper } else { // We have to sort the pricedetails manually $array = $pricedetails->map( - static function (Pricedetail $pricedetail) { - return $pricedetail->getMinDiscountQuantity(); - } + static fn(Pricedetail $pricedetail) => $pricedetail->getMinDiscountQuantity() )->toArray(); sort($array); $max_amount = end($array); @@ -103,7 +102,7 @@ class PricedetailHelper foreach ($orderdetails as $orderdetail) { $pricedetails = $orderdetail->getPricedetails(); //The orderdetail must have pricedetails, otherwise this will not work! - if (0 === count($pricedetails)) { + if (0 === (is_countable($pricedetails) ? count($pricedetails) : 0)) { continue; } @@ -153,14 +152,14 @@ class PricedetailHelper foreach ($orderdetails as $orderdetail) { $pricedetail = $orderdetail->findPriceForQty($amount); - //When we dont have informations about this amount, ignore it - if (null === $pricedetail) { + //When we don't have information about this amount, ignore it + if (!$pricedetail instanceof Pricedetail) { continue; } $converted = $this->convertMoneyToCurrency($pricedetail->getPricePerUnit(), $pricedetail->getCurrency(), $currency); - //Ignore price informations that can not be converted to base currency. - if (null !== $converted) { + //Ignore price information that can not be converted to base currency. + if ($converted instanceof BigDecimal) { $avg = $avg->plus($converted); ++$count; } @@ -193,9 +192,9 @@ class PricedetailHelper $val_base = $value; //Convert value to base currency - if (null !== $originCurrency) { + if ($originCurrency instanceof Currency) { //Without an exchange rate we can not calculate the exchange rate - if (null === $originCurrency->getExchangeRate() || $originCurrency->getExchangeRate()->isZero()) { + if (!$originCurrency->getExchangeRate() instanceof BigDecimal || $originCurrency->getExchangeRate()->isZero()) { return null; } @@ -204,9 +203,9 @@ class PricedetailHelper $val_target = $val_base; //Convert value in base currency to target currency - if (null !== $targetCurrency) { + if ($targetCurrency instanceof Currency) { //Without an exchange rate we can not calculate the exchange rate - if (null === $targetCurrency->getExchangeRate()) { + if (!$targetCurrency->getExchangeRate() instanceof BigDecimal) { return null; } diff --git a/src/Services/ProjectSystem/ProjectBuildHelper.php b/src/Services/ProjectSystem/ProjectBuildHelper.php index 8eee0772..269c7e4c 100644 --- a/src/Services/ProjectSystem/ProjectBuildHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildHelper.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\ProjectSystem; +use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Helpers\Projects\ProjectBuildRequest; use App\Services\Parts\PartLotWithdrawAddHelper; +/** + * @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest + */ class ProjectBuildHelper { - private PartLotWithdrawAddHelper $withdraw_add_helper; - - public function __construct(PartLotWithdrawAddHelper $withdraw_add_helper) + public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper) { - $this->withdraw_add_helper = $withdraw_add_helper; } /** * Returns the maximum buildable amount of the given BOM entry based on the stock of the used parts. * This function only works for BOM entries that are associated with a part. - * @param ProjectBOMEntry $projectBOMEntry - * @return int */ public function getMaximumBuildableCountForBOMEntry(ProjectBOMEntry $projectBOMEntry): int { $part = $projectBOMEntry->getPart(); - if ($part === null) { + if (!$part instanceof Part) { throw new \InvalidArgumentException('This function cannot determine the maximum buildable count for a BOM entry without a part!'); } @@ -59,13 +60,11 @@ class ProjectBuildHelper /** * Returns the maximum buildable amount of the given project, based on the stock of the used parts in the BOM. - * @param Project $project - * @return int */ public function getMaximumBuildableCount(Project $project): int { $maximum_buildable_count = PHP_INT_MAX; - foreach ($project->getBOMEntries() as $bom_entry) { + foreach ($project->getBomEntries() as $bom_entry) { //Skip BOM entries without a part (as we can not determine that) if (!$bom_entry->isPartBomEntry()) { continue; @@ -79,11 +78,9 @@ class ProjectBuildHelper } /** - * Checks if the given project can be build with the current stock. + * Checks if the given project can be built with the current stock. * This means that the maximum buildable count is greater or equal than the requested $number_of_projects - * @param Project $project - * @parm int $number_of_builds - * @return bool + * @param int $number_of_builds */ public function isProjectBuildable(Project $project, int $number_of_builds = 1): bool { @@ -91,11 +88,8 @@ class ProjectBuildHelper } /** - * Check if the given BOM entry can be build with the current stock. + * Check if the given BOM entry can be built with the current stock. * This means that the maximum buildable count is greater or equal than the requested $number_of_projects - * @param ProjectBOMEntry $bom_entry - * @param int $number_of_builds - * @return bool */ public function isBOMEntryBuildable(ProjectBOMEntry $bom_entry, int $number_of_builds = 1): bool { @@ -120,7 +114,7 @@ class ProjectBuildHelper $part = $bomEntry->getPart(); //Skip BOM entries without a part (as we can not determine that) - if ($part === null) { + if (!$part instanceof Part) { continue; } @@ -137,9 +131,7 @@ class ProjectBuildHelper /** * Withdraw the parts from the stock using the given ProjectBuildRequest and create the build parts entries, if needed. * The ProjectBuildRequest has to be validated before!! - * You have to flush changes to DB afterwards - * @param ProjectBuildRequest $buildRequest - * @return void + * You have to flush changes to DB afterward */ public function doBuild(ProjectBuildRequest $buildRequest): void { @@ -159,4 +151,4 @@ class ProjectBuildHelper $this->withdraw_add_helper->add($buildRequest->getBuildsPartLot(), $buildRequest->getNumberOfBuilds(), $message); } } -} \ No newline at end of file +} diff --git a/src/Services/ProjectSystem/ProjectBuildPartHelper.php b/src/Services/ProjectSystem/ProjectBuildPartHelper.php index 136e2ff7..218f456e 100644 --- a/src/Services/ProjectSystem/ProjectBuildPartHelper.php +++ b/src/Services/ProjectSystem/ProjectBuildPartHelper.php @@ -1,17 +1,20 @@ . + */ + +declare(strict_types=1); + + +namespace App\Services\System; + +/** + * Helper service to retrieve the banner of this Part-DB installation + */ +class BannerHelper +{ + public function __construct(private readonly string $project_dir, private readonly string $partdb_banner) + { + + } + + /** + * Retrieves the banner from either the env variable or the banner.md file. + * @return string + */ + public function getBanner(): string + { + $banner = $this->partdb_banner; + if ($banner === '') { + $banner_path = $this->project_dir + .DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'banner.md'; + + $tmp = file_get_contents($banner_path); + if (false === $tmp) { + throw new \RuntimeException('The banner file could not be read.'); + } + $banner = $tmp; + } + + return $banner; + } +} \ No newline at end of file diff --git a/src/Services/System/UpdateAvailableManager.php b/src/Services/System/UpdateAvailableManager.php new file mode 100644 index 00000000..31cb3266 --- /dev/null +++ b/src/Services/System/UpdateAvailableManager.php @@ -0,0 +1,143 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\System; + +use Psr\Log\LoggerInterface; +use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Version\Version; + +/** + * This class checks if a new version of Part-DB is available. + */ +class UpdateAvailableManager +{ + + private const API_URL = 'https://api.github.com/repos/Part-DB/Part-DB-server/releases/latest'; + private const CACHE_KEY = 'uam_latest_version'; + private const CACHE_TTL = 60 * 60 * 24 * 2; // 2 day + + public function __construct(private readonly HttpClientInterface $httpClient, + private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager, + private readonly bool $check_for_updates, private readonly LoggerInterface $logger, + #[Autowire(param: 'kernel.debug')] private readonly bool $is_dev_mode) + { + + } + + /** + * Gets the latest version of Part-DB as string (e.g. "1.2.3"). + * This value is cached for 2 days. + * @return string + */ + public function getLatestVersionString(): string + { + return $this->getLatestVersionInfo()['version']; + } + + /** + * Gets the latest version of Part-DB as Version object. + */ + public function getLatestVersion(): Version + { + return Version::fromString($this->getLatestVersionString()); + } + + /** + * Gets the URL to the latest version of Part-DB on GitHub. + * @return string + */ + public function getLatestVersionUrl(): string + { + return $this->getLatestVersionInfo()['url']; + } + + /** + * Checks if a new version of Part-DB is available. This value is cached for 2 days. + * @return bool + */ + public function isUpdateAvailable(): bool + { + //If we don't want to check for updates, we can return false + if (!$this->check_for_updates) { + return false; + } + + $latestVersion = $this->getLatestVersion(); + $currentVersion = $this->versionManager->getVersion(); + + return $latestVersion->isGreaterThan($currentVersion); + } + + /** + * Get the latest version info. The value is cached for 2 days. + * @return array + * @phpstan-return array{version: string, url: string} + */ + private function getLatestVersionInfo(): array + { + //If we don't want to check for updates, we can return dummy data + if (!$this->check_for_updates) { + return [ + 'version' => '0.0.1', + 'url' => 'update-checking-disabled' + ]; + } + + return $this->updateCache->get(self::CACHE_KEY, function (ItemInterface $item) { + $item->expiresAfter(self::CACHE_TTL); + try { + $response = $this->httpClient->request('GET', self::API_URL); + $result = $response->toArray(); + $tag_name = $result['tag_name']; + + // Remove the leading 'v' from the tag name + $version = substr($tag_name, 1); + + return [ + 'version' => $version, + 'url' => $result['html_url'], + ]; + } catch (\Exception $e) { + //When we are in dev mode, throw the exception, otherwise just silently log it + if ($this->is_dev_mode) { + throw $e; + } + + //In the case of an error, try it again after half of the cache time + $item->expiresAfter(self::CACHE_TTL / 2); + + $this->logger->error('Checking for updates failed: ' . $e->getMessage()); + + return [ + 'version' => '0.0.1', + 'url' => 'update-checking-error' + ]; + } + }); + } +} \ No newline at end of file diff --git a/src/Services/Tools/ExchangeRateUpdater.php b/src/Services/Tools/ExchangeRateUpdater.php index 241e2539..eac6de16 100644 --- a/src/Services/Tools/ExchangeRateUpdater.php +++ b/src/Services/Tools/ExchangeRateUpdater.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\Tools; use App\Entity\PriceInformations\Currency; @@ -27,13 +29,8 @@ use Swap\Swap; class ExchangeRateUpdater { - private string $base_currency; - private Swap $swap; - - public function __construct(string $base_currency, Swap $swap) + public function __construct(private readonly string $base_currency, private readonly Swap $swap) { - $this->base_currency = $base_currency; - $this->swap = $swap; } /** diff --git a/src/Services/Tools/StatisticsHelper.php b/src/Services/Tools/StatisticsHelper.php index 60ed568d..00bb05c9 100644 --- a/src/Services/Tools/StatisticsHelper.php +++ b/src/Services/Tools/StatisticsHelper.php @@ -49,7 +49,7 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Repository\AttachmentRepository; @@ -62,13 +62,11 @@ use InvalidArgumentException; class StatisticsHelper { - protected EntityManagerInterface $em; protected PartRepository $part_repo; protected AttachmentRepository $attachment_repo; - public function __construct(EntityManagerInterface $em) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $em; $this->part_repo = $this->em->getRepository(Part::class); $this->attachment_repo = $this->em->getRepository(Attachment::class); } @@ -93,7 +91,7 @@ class StatisticsHelper } /** - * Returns the number of all parts which have price informations. + * Returns the number of all parts which have price information. * * @throws NoResultException * @throws NonUniqueResultException @@ -115,7 +113,7 @@ class StatisticsHelper 'footprint' => Footprint::class, 'manufacturer' => Manufacturer::class, 'measurement_unit' => MeasurementUnit::class, - 'storelocation' => Storelocation::class, + 'storelocation' => StorageLocation::class, 'supplier' => Supplier::class, 'currency' => Currency::class, ]; @@ -124,7 +122,6 @@ class StatisticsHelper throw new InvalidArgumentException('No count for the given type available!'); } - /** @var EntityRepository $repo */ $repo = $this->em->getRepository($arr[$type]); return $repo->count([]); @@ -147,7 +144,7 @@ class StatisticsHelper } /** - * Gets the count of all external (only containing an URL) attachments. + * Gets the count of all external (only containing a URL) attachments. * * @throws NoResultException * @throws NonUniqueResultException @@ -158,7 +155,7 @@ class StatisticsHelper } /** - * Gets the count of all attachments where the user uploaded an file. + * Gets the count of all attachments where the user uploaded a file. * * @throws NoResultException * @throws NonUniqueResultException diff --git a/src/Services/Tools/TagFinder.php b/src/Services/Tools/TagFinder.php index aa0d02bd..80c89e0f 100644 --- a/src/Services/Tools/TagFinder.php +++ b/src/Services/Tools/TagFinder.php @@ -34,11 +34,8 @@ use function array_slice; */ class TagFinder { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $entityManager) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $entityManager; } /** @@ -59,7 +56,7 @@ class TagFinder $options = $resolver->resolve($options); - //If the keyword is too short we will get to much results, which takes too much time... + //If the keyword is too short we will get too much results, which takes too much time... if (mb_strlen($keyword) < $options['min_keyword_length']) { return []; } @@ -69,7 +66,7 @@ class TagFinder $qb->select('p.tags') ->from(Part::class, 'p') - ->where('p.tags LIKE ?1') + ->where('ILIKE(p.tags, ?1) = TRUE') ->setMaxResults($options['query_limit']) //->orderBy('RAND()') ->setParameter(1, '%'.$keyword.'%'); @@ -78,7 +75,7 @@ class TagFinder //Iterate over each possible tags (which are comma separated) and extract tags which match our keyword foreach ($possible_tags as $tags) { - $tags = explode(',', $tags['tags']); + $tags = explode(',', (string) $tags['tags']); $results = array_merge($results, preg_grep($keyword_regex, $tags)); } diff --git a/src/Services/TranslationExtractor/PermissionExtractor.php b/src/Services/TranslationExtractor/PermissionExtractor.php index 4994e054..e17cba7a 100644 --- a/src/Services/TranslationExtractor/PermissionExtractor.php +++ b/src/Services/TranslationExtractor/PermissionExtractor.php @@ -32,7 +32,7 @@ use Symfony\Component\Translation\MessageCatalogue; */ final class PermissionExtractor implements ExtractorInterface { - private array $permission_structure; + private readonly array $permission_structure; private bool $finished = false; public function __construct(PermissionManager $resolver) @@ -81,7 +81,7 @@ final class PermissionExtractor implements ExtractorInterface } /** - * Sets the prefix that should be used for new found messages. + * Sets the prefix that should be used for new-found messages. * * @param string $prefix The prefix */ diff --git a/src/Services/Trees/NodesListBuilder.php b/src/Services/Trees/NodesListBuilder.php index a7b70793..e65fa37e 100644 --- a/src/Services/Trees/NodesListBuilder.php +++ b/src/Services/Trees/NodesListBuilder.php @@ -22,65 +22,105 @@ declare(strict_types=1); namespace App\Services\Trees; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Repository\AttachmentContainingDBElementRepository; +use App\Repository\DBElementRepository; +use App\Repository\NamedDBElementRepository; use App\Repository\StructuralDBElementRepository; -use App\Services\UserSystem\UserCacheKeyGenerator; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; /** * This service gives you a flat list containing all structured entities in the order of the structure. + * @see \App\Tests\Services\Trees\NodesListBuilderTest */ class NodesListBuilder { - protected EntityManagerInterface $em; - protected TagAwareCacheInterface $cache; - protected UserCacheKeyGenerator $keyGenerator; - - public function __construct(EntityManagerInterface $em, TagAwareCacheInterface $treeCache, UserCacheKeyGenerator $keyGenerator) - { - $this->em = $em; - $this->keyGenerator = $keyGenerator; - $this->cache = $treeCache; + public function __construct( + protected EntityManagerInterface $em, + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected ElementCacheTagGenerator $tagGenerator, + ) { } /** * Gets a flattened hierarchical tree. Useful for generating option lists. * In difference to the Repository Function, the results here are cached. * - * @param string $class_name the class name of the entity you want to retrieve - * @param AbstractStructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root + * @template T of AbstractNamedDBElement * - * @return AbstractStructuralDBElement[] a flattened list containing the tree elements + * @param string $class_name the class name of the entity you want to retrieve + * @phpstan-param class-string $class_name + * @param AbstractStructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root + * + * @return AbstractDBElement[] a flattened list containing the tree elements + * @phpstan-return list */ public function typeToNodesList(string $class_name, ?AbstractStructuralDBElement $parent = null): array { - $parent_id = null !== $parent ? $parent->getID() : '0'; + /** + * We can not cache the entities directly, because loading them from cache will break the doctrine proxies. + */ + //Retrieve the IDs of the elements + $ids = $this->getFlattenedIDs($class_name, $parent); + + //Retrieve the elements from the IDs, the order is the same as in the $ids array + /** @var NamedDBElementRepository $repo */ + $repo = $this->em->getRepository($class_name); + + if ($repo instanceof AttachmentContainingDBElementRepository) { + return $repo->getElementsAndPreviewAttachmentByIDs($ids); + } + + return $repo->findByIDInMatchingOrder($ids); + } + + /** + * This functions returns the (cached) list of the IDs of the elements for the flattened tree. + * @template T of AbstractNamedDBElement + * @param string $class_name + * @phpstan-param class-string $class_name + * @param AbstractStructuralDBElement|null $parent + * @return int[] + */ + private function getFlattenedIDs(string $class_name, ?AbstractStructuralDBElement $parent = null): array + { + $parent_id = $parent instanceof AbstractStructuralDBElement ? $parent->getID() : '0'; // Backslashes are not allowed in cache keys - $secure_class_name = str_replace('\\', '_', $class_name); + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class_name); $key = 'list_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.$parent_id; return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) { - // Invalidate when groups, a element with the class or the user changes + // Invalidate when groups, an element with the class or the user changes $item->tag(['groups', 'tree_list', $this->keyGenerator->generateKey(), $secure_class_name]); - /** @var StructuralDBElementRepository $repo */ + + /** @var NamedDBElementRepository $repo */ $repo = $this->em->getRepository($class_name); - return $repo->toNodesList($parent); + return array_map(static fn(AbstractDBElement $element) => $element->getID(), + //@phpstan-ignore-next-line For some reason phpstan does not understand that $repo is a StructuralDBElementRepository + $repo->getFlatList($parent)); }); } /** - * Returns a flattened list of all (recursive) children elements of the given AbstractStructuralDBElement. - * The value is cached for performance reasons. + * Returns a flattened list of all (recursive) children elements of the given AbstractStructuralDBElement. + * The value is cached for performance reasons. * * @template T of AbstractStructuralDBElement - * @param T $element - * @return T[] + * @param T $element + * @return AbstractStructuralDBElement[] + * + * @phpstan-return list */ public function getChildrenFlatList(AbstractStructuralDBElement $element): array { - return $this->typeToNodesList(get_class($element), $element); + return $this->typeToNodesList($element::class, $element); } } diff --git a/src/Services/Trees/SidebarTreeUpdater.php b/src/Services/Trees/SidebarTreeUpdater.php index 13c3fb6c..c0f93b1f 100644 --- a/src/Services/Trees/SidebarTreeUpdater.php +++ b/src/Services/Trees/SidebarTreeUpdater.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\Trees; -use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; final class SidebarTreeUpdater { private const CACHE_KEY = 'sidebar_tree_updated'; - private const TTL = 60 * 60 * 24; // 24 hours + private const TTL = 60 * 60 * 24; - private CacheInterface $cache; - - public function __construct(TagAwareCacheInterface $treeCache) + public function __construct( + // 24 hours + private readonly TagAwareCacheInterface $cache + ) { - $this->cache = $treeCache; } /** * Returns the time when the sidebar tree was updated the last time. * The frontend uses this information to reload the sidebar tree. - * @return \DateTimeInterface */ public function getLastTreeUpdate(): \DateTimeInterface { @@ -49,7 +49,7 @@ final class SidebarTreeUpdater //This tag and therfore this whole cache gets cleared by TreeCacheInvalidationListener when a structural element is changed $item->tag('sidebar_tree_update'); - return new \DateTime(); + return new \DateTimeImmutable(); }); } -} \ No newline at end of file +} diff --git a/src/Services/Trees/StructuralElementRecursionHelper.php b/src/Services/Trees/StructuralElementRecursionHelper.php index 4038798f..bc46d7f7 100644 --- a/src/Services/Trees/StructuralElementRecursionHelper.php +++ b/src/Services/Trees/StructuralElementRecursionHelper.php @@ -27,15 +27,12 @@ use Doctrine\ORM\EntityManagerInterface; class StructuralElementRecursionHelper { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $em) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $em; } /** - * Executes an function (callable) recursivly for $element and every of its children. + * Executes a function (callable) recursivly for $element and every of its children. * * @param AbstractStructuralDBElement $element The element on which the func should be executed * @param callable $func The function which should be executed for each element. diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 841c2bd4..18571306 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -23,55 +23,38 @@ declare(strict_types=1); namespace App\Services\Trees; use App\Entity\Attachments\AttachmentType; -use App\Entity\Attachments\PartAttachment; -use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; +use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Helpers\Trees\TreeViewNode; -use App\Services\UserSystem\UserCacheKeyGenerator; +use App\Services\Cache\UserCacheKeyGenerator; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * This Service generates the tree structure for the tools. - * Whenever you change something here, you has to clear the cache, because the results are cached for performance reasons. + * Whenever you change something here, you have to clear the cache, because the results are cached for performance reasons. */ class ToolsTreeBuilder { - protected TranslatorInterface $translator; - protected UrlGeneratorInterface $urlGenerator; - protected UserCacheKeyGenerator $keyGenerator; - protected TagAwareCacheInterface $cache; - protected Security $security; - - public function __construct(TranslatorInterface $translator, UrlGeneratorInterface $urlGenerator, - TagAwareCacheInterface $treeCache, UserCacheKeyGenerator $keyGenerator, - Security $security) + public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security) { - $this->translator = $translator; - $this->urlGenerator = $urlGenerator; - - $this->cache = $treeCache; - - $this->keyGenerator = $keyGenerator; - - $this->security = $security; } /** - * Generates the tree for the tools menu. + * Generates the tree for the tools' menu. * The result is cached. * * @return TreeViewNode[] the array containing all Nodes for the tools menu @@ -85,20 +68,20 @@ class ToolsTreeBuilder $item->tag(['tree_tools', 'groups', $this->keyGenerator->generateKey()]); $tree = []; - if (!empty($this->getToolsNode())) { + if ($this->getToolsNode() !== []) { $tree[] = (new TreeViewNode($this->translator->trans('tree.tools.tools'), null, $this->getToolsNode())) ->setIcon('fa-fw fa-treeview fa-solid fa-toolbox'); } - if (!empty($this->getEditNodes())) { + if ($this->getEditNodes() !== []) { $tree[] = (new TreeViewNode($this->translator->trans('tree.tools.edit'), null, $this->getEditNodes())) ->setIcon('fa-fw fa-treeview fa-solid fa-pen-to-square'); } - if (!empty($this->getShowNodes())) { + if ($this->getShowNodes() !== []) { $tree[] = (new TreeViewNode($this->translator->trans('tree.tools.show'), null, $this->getShowNodes())) ->setIcon('fa-fw fa-treeview fa-solid fa-eye'); } - if (!empty($this->getSystemNodes())) { + if ($this->getSystemNodes() !== []) { $tree[] = (new TreeViewNode($this->translator->trans('tree.tools.system'), null, $this->getSystemNodes())) ->setIcon('fa-fw fa-treeview fa-solid fa-server'); } @@ -150,6 +133,13 @@ class ToolsTreeBuilder ))->setIcon('fa-treeview fa-fw fa-solid fa-file-import'); } + if ($this->security->isGranted('@info_providers.create_parts')) { + $nodes[] = (new TreeViewNode( + $this->translator->trans('info_providers.search.title'), + $this->urlGenerator->generate('info_providers_search') + ))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down'); + } + return $nodes; } @@ -192,7 +182,7 @@ class ToolsTreeBuilder $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } - if ($this->security->isGranted('read', new Storelocation())) { + if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.storelocation'), $this->urlGenerator->generate('store_location_new') diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index bc66ba47..23d6a406 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -25,65 +25,100 @@ namespace App\Services\Trees; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; +use App\Entity\ProjectSystem\Project; use App\Helpers\Trees\TreeViewNode; use App\Helpers\Trees\TreeViewNodeIterator; -use App\Helpers\Trees\TreeViewNodeState; +use App\Repository\NamedDBElementRepository; use App\Repository\StructuralDBElementRepository; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; use App\Services\EntityURLGenerator; -use App\Services\Formatters\MarkdownParser; -use App\Services\UserSystem\UserCacheKeyGenerator; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use RecursiveIteratorIterator; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Translation\TranslatorInterface; use function count; +/** + * @see \App\Tests\Services\Trees\TreeViewGeneratorTest + */ class TreeViewGenerator { - protected $urlGenerator; - protected $em; - protected $cache; - protected $keyGenerator; - protected $translator; + public function __construct( + protected EntityURLGenerator $urlGenerator, + protected EntityManagerInterface $em, + protected TagAwareCacheInterface $cache, + protected ElementCacheTagGenerator $tagGenerator, + protected UserCacheKeyGenerator $keyGenerator, + protected TranslatorInterface $translator, + private readonly UrlGeneratorInterface $router, + protected bool $rootNodeExpandedByDefault, + protected bool $rootNodeEnabled, - protected $rootNodeExpandedByDefault; - protected $rootNodeEnabled; + ) { + } - public function __construct(EntityURLGenerator $URLGenerator, EntityManagerInterface $em, - TagAwareCacheInterface $treeCache, UserCacheKeyGenerator $keyGenerator, TranslatorInterface $translator, bool $rootNodeExpandedByDefault, bool $rootNodeEnabled) + /** + * Gets a TreeView list for the entities of the given class. + * The result is cached, if the full tree should be shown and no element should be selected. + * + * @param string $class The class for which the treeView should be generated + * @param AbstractStructuralDBElement|null $parent The root nodes in the tree should have this element as parent (use null, if you want to get all entities) + * @param string $mode The link type that will be generated for the hyperlink section of each node (see EntityURLGenerator for possible values). + * Set to empty string, to disable href field. + * @param AbstractDBElement|null $selectedElement The element that should be selected. If set to null, no element will be selected. + * + * @return TreeViewNode[] an array of TreeViewNode[] elements of the root elements + */ + public function getTreeView( + string $class, + ?AbstractStructuralDBElement $parent = null, + string $mode = 'list_parts', + ?AbstractDBElement $selectedElement = null + ): array { - $this->urlGenerator = $URLGenerator; - $this->em = $em; - $this->cache = $treeCache; - $this->keyGenerator = $keyGenerator; - $this->translator = $translator; + //If we just want a part of a tree, don't cache it or select a specific element, don't cache it + if ($parent instanceof AbstractStructuralDBElement || $selectedElement instanceof AbstractDBElement) { + return $this->getTreeViewUncached($class, $parent, $mode, $selectedElement); + } - $this->rootNodeExpandedByDefault = $rootNodeExpandedByDefault; - $this->rootNodeEnabled = $rootNodeEnabled; + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class); + $key = 'sidebar_treeview_'.$this->keyGenerator->generateKey().'_'.$secure_class_name; + $key .= $mode; + + return $this->cache->get($key, function (ItemInterface $item) use ($class, $parent, $mode, $selectedElement, $secure_class_name) { + // Invalidate when groups, an element with the class or the user changes + $item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]); + return $this->getTreeViewUncached($class, $parent, $mode, $selectedElement); + }); } /** * Gets a TreeView list for the entities of the given class. * - * @param string $class The class for which the treeView should be generated - * @param AbstractStructuralDBElement|null $parent The root nodes in the tree should have this element as parent (use null, if you want to get all entities) - * @param string $mode The link type that will be generated for the hyperlink section of each node (see EntityURLGenerator for possible values). + * @param string $class The class for which the treeView should be generated + * @param AbstractStructuralDBElement|null $parent The root nodes in the tree should have this element as parent (use null, if you want to get all entities) + * @param string $mode The link type that will be generated for the hyperlink section of each node (see EntityURLGenerator for possible values). * Set to empty string, to disable href field. - * @param AbstractDBElement|null $selectedElement The element that should be selected. If set to null, no element will be selected. + * @param AbstractDBElement|null $selectedElement The element that should be selected. If set to null, no element will be selected. * * @return TreeViewNode[] an array of TreeViewNode[] elements of the root elements */ - public function getTreeView(string $class, ?AbstractStructuralDBElement $parent = null, string $mode = 'list_parts', ?AbstractDBElement $selectedElement = null): array - { + private function getTreeViewUncached( + string $class, + ?AbstractStructuralDBElement $parent = null, + string $mode = 'list_parts', + ?AbstractDBElement $selectedElement = null + ): array { $head = []; $href_type = $mode; @@ -91,10 +126,11 @@ class TreeViewGenerator //When we use the newEdit type, add the New Element node. if ('newEdit' === $mode) { //Generate the url for the new node - $href = $this->urlGenerator->createURL(new $class()); + //DO NOT try to create an object from the class, as this might be an proxy, which can not be easily initialized, so just pass the class_name directly + $href = $this->urlGenerator->createURL($class); $new_node = new TreeViewNode($this->translator->trans('entity.tree.new'), $href); //When the id of the selected element is null, then we have a new element, and we need to select "new" node - if (null === $selectedElement || null === $selectedElement->getID()) { + if (!$selectedElement instanceof AbstractDBElement || null === $selectedElement->getID()) { $new_node->setSelected(true); } $head[] = $new_node; @@ -118,27 +154,30 @@ class TreeViewGenerator $recursiveIterator = new RecursiveIteratorIterator($treeIterator, RecursiveIteratorIterator::SELF_FIRST); foreach ($recursiveIterator as $item) { /** @var TreeViewNode $item */ - if (null !== $selectedElement && $item->getId() === $selectedElement->getID()) { + if ($selectedElement instanceof AbstractDBElement && $item->getId() === $selectedElement->getID()) { $item->setSelected(true); } - if (!empty($item->getNodes())) { - $item->addTag((string) count($item->getNodes())); + if ($item->getNodes() !== null && $item->getNodes() !== []) { + $item->addTag((string)count($item->getNodes())); } - if (!empty($href_type) && null !== $item->getId()) { - $entity = $this->em->getPartialReference($class, $item->getId()); + if ($href_type !== '' && null !== $item->getId()) { + $entity = $this->em->find($class, $item->getId()); $item->setHref($this->urlGenerator->getURL($entity, $href_type)); } //Translate text if text starts with $$ - if (0 === strpos($item->getText(), '$$')) { + if (str_starts_with($item->getText(), '$$')) { $item->setText($this->translator->trans(substr($item->getText(), 2))); } } if (($mode === 'list_parts_root' || $mode === 'devices') && $this->rootNodeEnabled) { - $root_node = new TreeViewNode($this->entityClassToRootNodeString($class), null, $generic); + //We show the root node as a link to the list of all parts + $show_all_parts_url = $this->router->generate('parts_show_all'); + + $root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic); $root_node->setExpanded($this->rootNodeExpandedByDefault); $root_node->setIcon($this->entityClassToRootNodeIcon($class)); @@ -150,43 +189,29 @@ class TreeViewGenerator protected function entityClassToRootNodeString(string $class): string { - switch ($class) { - case Category::class: - return $this->translator->trans('category.labelp'); - case Storelocation::class: - return $this->translator->trans('storelocation.labelp'); - case Footprint::class: - return $this->translator->trans('footprint.labelp'); - case Manufacturer::class: - return $this->translator->trans('manufacturer.labelp'); - case Supplier::class: - return $this->translator->trans('supplier.labelp'); - case Project::class: - return $this->translator->trans('project.labelp'); - default: - return $this->translator->trans('tree.root_node.text'); - } + return match ($class) { + Category::class => $this->translator->trans('category.labelp'), + StorageLocation::class => $this->translator->trans('storelocation.labelp'), + Footprint::class => $this->translator->trans('footprint.labelp'), + Manufacturer::class => $this->translator->trans('manufacturer.labelp'), + Supplier::class => $this->translator->trans('supplier.labelp'), + Project::class => $this->translator->trans('project.labelp'), + default => $this->translator->trans('tree.root_node.text'), + }; } protected function entityClassToRootNodeIcon(string $class): ?string { $icon = "fa-fw fa-treeview fa-solid "; - switch ($class) { - case Category::class: - return $icon . 'fa-tags'; - case Storelocation::class: - return $icon . 'fa-cube'; - case Footprint::class: - return $icon . 'fa-microchip'; - case Manufacturer::class: - return $icon . 'fa-industry'; - case Supplier::class: - return $icon . 'fa-truck'; - case Project::class: - return $icon . 'fa-archive'; - default: - return null; - } + return match ($class) { + Category::class => $icon.'fa-tags', + StorageLocation::class => $icon.'fa-cube', + Footprint::class => $icon.'fa-microchip', + Manufacturer::class => $icon.'fa-industry', + Supplier::class => $icon.'fa-truck', + Project::class => $icon.'fa-archive', + default => null, + }; } /** @@ -194,8 +219,9 @@ class TreeViewGenerator * Gets a tree of TreeViewNode elements. The root elements has $parent as parent. * The treeview is generic, that means the href are null and ID values are set. * - * @param string $class The class for which the tree should be generated - * @param AbstractStructuralDBElement|null $parent the parent the root elements should have + * @param string $class The class for which the tree should be generated + * @phpstan-param class-string $class + * @param AbstractStructuralDBElement|null $parent the parent the root elements should have * * @return TreeViewNode[] */ @@ -204,26 +230,25 @@ class TreeViewGenerator if (!is_a($class, AbstractNamedDBElement::class, true)) { throw new InvalidArgumentException('$class must be a class string that implements StructuralDBElement or NamedDBElement!'); } - if (null !== $parent && !is_a($parent, $class)) { + if ($parent instanceof AbstractStructuralDBElement && !$parent instanceof $class) { throw new InvalidArgumentException('$parent must be of the type $class!'); } - /** @var StructuralDBElementRepository $repo */ + /** @var NamedDBElementRepository $repo */ $repo = $this->em->getRepository($class); - //If we just want a part of a tree, dont cache it - if (null !== $parent) { - return $repo->getGenericNodeTree($parent); + //If we just want a part of a tree, don't cache it + if ($parent instanceof AbstractStructuralDBElement) { + return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line PHPstan does not seem to recognize, that we have a StructuralDBElementRepository here, which have 1 argument } - $secure_class_name = str_replace('\\', '_', $class); + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class); $key = 'treeview_'.$this->keyGenerator->generateKey().'_'.$secure_class_name; return $this->cache->get($key, function (ItemInterface $item) use ($repo, $parent, $secure_class_name) { - // Invalidate when groups, a element with the class or the user changes + // Invalidate when groups, an element with the class or the user changes $item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]); - - return $repo->getGenericNodeTree($parent); + return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line }); } } diff --git a/src/Services/UserSystem/PasswordResetManager.php b/src/Services/UserSystem/PasswordResetManager.php index 7b8a5be3..1a78cb60 100644 --- a/src/Services/UserSystem/PasswordResetManager.php +++ b/src/Services/UserSystem/PasswordResetManager.php @@ -35,21 +35,13 @@ use Symfony\Contracts\Translation\TranslatorInterface; class PasswordResetManager { - protected MailerInterface $mailer; - protected EntityManagerInterface $em; protected PasswordHasherInterface $passwordEncoder; - protected TranslatorInterface $translator; - protected UserPasswordHasherInterface $userPasswordEncoder; - public function __construct(MailerInterface $mailer, EntityManagerInterface $em, - TranslatorInterface $translator, UserPasswordHasherInterface $userPasswordEncoder, + public function __construct(protected MailerInterface $mailer, protected EntityManagerInterface $em, + protected TranslatorInterface $translator, protected UserPasswordHasherInterface $userPasswordEncoder, PasswordHasherFactoryInterface $encoderFactory) { - $this->em = $em; - $this->mailer = $mailer; $this->passwordEncoder = $encoderFactory->getPasswordHasher(User::class); - $this->translator = $translator; - $this->userPasswordEncoder = $userPasswordEncoder; } public function request(string $name_or_email): void @@ -59,7 +51,7 @@ class PasswordResetManager //Try to find a user by the given string $user = $repo->findByEmailOrName($name_or_email); //Do nothing if no user was found - if (null === $user) { + if (!$user instanceof User) { return; } @@ -67,11 +59,10 @@ class PasswordResetManager $user->setPwResetToken($this->passwordEncoder->hash($unencrypted_token)); //Determine the expiration datetime of - $expiration_date = new DateTime(); - $expiration_date->add(date_interval_create_from_date_string('1 day')); + $expiration_date = new \DateTimeImmutable("+1 day"); $user->setPwResetExpires($expiration_date); - if (!empty($user->getEmail())) { + if ($user->getEmail() !== null && $user->getEmail() !== '') { $address = new Address($user->getEmail(), $user->getFullName()); $mail = new TemplatedEmail(); $mail->to($address); @@ -105,16 +96,15 @@ class PasswordResetManager { //Try to find the user $repo = $this->em->getRepository(User::class); - /** @var User|null $user */ - $user = $repo->findOneBy(['name' => $username]); + $user = $repo->findByUsername($username); //If no user matching the name, show an error message - if (null === $user) { + if (!$user instanceof User) { return false; } //Check if token is expired yet - if ($user->getPwResetExpires() < new DateTime()) { + if ($user->getPwResetExpires() < new \DateTimeImmutable()) { return false; } @@ -128,7 +118,7 @@ class PasswordResetManager //Remove token $user->setPwResetToken(null); - $user->setPwResetExpires(new DateTime()); + $user->setPwResetExpires(new \DateTimeImmutable()); //Save to DB $this->em->flush(); diff --git a/src/Services/UserSystem/PermissionManager.php b/src/Services/UserSystem/PermissionManager.php index 618e473a..663a7b67 100644 --- a/src/Services/UserSystem/PermissionManager.php +++ b/src/Services/UserSystem/PermissionManager.php @@ -36,23 +36,21 @@ use Symfony\Component\Yaml\Yaml; * This class manages the permissions of users and groups. * Permissions are defined in the config/permissions.yaml file, and are parsed and resolved by this class using the * user and hierachical group PermissionData information. + * @see \App\Tests\Services\UserSystem\PermissionManagerTest */ class PermissionManager { - protected $permission_structure; - - protected bool $is_debug; + protected array $permission_structure; protected string $cache_file; /** * PermissionResolver constructor. */ - public function __construct(bool $kernel_debug, string $kernel_cache_dir) + public function __construct(protected readonly bool $kernel_debug_enabled, string $kernel_cache_dir) { $cache_dir = $kernel_cache_dir; //Here the cached structure will be saved. $this->cache_file = $cache_dir.'/permissions.php.cache'; - $this->is_debug = $kernel_debug; $this->permission_structure = $this->generatePermissionStructure(); } @@ -113,8 +111,8 @@ class PermissionManager /** @var Group $parent */ $parent = $user->getGroup(); - while (null !== $parent) { //The top group, has parent == null - //Check if our current element gives a info about disallow/allow + while ($parent instanceof Group) { //The top group, has parent == null + //Check if our current element gives an info about disallow/allow $allowed = $this->dontInherit($parent, $permission, $operation); if (null !== $allowed) { return $allowed; @@ -123,7 +121,41 @@ class PermissionManager $parent = $parent->getParent(); } - return null; //The inherited value is never resolved. Should be treat as false, in Voters. + return null; //The inherited value is never resolved. Should be treated as false, in Voters. + } + + /** + * Same as inherit(), but it checks if the access token has the required role. + * @param User $user the user for which the operation should be checked + * @param array $roles The roles associated with the authentication token + * @param string $permission the name of the permission for which should be checked + * @param string $operation the name of the operation for which should be checked + * + * @return bool|null true, if the user is allowed to do the operation (ALLOW), false if not (DISALLOW), and null, + * if the value is set to inherit + */ + public function inheritWithAPILevel(User $user, array $roles, string $permission, string $operation): ?bool + { + //Check that the permission/operation combination is valid + if (! $this->isValidOperation($permission, $operation)) { + throw new InvalidArgumentException('The permission/operation combination "'.$permission.'/'.$operation.'" is not valid!'); + } + + //Get what API level we require for the permission/operation + $level_role = $this->permission_structure['perms'][$permission]['operations'][$operation]['apiTokenRole']; + + //When no role was set (or it is null), then the operation is blocked for API access + if (null === $level_role) { + return false; + } + + //Otherwise check if the token has the required role, if not, then the operation is blocked for API access + if (!in_array($level_role, $roles, true)) { + return false; + } + + //If we have the required role, then we can check the permission + return $this->inherit($user, $permission, $operation); } /** @@ -150,7 +182,7 @@ class PermissionManager /** * Lists the names of all operations that is supported for the given permission. * - * If the Permission is not existing at all, a exception is thrown. + * If the Permission is not existing at all, an exception is thrown. * * This function is useful for the support() function of the voters. * @@ -196,14 +228,15 @@ class PermissionManager /** * This functions sets all operations mentioned in the alsoSet value of a permission, so that the structure is always valid. - * @param HasPermissionsInterface $user - * @return void + * This function should be called after every setPermission() call. + * @return bool true if values were changed/corrected, false if not */ - public function ensureCorrectSetOperations(HasPermissionsInterface $user): void + public function ensureCorrectSetOperations(HasPermissionsInterface $user): bool { //If we have changed anything on the permission structure due to the alsoSet value, this becomes true, so we //redo the whole process, to ensure that all alsoSet values are set recursively. - $anything_changed = false; + + $return_value = false; do { $anything_changed = false; //Reset the variable for the next iteration @@ -216,31 +249,26 @@ class PermissionManager //Set every op listed in also Set foreach ($op['alsoSet'] as $set_also) { //If the alsoSet value contains a dot then we set the operation of another permission - if (false !== strpos($set_also, '.')) { - [$set_perm, $set_op] = explode('.', $set_also); - } else { - //Else we set the operation of the same permission - [$set_perm, $set_op] = [$perm_key, $set_also]; - } + [$set_perm, $set_op] = str_contains((string) $set_also, '.') ? explode('.', (string) $set_also) : [$perm_key, $set_also]; //Check if we change the value of the permission if ($this->dontInherit($user, $set_perm, $set_op) !== true) { $this->setPermission($user, $set_perm, $set_op, true); //Mark the change, so we redo the whole process $anything_changed = true; + $return_value = true; } } } } } } while($anything_changed); + + return $return_value; } /** * Sets all possible operations of all possible permissions of the given entity to the given value. - * @param HasPermissionsInterface $perm_holder - * @param bool|null $new_value - * @return void */ public function setAllPermissions(HasPermissionsInterface $perm_holder, ?bool $new_value): void { @@ -254,11 +282,6 @@ class PermissionManager /** * Sets all operations of the given permissions to the given value. * Please note that you have to call ensureCorrectSetOperations() after this function, to ensure that all alsoSet values are set. - * - * @param HasPermissionsInterface $perm_holder - * @param string $permission - * @param bool|null $new_value - * @return void */ public function setAllOperationsOfPermission(HasPermissionsInterface $perm_holder, string $permission, ?bool $new_value): void { @@ -273,11 +296,6 @@ class PermissionManager /** * This function sets all operations of the given permission to the given value, except the ones listed in the except array. - * @param HasPermissionsInterface $perm_holder - * @param string $permission - * @param bool|null $new_value - * @param array $except - * @return void */ public function setAllOperationsOfPermissionExcept(HasPermissionsInterface $perm_holder, string $permission, ?bool $new_value, array $except): void { @@ -293,9 +311,30 @@ class PermissionManager } } + /** + * This function checks if the given user has any permission set to allow, either directly or inherited. + * @param User $user + * @return bool + */ + public function hasAnyPermissionSetToAllowInherited(User $user): bool + { + //Iterate over all permissions + foreach ($this->permission_structure['perms'] as $perm_key => $permission) { + //Iterate over all operations of the permission + foreach ($permission['operations'] as $op_key => $op) { + //Check if the user has the permission set to allow + if ($this->inherit($user, $perm_key, $op_key) === true) { + return true; + } + } + } + + return false; + } + protected function generatePermissionStructure() { - $cache = new ConfigCache($this->cache_file, $this->is_debug); + $cache = new ConfigCache($this->cache_file, $this->kernel_debug_enabled); //Check if the cache is fresh, else regenerate it. if (!$cache->isFresh()) { diff --git a/src/Services/UserSystem/PermissionPresetsHelper.php b/src/Services/UserSystem/PermissionPresetsHelper.php index 3340f9bb..eeb80f61 100644 --- a/src/Services/UserSystem/PermissionPresetsHelper.php +++ b/src/Services/UserSystem/PermissionPresetsHelper.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\UserSystem; use App\Entity\UserSystem\PermissionData; @@ -25,18 +27,15 @@ use App\Security\Interfaces\HasPermissionsInterface; class PermissionPresetsHelper { - public const PRESET_ALL_INHERIT = 'all_inherit'; - public const PRESET_ALL_FORBID = 'all_forbid'; - public const PRESET_ALL_ALLOW = 'all_allow'; - public const PRESET_READ_ONLY = 'read_only'; - public const PRESET_EDITOR = 'editor'; - public const PRESET_ADMIN = 'admin'; + final public const PRESET_ALL_INHERIT = 'all_inherit'; + final public const PRESET_ALL_FORBID = 'all_forbid'; + final public const PRESET_ALL_ALLOW = 'all_allow'; + final public const PRESET_READ_ONLY = 'read_only'; + final public const PRESET_EDITOR = 'editor'; + final public const PRESET_ADMIN = 'admin'; - private PermissionManager $permissionResolver; - - public function __construct(PermissionManager $permissionResolver) + public function __construct(private readonly PermissionManager $permissionResolver) { - $this->permissionResolver = $permissionResolver; } /** @@ -44,11 +43,10 @@ class PermissionPresetsHelper * The permission data will be reset during the process and then the preset will be applied. * * @param string $preset_name The name of the preset to use - * @return HasPermissionsInterface */ public function applyPreset(HasPermissionsInterface $perm_holder, string $preset_name): HasPermissionsInterface { - //We need to reset the permission data first (afterwards all values are inherit) + //We need to reset the permission data first (afterward all values are inherit) $perm_holder->getPermissions()->resetPermissions(); switch($preset_name) { @@ -107,6 +105,11 @@ class PermissionPresetsHelper $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'suppliers', PermissionData::ALLOW); $this->permissionResolver->setAllOperationsOfPermission($perm_holder, 'projects', PermissionData::ALLOW); + //Allow to manage Oauth tokens + $this->permissionResolver->setPermission($perm_holder, 'system', 'manage_oauth_tokens', PermissionData::ALLOW); + //Allow to show updates + $this->permissionResolver->setPermission($perm_holder, 'system', 'show_updates', PermissionData::ALLOW); + } private function editor(HasPermissionsInterface $permHolder): HasPermissionsInterface @@ -141,6 +144,9 @@ class PermissionPresetsHelper //Various other permissions $this->permissionResolver->setPermission($permHolder, 'tools', 'lastActivity', PermissionData::ALLOW); + //Allow to create parts from information providers + $this->permissionResolver->setPermission($permHolder, 'info_providers', 'create_parts', PermissionData::ALLOW); + return $permHolder; } @@ -174,15 +180,15 @@ class PermissionPresetsHelper return $perm_holder; } - private function AllForbid(HasPermissionsInterface $perm_holder): HasPermissionsInterface + private function allForbid(HasPermissionsInterface $perm_holder): HasPermissionsInterface { $this->permissionResolver->setAllPermissions($perm_holder, PermissionData::DISALLOW); return $perm_holder; } - private function AllAllow(HasPermissionsInterface $perm_holder): HasPermissionsInterface + private function allAllow(HasPermissionsInterface $perm_holder): HasPermissionsInterface { $this->permissionResolver->setAllPermissions($perm_holder, PermissionData::ALLOW); return $perm_holder; } -} \ No newline at end of file +} diff --git a/src/Services/UserSystem/PermissionSchemaUpdater.php b/src/Services/UserSystem/PermissionSchemaUpdater.php index e8ebc6d0..104800dc 100644 --- a/src/Services/UserSystem/PermissionSchemaUpdater.php +++ b/src/Services/UserSystem/PermissionSchemaUpdater.php @@ -1,4 +1,7 @@ . */ - namespace App\Services\UserSystem; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\PermissionData; use App\Entity\UserSystem\User; +use App\Helpers\TrinaryLogicHelper; use App\Security\Interfaces\HasPermissionsInterface; +/** + * @see \App\Tests\Services\UserSystem\PermissionSchemaUpdaterTest + */ class PermissionSchemaUpdater { /** * Check if the given user/group needs an update of its permission schema. - * @param HasPermissionsInterface $holder * @return bool True if the permission schema needs an update, false otherwise. */ public function isSchemaUpdateNeeded(HasPermissionsInterface $holder): bool { $perm_data = $holder->getPermissions(); - if ($perm_data->getSchemaVersion() < PermissionData::CURRENT_SCHEMA_VERSION) { - return true; - } - - return false; + return $perm_data->getSchemaVersion() < PermissionData::CURRENT_SCHEMA_VERSION; } /** * Upgrades the permission schema of the given user/group to the chosen version. * Please note that this function does not flush the changes to DB! - * @param HasPermissionsInterface $holder - * @param int $target_version * @return bool True, if an upgrade was done, false if it was not needed. */ public function upgradeSchema(HasPermissionsInterface $holder, int $target_version = PermissionData::CURRENT_SCHEMA_VERSION): bool { + $e = null; if ($target_version > PermissionData::CURRENT_SCHEMA_VERSION) { throw new \InvalidArgumentException('The target version is higher than the maximum possible schema version!'); } @@ -66,11 +66,9 @@ class PermissionSchemaUpdater $reflectionClass = new \ReflectionClass(self::class); try { $method = $reflectionClass->getMethod('upgradeSchemaToVersion'.($n + 1)); - //Set the method accessible, so we can call it (needed for PHP < 8.1) - $method->setAccessible(true); $method->invoke($this, $holder); } catch (\ReflectionException $e) { - throw new \RuntimeException('Could not find update method for schema version '.($n + 1)); + throw new \RuntimeException('Could not find update method for schema version '.($n + 1), $e->getCode(), $e); } //Bump the schema version @@ -84,8 +82,6 @@ class PermissionSchemaUpdater /** * Upgrades the permission schema of the given group and all of its parent groups to the chosen version. * Please note that this function does not flush the changes to DB! - * @param Group $group - * @param int $target_version * @return bool True if an upgrade was done, false if it was not needed. */ public function groupUpgradeSchemaRecursively(Group $group, int $target_version = PermissionData::CURRENT_SCHEMA_VERSION): bool @@ -105,21 +101,19 @@ class PermissionSchemaUpdater /** * Upgrades the permissions schema of the given users and its parent (including parent groups) to the chosen version. * Please note that this function does not flush the changes to DB! - * @param User $user - * @param int $target_version * @return bool True if an upgrade was done, false if it was not needed. */ public function userUpgradeSchemaRecursively(User $user, int $target_version = PermissionData::CURRENT_SCHEMA_VERSION): bool { $updated = $this->upgradeSchema($user, $target_version); - if ($user->getGroup()) { + if ($user->getGroup() instanceof Group) { $updated = $this->groupUpgradeSchemaRecursively($user->getGroup(), $target_version) || $updated; } return $updated; } - private function upgradeSchemaToVersion1(HasPermissionsInterface $holder): void + private function upgradeSchemaToVersion1(HasPermissionsInterface $holder): void //@phpstan-ignore-line This is called via reflection { //Use the part edit permission to set the preset value for the new part stock permission if ( @@ -136,7 +130,7 @@ class PermissionSchemaUpdater } } - private function upgradeSchemaToVersion2(HasPermissionsInterface $holder): void + private function upgradeSchemaToVersion2(HasPermissionsInterface $holder): void //@phpstan-ignore-line This is called via reflection { //If the projects permissions are not defined yet, rename devices permission to projects (just copy its data over) if (!$holder->getPermissions()->isAnyOperationOfPermissionSet('projects')) { @@ -145,4 +139,22 @@ class PermissionSchemaUpdater $holder->getPermissions()->removePermission('devices'); } } -} \ No newline at end of file + + private function upgradeSchemaToVersion3(HasPermissionsInterface $holder): void //@phpstan-ignore-line This is called via reflection + { + $permissions = $holder->getPermissions(); + + //If the system.show_updates permission is not defined yet, set it to true, if the user can view server info, server logs or edit users or groups + if (!$permissions->isPermissionSet('system', 'show_updates')) { + + $new_value = TrinaryLogicHelper::or( + $permissions->getPermissionValue('system', 'server_infos'), + $permissions->getPermissionValue('system', 'show_logs'), + $permissions->getPermissionValue('users', 'edit'), + $permissions->getPermissionValue('groups', 'edit') + ); + + $permissions->setPermissionValue('system', 'show_updates', $new_value); + } + } +} diff --git a/src/Services/UserSystem/TFA/BackupCodeGenerator.php b/src/Services/UserSystem/TFA/BackupCodeGenerator.php index bc47cab8..a13b9804 100644 --- a/src/Services/UserSystem/TFA/BackupCodeGenerator.php +++ b/src/Services/UserSystem/TFA/BackupCodeGenerator.php @@ -26,12 +26,12 @@ use Exception; use RuntimeException; /** - * This class generates random backup codes for two factor authentication. + * This class generates random backup codes for two-factor authentication. + * @see \App\Tests\Services\UserSystem\TFA\BackupCodeGeneratorTest */ class BackupCodeGenerator { protected int $code_length; - protected int $code_count; /** * BackupCodeGenerator constructor. @@ -39,7 +39,7 @@ class BackupCodeGenerator * @param int $code_length how many characters a single code should have * @param int $code_count how many codes are generated for a whole backup set */ - public function __construct(int $code_length, int $code_count) + public function __construct(int $code_length, protected int $code_count) { if ($code_length > 32) { throw new RuntimeException('Backup code can have maximum 32 digits!'); @@ -47,8 +47,6 @@ class BackupCodeGenerator if ($code_length < 6) { throw new RuntimeException('Code must have at least 6 digits to ensure security!'); } - - $this->code_count = $code_count; $this->code_length = $code_length; } diff --git a/src/Services/UserSystem/TFA/BackupCodeManager.php b/src/Services/UserSystem/TFA/BackupCodeManager.php index 9a422aa3..07484618 100644 --- a/src/Services/UserSystem/TFA/BackupCodeManager.php +++ b/src/Services/UserSystem/TFA/BackupCodeManager.php @@ -25,30 +25,28 @@ namespace App\Services\UserSystem\TFA; use App\Entity\UserSystem\User; /** - * This services offers methods to manage backup codes for two factor authentication. + * This services offers methods to manage backup codes for two-factor authentication. + * @see \App\Tests\Services\UserSystem\TFA\BackupCodeManagerTest */ class BackupCodeManager { - protected BackupCodeGenerator $backupCodeGenerator; - - public function __construct(BackupCodeGenerator $backupCodeGenerator) + public function __construct(protected BackupCodeGenerator $backupCodeGenerator) { - $this->backupCodeGenerator = $backupCodeGenerator; } /** * Enable backup codes for the given user, by generating a set of backup codes. - * If the backup codes were already enabled before, they a. + * If the backup codes were already enabled before, nothing happens. */ public function enableBackupCodes(User $user): void { - if (empty($user->getBackupCodes())) { + if ($user->getBackupCodes() === []) { $this->regenerateBackupCodes($user); } } /** - * Disable (remove) the backup codes when no other 2 factor authentication methods are enabled. + * Disable (remove) the backup codes when no other two-factor authentication methods are enabled. */ public function disableBackupCodesIfUnused(User $user): void { diff --git a/src/Services/UserSystem/TFA/DecoratedGoogleAuthenticator.php b/src/Services/UserSystem/TFA/DecoratedGoogleAuthenticator.php new file mode 100644 index 00000000..05e5ed4c --- /dev/null +++ b/src/Services/UserSystem/TFA/DecoratedGoogleAuthenticator.php @@ -0,0 +1,72 @@ +. + */ +namespace App\Services\UserSystem\TFA; + +use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; +use Symfony\Component\HttpFoundation\RequestStack; + +#[AsDecorator(GoogleAuthenticatorInterface::class)] +class DecoratedGoogleAuthenticator implements GoogleAuthenticatorInterface +{ + + public function __construct( + #[AutowireDecorated] + private readonly GoogleAuthenticatorInterface $inner, + private readonly RequestStack $requestStack) + { + + } + + public function checkCode(TwoFactorInterface $user, string $code): bool + { + return $this->inner->checkCode($user, $code); + } + + public function getQRContent(TwoFactorInterface $user): string + { + $qr_content = $this->inner->getQRContent($user); + + //Replace $$DOMAIN$$ with the current domain + $request = $this->requestStack->getCurrentRequest(); + + //If no request is available, just put "Part-DB" as domain + $domain = "Part-DB"; + + if ($request !== null) { + $domain = $request->getHttpHost(); + } + + //Domain must be url encoded + $domain = urlencode($domain); + + return str_replace(urlencode('$$DOMAIN$$'), $domain, $qr_content); + } + + public function generateSecret(): string + { + return $this->inner->generateSecret(); + } +} diff --git a/src/Services/UserSystem/UserAvatarHelper.php b/src/Services/UserSystem/UserAvatarHelper.php index 95b94dca..a694fa77 100644 --- a/src/Services/UserSystem/UserAvatarHelper.php +++ b/src/Services/UserSystem/UserAvatarHelper.php @@ -1,4 +1,7 @@ use_gravatar = $use_gravatar; - $this->packages = $packages; - $this->attachmentURLGenerator = $attachmentURLGenerator; - $this->filterService = $filterService; - $this->entityManager = $entityManager; - $this->submitHandler = $attachmentSubmitHandler; + public function __construct( + private readonly bool $use_gravatar, + private readonly Packages $packages, + private readonly AttachmentURLGenerator $attachmentURLGenerator, + private readonly EntityManagerInterface $entityManager, + private readonly AttachmentSubmitHandler $submitHandler + ) { } /** - * Returns the URL to the profile picture of the given user (in big size) - * @param User $user + * Returns the URL to the profile picture of the given user (in big size) + * * @return string */ public function getAvatarURL(User $user): string { //Check if the user has a master attachment defined (meaning he has explicitly defined a profile picture) - if ($user->getMasterPictureAttachment() !== null) { - return $this->attachmentURLGenerator->getThumbnailURL($user->getMasterPictureAttachment(), 'thumbnail_md'); + if ($user->getMasterPictureAttachment() instanceof Attachment) { + return $this->attachmentURLGenerator->getThumbnailURL($user->getMasterPictureAttachment(), 'thumbnail_md') + ?? $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH); } //If not check if gravatar is enabled (then use gravatar URL) @@ -70,14 +70,15 @@ class UserAvatarHelper } //Fallback to the default avatar picture - return $this->packages->getUrl('/img/default_avatar.png'); + return $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH); } public function getAvatarSmURL(User $user): string { //Check if the user has a master attachment defined (meaning he has explicitly defined a profile picture) - if ($user->getMasterPictureAttachment() !== null) { - return $this->attachmentURLGenerator->getThumbnailURL($user->getMasterPictureAttachment(), 'thumbnail_xs'); + if ($user->getMasterPictureAttachment() instanceof Attachment) { + return $this->attachmentURLGenerator->getThumbnailURL($user->getMasterPictureAttachment(), 'thumbnail_xs') + ?? $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH); } //If not check if gravatar is enabled (then use gravatar URL) @@ -85,20 +86,16 @@ class UserAvatarHelper return $this->getGravatar($user, 50); //50px wide picture } - try { - //Otherwise we can serve the relative path via Asset component - return $this->filterService->getUrlOfFilteredImage('/img/default_avatar.png', 'thumbnail_xs'); - } catch (\Imagine\Exception\RuntimeException $e) { - //If the filter fails, we can not serve the thumbnail and fall back to the original image and log an warning - return $this->packages->getUrl('/img/default_avatar.png'); - } + //Otherwise serve the default image (its an SVG, so we dont need to thumbnail it) + return $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH); } public function getAvatarMdURL(User $user): string { //Check if the user has a master attachment defined (meaning he has explicitly defined a profile picture) - if ($user->getMasterPictureAttachment() !== null) { - return $this->attachmentURLGenerator->getThumbnailURL($user->getMasterPictureAttachment(), 'thumbnail_sm'); + if ($user->getMasterPictureAttachment() instanceof Attachment) { + return $this->attachmentURLGenerator->getThumbnailURL($user->getMasterPictureAttachment(), 'thumbnail_sm') + ?? $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH); } //If not check if gravatar is enabled (then use gravatar URL) @@ -106,20 +103,15 @@ class UserAvatarHelper return $this->getGravatar($user, 150); } - try { - //Otherwise we can serve the relative path via Asset component - return $this->filterService->getUrlOfFilteredImage('/img/default_avatar.png', 'thumbnail_xs'); - } catch (\Imagine\Exception\RuntimeException $e) { - //If the filter fails, we can not serve the thumbnail and fall back to the original image and log an warning - return $this->packages->getUrl('/img/default_avatar.png'); - } + //Otherwise serve the default image (its an SVG, so we dont need to thumbnail it) + return $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH); } /** * Get either a Gravatar URL or complete image tag for a specified email address. * - * @param User $user The user for which the gravator should be generated + * @param User $user The user for which the gravator should be generated * @param int $s Size in pixels, defaults to 80px [ 1 - 2048 ] * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ] * @param string $r Maximum rating (inclusive) [ g | pg | r | x ] @@ -130,28 +122,24 @@ class UserAvatarHelper private function getGravatar(User $user, int $s = 80, string $d = 'identicon', string $r = 'g'): string { $email = $user->getEmail(); - if (empty($email)) { + if ($email === null || $email === '') { $email = 'Part-DB'; } $url = 'https://www.gravatar.com/avatar/'; $url .= md5(strtolower(trim($email))); - $url .= "?s=${s}&d=${d}&r=${r}"; - return $url; + return $url."?s=$s&d=$d&r=$r"; } /** * Handles the upload of the user avatar. - * @param User $user - * @param UploadedFile $file - * @return Attachment */ public function handleAvatarUpload(User $user, UploadedFile $file): Attachment { //Determine which attachment to user //If the user already has a master attachment, we use this one - if ($user->getMasterPictureAttachment()) { + if ($user->getMasterPictureAttachment() instanceof Attachment) { $attachment = $user->getMasterPictureAttachment(); } else { //Otherwise we have to create one $attachment = new UserAttachment(); @@ -169,15 +157,14 @@ class UserAvatarHelper } $attachment->setAttachmentType($attachment_type); - //$user->setMasterPictureAttachment($attachment); } //Handle the upload - $this->submitHandler->handleFormSubmit($attachment, $file); + $this->submitHandler->handleUpload($attachment, new AttachmentUpload(file: $file)); //Set attachment as master picture $user->setMasterPictureAttachment($attachment); return $attachment; } -} \ No newline at end of file +} diff --git a/src/Services/UserSystem/VoterHelper.php b/src/Services/UserSystem/VoterHelper.php new file mode 100644 index 00000000..644351f4 --- /dev/null +++ b/src/Services/UserSystem/VoterHelper.php @@ -0,0 +1,127 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\UserSystem; + +use App\Entity\UserSystem\User; +use App\Repository\UserRepository; +use App\Security\ApiTokenAuthenticatedToken; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * @see \App\Tests\Services\UserSystem\VoterHelperTest + */ +final class VoterHelper +{ + private readonly UserRepository $userRepository; + + public function __construct(private readonly PermissionManager $permissionManager, private readonly EntityManagerInterface $entityManager) + { + $this->userRepository = $this->entityManager->getRepository(User::class); + } + + /** + * Checks if the operation on the given permission is granted for the given token. + * Similar to isGrantedTrinary, but returns false if the permission is not granted. + * @param TokenInterface $token The token to check + * @param string $permission The permission to check + * @param string $operation The operation to check + * @return bool + */ + public function isGranted(TokenInterface $token, string $permission, string $operation): bool + { + return $this->isGrantedTrinary($token, $permission, $operation) ?? false; + } + + /** + * Checks if the operation on the given permission is granted for the given token. + * The result is returned in trinary value, where null means inherted from the parent. + * @param TokenInterface $token The token to check + * @param string $permission The permission to check + * @param string $operation The operation to check + * @return bool|null The result of the check. Null means inherted from the parent. + */ + public function isGrantedTrinary(TokenInterface $token, string $permission, string $operation): ?bool + { + $user = $token->getUser(); + + if ($user instanceof User) { + //A disallowed user is not allowed to do anything... + if ($user->isDisabled()) { + return false; + } + } else { + //Try to resolve the user from the token + $user = $this->resolveUser($token); + } + + //If the token is a APITokenAuthenticated + if ($token instanceof ApiTokenAuthenticatedToken) { + //Use the special API token checker + return $this->permissionManager->inheritWithAPILevel($user, $token->getRoleNames(), $permission, $operation); + } + + //Otherwise use the normal permission checker + return $this->permissionManager->inherit($user, $permission, $operation); + } + + /** + * Resolves the user from the given token. If the token is anonymous, the anonymous user is returned. + * @return User + */ + public function resolveUser(TokenInterface $token): User + { + $user = $token->getUser(); + //If the user is a User entity, just return it + if ($user instanceof User) { + return $user; + } + + //If the user is null, return the anonymous user + if ($user === null) { + $user = $this->userRepository->getAnonymousUser(); + if (!$user instanceof User) { + throw new \RuntimeException('The anonymous user could not be resolved.'); + } + return $user; + } + + //Otherwise throw an exception + throw new \RuntimeException('The user could not be resolved.'); + } + + /** + * Checks if the permission operation combination with the given names is existing. + * Just a proxy to the permission manager. + * + * @param string $permission the name of the permission which should be checked + * @param string $operation the name of the operation which should be checked + * + * @return bool true if the given permission operation combination is existing + */ + public function isValidOperation(string $permission, string $operation): bool + { + return $this->permissionManager->isValidOperation($permission, $operation); + } +} \ No newline at end of file diff --git a/src/State/CurrentApiTokenProvider.php b/src/State/CurrentApiTokenProvider.php new file mode 100644 index 00000000..f989d504 --- /dev/null +++ b/src/State/CurrentApiTokenProvider.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types=1); + + +namespace App\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use App\Security\ApiTokenAuthenticatedToken; +use Symfony\Bundle\SecurityBundle\Security; + + +class CurrentApiTokenProvider implements ProviderInterface +{ + + public function __construct(private readonly Security $security) + { + + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $securityToken = $this->security->getToken(); + if (!$securityToken instanceof ApiTokenAuthenticatedToken) { + return null; + } + + return $securityToken->getApiToken(); + } +} \ No newline at end of file diff --git a/src/State/PartDBInfoProvider.php b/src/State/PartDBInfoProvider.php new file mode 100644 index 00000000..c6760ede --- /dev/null +++ b/src/State/PartDBInfoProvider.php @@ -0,0 +1,44 @@ +versionManager->getVersion()->toString(), + git_branch: $this->gitVersionInfo->getGitBranchName(), + git_commit: $this->gitVersionInfo->getGitCommitHash(), + title: $this->partdb_title, + banner: $this->bannerHelper->getBanner(), + default_uri: $this->default_uri, + global_timezone: $this->global_timezone, + base_currency: $this->base_currency, + global_locale: $this->global_locale, + ); + } +} diff --git a/src/Translation/Fixes/SegmentAwareXliffFileDumper.php b/src/Translation/Fixes/SegmentAwareXliffFileDumper.php new file mode 100644 index 00000000..4b44ef01 --- /dev/null +++ b/src/Translation/Fixes/SegmentAwareXliffFileDumper.php @@ -0,0 +1,242 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Translation\Fixes; + +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\Translation\Dumper\FileDumper; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * Backport of the XliffFile dumper from Symfony 7.2, which supports segment attributes and notes, this keeps the + * metadata when editing the translations from inside Symfony. + */ +#[AsDecorator("translation.dumper.xliff")] +class SegmentAwareXliffFileDumper extends FileDumper +{ + + public function __construct( + private string $extension = 'xlf', + ) { + } + + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $xliffVersion = '1.2'; + if (\array_key_exists('xliff_version', $options)) { + $xliffVersion = $options['xliff_version']; + } + + if (\array_key_exists('default_locale', $options)) { + $defaultLocale = $options['default_locale']; + } else { + $defaultLocale = \Locale::getDefault(); + } + + if ('1.2' === $xliffVersion) { + return $this->dumpXliff1($defaultLocale, $messages, $domain, $options); + } + if ('2.0' === $xliffVersion) { + return $this->dumpXliff2($defaultLocale, $messages, $domain); + } + + throw new InvalidArgumentException(\sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion)); + } + + protected function getExtension(): string + { + return $this->extension; + } + + private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ?string $domain, array $options = []): string + { + $toolInfo = ['tool-id' => 'symfony', 'tool-name' => 'Symfony']; + if (\array_key_exists('tool_info', $options)) { + $toolInfo = array_merge($toolInfo, $options['tool_info']); + } + + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + + $xliff = $dom->appendChild($dom->createElement('xliff')); + $xliff->setAttribute('version', '1.2'); + $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); + + $xliffFile = $xliff->appendChild($dom->createElement('file')); + $xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale)); + $xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale())); + $xliffFile->setAttribute('datatype', 'plaintext'); + $xliffFile->setAttribute('original', 'file.ext'); + + $xliffHead = $xliffFile->appendChild($dom->createElement('header')); + $xliffTool = $xliffHead->appendChild($dom->createElement('tool')); + foreach ($toolInfo as $id => $value) { + $xliffTool->setAttribute($id, $value); + } + + if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { + $xliffPropGroup = $xliffHead->appendChild($dom->createElement('prop-group')); + foreach ($catalogueMetadata as $key => $value) { + $xliffProp = $xliffPropGroup->appendChild($dom->createElement('prop')); + $xliffProp->setAttribute('prop-type', $key); + $xliffProp->appendChild($dom->createTextNode($value)); + } + } + + $xliffBody = $xliffFile->appendChild($dom->createElement('body')); + foreach ($messages->all($domain) as $source => $target) { + $translation = $dom->createElement('trans-unit'); + + $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); + $translation->setAttribute('resname', $source); + + $s = $translation->appendChild($dom->createElement('source')); + $s->appendChild($dom->createTextNode($source)); + + // Does the target contain characters requiring a CDATA section? + $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); + + $targetElement = $dom->createElement('target'); + $metadata = $messages->getMetadata($source, $domain); + if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { + foreach ($metadata['target-attributes'] as $name => $value) { + $targetElement->setAttribute($name, $value); + } + } + $t = $translation->appendChild($targetElement); + $t->appendChild($text); + + if ($this->hasMetadataArrayInfo('notes', $metadata)) { + foreach ($metadata['notes'] as $note) { + if (!isset($note['content'])) { + continue; + } + + $n = $translation->appendChild($dom->createElement('note')); + $n->appendChild($dom->createTextNode($note['content'])); + + if (isset($note['priority'])) { + $n->setAttribute('priority', $note['priority']); + } + + if (isset($note['from'])) { + $n->setAttribute('from', $note['from']); + } + } + } + + $xliffBody->appendChild($translation); + } + + return $dom->saveXML(); + } + + private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?string $domain): string + { + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + + $xliff = $dom->appendChild($dom->createElement('xliff')); + $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); + $xliff->setAttribute('version', '2.0'); + $xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale)); + $xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale())); + + $xliffFile = $xliff->appendChild($dom->createElement('file')); + if (str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX)) { + $xliffFile->setAttribute('id', substr($domain, 0, -\strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)).'.'.$messages->getLocale()); + } else { + $xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale()); + } + + if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { + $xliff->setAttribute('xmlns:m', 'urn:oasis:names:tc:xliff:metadata:2.0'); + $xliffMetadata = $xliffFile->appendChild($dom->createElement('m:metadata')); + foreach ($catalogueMetadata as $key => $value) { + $xliffMeta = $xliffMetadata->appendChild($dom->createElement('prop')); + $xliffMeta->setAttribute('type', $key); + $xliffMeta->appendChild($dom->createTextNode($value)); + } + } + + foreach ($messages->all($domain) as $source => $target) { + $translation = $dom->createElement('unit'); + $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); + + if (\strlen($source) <= 80) { + $translation->setAttribute('name', $source); + } + + $metadata = $messages->getMetadata($source, $domain); + + // Add notes section + if ($this->hasMetadataArrayInfo('notes', $metadata)) { + $notesElement = $dom->createElement('notes'); + foreach ($metadata['notes'] as $note) { + $n = $dom->createElement('note'); + $n->appendChild($dom->createTextNode($note['content'] ?? '')); + unset($note['content']); + + foreach ($note as $name => $value) { + $n->setAttribute($name, $value); + } + $notesElement->appendChild($n); + } + $translation->appendChild($notesElement); + } + + $segment = $translation->appendChild($dom->createElement('segment')); + + if ($this->hasMetadataArrayInfo('segment-attributes', $metadata)) { + foreach ($metadata['segment-attributes'] as $name => $value) { + $segment->setAttribute($name, $value); + } + } + + $s = $segment->appendChild($dom->createElement('source')); + $s->appendChild($dom->createTextNode($source)); + + // Does the target contain characters requiring a CDATA section? + $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); + + $targetElement = $dom->createElement('target'); + if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { + foreach ($metadata['target-attributes'] as $name => $value) { + $targetElement->setAttribute($name, $value); + } + } + $t = $segment->appendChild($targetElement); + $t->appendChild($text); + + $xliffFile->appendChild($translation); + } + + return $dom->saveXML(); + } + + private function hasMetadataArrayInfo(string $key, ?array $metadata = null): bool + { + return is_iterable($metadata[$key] ?? null); + } +} \ No newline at end of file diff --git a/src/Translation/Fixes/SegmentAwareXliffFileLoader.php b/src/Translation/Fixes/SegmentAwareXliffFileLoader.php new file mode 100644 index 00000000..12455e87 --- /dev/null +++ b/src/Translation/Fixes/SegmentAwareXliffFileLoader.php @@ -0,0 +1,262 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Translation\Fixes; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Util\Exception\InvalidXmlException; +use Symfony\Component\Config\Util\Exception\XmlParsingException; +use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; +use Symfony\Component\Translation\Loader\LoaderInterface; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Util\XliffUtils; + +/** + * Backport of the XliffFile dumper from Symfony 7.2, which supports segment attributes and notes, this keeps the + * metadata when editing the translations from inside Symfony. + */ +#[AsDecorator("translation.loader.xliff")] +class SegmentAwareXliffFileLoader implements LoaderInterface +{ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue + { + if (!class_exists(XmlUtils::class)) { + throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); + } + + if (!$this->isXmlString($resource)) { + if (!stream_is_local($resource)) { + throw new InvalidResourceException(\sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource)) { + throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource)); + } + + if (!is_file($resource)) { + throw new InvalidResourceException(\sprintf('This is neither a file nor an XLIFF string "%s".', $resource)); + } + } + + try { + if ($this->isXmlString($resource)) { + $dom = XmlUtils::parse($resource); + } else { + $dom = XmlUtils::loadFile($resource); + } + } catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) { + throw new InvalidResourceException(\sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); + } + + if ($errors = XliffUtils::validateSchema($dom)) { + throw new InvalidResourceException(\sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); + } + + $catalogue = new MessageCatalogue($locale); + $this->extract($dom, $catalogue, $domain); + + if (is_file($resource) && class_exists(FileResource::class)) { + $catalogue->addResource(new FileResource($resource)); + } + + return $catalogue; + } + + private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void + { + $xliffVersion = XliffUtils::getVersionNumber($dom); + + if ('1.2' === $xliffVersion) { + $this->extractXliff1($dom, $catalogue, $domain); + } + + if ('2.0' === $xliffVersion) { + $this->extractXliff2($dom, $catalogue, $domain); + } + } + + /** + * Extract messages and metadata from DOMDocument into a MessageCatalogue. + */ + private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void + { + $xml = simplexml_import_dom($dom); + $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; + + $namespace = 'urn:oasis:names:tc:xliff:document:1.2'; + $xml->registerXPathNamespace('xliff', $namespace); + + foreach ($xml->xpath('//xliff:file') as $file) { + $fileAttributes = $file->attributes(); + + $file->registerXPathNamespace('xliff', $namespace); + + foreach ($file->xpath('.//xliff:prop') as $prop) { + $catalogue->setCatalogueMetadata($prop->attributes()['prop-type'], (string) $prop, $domain); + } + + foreach ($file->xpath('.//xliff:trans-unit') as $translation) { + $attributes = $translation->attributes(); + + if (!(isset($attributes['resname']) || isset($translation->source))) { + continue; + } + + $source = (string) (isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source); + + if (isset($translation->target) + && 'needs-translation' === (string) $translation->target->attributes()['state'] + && \in_array((string) $translation->target, [$source, (string) $translation->source], true) + ) { + continue; + } + + // If the xlf file has another encoding specified, try to convert it because + // simple_xml will always return utf-8 encoded values + $target = $this->utf8ToCharset((string) ($translation->target ?? $translation->source), $encoding); + + $catalogue->set($source, $target, $domain); + + $metadata = [ + 'source' => (string) $translation->source, + 'file' => [ + 'original' => (string) $fileAttributes['original'], + ], + ]; + if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) { + $metadata['notes'] = $notes; + } + + if (isset($translation->target) && $translation->target->attributes()) { + $metadata['target-attributes'] = []; + foreach ($translation->target->attributes() as $key => $value) { + $metadata['target-attributes'][$key] = (string) $value; + } + } + + if (isset($attributes['id'])) { + $metadata['id'] = (string) $attributes['id']; + } + + $catalogue->setMetadata($source, $metadata, $domain); + } + } + } + + private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void + { + $xml = simplexml_import_dom($dom); + $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; + + $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0'); + + foreach ($xml->xpath('//xliff:unit') as $unit) { + foreach ($unit->segment as $segment) { + $attributes = $unit->attributes(); + $source = $attributes['name'] ?? $segment->source; + + // If the xlf file has another encoding specified, try to convert it because + // simple_xml will always return utf-8 encoded values + $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding); + + $catalogue->set((string) $source, $target, $domain); + + $metadata = []; + if ($segment->attributes()) { + $metadata['segment-attributes'] = []; + foreach ($segment->attributes() as $key => $value) { + $metadata['segment-attributes'][$key] = (string) $value; + } + } + + if (isset($segment->target) && $segment->target->attributes()) { + $metadata['target-attributes'] = []; + foreach ($segment->target->attributes() as $key => $value) { + $metadata['target-attributes'][$key] = (string) $value; + } + } + + if (isset($unit->notes)) { + $metadata['notes'] = []; + foreach ($unit->notes->note as $noteNode) { + $note = []; + foreach ($noteNode->attributes() as $key => $value) { + $note[$key] = (string) $value; + } + $note['content'] = (string) $noteNode; + $metadata['notes'][] = $note; + } + } + + $catalogue->setMetadata((string) $source, $metadata, $domain); + } + } + } + + /** + * Convert a UTF8 string to the specified encoding. + */ + private function utf8ToCharset(string $content, ?string $encoding = null): string + { + if ('UTF-8' !== $encoding && $encoding) { + return mb_convert_encoding($content, $encoding, 'UTF-8'); + } + + return $content; + } + + private function parseNotesMetadata(?\SimpleXMLElement $noteElement = null, ?string $encoding = null): array + { + $notes = []; + + if (null === $noteElement) { + return $notes; + } + + /** @var \SimpleXMLElement $xmlNote */ + foreach ($noteElement as $xmlNote) { + $noteAttributes = $xmlNote->attributes(); + $note = ['content' => $this->utf8ToCharset((string) $xmlNote, $encoding)]; + if (isset($noteAttributes['priority'])) { + $note['priority'] = (int) $noteAttributes['priority']; + } + + if (isset($noteAttributes['from'])) { + $note['from'] = (string) $noteAttributes['from']; + } + + $notes[] = $note; + } + + return $notes; + } + + private function isXmlString(string $resource): bool + { + return str_starts_with($resource, '. */ - namespace App\Twig; +use App\Entity\Attachments\Attachment; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Misc\FAIconGenerator; use Twig\Extension\AbstractExtension; @@ -27,22 +30,17 @@ use Twig\TwigFunction; final class AttachmentExtension extends AbstractExtension { - protected AttachmentURLGenerator $attachmentURLGenerator; - protected FAIconGenerator $FAIconGenerator; - - public function __construct(AttachmentURLGenerator $attachmentURLGenerator, FAIconGenerator $FAIconGenerator) + public function __construct(protected AttachmentURLGenerator $attachmentURLGenerator, protected FAIconGenerator $FAIconGenerator) { - $this->attachmentURLGenerator = $attachmentURLGenerator; - $this->FAIconGenerator = $FAIconGenerator; } public function getFunctions(): array { return [ /* Returns the URL to a thumbnail of the given attachment */ - new TwigFunction('attachment_thumbnail', [$this->attachmentURLGenerator, 'getThumbnailURL']), - /* Returns the font awesome icon class which is representing the given file extension */ - new TwigFunction('ext_to_fa_icon', [$this->FAIconGenerator, 'fileExtensionToFAType']), + new TwigFunction('attachment_thumbnail', fn(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string => $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name)), + /* Returns the font awesome icon class which is representing the given file extension (We allow null here for attachments without extension) */ + new TwigFunction('ext_to_fa_icon', fn(?string $extension): string => $this->FAIconGenerator->fileExtensionToFAType($extension ?? '')), ]; } -} \ No newline at end of file +} diff --git a/src/Twig/BarcodeExtension.php b/src/Twig/BarcodeExtension.php index 051995f6..ae1973e3 100644 --- a/src/Twig/BarcodeExtension.php +++ b/src/Twig/BarcodeExtension.php @@ -1,4 +1,7 @@ . */ - namespace App\Twig; use Com\Tecnick\Barcode\Barcode; use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; use Twig\TwigFunction; final class BarcodeExtension extends AbstractExtension @@ -31,7 +32,7 @@ final class BarcodeExtension extends AbstractExtension { return [ /* Generates a barcode with the given Type and Data and returns it as an SVG represenation */ - new TwigFunction('barcode_svg', [$this, 'barcodeSVG']), + new TwigFunction('barcode_svg', fn(string $content, string $type = 'QRCODE'): string => $this->barcodeSVG($content, $type)), ]; } diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 6d477d88..762ebb09 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -1,4 +1,7 @@ . */ - namespace App\Twig; use App\Entity\Attachments\Attachment; @@ -29,11 +31,12 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; +use App\Exceptions\EntityNotSupportedException; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Services\Trees\TreeViewGenerator; @@ -41,26 +44,20 @@ use Twig\Extension\AbstractExtension; use Twig\TwigFunction; use Twig\TwigTest; +/** + * @see \App\Tests\Twig\EntityExtensionTest + */ final class EntityExtension extends AbstractExtension { - protected EntityURLGenerator $entityURLGenerator; - protected TreeViewGenerator $treeBuilder; - private ElementTypeNameGenerator $nameGenerator; - - public function __construct(EntityURLGenerator $entityURLGenerator, TreeViewGenerator $treeBuilder, ElementTypeNameGenerator $elementTypeNameGenerator) + public function __construct(protected EntityURLGenerator $entityURLGenerator, protected TreeViewGenerator $treeBuilder, private readonly ElementTypeNameGenerator $nameGenerator) { - $this->entityURLGenerator = $entityURLGenerator; - $this->treeBuilder = $treeBuilder; - $this->nameGenerator = $elementTypeNameGenerator; } public function getTests(): array { return [ /* Checks if the given variable is an entitity (instance of AbstractDBElement) */ - new TwigTest('entity', static function ($var) { - return $var instanceof AbstractDBElement; - }), + new TwigTest('entity', static fn($var) => $var instanceof AbstractDBElement), ]; } @@ -68,20 +65,31 @@ final class EntityExtension extends AbstractExtension { return [ /* Returns a string representation of the given entity */ - new TwigFunction('entity_type', [$this, 'getEntityType']), + new TwigFunction('entity_type', fn(object $entity): ?string => $this->getEntityType($entity)), /* Returns the URL to the given entity */ - new TwigFunction('entity_url', [$this, 'generateEntityURL']), + new TwigFunction('entity_url', fn(AbstractDBElement $entity, string $method = 'info'): string => $this->generateEntityURL($entity, $method)), + /* Returns the URL to the given entity in timetravel mode */ + new TwigFunction('timetravel_url', fn(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string => $this->timeTravelURL($element, $dateTime)), /* Generates a JSON array of the given tree */ - new TwigFunction('tree_data', [$this, 'treeData']), + new TwigFunction('tree_data', fn(AbstractDBElement $element, string $type = 'newEdit'): string => $this->treeData($element, $type)), /* Gets a human readable label for the type of the given entity */ - new TwigFunction('entity_type_label', [$this->nameGenerator, 'getLocalizedTypeLabel']), + new TwigFunction('entity_type_label', fn(object|string $entity): string => $this->nameGenerator->getLocalizedTypeLabel($entity)), ]; } + public function timeTravelURL(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string + { + try { + return $this->entityURLGenerator->timeTravelURL($element, $dateTime); + } catch (EntityNotSupportedException) { + return null; + } + } + public function treeData(AbstractDBElement $element, string $type = 'newEdit'): string { - $tree = $this->treeBuilder->getTreeView(get_class($element), null, $type, $element); + $tree = $this->treeBuilder->getTreeView($element::class, null, $type, $element); return json_encode($tree, JSON_THROW_ON_ERROR); } @@ -96,7 +104,7 @@ final class EntityExtension extends AbstractExtension $map = [ Part::class => 'part', Footprint::class => 'footprint', - Storelocation::class => 'storelocation', + StorageLocation::class => 'storelocation', Manufacturer::class => 'manufacturer', Category::class => 'category', Project::class => 'device', @@ -115,6 +123,6 @@ final class EntityExtension extends AbstractExtension } } - return false; + return null; } -} \ No newline at end of file +} diff --git a/src/Twig/FormatExtension.php b/src/Twig/FormatExtension.php index 36a8dad4..76628ccd 100644 --- a/src/Twig/FormatExtension.php +++ b/src/Twig/FormatExtension.php @@ -22,72 +22,38 @@ declare(strict_types=1); namespace App\Twig; -use App\Entity\Attachments\Attachment; -use App\Entity\Base\AbstractDBElement; -use App\Entity\ProjectSystem\Project; -use App\Entity\LabelSystem\LabelProfile; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; -use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; -use App\Entity\Parts\Part; -use App\Entity\Parts\Storelocation; -use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; -use App\Entity\UserSystem\Group; -use App\Entity\UserSystem\User; use App\Services\Formatters\AmountFormatter; -use App\Services\Attachments\AttachmentURLGenerator; -use App\Services\EntityURLGenerator; -use App\Services\Misc\FAIconGenerator; use App\Services\Formatters\MarkdownParser; use App\Services\Formatters\MoneyFormatter; use App\Services\Formatters\SIFormatter; -use App\Services\Trees\TreeViewGenerator; use Brick\Math\BigDecimal; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; -use Twig\TwigFunction; -use Twig\TwigTest; - -use function get_class; final class FormatExtension extends AbstractExtension { - protected MarkdownParser $markdownParser; - protected MoneyFormatter $moneyFormatter; - protected SIFormatter $siformatter; - protected AmountFormatter $amountFormatter; - - - public function __construct(MarkdownParser $markdownParser, MoneyFormatter $moneyFormatter, - SIFormatter $SIFormatter, AmountFormatter $amountFormatter) + public function __construct(protected MarkdownParser $markdownParser, protected MoneyFormatter $moneyFormatter, protected SIFormatter $siformatter, protected AmountFormatter $amountFormatter) { - $this->markdownParser = $markdownParser; - $this->moneyFormatter = $moneyFormatter; - $this->siformatter = $SIFormatter; - $this->amountFormatter = $amountFormatter; } public function getFilters(): array { return [ /* Mark the given text as markdown, which will be rendered in the browser */ - new TwigFilter('format_markdown', [$this->markdownParser, 'markForRendering'], [ + new TwigFilter('format_markdown', fn(string $markdown, bool $inline_mode = false): string => $this->markdownParser->markForRendering($markdown, $inline_mode), [ 'pre_escape' => 'html', 'is_safe' => ['html'], ]), /* Format the given amount as money, using a given currency */ - new TwigFilter('format_money', [$this, 'formatCurrency']), + new TwigFilter('format_money', fn($amount, ?Currency $currency = null, int $decimals = 5): string => $this->formatCurrency($amount, $currency, $decimals)), /* Format the given number using SI prefixes and the given unit (string) */ - new TwigFilter('format_si', [$this, 'siFormat']), + new TwigFilter('format_si', fn($value, $unit, $decimals = 2, bool $show_all_digits = false): string => $this->siFormat($value, $unit, $decimals, $show_all_digits)), /** Format the given amount using the given MeasurementUnit */ - new TwigFilter('format_amount', [$this, 'amountFormat']), - /** Format the given number of bytes as human readable number */ - new TwigFilter('format_bytes', [$this, 'formatBytes']), + new TwigFilter('format_amount', fn($value, ?MeasurementUnit $unit, array $options = []): string => $this->amountFormat($value, $unit, $options)), + /** Format the given number of bytes as human-readable number */ + new TwigFilter('format_bytes', fn(int $bytes, int $precision = 2): string => $this->formatBytes($bytes, $precision)), ]; } @@ -112,8 +78,6 @@ final class FormatExtension extends AbstractExtension /** * @param $bytes - * @param int $precision - * @return string */ public function formatBytes(int $bytes, int $precision = 2): string { diff --git a/src/Twig/InfoProviderExtension.php b/src/Twig/InfoProviderExtension.php new file mode 100644 index 00000000..a963b778 --- /dev/null +++ b/src/Twig/InfoProviderExtension.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Twig; + +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +class InfoProviderExtension extends AbstractExtension +{ + public function __construct( + private readonly ProviderRegistry $providerRegistry + ) {} + + public function getFunctions(): array + { + return [ + new TwigFunction('info_provider', $this->getInfoProvider(...)), + new TwigFunction('info_provider_label', $this->getInfoProviderName(...)) + ]; + } + + /** + * Gets the info provider with the given key. Returns null, if the provider does not exist. + * @param string $key + * @return InfoProviderInterface|null + */ + private function getInfoProvider(string $key): ?InfoProviderInterface + { + try { + return $this->providerRegistry->getProviderByKey($key); + } catch (\InvalidArgumentException) { + return null; + } + } + + /** + * Gets the label of the info provider with the given key. Returns null, if the provider does not exist. + * @param string $key + * @return string|null + */ + private function getInfoProviderName(string $key): ?string + { + try { + return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()['name']; + } catch (\InvalidArgumentException) { + return null; + } + } +} \ No newline at end of file diff --git a/src/Twig/LogExtension.php b/src/Twig/LogExtension.php new file mode 100644 index 00000000..34dad988 --- /dev/null +++ b/src/Twig/LogExtension.php @@ -0,0 +1,45 @@ +. + */ +namespace App\Twig; + +use App\Entity\LogSystem\AbstractLogEntry; +use App\Services\LogSystem\LogDataFormatter; +use App\Services\LogSystem\LogDiffFormatter; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +final class LogExtension extends AbstractExtension +{ + + public function __construct(private readonly LogDataFormatter $logDataFormatter, private readonly LogDiffFormatter $logDiffFormatter) + { + } + + public function getFunctions(): array + { + return [ + new TwigFunction('format_log_data', fn($data, AbstractLogEntry $logEntry, string $fieldName): string => $this->logDataFormatter->formatData($data, $logEntry, $fieldName), ['is_safe' => ['html']]), + new TwigFunction('format_log_diff', fn($old_data, $new_data): string => $this->logDiffFormatter->formatDiff($old_data, $new_data), ['is_safe' => ['html']]), + ]; + } +} diff --git a/src/Twig/MiscExtension.php b/src/Twig/MiscExtension.php index e154ccf8..93762d35 100644 --- a/src/Twig/MiscExtension.php +++ b/src/Twig/MiscExtension.php @@ -1,4 +1,7 @@ . */ - namespace App\Twig; +use Symfony\Component\HttpFoundation\Request; +use Twig\TwigFunction; use App\Services\LogSystem\EventCommentNeededHelper; use Twig\Extension\AbstractExtension; final class MiscExtension extends AbstractExtension { - private EventCommentNeededHelper $eventCommentNeededHelper; - - public function __construct(EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(private readonly EventCommentNeededHelper $eventCommentNeededHelper) { - $this->eventCommentNeededHelper = $eventCommentNeededHelper; } - public function getFunctions() + public function getFunctions(): array { return [ - new \Twig\TwigFunction('event_comment_needed', + new TwigFunction('event_comment_needed', fn(string $operation_type) => $this->eventCommentNeededHelper->isCommentNeeded($operation_type) ), + + new TwigFunction('uri_without_host', $this->uri_without_host(...)) ]; } -} \ No newline at end of file + + /** + * Similar to the getUri function of the request, but does not contain protocol and host. + * @param Request $request + * @return string + */ + public function uri_without_host(Request $request): string + { + if (null !== $qs = $request->getQueryString()) { + $qs = '?'.$qs; + } + + return $request->getBaseUrl().$request->getPathInfo().$qs; + } +} diff --git a/src/Twig/Sandbox/InheritanceSecurityPolicy.php b/src/Twig/Sandbox/InheritanceSecurityPolicy.php index 052366c0..93e874e9 100644 --- a/src/Twig/Sandbox/InheritanceSecurityPolicy.php +++ b/src/Twig/Sandbox/InheritanceSecurityPolicy.php @@ -22,7 +22,6 @@ use Twig\Sandbox\SecurityNotAllowedTagError; use Twig\Sandbox\SecurityPolicyInterface; use Twig\Template; -use function get_class; use function in_array; use function is_array; @@ -35,19 +34,11 @@ use function is_array; */ final class InheritanceSecurityPolicy implements SecurityPolicyInterface { - private array $allowedTags; - private array $allowedFilters; private array $allowedMethods; - private array $allowedProperties; - private array $allowedFunctions; - public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = []) + public function __construct(private array $allowedTags = [], private array $allowedFilters = [], array $allowedMethods = [], private array $allowedProperties = [], private array $allowedFunctions = []) { - $this->allowedTags = $allowedTags; - $this->allowedFilters = $allowedFilters; $this->setAllowedMethods($allowedMethods); - $this->allowedProperties = $allowedProperties; - $this->allowedFunctions = $allowedFunctions; } public function setAllowedTags(array $tags): void @@ -65,7 +56,7 @@ final class InheritanceSecurityPolicy implements SecurityPolicyInterface $this->allowedMethods = []; foreach ($methods as $class => $m) { $this->allowedMethods[$class] = array_map( - static function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, is_array($m) ? $m : [$m]); + static fn($value): string => strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), is_array($m) ? $m : [$m]); } } @@ -112,7 +103,7 @@ final class InheritanceSecurityPolicy implements SecurityPolicyInterface if ($obj instanceof $class) { $allowed = in_array($method, $methods, true); - //CHANGED: Only break if we the method is allowed, otherwise try it on the other methods + //CHANGED: Only break if the method is allowed, otherwise try it on the other methods if ($allowed) { break; } @@ -120,7 +111,7 @@ final class InheritanceSecurityPolicy implements SecurityPolicyInterface } if (!$allowed) { - $class = get_class($obj); + $class = $obj::class; throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); } @@ -133,7 +124,7 @@ final class InheritanceSecurityPolicy implements SecurityPolicyInterface if ($obj instanceof $class) { $allowed = in_array($property, is_array($properties) ? $properties : [$properties], true); - //CHANGED: Only break if we the method is allowed, otherwise try it on the other methods + //CHANGED: Only break if the method is allowed, otherwise try it on the other methods if ($allowed) { break; } @@ -141,7 +132,7 @@ final class InheritanceSecurityPolicy implements SecurityPolicyInterface } if (!$allowed) { - $class = get_class($obj); + $class = $obj::class; throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); } diff --git a/src/Twig/Sandbox/SandboxedLabelExtension.php b/src/Twig/Sandbox/SandboxedLabelExtension.php new file mode 100644 index 00000000..59fb0af0 --- /dev/null +++ b/src/Twig/Sandbox/SandboxedLabelExtension.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Twig\Sandbox; + +use App\Services\LabelSystem\LabelTextReplacer; +use Twig\Extension\AbstractExtension; +use Twig\TwigFilter; +use Twig\TwigFunction; + +class SandboxedLabelExtension extends AbstractExtension +{ + public function __construct(private readonly LabelTextReplacer $labelTextReplacer) + { + + } + + public function getFunctions(): array + { + return [ + new TwigFunction('placeholder', fn(string $text, object $label_target) => $this->labelTextReplacer->handlePlaceholderOrReturnNull($text, $label_target)), + ]; + } + + public function getFilters(): array + { + return [ + new TwigFilter('placeholders', fn(string $text, object $label_target) => $this->labelTextReplacer->replace($text, $label_target)), + ]; + } +} \ No newline at end of file diff --git a/src/Twig/TwigCoreExtension.php b/src/Twig/TwigCoreExtension.php index aecbdd17..352e09d3 100644 --- a/src/Twig/TwigCoreExtension.php +++ b/src/Twig/TwigCoreExtension.php @@ -1,4 +1,7 @@ . */ - namespace App\Twig; -use App\Entity\Base\AbstractDBElement; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; +use Twig\TwigFunction; use Twig\TwigTest; /** * The functionalities here extend the Twig with some core functions, which are independently of Part-DB. + * @see \App\Tests\Twig\TwigCoreExtensionTest */ final class TwigCoreExtension extends AbstractExtension { - protected ObjectNormalizer $objectNormalizer; - - public function __construct(ObjectNormalizer $objectNormalizer) + public function __construct(protected ObjectNormalizer $objectNormalizer) { - $this->objectNormalizer = $objectNormalizer; + } + + public function getFunctions(): array + { + return [ + /* Returns the enum cases as values */ + new TwigFunction('enum_cases', $this->getEnumCases(...)), + ]; } public function getTests(): array @@ -44,30 +52,37 @@ final class TwigCoreExtension extends AbstractExtension /* * Checks if a given variable is an instance of a given class. E.g. ` x is instanceof('App\Entity\Parts\Part')` */ - new TwigTest('instanceof', static function ($var, $instance) { - return $var instanceof $instance; - }), + new TwigTest('instanceof', static fn($var, $instance) => $var instanceof $instance), /* Checks if a given variable is an object. E.g. `x is object` */ - new TwigTest('object', static function ($var) { - return is_object($var); - }), + new TwigTest('object', static fn($var): bool => is_object($var)), + new TwigTest('enum', fn($var) => $var instanceof \UnitEnum), ]; } - public function getFilters() + /** + * @param string $enum_class + * @phpstan-param class-string $enum_class + */ + public function getEnumCases(string $enum_class): array + { + if (!enum_exists($enum_class)) { + throw new \InvalidArgumentException(sprintf('The given class "%s" is not an enum!', $enum_class)); + } + + /** @noinspection PhpUndefinedMethodInspection */ + return ($enum_class)::cases(); + } + + public function getFilters(): array { return [ /* Converts the given object to an array representation of the public/accessible properties */ - new TwigFilter('to_array', [$this, 'toArray']), + new TwigFilter('to_array', fn($object) => $this->toArray($object)), ]; } - public function toArray($object) + public function toArray(object|array $object): array { - if(! is_object($object) && ! is_array($object)) { - throw new \InvalidArgumentException('The given variable is not an object or array!'); - } - //If it is already an array, we can just return it if(is_array($object)) { return $object; @@ -75,4 +90,4 @@ final class TwigCoreExtension extends AbstractExtension return $this->objectNormalizer->normalize($object, null); } -} \ No newline at end of file +} diff --git a/src/Twig/UserExtension.php b/src/Twig/UserExtension.php index 869bea84..5045257a 100644 --- a/src/Twig/UserExtension.php +++ b/src/Twig/UserExtension.php @@ -41,18 +41,29 @@ declare(strict_types=1); namespace App\Twig; +use App\Entity\Base\AbstractDBElement; +use App\Entity\UserSystem\User; use App\Entity\LogSystem\AbstractLogEntry; use App\Repository\LogEntryRepository; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; +/** + * @see \App\Tests\Twig\UserExtensionTest + */ final class UserExtension extends AbstractExtension { - private LogEntryRepository $repo; + private readonly LogEntryRepository $repo; - public function __construct(EntityManagerInterface $em) + public function __construct(EntityManagerInterface $em, + private readonly Security $security, + private readonly UrlGeneratorInterface $urlGenerator) { $this->repo = $em->getRepository(AbstractLogEntry::class); } @@ -60,7 +71,7 @@ final class UserExtension extends AbstractExtension public function getFilters(): array { return [ - new TwigFilter('remove_locale_from_path', [$this, 'removeLocaleFromPath']), + new TwigFilter('remove_locale_from_path', fn(string $path): string => $this->removeLocaleFromPath($path)), ]; } @@ -68,19 +79,55 @@ final class UserExtension extends AbstractExtension { return [ /* Returns the user which has edited the given entity the last time. */ - new TwigFunction('last_editing_user', [$this->repo, 'getLastEditingUser']), + new TwigFunction('last_editing_user', fn(AbstractDBElement $element): ?User => $this->repo->getLastEditingUser($element)), /* Returns the user which has created the given entity. */ - new TwigFunction('creating_user', [$this->repo, 'getCreatingUser']), + new TwigFunction('creating_user', fn(AbstractDBElement $element): ?User => $this->repo->getCreatingUser($element)), + new TwigFunction('impersonator_user', $this->getImpersonatorUser(...)), + new TwigFunction('impersonation_active', $this->isImpersonationActive(...)), + new TwigFunction('impersonation_path', $this->getImpersonationPath(...)), ]; } /** - * This function/filter generates an path. + * This function returns the user which has impersonated the current user. + * If the current user is not impersonated, null is returned. + * @return User|null + */ + public function getImpersonatorUser(): ?User + { + $token = $this->security->getToken(); + if ($token instanceof SwitchUserToken) { + $tmp = $token->getOriginalToken()->getUser(); + + if ($tmp instanceof User) { + return $tmp; + } + } + + return null; + } + + public function isImpersonationActive(): bool + { + return $this->security->isGranted('IS_IMPERSONATOR'); + } + + public function getImpersonationPath(User $user, string $route_name = 'homepage'): string + { + if (! $this->security->isGranted('CAN_SWITCH_USER', $user)) { + throw new AccessDeniedException('You are not allowed to impersonate this user!'); + } + + return $this->urlGenerator->generate($route_name, ['_switch_user' => $user->getUsername()]); + } + + /** + * This function/filter generates a path. */ public function removeLocaleFromPath(string $path): string { //Ensure the path has the correct format - if (!preg_match('/^\/\w{2}\//', $path)) { + if (!preg_match('/^\/\w{2}(?:_\w{2})?\//', $path)) { throw new \InvalidArgumentException('The given path is not a localized path!'); } diff --git a/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThanValidator.php b/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThanValidator.php index 62231ea8..76acb25a 100644 --- a/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThanValidator.php +++ b/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThanValidator.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints\BigDecimal; use Brick\Math\BigDecimal; diff --git a/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThenOrEqualValidator.php b/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThenOrEqualValidator.php index 3e4c8444..b6df06d0 100644 --- a/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThenOrEqualValidator.php +++ b/src/Validator/Constraints/BigDecimal/BigDecimalGreaterThenOrEqualValidator.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints\BigDecimal; use Brick\Math\BigDecimal; diff --git a/src/Validator/Constraints/BigDecimal/BigDecimalPositive.php b/src/Validator/Constraints/BigDecimal/BigDecimalPositive.php index bb4e61eb..0e7e755f 100644 --- a/src/Validator/Constraints/BigDecimal/BigDecimalPositive.php +++ b/src/Validator/Constraints/BigDecimal/BigDecimalPositive.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints\BigDecimal; -use Symfony\Component\Validator\Constraints\GreaterThan; +use Symfony\Component\Validator\Constraints\Positive; -/** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - * - * @author Jan Schädlich - */ -class BigDecimalPositive extends GreaterThan +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class BigDecimalPositive extends Positive { - use BigNumberConstraintTrait; - - public $message = 'This value should be positive.'; - - public function __construct($options = null) - { - parent::__construct($this->configureNumberConstraintOptions($options)); - } - public function validatedBy(): string { return BigDecimalGreaterThanValidator::class; diff --git a/src/Validator/Constraints/BigDecimal/BigDecimalPositiveOrZero.php b/src/Validator/Constraints/BigDecimal/BigDecimalPositiveOrZero.php index 1e6558a2..408cd582 100644 --- a/src/Validator/Constraints/BigDecimal/BigDecimalPositiveOrZero.php +++ b/src/Validator/Constraints/BigDecimal/BigDecimalPositiveOrZero.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints\BigDecimal; -use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; +use Symfony\Component\Validator\Constraints\PositiveOrZero; -/** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - * - * @author Jan Schädlich - */ -class BigDecimalPositiveOrZero extends GreaterThanOrEqual +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class BigDecimalPositiveOrZero extends PositiveOrZero { - use BigNumberConstraintTrait; - - public $message = 'This value should be either positive or zero.'; - - public function __construct($options = null) - { - parent::__construct($this->configureNumberConstraintOptions($options)); - } - public function validatedBy(): string { return BigDecimalGreaterThenOrEqualValidator::class; diff --git a/src/Validator/Constraints/BigDecimal/BigNumberConstraintTrait.php b/src/Validator/Constraints/BigDecimal/BigNumberConstraintTrait.php deleted file mode 100644 index a9858730..00000000 --- a/src/Validator/Constraints/BigDecimal/BigNumberConstraintTrait.php +++ /dev/null @@ -1,49 +0,0 @@ -. - */ - -namespace App\Validator\Constraints\BigDecimal; - -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; - -use function is_array; - -trait BigNumberConstraintTrait -{ - private function configureNumberConstraintOptions($options): array - { - if (null === $options) { - $options = []; - } elseif (!is_array($options)) { - $options = [$this->getDefaultOption() => $options]; - } - - if (isset($options['propertyPath'])) { - throw new ConstraintDefinitionException(sprintf('The "propertyPath" option of the "%s" constraint cannot be set.', static::class)); - } - - if (isset($options['value'])) { - throw new ConstraintDefinitionException(sprintf('The "value" option of the "%s" constraint cannot be set.', static::class)); - } - - $options['value'] = 0; - - return $options; - } -} \ No newline at end of file diff --git a/src/Validator/Constraints/Misc/ValidRange.php b/src/Validator/Constraints/Misc/ValidRange.php index eb14cf0d..680eb04d 100644 --- a/src/Validator/Constraints/Misc/ValidRange.php +++ b/src/Validator/Constraints/Misc/ValidRange.php @@ -43,10 +43,8 @@ namespace App\Validator\Constraints\Misc; use Symfony\Component\Validator\Constraint; -/** - * @Annotation - */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class ValidRange extends Constraint { - public $message = 'validator.invalid_range'; + public string $message = 'validator.invalid_range'; } diff --git a/src/Validator/Constraints/Misc/ValidRangeValidator.php b/src/Validator/Constraints/Misc/ValidRangeValidator.php index 8385cc92..8bc5af0c 100644 --- a/src/Validator/Constraints/Misc/ValidRangeValidator.php +++ b/src/Validator/Constraints/Misc/ValidRangeValidator.php @@ -49,11 +49,8 @@ use Symfony\Component\Validator\Exception\UnexpectedValueException; class ValidRangeValidator extends ConstraintValidator { - protected RangeParser $rangeParser; - - public function __construct(RangeParser $rangeParser) + public function __construct(protected RangeParser $rangeParser) { - $this->rangeParser = $rangeParser; } public function validate($value, Constraint $constraint): void diff --git a/src/Validator/Constraints/NoLockout.php b/src/Validator/Constraints/NoLockout.php index 1a515e4d..30eb770f 100644 --- a/src/Validator/Constraints/NoLockout.php +++ b/src/Validator/Constraints/NoLockout.php @@ -26,10 +26,14 @@ use Symfony\Component\Validator\Constraint; /** * This constraint restricts a user in that way that it can not lock itself out of the user system. - * - * @Annotation */ +#[\Attribute(\Attribute::TARGET_CLASS)] class NoLockout extends Constraint { - public $message = 'validator.noLockout'; + public string $message = 'validator.noLockout'; + + public function getTargets(): string|array + { + return [self::CLASS_CONSTRAINT]; + } } diff --git a/src/Validator/Constraints/NoLockoutValidator.php b/src/Validator/Constraints/NoLockoutValidator.php index 4622d7fe..f3998188 100644 --- a/src/Validator/Constraints/NoLockoutValidator.php +++ b/src/Validator/Constraints/NoLockoutValidator.php @@ -22,28 +22,20 @@ declare(strict_types=1); namespace App\Validator\Constraints; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Security\Core\User\UserInterface; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Services\UserSystem\PermissionManager; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class NoLockoutValidator extends ConstraintValidator { - protected PermissionManager $resolver; - protected array $perm_structure; - protected Security $security; - protected EntityManagerInterface $entityManager; - - public function __construct(PermissionManager $resolver, Security $security, EntityManagerInterface $entityManager) + public function __construct(protected PermissionManager $resolver, protected Security $security, protected EntityManagerInterface $entityManager) { - $this->resolver = $resolver; - $this->perm_structure = $resolver->getPermissionStructure(); - $this->security = $security; - $this->entityManager = $entityManager; } /** @@ -52,7 +44,7 @@ class NoLockoutValidator extends ConstraintValidator * @param mixed $value The value that should be validated * @param Constraint $constraint The constraint for the validation */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof NoLockout) { throw new UnexpectedTypeException($constraint, NoLockout::class); @@ -64,18 +56,20 @@ class NoLockoutValidator extends ConstraintValidator if ($perm_holder instanceof User || $perm_holder instanceof Group) { $user = $this->security->getUser(); - if (null === $user) { + if (!$user instanceof UserInterface) { $user = $this->entityManager->getRepository(User::class)->getAnonymousUser(); } - //Check if we the change_permission permission has changed from allow to disallow - if (($user instanceof User) && false === ($this->resolver->inherit( + //Check if the change_permission permission has changed from allow to disallow + if (($user instanceof User) && !($this->resolver->inherit( $user, 'users', 'edit_permissions' ) ?? false)) { $this->context->addViolation($constraint->message); } + } else { + throw new \LogicException('The NoLockout constraint can only be used on User or Group objects.'); } } } diff --git a/src/Validator/Constraints/NoneOfItsChildren.php b/src/Validator/Constraints/NoneOfItsChildren.php index e3bed2f8..8f1e059a 100644 --- a/src/Validator/Constraints/NoneOfItsChildren.php +++ b/src/Validator/Constraints/NoneOfItsChildren.php @@ -25,19 +25,18 @@ namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** - * Constraints the parent property on StructuralDBElement objects in the way, that neither the object self or any + * Constraints the parent property on StructuralDBElement objects in the way, that neither the object self nor any * of its children can be assigned. - * - * @Annotation */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class NoneOfItsChildren extends Constraint { /** - * @var string The message used if it is tried to assign a object as its own parent + * @var string The message used if it is tried to assign an object as its own parent */ - public $self_message = 'validator.noneofitschild.self'; + public string $self_message = 'validator.noneofitschild.self'; /** * @var string The message used if it is tried to use one of the children for as parent */ - public $children_message = 'validator.noneofitschild.children'; + public string $children_message = 'validator.noneofitschild.children'; } diff --git a/src/Validator/Constraints/NoneOfItsChildrenValidator.php b/src/Validator/Constraints/NoneOfItsChildrenValidator.php index 7bc3fd5a..2be5f16b 100644 --- a/src/Validator/Constraints/NoneOfItsChildrenValidator.php +++ b/src/Validator/Constraints/NoneOfItsChildrenValidator.php @@ -30,6 +30,7 @@ use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * The validator for the NoneOfItsChildren annotation. + * @see \App\Tests\Validator\Constraints\NoneOfItsChildrenValidatorTest */ class NoneOfItsChildrenValidator extends ConstraintValidator { @@ -39,7 +40,7 @@ class NoneOfItsChildrenValidator extends ConstraintValidator * @param mixed $value The value that should be validated * @param Constraint $constraint The constraint for the validation */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof NoneOfItsChildren) { throw new UnexpectedTypeException($constraint, NoneOfItsChildren::class); @@ -63,7 +64,7 @@ class NoneOfItsChildrenValidator extends ConstraintValidator // Check if the targeted parent is the object itself: $entity_id = $entity->getID(); - if (null !== $entity_id && $entity_id === $value->getID()) { + if ($entity === $value || (null !== $entity_id && $entity_id === $value->getID())) { //Set the entity to a valid state $entity->setParent(null); $this->context->buildViolation($constraint->self_message)->addViolation(); diff --git a/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php index b0c99947..1e9ac834 100644 --- a/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php +++ b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequest.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints\ProjectSystem; use Symfony\Component\Validator\Constraint; /** * This constraint checks that the given ProjectBuildRequest is valid. - * - * @Annotation */ +#[\Attribute(\Attribute::TARGET_CLASS)] class ValidProjectBuildRequest extends Constraint { public function getTargets(): string { return self::CLASS_CONSTRAINT; } -} \ No newline at end of file +} diff --git a/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php index 03a9b81f..2d59e648 100644 --- a/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php +++ b/src/Validator/Constraints/ProjectSystem/ValidProjectBuildRequestValidator.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints\ProjectSystem; use App\Entity\Parts\PartLot; @@ -36,7 +38,7 @@ class ValidProjectBuildRequestValidator extends ConstraintValidator ->setParameter('{{ lot }}', $partLot->getName()); } - public function validate($value, Constraint $constraint) + public function validate($value, Constraint $constraint): void { if (!$constraint instanceof ValidProjectBuildRequest) { throw new UnexpectedTypeException($constraint, ValidProjectBuildRequest::class); @@ -67,16 +69,16 @@ class ValidProjectBuildRequestValidator extends ConstraintValidator ->addViolation(); } - if ($withdraw_sum > $needed_amount) { + if ($withdraw_sum > $needed_amount && $value->isDontCheckQuantity() === false) { $this->buildViolationForLot($lot, 'validator.project_build.lot_bigger_than_needed') ->addViolation(); } - if ($withdraw_sum < $needed_amount) { + if ($withdraw_sum < $needed_amount && $value->isDontCheckQuantity() === false) { $this->buildViolationForLot($lot, 'validator.project_build.lot_smaller_than_needed') ->addViolation(); } } } } -} \ No newline at end of file +} diff --git a/src/Validator/Constraints/Selectable.php b/src/Validator/Constraints/Selectable.php index 20a51aad..c26e47fa 100644 --- a/src/Validator/Constraints/Selectable.php +++ b/src/Validator/Constraints/Selectable.php @@ -27,10 +27,9 @@ use Symfony\Component\Validator\Constraint; /** * If a property is marked with this constraint, the choosen value (of type StructuralDBElement) * must NOT be marked as not selectable. - * - * @Annotation */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class Selectable extends Constraint { - public $message = 'validator.isSelectable'; + public string $message = 'validator.isSelectable'; } diff --git a/src/Validator/Constraints/SelectableValidator.php b/src/Validator/Constraints/SelectableValidator.php index 8b93865d..013a3964 100644 --- a/src/Validator/Constraints/SelectableValidator.php +++ b/src/Validator/Constraints/SelectableValidator.php @@ -30,6 +30,7 @@ use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * The validator for the Selectable constraint. + * @see \App\Tests\Validator\Constraints\SelectableValidatorTest */ class SelectableValidator extends ConstraintValidator { @@ -39,7 +40,7 @@ class SelectableValidator extends ConstraintValidator * @param mixed $value The value that should be validated * @param Constraint $constraint The constraint for the validation */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Selectable) { throw new UnexpectedTypeException($constraint, Selectable::class); @@ -53,7 +54,7 @@ class SelectableValidator extends ConstraintValidator //Check type of value. Validating only works for StructuralDBElements if (!$value instanceof AbstractStructuralDBElement) { - throw new UnexpectedValueException($value, 'StructuralDBElement'); + throw new UnexpectedValueException($value, AbstractStructuralDBElement::class); } //Check if the value is not selectable -> show error message then. diff --git a/src/Validator/Constraints/UniqueObjectCollection.php b/src/Validator/Constraints/UniqueObjectCollection.php new file mode 100644 index 00000000..6548494e --- /dev/null +++ b/src/Validator/Constraints/UniqueObjectCollection.php @@ -0,0 +1,64 @@ +. + */ +namespace App\Validator\Constraints; + +use InvalidArgumentException; +use Symfony\Component\Validator\Constraint; + +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class UniqueObjectCollection extends Constraint +{ + public const IS_NOT_UNIQUE = '7911c98d-b845-4da0-94b7-a8dac36bc55a'; + + public array|string $fields = []; + + protected const ERROR_NAMES = [ + self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE', + ]; + + public string $message = 'This value is already used.'; + public $normalizer; + + /** + * @param array|string $fields the combination of fields that must contain unique values or a set of options + */ + public function __construct( + ?array $options = null, + ?string $message = null, + ?callable $normalizer = null, + ?array $groups = null, + mixed $payload = null, + array|string|null $fields = null, + public bool $allowNull = true, + ) { + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->normalizer = $normalizer ?? $this->normalizer; + $this->fields = $fields ?? $this->fields; + + if (null !== $this->normalizer && !\is_callable($this->normalizer)) { + throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); + } + } +} diff --git a/src/Validator/Constraints/UniqueObjectCollectionValidator.php b/src/Validator/Constraints/UniqueObjectCollectionValidator.php new file mode 100644 index 00000000..b80889a4 --- /dev/null +++ b/src/Validator/Constraints/UniqueObjectCollectionValidator.php @@ -0,0 +1,114 @@ +. + */ +namespace App\Validator\Constraints; + +use App\Validator\UniqueValidatableInterface; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @see \App\Tests\Validator\Constraints\UniqueObjectCollectionValidatorTest + */ +class UniqueObjectCollectionValidator extends ConstraintValidator +{ + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof UniqueObjectCollection) { + throw new UnexpectedTypeException($constraint, UniqueObjectCollection::class); + } + + $fields = (array) $constraint->fields; + + if (null === $value) { + return; + } + + if (!\is_array($value) && !$value instanceof \IteratorAggregate) { + throw new UnexpectedValueException($value, 'array|IteratorAggregate'); + } + + $collectionElements = []; + $normalizer = $this->getNormalizer($constraint); + foreach ($value as $key => $object) { + + if (!$object instanceof UniqueValidatableInterface) { + throw new UnexpectedValueException($object, UniqueValidatableInterface::class); + } + + //Convert the object to an array using the helper function + $element = $object->getComparableFields(); + + if ($fields && !$element = $this->reduceElementKeys($fields, $element, $constraint)) { + continue; + } + + $element = $normalizer($element); + + if (\in_array($element, $collectionElements, true)) { + + $violation = $this->context->buildViolation($constraint->message); + + //Use the first supplied field as the target field, or the first defined field name of the element if none is supplied + $target_field = $constraint->fields[0] ?? array_keys($element)[0]; + + $violation->atPath('[' . $key . ']' . '.' . $target_field); + + $violation->setParameter('{{ object }}', $this->formatValue($object, ConstraintValidator::OBJECT_TO_STRING)) + ->setCode(UniqueObjectCollection::IS_NOT_UNIQUE) + ->addViolation(); + + return; + } + $collectionElements[] = $element; + } + } + + private function getNormalizer(UniqueObjectCollection $unique): callable + { + return $unique->normalizer ?? static fn($value) => $value; + } + + private function reduceElementKeys(array $fields, array $element, UniqueObjectCollection $constraint): array + { + $output = []; + foreach ($fields as $field) { + if (!\is_string($field)) { + throw new UnexpectedTypeException($field, 'string'); + } + if (\array_key_exists($field, $element)) { + //Ignore null values if specified + if ($element[$field] === null && $constraint->allowNull) { + continue; + } + + $output[$field] = $element[$field]; + } + } + + return $output; + } + +} diff --git a/src/Validator/Constraints/UrlOrBuiltin.php b/src/Validator/Constraints/UrlOrBuiltin.php index d2c5715f..ceec5d07 100644 --- a/src/Validator/Constraints/UrlOrBuiltin.php +++ b/src/Validator/Constraints/UrlOrBuiltin.php @@ -26,14 +26,13 @@ use App\Entity\Attachments\Attachment; use Symfony\Component\Validator\Constraints\Url; /** - * Constraints the field that way that the content is either a url or a path to a builtin ressource (like %FOOTPRINTS%). - * - * @Annotation + * Constraints the field that way that the content is either an url or a path to a builtin ressource (like %FOOTPRINTS%). */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class UrlOrBuiltin extends Url { /** * @var array A list of the placeholders that are treated as builtin */ - public $allowed_placeholders = Attachment::BUILTIN_PLACEHOLDER; + public array $allowed_placeholders = Attachment::BUILTIN_PLACEHOLDER; } diff --git a/src/Validator/Constraints/UrlOrBuiltinValidator.php b/src/Validator/Constraints/UrlOrBuiltinValidator.php index b8ad3b6a..71407a6a 100644 --- a/src/Validator/Constraints/UrlOrBuiltinValidator.php +++ b/src/Validator/Constraints/UrlOrBuiltinValidator.php @@ -34,6 +34,7 @@ use function is_object; * The validator for UrlOrBuiltin. * It checks if the value is either a builtin ressource or a valid url. * In both cases it is not checked, if the ressource is really existing. + * @see \App\Tests\Validator\Constraints\UrlOrBuiltinValidatorTest */ class UrlOrBuiltinValidator extends UrlValidator { @@ -57,7 +58,7 @@ class UrlOrBuiltinValidator extends UrlValidator //After the %PLACEHOLDER% comes a slash, so we can check if we have a placholder via explode $tmp = explode('/', $value); //Builtins must have a %PLACEHOLDER% construction - if (in_array($tmp[0], $constraint->allowed_placeholders, false)) { + if (in_array($tmp[0], $constraint->allowed_placeholders, true)) { return; } diff --git a/src/Validator/Constraints/ValidFileFilter.php b/src/Validator/Constraints/ValidFileFilter.php index 8a7b70d0..d962c0ea 100644 --- a/src/Validator/Constraints/ValidFileFilter.php +++ b/src/Validator/Constraints/ValidFileFilter.php @@ -24,9 +24,7 @@ namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** - * @Annotation - */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class ValidFileFilter extends Constraint { } diff --git a/src/Validator/Constraints/ValidFileFilterValidator.php b/src/Validator/Constraints/ValidFileFilterValidator.php index ccce30ce..2a90a010 100644 --- a/src/Validator/Constraints/ValidFileFilterValidator.php +++ b/src/Validator/Constraints/ValidFileFilterValidator.php @@ -32,11 +32,8 @@ use function is_string; class ValidFileFilterValidator extends ConstraintValidator { - protected FileTypeFilterTools $filterTools; - - public function __construct(FileTypeFilterTools $filterTools) + public function __construct(protected FileTypeFilterTools $filterTools) { - $this->filterTools = $filterTools; } /** @@ -45,7 +42,7 @@ class ValidFileFilterValidator extends ConstraintValidator * @param mixed $value The value that should be validated * @param Constraint $constraint The constraint for the validation */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof ValidFileFilter) { throw new UnexpectedTypeException($constraint, ValidFileFilter::class); diff --git a/src/Validator/Constraints/ValidGoogleAuthCode.php b/src/Validator/Constraints/ValidGoogleAuthCode.php index 0956b961..180d346e 100644 --- a/src/Validator/Constraints/ValidGoogleAuthCode.php +++ b/src/Validator/Constraints/ValidGoogleAuthCode.php @@ -22,8 +22,20 @@ declare(strict_types=1); namespace App\Validator\Constraints; +use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; use Symfony\Component\Validator\Constraint; class ValidGoogleAuthCode extends Constraint { + /** + * @param TwoFactorInterface|null $user The user to use for the validation process, if null, the current user is used + */ + public function __construct( + ?array $options = null, + ?array $groups = null, + mixed $payload = null, + public ?TwoFactorInterface $user = null) + { + parent::__construct($options, $groups, $payload); + } } diff --git a/src/Validator/Constraints/ValidGoogleAuthCodeValidator.php b/src/Validator/Constraints/ValidGoogleAuthCodeValidator.php index bb66f395..25afe57b 100644 --- a/src/Validator/Constraints/ValidGoogleAuthCodeValidator.php +++ b/src/Validator/Constraints/ValidGoogleAuthCodeValidator.php @@ -22,10 +22,9 @@ declare(strict_types=1); namespace App\Validator\Constraints; -use App\Entity\UserSystem\User; -use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator; +use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface; -use Symfony\Component\Form\FormInterface; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -34,13 +33,13 @@ use Symfony\Component\Validator\Exception\UnexpectedValueException; use function is_string; use function strlen; +/** + * @see \App\Tests\Validator\Constraints\ValidGoogleAuthCodeValidatorTest + */ class ValidGoogleAuthCodeValidator extends ConstraintValidator { - protected GoogleAuthenticatorInterface $googleAuthenticator; - - public function __construct(GoogleAuthenticatorInterface $googleAuthenticator) + public function __construct(private readonly GoogleAuthenticatorInterface $googleAuthenticator, private readonly Security $security) { - $this->googleAuthenticator = $googleAuthenticator; } public function validate($value, Constraint $constraint): void @@ -59,23 +58,24 @@ class ValidGoogleAuthCodeValidator extends ConstraintValidator if (!ctype_digit($value)) { $this->context->addViolation('validator.google_code.only_digits_allowed'); + return; } //Number must have 6 digits if (6 !== strlen($value)) { $this->context->addViolation('validator.google_code.wrong_digit_count'); + return; } - //Try to retrieve the user we want to check - if ($this->context->getObject() instanceof FormInterface && - $this->context->getObject()->getParent() instanceof FormInterface - && $this->context->getObject()->getParent()->getData() instanceof User) { - $user = $this->context->getObject()->getParent()->getData(); + //Use the current user to check the code + $user = $constraint->user ?? $this->security->getUser(); + if (!$user instanceof TwoFactorInterface) { + throw new UnexpectedValueException($user, TwoFactorInterface::class); + } - //Check if the given code is valid - if (!$this->googleAuthenticator->checkCode($user, $value)) { - $this->context->addViolation('validator.google_code.wrong_code'); - } + //Check if the given code is valid + if (!$this->googleAuthenticator->checkCode($user, $value)) { + $this->context->addViolation('validator.google_code.wrong_code'); } } } diff --git a/src/Validator/Constraints/ValidPartLot.php b/src/Validator/Constraints/ValidPartLot.php index 3b9658ac..a82c6a10 100644 --- a/src/Validator/Constraints/ValidPartLot.php +++ b/src/Validator/Constraints/ValidPartLot.php @@ -27,9 +27,8 @@ use Symfony\Component\Validator\Constraint; /** * A constraint "dummy" to validate the PartLot. * We need to access services in our Validator, so we can not use a simple callback on PartLot. - * - * @Annotation */ +#[\Attribute(\Attribute::TARGET_CLASS)] class ValidPartLot extends Constraint { public function getTargets(): string diff --git a/src/Validator/Constraints/ValidPartLotValidator.php b/src/Validator/Constraints/ValidPartLotValidator.php index d77ecd0e..316fedea 100644 --- a/src/Validator/Constraints/ValidPartLotValidator.php +++ b/src/Validator/Constraints/ValidPartLotValidator.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Validator\Constraints; use App\Entity\Parts\PartLot; -use App\Entity\Parts\Storelocation; +use App\Entity\Parts\StorageLocation; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\Exception\UnexpectedTypeException; @@ -32,11 +32,8 @@ use Symfony\Component\Validator\ConstraintValidator; class ValidPartLotValidator extends ConstraintValidator { - protected EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $em) + public function __construct(protected EntityManagerInterface $em) { - $this->em = $em; } /** @@ -45,7 +42,7 @@ class ValidPartLotValidator extends ConstraintValidator * @param mixed $value The value that should be validated * @param Constraint $constraint The constraint for the validation */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof ValidPartLot) { throw new UnexpectedTypeException($constraint, ValidPartLot::class); @@ -56,8 +53,8 @@ class ValidPartLotValidator extends ConstraintValidator } //We can only validate the values if we know the storelocation - if ($value->getStorageLocation()) { - $repo = $this->em->getRepository(Storelocation::class); + if ($value->getStorageLocation() instanceof StorageLocation) { + $repo = $this->em->getRepository(StorageLocation::class); //We can only determine associated parts, if the part have an ID //When the storage location is new (no ID), we can just assume there are no other parts if (null !== $value->getID() && $value->getStorageLocation()->getID()) { diff --git a/src/Validator/Constraints/ValidPermission.php b/src/Validator/Constraints/ValidPermission.php index 58a4ad2c..4d05f6cf 100644 --- a/src/Validator/Constraints/ValidPermission.php +++ b/src/Validator/Constraints/ValidPermission.php @@ -28,8 +28,8 @@ use Symfony\Component\Validator\Constraint; * A PermissionEmbed object with this annotation will be checked with ValidPermissionValidator. * That means the alsoSet values of the permission operations are set. * - * @Annotation */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class ValidPermission extends Constraint { } diff --git a/src/Validator/Constraints/ValidPermissionValidator.php b/src/Validator/Constraints/ValidPermissionValidator.php index 9b31048f..afb7721b 100644 --- a/src/Validator/Constraints/ValidPermissionValidator.php +++ b/src/Validator/Constraints/ValidPermissionValidator.php @@ -22,20 +22,22 @@ declare(strict_types=1); namespace App\Validator\Constraints; +use App\Controller\GroupController; +use App\Controller\UserController; use App\Security\Interfaces\HasPermissionsInterface; use App\Services\UserSystem\PermissionManager; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; +use function Symfony\Component\Translation\t; + class ValidPermissionValidator extends ConstraintValidator { - protected PermissionManager $resolver; - protected array $perm_structure; - - public function __construct(PermissionManager $resolver) + public function __construct(protected PermissionManager $resolver, protected RequestStack $requestStack) { - $this->resolver = $resolver; } /** @@ -44,7 +46,7 @@ class ValidPermissionValidator extends ConstraintValidator * @param mixed $value The value that should be validated * @param Constraint $constraint The constraint for the validation */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof ValidPermission) { throw new UnexpectedTypeException($constraint, ValidPermission::class); @@ -53,6 +55,26 @@ class ValidPermissionValidator extends ConstraintValidator /** @var HasPermissionsInterface $perm_holder */ $perm_holder = $this->context->getObject(); - $this->resolver->ensureCorrectSetOperations($perm_holder); + $changed = $this->resolver->ensureCorrectSetOperations($perm_holder); + + //Sending a flash message if the permissions were fixed (only if called from UserController or GroupController) + //This is pretty hacky and bad design but I dont see a better way without a complete rewrite of how permissions are validated + //on the admin pages + if ($changed) { + //Check if this was called in context of UserController + $request = $this->requestStack->getMainRequest(); + if ($request === null) { + return; + } + //Determine the controller class (the part before the ::) + $controller_class = explode('::', (string) $request->attributes->get('_controller'))[0]; + + if (in_array($controller_class, [UserController::class, GroupController::class], true)) { + /** @var Session $session */ + $session = $this->requestStack->getSession(); + $flashBag = $session->getFlashBag(); + $flashBag->add('warning', t('user.edit.flash.permissions_fixed')); + } + } } } diff --git a/src/Validator/Constraints/ValidTheme.php b/src/Validator/Constraints/ValidTheme.php index 70a32a20..92a19f5a 100644 --- a/src/Validator/Constraints/ValidTheme.php +++ b/src/Validator/Constraints/ValidTheme.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * A constraint to validate the theme setting of the user. - * @Annotation */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] class ValidTheme extends Constraint { public string $message = 'validator.selected_theme_is_invalid'; -} \ No newline at end of file +} diff --git a/src/Validator/Constraints/ValidThemeValidator.php b/src/Validator/Constraints/ValidThemeValidator.php index ec437b87..713be9a5 100644 --- a/src/Validator/Constraints/ValidThemeValidator.php +++ b/src/Validator/Constraints/ValidThemeValidator.php @@ -1,4 +1,7 @@ . */ - namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; +/** + * @see \App\Tests\Validator\Constraints\ValidThemeValidatorTest + */ class ValidThemeValidator extends ConstraintValidator { - private array $available_themes; - - public function __construct(array $available_themes) + public function __construct(private readonly array $available_themes) { - $this->available_themes = $available_themes; } - public function validate($value, Constraint $constraint) + public function validate($value, Constraint $constraint): void { if (!$constraint instanceof ValidTheme) { throw new UnexpectedTypeException($constraint, ValidTheme::class); @@ -51,4 +53,4 @@ class ValidThemeValidator extends ConstraintValidator ->addViolation(); } } -} \ No newline at end of file +} diff --git a/src/Validator/Constraints/Year2038BugWorkaround.php b/src/Validator/Constraints/Year2038BugWorkaround.php new file mode 100644 index 00000000..04a07908 --- /dev/null +++ b/src/Validator/Constraints/Year2038BugWorkaround.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * Datetime interfaces properties with this constraint are limited to the year 2038 on 32-bit systems, to prevent a + * Year 2038 bug during rendering. + * + * Current PHP versions can not format dates after 2038 on 32-bit systems and throw an exception. + * (See https://github.com/Part-DB/Part-DB-server/discussions/548). + * + * This constraint does not fix that problem, but can prevent users from entering such invalid dates. + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class Year2038BugWorkaround extends Constraint +{ + public string $message = 'validator.year_2038_bug_on_32bit'; +} \ No newline at end of file diff --git a/src/Validator/Constraints/Year2038BugWorkaroundValidator.php b/src/Validator/Constraints/Year2038BugWorkaroundValidator.php new file mode 100644 index 00000000..747721f9 --- /dev/null +++ b/src/Validator/Constraints/Year2038BugWorkaroundValidator.php @@ -0,0 +1,74 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Validator\Constraints; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +class Year2038BugWorkaroundValidator extends ConstraintValidator +{ + + public function __construct( + #[Autowire(env: "DISABLE_YEAR2038_BUG_CHECK")] + private readonly bool $disable_validation = false + ) + { + } + + public function isActivated(): bool + { + //If we are on a 32 bit system and the validation is not disabled, we should activate the validation + return !$this->disable_validation && PHP_INT_SIZE === 4; + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$this->isActivated()) { + return; + } + + //If the value is null, we don't need to validate it + if ($value === null) { + return; + } + + //Ensure that we check the correct constraint + if (!$constraint instanceof Year2038BugWorkaround) { + throw new \InvalidArgumentException('This validator can only validate Year2038Bug constraints'); + } + + //We can only validate DateTime objects + if (!$value instanceof \DateTimeInterface) { + throw new UnexpectedTypeException($value, \DateTimeInterface::class); + } + + //If we reach here the validation is active and we should forbid any date after 2038. + if ($value->diff(new \DateTime('2038-01-19 03:14:06'))->invert === 1) { + $this->context->buildViolation($constraint->message) + ->addViolation(); + } + } +} \ No newline at end of file diff --git a/src/Validator/UniqueValidatableInterface.php b/src/Validator/UniqueValidatableInterface.php new file mode 100644 index 00000000..3d954490 --- /dev/null +++ b/src/Validator/UniqueValidatableInterface.php @@ -0,0 +1,34 @@ +. + */ +namespace App\Validator; + +interface UniqueValidatableInterface +{ + /** + * This method should return an array of fields that should be used to compare the objects for the UniqueObjectCollection constraint. + * All instances of the same class should return the same fields. + * The value must be a comparable value (e.g. string, int, float, bool, null). + * @return array An array of the form ['field1' => 'value1', 'field2' => 'value2', ...] + */ + public function getComparableFields(): array; +} diff --git a/symfony.lock b/symfony.lock index a4bb23ff..c7471b73 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,9 +1,17 @@ { - "amphp/amp": { - "version": "v2.2.1" - }, - "amphp/byte-stream": { - "version": "v1.6.1" + "api-platform/core": { + "version": "3.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.2", + "ref": "696d44adc3c0d4f5d25a2f1c4f3700dd8a5c6db9" + }, + "files": [ + "config/packages/api_platform.yaml", + "config/routes/api_platform.yaml", + "src/ApiResource/.gitignore" + ] }, "beberlei/assert": { "version": "v3.2.6" @@ -20,40 +28,16 @@ "composer/package-versions-deprecated": { "version": "1.11.99.4" }, - "composer/pcre": { - "version": "1.0.0" - }, - "composer/semver": { - "version": "1.5.0" - }, - "composer/xdebug-handler": { - "version": "1.3.3" - }, "dama/doctrine-test-bundle": { - "version": "4.0", + "version": "8.0", "recipe": { "repo": "github.com/symfony/recipes-contrib", - "branch": "master", - "version": "4.0", - "ref": "56eaa387b5e48ebcc7c95a893b47dfa1ad51449c" + "branch": "main", + "version": "7.2", + "ref": "896306d79d4ee143af9eadf9b09fd34a8c391b70" }, "files": [ - "./config/packages/test/dama_doctrine_test_bundle.yaml" - ] - }, - "dnoegel/php-xdg-base-dir": { - "version": "v0.1.1" - }, - "doctrine/annotations": { - "version": "1.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "1.0", - "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" - }, - "files": [ - "./config/routes/annotations.yaml" + "./config/packages/dama_doctrine_test_bundle.yaml" ] }, "doctrine/cache": { @@ -62,9 +46,6 @@ "doctrine/collections": { "version": "v1.5.0" }, - "doctrine/common": { - "version": "v2.10.0" - }, "doctrine/data-fixtures": { "version": "v1.3.2" }, @@ -75,17 +56,17 @@ "version": "v0.5.3" }, "doctrine/doctrine-bundle": { - "version": "2.8", + "version": "2.11", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "2.4", - "ref": "013b823e7fee65890b23e40f31e6667a1ac519ac" + "version": "2.10", + "ref": "c170ded8fc587d6bd670550c43dafcf093762245" }, "files": [ - "config/packages/doctrine.yaml", - "src/Entity/.gitignore", - "src/Repository/.gitignore" + "./config/packages/doctrine.yaml", + "./src/Entity/.gitignore", + "./src/Repository/.gitignore" ] }, "doctrine/doctrine-fixtures-bundle": { @@ -149,12 +130,6 @@ "erusev/parsedown": { "version": "1.7.4" }, - "felixfbecker/advanced-json-rpc": { - "version": "v3.0.4" - }, - "felixfbecker/language-server-protocol": { - "version": "v1.4.0" - }, "florianv/exchanger": { "version": "1.4.1" }, @@ -162,28 +137,37 @@ "version": "3.5.0" }, "florianv/swap-bundle": { - "version": "5.0.0" - }, - "friendsofphp/proxy-manager-lts": { - "version": "v1.0.5" + "version": "5.0.x-dev" }, "gregwar/captcha": { "version": "v1.1.7" }, "gregwar/captcha-bundle": { - "version": "v2.0.6" - }, - "hslavich/oneloginsaml-bundle": { - "version": "v2.10.0" + "version": "v2.2.0" }, "imagine/imagine": { "version": "1.2.2" }, "jbtronics/2fa-webauthn": { - "version": "dev-master" + "version": "v2.2.1" }, - "laminas/laminas-code": { - "version": "3.4.1" + "jbtronics/dompdf-font-loader-bundle": { + "version": "v1.1.1" + }, + "jbtronics/translation-editor-bundle": { + "version": "v1.0" + }, + "knpuniversity/oauth2-client-bundle": { + "version": "2.15", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.20", + "ref": "1ff300d8c030f55c99219cc55050b97a695af3f6" + }, + "files": [ + "./config/packages/knpu_oauth2_client.yaml" + ] }, "league/html-to-markdown": { "version": "4.8.2" @@ -207,6 +191,21 @@ "monolog/monolog": { "version": "1.24.0" }, + "nbgrp/onelogin-saml-bundle": { + "version": "v1.4.0" + }, + "nelmio/cors-bundle": { + "version": "2.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "./config/packages/nelmio_cors.yaml" + ] + }, "nelmio/security-bundle": { "version": "2.4", "recipe": { @@ -219,18 +218,12 @@ "./config/packages/nelmio_security.yaml" ] }, - "netresearch/jsonmapper": { - "version": "v1.6.0" - }, "nikic/php-parser": { "version": "v4.2.1" }, "nikolaposa/version": { "version": "2.2.2" }, - "nyholm/nsa": { - "version": "1.1.0" - }, "nyholm/psr7": { "version": "1.0", "recipe": { @@ -262,7 +255,16 @@ "version": "v0.3.3" }, "php-http/discovery": { - "version": "1.7.0" + "version": "1.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.18", + "ref": "f45b5dd173a27873ab19f5e3180b2f661c21de02" + }, + "files": [ + "./config/packages/http_discovery.yaml" + ] }, "php-http/httplug": { "version": "v2.0.0" @@ -273,30 +275,6 @@ "php-http/promise": { "version": "v1.0.0" }, - "php-translation/common": { - "version": "1.0.0" - }, - "php-translation/extractor": { - "version": "1.7.1" - }, - "php-translation/symfony-bundle": { - "version": "0.12", - "recipe": { - "repo": "github.com/symfony/recipes-contrib", - "branch": "master", - "version": "0.10", - "ref": "f3ca4e4da63897d177e58da78626c20648c0e102" - }, - "files": [ - "config/packages/dev/php_translation.yaml", - "config/packages/php_translation.yaml", - "config/routes/dev/php_translation.yaml", - "config/routes/php_translation.yaml" - ] - }, - "php-translation/symfony-storage": { - "version": "1.0.1" - }, "phpdocumentor/reflection-common": { "version": "1.0.1" }, @@ -310,7 +288,16 @@ "version": "1.0.3" }, "phpstan/phpstan": { - "version": "0.12.8" + "version": "1.10", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + }, + "files": [ + "phpstan.dist.neon" + ] }, "phpstan/phpstan-doctrine": { "version": "0.12.9" @@ -318,8 +305,19 @@ "phpstan/phpstan-symfony": { "version": "0.12.4" }, - "psalm/plugin-symfony": { - "version": "v1.2.1" + "phpunit/phpunit": { + "version": "9.6", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "9.6", + "ref": "7364a21d87e658eb363c5020c072ecfdc12e2326" + }, + "files": [ + "./.env.test", + "./phpunit.xml.dist", + "./tests/bootstrap.php" + ] }, "psr/cache": { "version": "1.0.1" @@ -364,38 +362,23 @@ "version": "8.3.0" }, "scheb/2fa-bundle": { - "version": "5.13", + "version": "6.8", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "5.0", - "ref": "0a83961ef50ff91812b229a6f0caf28431d94aec" + "version": "6.0", + "ref": "1e6f68089146853a790b5da9946fc5974f6fcd49" }, "files": [ - "./config/packages/scheb_2fa.yaml", - "./config/routes/scheb_2fa.yaml" + "config/packages/scheb_2fa.yaml", + "config/routes/scheb_2fa.yaml" ] }, "sebastian/diff": { "version": "3.0.2" }, - "sensio/framework-extra-bundle": { - "version": "5.2", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.2", - "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" - }, - "files": [ - "./config/packages/sensio_framework_extra.yaml" - ] - }, "shivas/versioning-bundle": { - "version": "3.1.3" - }, - "spomky-labs/cbor-bundle": { - "version": "v2.0.3" + "version": "4.0.3" }, "symfony/apache-pack": { "version": "1.0", @@ -403,10 +386,10 @@ "repo": "github.com/symfony/recipes-contrib", "branch": "main", "version": "1.0", - "ref": "efb318193e48384eb5c5aadff15396ed698f8ffc" + "ref": "0f18b4decdf5695d692c1d0dfd65516a07a6adf1" }, "files": [ - "public/.htaccess" + "./public/.htaccess" ] }, "symfony/asset": { @@ -425,15 +408,15 @@ "version": "v4.2.3" }, "symfony/console": { - "version": "5.3", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", + "branch": "main", "version": "5.3", - "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" + "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461" }, "files": [ - "./bin/console" + "bin/console" ] }, "symfony/css-selector": { @@ -485,27 +468,28 @@ "version": "v4.2.3" }, "symfony/flex": { - "version": "1.19", + "version": "2.4", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "1.0", - "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" + "version": "2.4", + "ref": "52e9754527a15e2b79d9a610f98185a1fe46622a" }, "files": [ - ".env" + ".env", + ".env.dev" ] }, "symfony/form": { "version": "v4.2.3" }, "symfony/framework-bundle": { - "version": "5.4", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.4", - "ref": "3cd216a4d007b78d8554d44a5b1c0a446dab24fb" + "branch": "main", + "version": "6.4", + "ref": "a91c965766ad3ff2ae15981801643330eb42b6a5" }, "files": [ "config/packages/cache.yaml", @@ -533,28 +517,16 @@ "symfony/intl": { "version": "v4.2.3" }, - "symfony/lock": { - "version": "5.4", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "main", - "version": "5.2", - "ref": "8e937ff2b4735d110af1770f242c1107fdab4c8e" - }, - "files": [ - "./config/packages/lock.yaml" - ] - }, "symfony/mailer": { - "version": "5.4", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", "version": "4.3", - "ref": "2bf89438209656b85b9a49238c4467bff1b1f939" + "ref": "df66ee1f226c46f01e85c29c2f7acce0596ba35a" }, "files": [ - "config/packages/mailer.yaml" + "./config/packages/mailer.yaml" ] }, "symfony/maker-bundle": { @@ -573,12 +545,12 @@ "version": "v4.4.2" }, "symfony/monolog-bundle": { - "version": "3.7", + "version": "3.10", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", + "branch": "main", "version": "3.7", - "ref": "213676c4ec929f046dfde5ea8e97625b81bc0578" + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" }, "files": [ "./config/packages/monolog.yaml" @@ -591,18 +563,18 @@ "version": "v5.3.8" }, "symfony/phpunit-bridge": { - "version": "5.4", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "5.3", - "ref": "819d3d2ffa4590eba0b8f4f3e5e89415ee4e45c3" + "version": "6.3", + "ref": "a411a0480041243d97382cac7984f7dce7813c08" }, "files": [ - ".env.test", - "bin/phpunit", - "phpunit.xml.dist", - "tests/bootstrap.php" + "./.env.test", + "./bin/phpunit", + "./phpunit.xml.dist", + "./tests/bootstrap.php" ] }, "symfony/polyfill-ctype": { @@ -623,18 +595,9 @@ "symfony/polyfill-mbstring": { "version": "v1.10.0" }, - "symfony/polyfill-php72": { - "version": "v1.10.0" - }, - "symfony/polyfill-php73": { - "version": "v1.11.0" - }, "symfony/polyfill-php80": { "version": "v1.17.0" }, - "symfony/polyfill-php81": { - "version": "v1.23.0" - }, "symfony/process": { "version": "v4.2.3" }, @@ -644,16 +607,13 @@ "symfony/property-info": { "version": "v4.2.3" }, - "symfony/proxy-manager-bridge": { - "version": "v5.2.1" - }, "symfony/routing": { - "version": "5.4", + "version": "6.2", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "85de1d8ae45b284c3c84b668171d2615049e698f" + "branch": "main", + "version": "6.2", + "ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6" }, "files": [ "config/packages/routing.yaml", @@ -664,15 +624,16 @@ "version": "v5.3.4" }, "symfony/security-bundle": { - "version": "5.4", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "5.3", - "ref": "98f1f2b0d635908c2b40f3675da2d23b1a069d30" + "version": "6.4", + "ref": "2ae08430db28c8eb4476605894296c82a642028f" }, "files": [ - "config/packages/security.yaml" + "config/packages/security.yaml", + "config/routes/security.yaml" ] }, "symfony/security-core": { @@ -681,9 +642,6 @@ "symfony/security-csrf": { "version": "v4.2.3" }, - "symfony/security-guard": { - "version": "v4.2.3" - }, "symfony/security-http": { "version": "v4.2.3" }, @@ -693,6 +651,20 @@ "symfony/service-contracts": { "version": "v1.1.5" }, + "symfony/stimulus-bundle": { + "version": "2.16", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.13", + "ref": "6acd9ff4f7fd5626d2962109bd4ebab351d43c43" + }, + "files": [ + "./assets/bootstrap.js", + "./assets/controllers.json", + "./assets/controllers/hello_controller.js" + ] + }, "symfony/stopwatch": { "version": "v4.2.3" }, @@ -700,12 +672,12 @@ "version": "v5.1.0" }, "symfony/translation": { - "version": "5.3", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43" + "branch": "main", + "version": "6.3", + "ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b" }, "files": [ "./config/packages/translation.yaml", @@ -719,20 +691,47 @@ "version": "v4.2.3" }, "symfony/twig-bundle": { - "version": "5.4", + "version": "6.4", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.4", - "ref": "bb2178c57eee79e6be0b297aa96fc0c0def81387" + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" }, "files": [ - "config/packages/twig.yaml", - "templates/base.html.twig" + "./config/packages/twig.yaml", + "./templates/base.html.twig" + ] + }, + "symfony/uid": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558" + }, + "files": [ + "./config/packages/uid.yaml" + ] + }, + "symfony/ux-translator": { + "version": "2.9", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.9", + "ref": "bc396565cc4cab95692dd6df810553dc22e352e1" + }, + "files": [ + "./assets/translator.js", + "./config/packages/ux_translator.yaml", + "./var/translations/configuration.js", + "./var/translations/index.js" ] }, "symfony/ux-turbo": { - "version": "v2.0.1" + "version": "v2.16.0" }, "symfony/validator": { "version": "5.4", @@ -756,12 +755,12 @@ "version": "v4.2.3" }, "symfony/web-profiler-bundle": { - "version": "5.4", + "version": "6.3", "recipe": { "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "24bbc3d84ef2f427f82104f766014e799eefcc3e" + "branch": "main", + "version": "6.1", + "ref": "e42b3f0177df239add25373083a564e5ead4e13a" }, "files": [ "config/packages/web_profiler.yaml", @@ -769,18 +768,15 @@ ] }, "symfony/webpack-encore-bundle": { - "version": "1.16", + "version": "2.1", "recipe": { "repo": "github.com/symfony/recipes", "branch": "main", - "version": "1.10", - "ref": "f8fc53f1942f76679e9ee3c25fd44865355707b5" + "version": "2.0", + "ref": "082d754b3bd54b3fc669f278f1eea955cfd23cf5" }, "files": [ "assets/app.js", - "assets/bootstrap.js", - "assets/controllers.json", - "assets/controllers/hello_controller.js", "assets/styles/app.css", "config/packages/webpack_encore.yaml", "package.json", @@ -806,7 +802,7 @@ "version": "v3.0.0" }, "twig/extra-bundle": { - "version": "v3.0.0" + "version": "v3.8.0" }, "twig/html-extra": { "version": "v3.0.3" @@ -826,17 +822,18 @@ "ua-parser/uap-php": { "version": "v3.9.8" }, - "vimeo/psalm": { - "version": "3.5.1" - }, "web-auth/webauthn-symfony-bundle": { - "version": "3.3", + "version": "4.7", "recipe": { "repo": "github.com/symfony/recipes-contrib", "branch": "main", "version": "3.0", - "ref": "9926090a80c2cceeffe96e6c3312b397ea55d4a7" - } + "ref": "a5dff33bd46575bea263af94069650af7742dcb6" + }, + "files": [ + "config/packages/webauthn.yaml", + "config/routes/webauthn_routes.yaml" + ] }, "webmozart/assert": { "version": "1.4.0" diff --git a/templates/_navbar.html.twig b/templates/_navbar.html.twig index 14e40151..cd1f641f 100644 --- a/templates/_navbar.html.twig +++ b/templates/_navbar.html.twig @@ -1,12 +1,20 @@ {% import "helper.twig" as helper %} +{% import "components/search.macro.html.twig" as search %} -
{% for attachment in form %} - {{ form_widget(attachment) }} + {{ form_widget(attachment) }} {% endfor %}
- +
+ + + +
{% endmacro %} -{% macro attachment_icon(attachment, attachment_helper, class = "fa-fw fas fa-3x hoverpic", link = true) %} - {% set disabled = attachment.secure and not is_granted("show_secure", attachment) %} +{% macro attachment_icon(attachment, attachment_helper, class = "fa-fw fas fa-2x hoverpic", link = true) %} + {# @var App\Entity\Attachments\Attachment attachment #} + {% set disabled = attachment.secure and not is_granted("show_private", attachment) %} {% if not attachment_helper or attachment_helper.fileExisting(attachment) %} {% if link and not disabled %} {% endif %} {% if attachment.picture %} - + {% else %} {% endif %} diff --git a/templates/components/collection_type.macro.html.twig b/templates/components/collection_type.macro.html.twig index 1db04763..fde2b961 100644 --- a/templates/components/collection_type.macro.html.twig +++ b/templates/components/collection_type.macro.html.twig @@ -29,4 +29,13 @@ {% macro delete_btn() %} {{ stimulus_action('elements/collection_type', 'deleteElement') }} +{% endmacro %} + +{% macro new_element_indicator(value) %} + {% if value.id is not defined or value.id is null %} + + New alerts + + {% endif %} {% endmacro %} \ No newline at end of file diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig index dadfad69..5ce0f23f 100644 --- a/templates/components/datatables.macro.html.twig +++ b/templates/components/datatables.macro.html.twig @@ -1,5 +1,5 @@ {% macro datatable(datatable, controller = 'elements/datatables/datatables', state_save_tag = null) %} -
+
@@ -19,12 +19,13 @@ {% macro partsDatatableWithForm(datatable, state_save_tag = 'parts') %}
- + @@ -32,6 +33,8 @@ {# #}
+ {% trans with {'%count%': ''} %}part_list.action.part_count{% endtrans %} @@ -51,6 +54,7 @@ + @@ -75,7 +79,7 @@ {# This is left empty, as this will be filled by Javascript #} - +
diff --git a/templates/components/history_log_macros.html.twig b/templates/components/history_log_macros.html.twig index f3481305..68df2015 100644 --- a/templates/components/history_log_macros.html.twig +++ b/templates/components/history_log_macros.html.twig @@ -17,7 +17,7 @@ {{ stimulus_controller('elements/delete_btn') }} {{ stimulus_action('elements/delete_btn', "submit", "submit") }} data-delete-title="{% trans %}log.undo.confirm_title{% endtrans %}" data-delete-message="{% trans %}log.undo.confirm_message{% endtrans %}"> - + {{ datatables.logDataTable(datatable, tag) }} diff --git a/templates/components/new_version.macro.html.twig b/templates/components/new_version.macro.html.twig new file mode 100644 index 00000000..f8bc1e2e --- /dev/null +++ b/templates/components/new_version.macro.html.twig @@ -0,0 +1,9 @@ +{% macro new_version_alert(is_available, new_version, new_version_url) %} + {% if is_available %} +
+ {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/templates/_navbar_search.html.twig b/templates/components/search.macro.html.twig similarity index 56% rename from templates/_navbar_search.html.twig rename to templates/components/search.macro.html.twig index 642021da..e62af2b1 100644 --- a/templates/_navbar_search.html.twig +++ b/templates/components/search.macro.html.twig @@ -1,74 +1,105 @@ - \ No newline at end of file +{# Render a complete usable search form including the form tags. mode can be "standalone" or "navbar" #} +{% macro search_form(mode = "standalone") %} + {% set is_navbar = (mode == "navbar") %} + +
+ + {# Show the options left in navbar #} + {% if is_navbar %} + {{ _self.settings_drodown(is_navbar) }} + {% endif %} + +
+ + +
+ + {# And right in the standalone mode #} + {% if not is_navbar %} + {{ _self.settings_drodown(is_navbar) }} + {% endif %} +
+{% endmacro %} \ No newline at end of file diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index b85fcc88..12bef78f 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -28,13 +28,13 @@ {% macro treeview_sidebar(id, default_mode) %}
- - +
diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 53311061..96b71bf0 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -38,20 +38,21 @@ - {{ form_errors(form.quantity) }} {{ form_widget(form.quantity) }} + {{ form_errors(form.quantity) }} - {{ form_errors(form.part) }} {{ form_widget(form.part) }} + {{ form_errors(form.part) }} - {{ form_errors(form.name) }} {{ form_widget(form.name) }} + {{ form_errors(form.name) }} - {{ form_errors(form) }} diff --git a/templates/form/extended_bootstrap_layout.html.twig b/templates/form/extended_bootstrap_layout.html.twig index 2a599672..811f57ac 100644 --- a/templates/form/extended_bootstrap_layout.html.twig +++ b/templates/form/extended_bootstrap_layout.html.twig @@ -122,4 +122,21 @@ {% block part_select_widget %} {{ form_widget(form.autocomplete) }} +{% endblock %} + +{% block password_widget %} + {# If password_estimator setting is not set render it like normal #} + {% if password_estimator %} +
+
+ + + + {{- parent() -}} +
+ +
+ {% else %} + {{- parent() -}} + {% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/form/filter_types_layout.html.twig b/templates/form/filter_types_layout.html.twig index 70927d57..cae9e3ea 100644 --- a/templates/form/filter_types_layout.html.twig +++ b/templates/form/filter_types_layout.html.twig @@ -44,13 +44,17 @@ {{ block('text_constraint_widget') }} {% endblock %} +{% block enum_constraint_widget %} + {{ block('text_constraint_widget') }} +{% endblock %} + {% block parameter_constraint_widget %} {% import 'components/collection_type.macro.html.twig' as collection %} {{ form_widget(form.name, {"attr": {"data-pages--parameters-autocomplete-target": "name"}}) }} {{ form_widget(form.symbol, {"attr": {"data-pages--parameters-autocomplete-target": "symbol", "data-pages--latex-preview-target": "input"}}) }} {{ form_widget(form.value) }} - {{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }} + {{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }} {{ form_widget(form.value_text) }}
+ {% if show_dependency_notice %} + {% trans %}permission.legend.dependency_note{% endtrans %} + {% endif %} +
+ + {% if is_granted("@labels.read_profiles") %} + + {% endif %} + +
+
+
+ {{ form_widget(form.save_profile_name) }} + {{ form_widget(form.save_profile) }} +
+ {{ form_errors(form.save_profile_name) }} +
+
+
diff --git a/templates/label_system/labels/base_label.html.twig b/templates/label_system/labels/base_label.html.twig index da6a3e5c..494b99e4 100644 --- a/templates/label_system/labels/base_label.html.twig +++ b/templates/label_system/labels/base_label.html.twig @@ -8,17 +8,18 @@ {% for element in elements %}
- {% if options.barcodeType == 'none' %} + {% if options.barcodeType.none %} {% include "label_system/labels/label_page_none.html.twig" %} - {% elseif options.barcodeType in ['qr', 'datamatrix'] %} + {% elseif options.barcodeType.is2D() %} {% include "label_system/labels/label_page_qr.html.twig" %} - {% elseif options.barcodeType in ['code39', 'code93', 'code128'] %} + {% elseif options.barcodeType.is1D() %} {% include "label_system/labels/label_page_1d.html.twig" %} {% endif %}
diff --git a/templates/label_system/labels/label_style.css.twig b/templates/label_system/labels/label_style.css.twig index c2434a77..729e8cda 100644 --- a/templates/label_system/labels/label_style.css.twig +++ b/templates/label_system/labels/label_style.css.twig @@ -22,7 +22,7 @@ } body { - font-family: "DejaVu Sans Mono"; + font-family: "DejaVu Sans Mono", "unifont", monospace; font-size: 12px; line-height: 1.0; } @@ -37,6 +37,7 @@ hr { .qr { max-width: 80%; + max-height: 100%; } .qr-container a { diff --git a/templates/label_system/scanner/scanner.html.twig b/templates/label_system/scanner/scanner.html.twig index 39f4e140..1f978a9b 100644 --- a/templates/label_system/scanner/scanner.html.twig +++ b/templates/label_system/scanner/scanner.html.twig @@ -23,4 +23,22 @@ {{ form_end(form) }} + + {% if infoModeData %} +
+

{% trans %}label_scanner.decoded_info.title{% endtrans %}

+ + + + {% for key, value in infoModeData %} + + + + + {% endfor %} + +
{{ key }}{{ value }}
+ + {% endif %} + {% endblock %} diff --git a/templates/log_system/details/_extra_collection_element_deleted.html.twig b/templates/log_system/details/_extra_collection_element_deleted.html.twig new file mode 100644 index 00000000..221fae95 --- /dev/null +++ b/templates/log_system/details/_extra_collection_element_deleted.html.twig @@ -0,0 +1,15 @@ +{# @var entry \App\Entity\LogSystem\CollectionElementDeleted #} + +{% import "log_system/details/helper.macro.html.twig" as log_helper %} + +

+ {% trans %}log.collection_deleted.deleted{% endtrans %}: + {{ entity_type_label(entry.deletedElementClass) }} #{{ entry.deletedElementID }} + {% if entry.oldName is not empty %} + ({{ entry.oldName }}) + {% endif %} +

+

+ {% trans %}log.collection_deleted.on_collection{% endtrans %}: + {{ log_helper.translate_field(entry.collectionName) }} +

\ No newline at end of file diff --git a/templates/log_system/details/_extra_database_updated.html.twig b/templates/log_system/details/_extra_database_updated.html.twig new file mode 100644 index 00000000..7ee42c4f --- /dev/null +++ b/templates/log_system/details/_extra_database_updated.html.twig @@ -0,0 +1,23 @@ +{# @var entry \App\Entity\LogSystem\DatabaseUpdatedLogEntry #} + +{% if entry.successful %} +
+ + {% trans %}log.database_updated.success{% endtrans %} +
+{% else %} +
+ + {% trans %}log.database_updated.failed{% endtrans %} +
+{% endif %} + + + + {{ entry.oldVersion }} + + + + + {{ entry.newVersion }} + \ No newline at end of file diff --git a/templates/log_system/details/_extra_element_created.html.twig b/templates/log_system/details/_extra_element_created.html.twig new file mode 100644 index 00000000..8f7ce457 --- /dev/null +++ b/templates/log_system/details/_extra_element_created.html.twig @@ -0,0 +1,11 @@ +{# @var entry \App\Entity\LogSystem\ElementCreatedLogEntry #} + +{% import "log_system/details/helper.macro.html.twig" as log_helper %} + +{{ log_helper.comment_field(entry) }} +{% if entry.creationInstockValue %} +

+ {% trans %}log.element_created.original_instock{% endtrans %}: + {{ entry.creationInstockValue }} +

+{% endif %} \ No newline at end of file diff --git a/templates/log_system/details/_extra_element_deleted.html.twig b/templates/log_system/details/_extra_element_deleted.html.twig new file mode 100644 index 00000000..ceb9e9e2 --- /dev/null +++ b/templates/log_system/details/_extra_element_deleted.html.twig @@ -0,0 +1,8 @@ +{# @var entry \App\Entity\LogSystem\ElementDeletedLogEntry #} + +{% import "log_system/details/helper.macro.html.twig" as log_helper %} + + + +{{ log_helper.comment_field(entry) }} +{{ log_helper.data_change_table(entry) }} \ No newline at end of file diff --git a/templates/log_system/details/_extra_element_edited.html.twig b/templates/log_system/details/_extra_element_edited.html.twig new file mode 100644 index 00000000..33bd7901 --- /dev/null +++ b/templates/log_system/details/_extra_element_edited.html.twig @@ -0,0 +1,7 @@ +{# @var entry \App\Entity\LogSystem\ElementDeletedLogEntry #} + +{% import "log_system/details/helper.macro.html.twig" as log_helper %} + +{{ log_helper.comment_field(entry) }} + +{{ log_helper.data_change_table(entry) }} diff --git a/templates/log_system/details/_extra_security_event.html.twig b/templates/log_system/details/_extra_security_event.html.twig new file mode 100644 index 00000000..fa023db6 --- /dev/null +++ b/templates/log_system/details/_extra_security_event.html.twig @@ -0,0 +1,9 @@ +{# @var entry \App\Entity\LogSystem\UserLoginLogEntry #} + +IP:   + + + {{ entry.iPAddress }} + + +

{% trans %}log.user_login.ip_anonymize_hint{% endtrans %}

\ No newline at end of file diff --git a/templates/log_system/details/_extra_user_login.html.twig b/templates/log_system/details/_extra_user_login.html.twig new file mode 100644 index 00000000..4c7e8680 --- /dev/null +++ b/templates/log_system/details/_extra_user_login.html.twig @@ -0,0 +1,9 @@ +{# @var entry \App\Entity\LogSystem\UserLoginLogEntry #} + +{% trans %}log.user_login.login_from_ip{% endtrans %}:   + + + {{ entry.iPAddress }} + + +

{% trans %}log.user_login.ip_anonymize_hint{% endtrans %}

\ No newline at end of file diff --git a/templates/log_system/details/_extra_user_not_allowed.html.twig b/templates/log_system/details/_extra_user_not_allowed.html.twig new file mode 100644 index 00000000..43ab1500 --- /dev/null +++ b/templates/log_system/details/_extra_user_not_allowed.html.twig @@ -0,0 +1,4 @@ +{# @var entry \App\Entity\LogSystem\UserNotAllowedLogEntry #} + +{% trans %}log.user_not_allowed.unauthorized_access_attempt_to{% endtrans %}:  {{ entry.path }} +

{% trans %}log.user_not_allowed.hint{% endtrans %}

\ No newline at end of file diff --git a/templates/log_system/details/helper.macro.html.twig b/templates/log_system/details/helper.macro.html.twig new file mode 100644 index 00000000..e8957b66 --- /dev/null +++ b/templates/log_system/details/helper.macro.html.twig @@ -0,0 +1,149 @@ +{% macro undo_buttons(entry, target_element) %} + {# @var entry \App\Entity\LogSystem\ElementEditedLogEntry|\App\Entity\LogSystem\ElementDeletedLogEntry entry #} + {% set disabled = not is_granted('revert_element', entry.targetClass) %} + + {% if entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') + or (entry is instanceof('App\\Entity\\LogSystem\\ElementDeletedLogEntry') and entry.hasOldDataInformation) %} + + {% set icon = 'fa-trash-restore' %} + {% set title = 'log.undo.undelete'|trans %} + {% set title_short = 'log.undo.undelete.short'|trans %} + + {% elseif entry is instanceof('App\\Entity\\LogSystem\\ElementCreatedLogEntry') + or (entry is instanceof('App\\Entity\\LogSystem\\ElementEditedLogEntry') and entry.hasOldDataInformation) %} + + {% set icon = 'fa-undo' %} + {% set title = 'log.undo.undo'|trans %} + {% set title_short = 'log.undo.undo.short'|trans %} + {% endif %} + +
+ + + +
+ + + + {# View button #} + {% if target_element and ((attribute(entry, 'oldDataInformation') is defined and entry.oldDataInformation) + or entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted')) + %} + + {% set url = timetravel_url(target_element, entry.timestamp) %} + + {% if url %} + + {% trans %}log.view_version{% endtrans %} + + {% endif %} + {% endif %} +
+
+{% endmacro %} + + +{% macro comment_field(entry) %} + {# @var entry \App\Entity\Contracts\LogWithComment #} +

+ {% trans %}edit.log_comment{% endtrans %}: + {% if entry.comment %} + {{ entry.comment }} + {% else %} + {% trans %}log.no_comment{% endtrans %} + {% endif %} +

+{% endmacro %} + +{% macro translate_field(field) %} + {% set trans_key = 'log.element_edited.changed_fields.'~field %} + {# If the translation key is not found, the translation key is returned, and we dont show the translation #} + {% if trans_key|trans != trans_key %} + {{ ('log.element_edited.changed_fields.'~field) | trans }} + ({{ field }}) + {% else %} + {{ field }} + {% endif %} +{% endmacro %} + +{% macro data_change_table(entry) %} + {# @var entry \App\Entity\LogSystem\ElementEditedLogEntry|\App\Entity\LogSystem\ElementDeletedLogEntry entry #} + + {% set fields, old_data, new_data = {}, {}, {} %} + + {# For log entries where only the changed fields are saved, this is the last executed assignment #} + {% if attribute(entry, 'changedFieldInfo') is defined and entry.changedFieldsInfo %} + {% set fields = entry.changedFields %} + {% endif %} + + {# For log entries, where we know the old data, this is the last exectuted assignment #} + {% if attribute(entry, 'oldDataInformation') is defined and entry.oldDataInformation %} + {# We have to use the keys of oldData here, as changedFields might not be available #} + {% set fields = entry.oldData | keys %} + {% set old_data = entry.oldData %} + {% endif %} + + {# For log entries, where we have new data, we define it #} + {% if attribute(entry, 'newDataInformation') is defined and entry.newDataInformation %} + {# We have to use the keys of oldData here, as changedFields might not be available #} + {% set fields = entry.newData | keys %} + {% set new_data = entry.newData %} + {% endif %} + + {% if fields is not empty %} + + + + + {% if old_data is not empty %} + + {% endif %} + {% if new_data is not empty %} + + {% endif %} + {% if new_data is not empty and old_data is not empty %} {# Diff column #} + + {% endif %} + + + + {% for field in fields %} + + + {% if old_data is not empty %} + + {% endif %} + {% if new_data is not empty %} + + {% endif %} + + {% if new_data is not empty and old_data is not empty %} + + {% endif %} + + {% endfor %} + +
{% trans %}log.element_changed.field{% endtrans %}{% trans %}log.element_changed.data_before{% endtrans %}{% trans %}log.element_changed.data_after{% endtrans %}{% trans %}log.element_changed.diff{% endtrans %}
+ {{ _self.translate_field(field) }} + + {% if old_data[field] is defined %} + {{ format_log_data(old_data[field], entry, field) }} + {% endif %} + + {% if new_data[field] is defined %} + {{ format_log_data(new_data[field], entry, field) }} + {% endif %} + + {% if new_data[field] is defined and old_data[field] is defined %} + {{ format_log_diff(old_data[field], new_data[field]) }} + {% endif %} +
+ {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/templates/log_system/details/log_details.html.twig b/templates/log_system/details/log_details.html.twig new file mode 100644 index 00000000..aff127f4 --- /dev/null +++ b/templates/log_system/details/log_details.html.twig @@ -0,0 +1,117 @@ +{% extends "main_card.html.twig" %} + +{% import "helper.twig" as helper %} +{% import "log_system/details/helper.macro.html.twig" as log_helper %} + +{% block title %} + {% trans %}log.details.title{% endtrans %}: + {{ ('log.type.' ~ log_entry.type) | trans }} ({{ log_entry.timestamp | format_datetime('short') }}) +{% endblock %} + +{% block card_title %} + + {% trans %}log.details.title{% endtrans %}: + {{ ('log.type.' ~ log_entry.type) | trans }} ({{ log_entry.timestamp | format_datetime('short') }}) + ID: {{ log_entry.iD }} +{% endblock %} + +{% block card_body %} + + + + + + + + + + + + + + + + + + + + +
{% trans %}log.timestamp{% endtrans %}{{ log_entry.timestamp | format_datetime('full') }}
{% trans %}log.type{% endtrans %} + {{ ('log.type.' ~ log_entry.type) | trans }} + {% if log_entry.type == 'part_stock_changed' %} + ({{ ('log.part_stock_changed.' ~ log_entry.instockChangeType.value)|trans }}) + {% endif %} + + {% if log_entry is instanceof('App\\Entity\\Contracts\\LogWithEventUndoInterface') and log_entry.undoEvent %} + ({{ ('log.undo_mode.' ~ log_entry.undoMode.value)|trans }}: #{{ log_entry.UndoEventID }}) + {% endif %} +
{% trans %}log.level{% endtrans %} + + {{ ('log.level.'~ log_entry.levelString)|trans }} +
{% trans %}log.user{% endtrans %} + + {% if log_entry.cLIEntry %} + + {{ log_entry.cLIUsername }} ({% trans %}log.cli_user{% endtrans %}) + {% else %} + {% if log_entry.user %} + {{ helper.user_icon_link(log_entry.user) }} (@{{ log_entry.user.username }}) + {% else %} + @{{ log_entry.username }} ({% trans %}log.target_deleted{% endtrans %}) + {% endif %} + {% endif %} +
{% trans %}log.target{% endtrans %}{{ target_html|raw }}
+ +
+ +
+
+ {% if log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') + or log_entry is instanceof('App\\Entity\\LogSystem\\ElementDeletedLogEntry') + or log_entry is instanceof('App\\Entity\\LogSystem\\ElementCreatedLogEntry') + or log_entry is instanceof('App\\Entity\\LogSystem\\ElementEditedLogEntry') + %} + {{ log_helper.undo_buttons(log_entry, target_element) }} + {% endif %} +
+ +
+
+ + + + + +
+
+
+ + {# This assignment is to improve autocomplete on the subpages, as PHPstorm ignores typehints for log_entry #} + {% set entry = log_entry %} + {% if log_entry is instanceof('App\\Entity\\LogSystem\\DatabaseUpdatedLogEntry') %} + {% include "log_system/details/_extra_database_updated.html.twig" %} + {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementCreatedLogEntry') %} + {% include "log_system/details/_extra_element_created.html.twig" %} + {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementEditedLogEntry') %} + {% include "log_system/details/_extra_element_edited.html.twig" %} + {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementDeletedLogEntry') %} + {% include "log_system/details/_extra_element_deleted.html.twig" %} + {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\UserLoginLogEntry') + or log_entry is instanceof('App\\Entity\\LogSystem\\UserLogoutLogEntry') %} + {% include "log_system/details/_extra_user_login.html.twig" %} + {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\UserNotAllowedLogEntry') %} + {% include "log_system/details/_extra_user_not_allowed.html.twig" %} + {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\SecurityEventLogEntry') %} + {% include "log_system/details/_extra_security_event.html.twig" %} + {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\CollectionElementDeleted') %} + {% include "log_system/details/_extra_collection_element_deleted.html.twig" %} + {% else %} + {{ extra_html | raw }} + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/parts/edit/_associated_parts.html.twig b/templates/parts/edit/_associated_parts.html.twig new file mode 100644 index 00000000..9ed0fd88 --- /dev/null +++ b/templates/parts/edit/_associated_parts.html.twig @@ -0,0 +1,18 @@ +{% form_theme form with ['parts/edit/edit_form_styles.html.twig'] %} +{% import 'components/collection_type.macro.html.twig' as collection %} + +
+ + + {% for assoc in form.associated_parts_as_owner %} + {{ form_widget(assoc) }} + {% endfor %} + +
+ + +
\ No newline at end of file diff --git a/templates/parts/edit/_eda.html.twig b/templates/parts/edit/_eda.html.twig new file mode 100644 index 00000000..4df675c4 --- /dev/null +++ b/templates/parts/edit/_eda.html.twig @@ -0,0 +1,24 @@ +{{ form_row(form.eda_info.reference_prefix) }} +{{ form_row(form.eda_info.value) }} + +
+
+ {{ form_row(form.eda_info.visibility) }} +
+
+ +
+
+ {{ form_widget(form.eda_info.exclude_from_bom) }} + {{ form_widget(form.eda_info.exclude_from_board) }} + {{ form_widget(form.eda_info.exclude_from_sim) }} +
+
+ +
+
+
{% trans %}eda_info.kicad_section.title{% endtrans %}:
+
+
+{{ form_row(form.eda_info.kicad_symbol) }} +{{ form_row(form.eda_info.kicad_footprint) }} \ No newline at end of file diff --git a/templates/parts/edit/edit_form_styles.html.twig b/templates/parts/edit/edit_form_styles.html.twig index 7ff81b77..c2a89b6a 100644 --- a/templates/parts/edit/edit_form_styles.html.twig +++ b/templates/parts/edit/edit_form_styles.html.twig @@ -14,9 +14,10 @@ {{ form_widget(form.price_related_quantity, {'attr': {'class': 'form-control-sm'}}) }} {{ form_errors(form.price_related_quantity) }} - {{ form_errors(form) }} @@ -57,8 +58,9 @@
- {{ form_errors(form) }} @@ -73,12 +75,14 @@ {{ form_widget(form.value_min) }}{{ form_errors(form.value_min) }} {{ form_widget(form.value_typical) }}{{ form_errors(form.value_typical) }} {{ form_widget(form.value_max) }}{{ form_errors(form.value_max) }} - {{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.unit) }} + {{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.unit) }} {{ form_widget(form.value_text) }}{{ form_errors(form.value_text) }} {{ form_widget(form.group) }}{{ form_errors(form.group) }} - {{ form_errors(form) }} @@ -89,12 +93,29 @@ {% import 'components/collection_type.macro.html.twig' as collection %} - {{ form_widget(form) }} + {{ form_row(form.description) }} + {{ form_row(form.storage_location) }} + {{ form_row(form.amount) }} + {{ form_row(form.instock_unknown) }} + {{ form_row(form.needs_refill) }} + {{ form_row(form.expiration_date) }} + + {% set id = 'collapse_' ~ random() %} + + +
+ {{ form_row(form.comment) }} + {{ form_row(form.owner) }} + {{ form_row(form.user_barcode) }} +
- {{ form_errors(form) }} @@ -124,41 +145,39 @@ - {% set attach = form.vars.value %} + {# @var \App\Entity\Attachments\Attachment attach #} {% if attach is not null %} - {% if attachment_manager.fileExisting(attach) %} - {% if not attach.external %} -

-
+ {% if not attach.hasInternal() and attach.external %} +
- {{ attach.filename }} + {% trans %}attachment.external_only{% endtrans %} -
- +
+ {% elseif attachment_manager.isInternalFileExisting(attach) %} +
+
+ {{ attach.filename|u.truncate(25, ' ...') }} +
+
+
{{ attachment_manager.humanFileSize(attach) }} - -
- {% else %} -

-
- - {% trans %}attachment.external{% endtrans %} - -
- {% endif %} +
+ {% if attach.secure %} -
+
{% trans %}attachment.secure{% endtrans %} -
+ {% endif %} {% if attach.secure and not is_granted('show_private', attach) %} @@ -168,19 +187,43 @@ {% trans %}attachment.preview.alt{% endtrans %} {% else %} - {% trans %}attachment.view{% endtrans %} + {% trans %}attachment.view_local{% endtrans %} {% endif %} {% else %} -

-
+
{% trans %}attachment.file_not_found{% endtrans %} -
+ + {% endif %} + {% if attach.external %} + {% endif %} {% endif %} +{% endblock %} + +{% block part_association_widget %} + {% import 'components/collection_type.macro.html.twig' as collection %} + + +
+ {{ form_widget(form) }} +
+ + + + {{ form_errors(form) }} + + {% endblock %} \ No newline at end of file diff --git a/templates/parts/edit/edit_part_info.html.twig b/templates/parts/edit/edit_part_info.html.twig index 51b5d865..20cddbd7 100644 --- a/templates/parts/edit/edit_part_info.html.twig +++ b/templates/parts/edit/edit_part_info.html.twig @@ -58,6 +58,18 @@ {% trans %}part.edit.tab.specifications{% endtrans %} + + - \ No newline at end of file + + +{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }} \ No newline at end of file diff --git a/templates/projects/info/_builds.html.twig b/templates/projects/info/_builds.html.twig index 5418a614..fc49b50d 100644 --- a/templates/projects/info/_builds.html.twig +++ b/templates/projects/info/_builds.html.twig @@ -28,7 +28,7 @@
- +
diff --git a/templates/projects/info/_info.html.twig b/templates/projects/info/_info.html.twig index 546c433b..b95be253 100644 --- a/templates/projects/info/_info.html.twig +++ b/templates/projects/info/_info.html.twig @@ -6,10 +6,10 @@
{% if project.masterPictureAttachment %} - + {% else %} - Part main image + Part main image {% endif %}
diff --git a/templates/security/2fa_base_form.html.twig b/templates/security/2fa_base_form.html.twig index 8190e7a1..847048e4 100644 --- a/templates/security/2fa_base_form.html.twig +++ b/templates/security/2fa_base_form.html.twig @@ -7,7 +7,7 @@ {% block content %} {% if authenticationError %} {% endif %} diff --git a/templates/tools/server_infos/_db.html.twig b/templates/tools/server_infos/_db.html.twig index 0c0e60a2..828783fe 100644 --- a/templates/tools/server_infos/_db.html.twig +++ b/templates/tools/server_infos/_db.html.twig @@ -21,5 +21,13 @@ Database User {{ db_user }} + + Natural sort method + {{ db_natsort_method }} + + + Slow natural sort allowed + {{ helper.boolean_badge(db_natsort_slow_allowed) }} + \ No newline at end of file diff --git a/templates/tools/server_infos/_partdb.html.twig b/templates/tools/server_infos/_partdb.html.twig index 52d19c93..ca2b82a8 100644 --- a/templates/tools/server_infos/_partdb.html.twig +++ b/templates/tools/server_infos/_partdb.html.twig @@ -11,7 +11,7 @@ Symfony environment - {{ enviroment }} (Debug: {{ helper.boolean_badge(is_debug) }}) + {{ environment }} (Debug: {{ helper.boolean_badge(is_debug) }}) Part-DB Instance name @@ -42,8 +42,8 @@ {{ helper.boolean_badge(demo_mode) }} - GPDR Compliance Mode - {{ helper.boolean_badge(gpdr_compliance) }} + GDPR Compliance Mode + {{ helper.boolean_badge(gdpr_compliance) }} Users diff --git a/templates/tools/server_infos/_php.html.twig b/templates/tools/server_infos/_php.html.twig index ae485639..ea5a0b4c 100644 --- a/templates/tools/server_infos/_php.html.twig +++ b/templates/tools/server_infos/_php.html.twig @@ -9,6 +9,12 @@ Server Operating System {{ php_uname }} + + PHP Bit Size + {{ php_bit_size }}-bit + {% if php_bit_size < 64 %}(32-bit PHP is affected by Year 2038 problem, and can not work with dates after 2038!){% endif %} + + Opcache enabled {{ helper.boolean_badge(php_opcache_enabled) }} @@ -25,5 +31,19 @@ Server time {{ "now" | format_datetime("long", "long") }} + + Symfony Kernel Runtime + {{ kernel_runtime }} + + + Symfony Kernel Runtime Environment + {{ kernel_runtime_environment }} + + + Symfony Kernel Runtime mode + {% for key, value in kernel_runtime_mode %} + {{ key }}: {{ value }}
+ {% endfor %} + \ No newline at end of file diff --git a/templates/tools/server_infos/server_infos.html.twig b/templates/tools/server_infos/server_infos.html.twig index fd3d2759..e4e02d32 100644 --- a/templates/tools/server_infos/server_infos.html.twig +++ b/templates/tools/server_infos/server_infos.html.twig @@ -1,4 +1,5 @@ {% extends "main_card.html.twig" %} +{% import "components/new_version.macro.html.twig" as nv %} {% block title %}{% trans %}tools.server_infos.title{% endtrans %}{% endblock %} @@ -6,6 +7,12 @@ {% trans %}tools.server_infos.title{% endtrans %} {% endblock %} +{% block before_card %} + {% if is_granted('@system.show_updates') %} + {{ nv.new_version_alert(new_version_available, new_version, new_version_url) }} + {% endif %} +{% endblock %} + {% block card_content %}