Compare commits

..

84 Commits

Author SHA1 Message Date
Jan Böhmer
0234463b68 Bumped version to 1.14.4 2024-11-08 23:32:34 +01:00
Jan Böhmer
ef412eef92 Fixed tests 2024-11-08 23:32:14 +01:00
Marc
76ebd22eab Fixed Typos and mistranslations in GDPR mode (DSGVO Modus) (#757)
Fixed Typo enviroment

Co-authored-by: Marc Kreidler <kreidler@nedo.com>
2024-11-08 23:22:18 +01:00
Jan Böhmer
5b0ca8e346 Updated dependencies 2024-11-08 23:15:40 +01:00
Jan Böhmer
0b6b10c27b Bumped version to 1.14.3 2024-11-05 00:11:39 +01:00
Jan Böhmer
6225d2c9b3 Implemented an easy to use APIFilter for tags
This makes the process of filters more easily and intuitive. This fixes issue #750
2024-11-05 00:07:25 +01:00
Jan Böhmer
01fc6524a4 Added a aggregate function for storelocation sorting to avoid exceptions on Postgres
Actually this was not good on other DB types too, but they just ignored the problems.

This fixes issue #734
2024-11-04 23:46:45 +01:00
Jan Böhmer
2575e6a160 Improved size and position of back to top button to avoid overlapping with buttons, etc.
This should fix issue #737
2024-11-03 23:58:06 +01:00
Jan Böhmer
484ba5ebd7 If user password set command is run in non-interactive mode, show a warning message if no password is inputted
Related to issue #748
2024-11-03 23:39:04 +01:00
Jan Böhmer
b42d98e9f8 Increase font-weight of the <dl> element in part info page to match its look to the <h5> element 2024-11-03 23:21:58 +01:00
Sam Edwards
65b2f045ac Responsive tweaks (#755)
* Change datatables markup to be responsive with BS5

* Responsive tweaks to single part info
2024-11-03 23:14:52 +01:00
Jan Böhmer
5e76451d46 Try to guess the character encodings and convert it to UTF-8 on importing
This should fix issue #749
2024-11-03 22:27:24 +01:00
Jan Böhmer
a873ad3316 Replace all unicode characters with ASCII chars in FilenameSanatizer to make filenames more sanatized 2024-11-03 22:06:42 +01:00
Jan Böhmer
b1e03f49ee Pass the docker envs for oemsecrets to Part-DB
This fixes issue #742
2024-11-03 21:46:53 +01:00
Jan Böhmer
011e23f8e6 Added polish to language selector 2024-11-03 20:04:43 +01:00
Jan Böhmer
646cd8cf22 Merge remote-tracking branch 'origin/l10n_master' 2024-11-03 20:03:28 +01:00
Jan Böhmer
52ac8a70d5 Updated dependencies 2024-11-03 20:03:24 +01:00
Jan Böhmer
e020334b73 New translations messages.en.xlf (Polish) 2024-10-19 22:40:21 +02:00
Jan Böhmer
7698e83f0b New translations messages.en.xlf (Polish) 2024-10-19 21:40:19 +02:00
Jan Böhmer
dd56f5e0c8 New translations messages.en.xlf (Polish) 2024-10-19 19:30:24 +02:00
Jan Böhmer
92c32eef74 New Crowdin updates (#738)
* New translations validators.en.xlf (Polish)

* New translations messages.en.xlf (Polish)
2024-10-19 19:24:55 +02:00
Jan Böhmer
08770c7dc5 Bumped version to 1.14.2 2024-10-17 00:20:48 +02:00
Priit Laes
808a94e4df Document APP_SECRET and PostgreSQL specific bits in configuration variables (#727)
* docs: Mention APP_SECRET

* docs: Add PostgreSQL specific bits to DATABASE_URL description
2024-10-16 23:59:53 +02:00
Jan Böhmer
490086d531 Use the same translation for the panel with enabled search options, like in the checkbox options in navbar 2024-10-16 23:59:23 +02:00
Jan Böhmer
2ef3fbb81b Merge remote-tracking branch 'origin/l10n_master' 2024-10-16 23:57:07 +02:00
Jan Böhmer
7d834ac8d7 Include the query part of the request, when generating the url for the datatables via a custom twig function.
This fixes issue #735, as without this the query gets not passed to the datatable
2024-10-16 23:57:02 +02:00
Jan Böhmer
15ad0ec9c0 Updated dependencies 2024-10-16 23:40:48 +02:00
Jan Böhmer
f0b78e8b2c New translations validators.en.xlf (Italian) 2024-10-16 13:50:37 +02:00
Jan Böhmer
e616faa76f New translations messages.en.xlf (Italian) 2024-10-16 13:50:36 +02:00
Jan Böhmer
8159f4d8ee Bumped version to 1.14.1 2024-10-13 23:43:55 +02:00
Jan Böhmer
021c576468 Exclude the translation dumper fix files, to avoid phpstan issues, which we cannot control 2024-10-13 23:23:48 +02:00
Jan Böhmer
1b2339a82c Merge remote-tracking branch 'origin/l10n_master' 2024-10-13 23:22:24 +02:00
Jan Böhmer
2b6bb3f773 New translations messages.en.xlf (German) 2024-10-13 23:20:14 +02:00
Jan Böhmer
abc5c61a06 Fixed problem, that search field and search options did not close when clicking outside
This fixes issue #701. For the search field this was caused by algolia/autocomplete lib, which do not support multiple autocomplete fields on a single page. If initailly loaded on the homepage, which features a second autocomplete, this one "steals" the input listening, and the one in the navbar do not close anymore when clicking outside.
Custom code which triggers the closing of the autocomplete manually when clicking outside, was added as a workaround.
2024-10-13 23:19:03 +02:00
Jan Böhmer
7145bce605 Construct the correct current path, when serving from a subdirectory
This fixes issue #274
2024-10-13 22:49:42 +02:00
Jan Böhmer
bb92e5e9ee New translations validators.en.xlf (German) 2024-10-13 21:40:20 +02:00
Jan Böhmer
0c47aa226c Fixed imports of parameters on parts
It was missing the required serialization group. This fixes issue #718
2024-10-13 21:35:31 +02:00
Jan Böhmer
76e945bbbd Fixed issue that the document could not be scrolled anymore, when redirected from a modal
This fixes issue #696
2024-10-13 21:23:57 +02:00
Jan Böhmer
4a6ec2581d Removed wrongly used controller for merge modal 2024-10-13 20:59:28 +02:00
Jan Böhmer
3d75bf5f9f Added translation for the confirmation code field in the authenticator app 2FA setup section 2024-10-13 20:46:12 +02:00
Jan Böhmer
c27648b89b New translations validators.en.xlf (English) 2024-10-13 20:40:16 +02:00
Jan Böhmer
ccf67c0662 Added translation if authentication confirmation code is wrong 2024-10-13 20:35:56 +02:00
Jan Böhmer
ca116cae91 Keeep the segment annotations in the translation files, when editing them from inside the application 2024-10-13 20:30:56 +02:00
Jan Böhmer
a29d933f99 Fixed 2FA TOTP for non-admins, while also retaining validation of auth code
This fixes issue #717
2024-10-13 20:29:22 +02:00
Jan Böhmer
49acf3e0cf Fixed problem preventing non-admins to add TOTP 2FA to their account
This was caused by the no-lockout constraint, which was accidentially triggered here
2024-10-13 20:13:03 +02:00
Jan Böhmer
234b5abb96 Merge remote-tracking branch 'origin/master' 2024-10-13 19:56:29 +02:00
Jan Böhmer
839bcf91d6 Updated dependencies. 2024-10-13 19:56:21 +02:00
Jan Böhmer
58ed57fab7 New translations messages.en.xlf (English) (#703) 2024-09-12 21:52:34 +02:00
Jan Böhmer
fa42997733 Bumped version to 1.14.0 2024-09-09 21:42:29 +02:00
Jan Böhmer
ac416141d0 Merge remote-tracking branch 'origin/master' 2024-09-09 21:42:15 +02:00
Jan Böhmer
c629a85b14 Updated dependencies 2024-09-09 21:42:03 +02:00
Jan Böhmer
7ccfea208f New Crowdin updates (#695)
* New translations messages.en.xlf (English)

* New translations validators.en.xlf (German)

* New translations messages.en.xlf (German)

* New translations messages.en.xlf (Italian)
2024-09-09 21:38:07 +02:00
Jan Böhmer
f3c802bcff Made parameter type fields wider to fit more digits 2024-09-09 21:36:05 +02:00
Jan Böhmer
574583bd6a Do not round values of parameters, we can now use the full double precision
This fixes issue #681
2024-09-09 21:33:28 +02:00
Jan Böhmer
84c54d0b25 Removed NumberType fixes, as these is now part of the upstream symfony 2024-09-09 21:13:44 +02:00
Jan Böhmer
86d3f87694 [Digikey provider] Do not try to interpret certain parameters (like packages) as numbers
This fixes issue #682
2024-09-09 20:44:09 +02:00
André Lademann
ddd7252051 Increase image size in list view #688 (#689) 2024-09-09 20:29:25 +02:00
Jan Böhmer
b4e8136618 Fixed problem with undeleting elements containing an embedded and propertly restore the infos of the embed
This fixes issue #685
2024-09-09 20:26:26 +02:00
Jan Böhmer
c2638991f2 Added documentation for OEMSecrets info provider 2024-09-09 17:02:45 +02:00
Jan Böhmer
8554be9abd Show number of results for info provider search and show a notice, if no results were found 2024-09-09 16:41:19 +02:00
Jan Böhmer
87a518703f Escape spaces in unnwrapped urls to avoid invalid URLs 2024-09-09 16:23:12 +02:00
Jan Böhmer
dd03ca943d Fixed phpstan issues 2024-09-09 14:52:18 +02:00
Jan Böhmer
6997861811 [OEMSecrets provider] Extract real URLs and remove tracking parts 2024-09-09 14:52:09 +02:00
Pasquale D'Orsi
1cc1530b20 OEMSecrets provider interface v.1.0 (#679)
* OEMSecrets provider interface v.1.0

New class for interacting with the OEMSecrets (https://www.oemsecrets.com) API version 3.0.1.

* Refactored info provider to be stateless and independent from session, optimized Part-DB API usage, and fixed PHPStan issues.

Refactored info provider to be stateless and independent from session, now use Psr\Cache, fixed issues identified by PHPStan, additional minor enhancements and bug fixes.

* Prefix cache keys with oemsecrets_ to avoid key collissions

* Use uniqid with more entropy to reduce probability of collisions

* Made $resultData local as it is only used inside searchByKeyword

* Use the parameter name $id from interface declaration for getDetails to avoid problems with named arguments

* Use unicode modifier for preg_match to avoid problems when parameters contain non-unicode strings

* Various small code quality improvements

* Try to retrieve the part from the API in getDetails, if the DTO was not cached before

* Improved code formatting

* Channged OEMSecret default country to DE to be consistent with other default values

* Do not call gc_collect_cycles in the loop to process the results, but only after all processBatch calls

---------

Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2024-09-09 00:59:44 +02:00
Jan Böhmer
98597fb3aa Use new repository pathes (Part-DB-server) instead of the Part-DB-symfony ones 2024-09-08 20:05:06 +02:00
Jan Böhmer
283a445198 Use jbtronics/translation-editor bundle instead of php-translation/symfony-bundle for profiler translation editing
This new bundle has less dependencies and less overhead
2024-09-08 20:03:33 +02:00
Jan Böhmer
7db44f0ec5 Upgraded dependencies 2024-09-08 19:54:31 +02:00
Jan Böhmer
abb5395cae Use "log-bin-trust-function-creators" option for mysql in recommended docker-compose file
This avoids errors, while creating the MySQL functions for the natural sort: "1419 You do not have the SUPER privilege an
  d binary logging is enabled"
2024-09-08 19:46:55 +02:00
Jan Böhmer
8c8b44baef Use debian bookworm, PHP 8.2 and node 20 for the docker image by default 2024-09-08 19:40:43 +02:00
Jan Böhmer
7366a33fe5 Apply the PHP_VERSION arg also to the partdb-entrypoint during build, to make it really version independent 2024-09-08 19:40:19 +02:00
Jan Böhmer
ad02d7e525 New Crowdin updates (#692)
* New translations messages.en.xlf (English)

* New translations validators.en.xlf (English)

* New translations security.en.xlf (English)

* New translations validators.en.xlf (Italian)
2024-09-08 19:14:02 +02:00
David Girón
b5a0189f29 feat(docker): Refactor Dockerfile (#683)
* reorder nodejs/yarn install, separate packages per line

* reduce run actions and reorganize commands

* simplify file creation, copy in one layer only

* fix lint LegacyKeyValueFormat

* arg php_version to run different version

* reorder copy from generated config

* update dockerfile-frankenphp
2024-09-08 19:13:13 +02:00
Jan Böhmer
756152dd68 Bumped to version 1.13.3 2024-08-24 15:58:46 +02:00
Jan Böhmer
173a8ee680 Improved assymmetric padding in datatables footer 2024-08-24 15:55:45 +02:00
Jan Böhmer
b99777cde1 Return a 404 message, instead of creating an 500 Runtime exception, when a file associated with an attachment is not existing.
This fails more gracefully, and do not pollute log files.
2024-08-24 15:49:45 +02:00
Jan Böhmer
8193e7a68e Allow to show attachment IDs in attachment table 2024-08-24 15:48:50 +02:00
Jan Böhmer
f18c024daa Remove -> prefix if no element is selected yet 2024-08-24 15:35:30 +02:00
Jan Böhmer
f6577a8f33 Allow to create sub elements for existing elements, by typing "->"
This fixes issue #666 and #560
2024-08-24 15:31:44 +02:00
Jan Böhmer
7fc3153dde Fixed filter logic for exclusion of entities. Before parts with null values as property value were wrongly not shown
This fixes  issue #658
2024-08-23 22:58:04 +02:00
Jan Böhmer
5231dbd6e7 Remove project path in twig label error messages to prevent information leakage 2024-08-23 22:28:29 +02:00
Jan Böhmer
77671550a7 Fail gracefully, when an exception occurs during rendering of the example labels for label profiles
This fixes issue #671
2024-08-23 22:15:29 +02:00
Jan Böhmer
e231404128 Load HTMLExtension in SandboxedTwig, so that the data_uri filter can be used in twig labels
This fixes issue #665
2024-08-23 22:06:37 +02:00
Jan Böhmer
6650e2da3d Updated dependencies 2024-08-23 21:57:37 +02:00
frank-f
fd521acaa4 Update LCSCProvider field for real datasheet URL (#670) 2024-08-21 17:35:55 +02:00
94 changed files with 23356 additions and 9240 deletions

View File

@@ -39,8 +39,8 @@ 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
# 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

View File

@@ -43,6 +43,7 @@
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 EDA_KICAD_CATEGORY_DEPTH
# For most configuration files from conf-available/, which are

34
.env
View File

@@ -181,6 +181,40 @@ 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
##################################################################################
# EDA integration related settings
##################################################################################

View File

@@ -1,22 +1,64 @@
FROM debian:bullseye-slim
ARG BASE_IMAGE=debian:bookworm-slim
ARG PHP_VERSION=8.2
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 mariadb-client postgresql-client \
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 php8.1-pgsql gpg sudo \
&& 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,8 +69,6 @@ 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"; \
@@ -36,82 +76,86 @@ RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.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
RUN { \
echo '[global]'; \
echo 'error_log = /proc/1/fd/1'; \
echo; \
echo '[www]'; \
echo 'access.log = /proc/1/fd/1'; \
echo 'catch_workers_output = yes'; \
echo 'decorate_workers_output = no'; \
echo 'clear_env = no'; \
} | tee "/etc/php/8.1/fpm/pool.d/zz-docker.conf"
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker.conf
[global]
error_log = /proc/1/fd/1
[www]
access.log = /proc/1/fd/1
catch_workers_output = yes
decorate_workers_output = no
clear_env = no
EOF
# PHP files should be handled by PHP, and should be preferred over any other file type
RUN { \
echo '<FilesMatch \.php$>'; \
echo '\tSetHandler application/x-httpd-php'; \
echo '</FilesMatch>'; \
echo; \
echo 'DirectoryIndex disabled'; \
echo 'DirectoryIndex index.php index.html'; \
echo; \
echo '<Directory /var/www/>'; \
echo '\tOptions -Indexes'; \
echo '\tAllowOverride All'; \
echo '</Directory>'; \
} | tee "$APACHE_CONFDIR/conf-available/docker-php.conf" \
&& a2enconf docker-php
COPY <<EOF /etc/apache2/conf-available/docker-php.conf
<FilesMatch \\.php$>
SetHandler application/x-httpd-php
</FilesMatch>
DirectoryIndex disabled
DirectoryIndex index.php index.html
<Directory /var/www/>
Options -Indexes
AllowOverride All
</Directory>
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 <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/symfony-recommended.ini
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamp=0
# Configure Realpath cache for performance
realpath_cache_size=4096K
realpath_cache_ttl=600
EOF
# Increase upload limit and enable preloading
RUN \
{ \
echo 'upload_max_filesize=256M'; \
echo 'post_max_size=300M'; \
echo 'opcache.preload_user=www-data'; \
echo 'opcache.preload=/var/www/html/config/preload.php'; \
} > /etc/php/8.1/fpm/conf.d/partdb.ini
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/partdb.ini
upload_max_filesize=256M
post_max_size=300M
opcache.preload_user=www-data
opcache.preload=/var/www/html/config/preload.php
EOF
# Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# ---
FROM base
ARG PHP_VERSION
# Set working dir
WORKDIR /var/www/html
COPY --from=apache-config / /
COPY --chown=www-data:www-data . .
# Setup apache2
RUN a2dissite 000-default.conf
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
RUN a2ensite symfony.conf
RUN a2enmod rewrite
RUN a2dissite 000-default.conf && \
a2ensite symfony.conf && \
# Enable php-fpm
a2enmod proxy_fcgi setenvif && \
a2enconf php${PHP_VERSION}-fpm && \
a2enconf docker-php && \
a2enmod rewrite
# Install composer and yarn dependencies for Part-DB
USER www-data
RUN composer install -a --no-dev && composer clear-cache
RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
RUN composer install -a --no-dev && \
composer clear-cache
RUN yarn install --network-timeout 600000 && \
yarn build && \
yarn cache clean && \
rm -rf node_modules/
# Use docker env to output logs to stdout
ENV APP_ENV=docker
@@ -119,10 +163,12 @@ ENV DATABASE_URL="sqlite:///%kernel.project_dir%/uploads/app.db"
USER root
# Copy entrypoint to /usr/local/bin and make it executable
RUN cp ./.docker/partdb-entrypoint.sh /usr/local/bin/partdb-entrypoint.sh && chmod +x /usr/local/bin/partdb-entrypoint.sh
# Copy apache2-foreground to /usr/local/bin and make it executable
RUN cp ./.docker/apache2-foreground /usr/local/bin/apache2-foreground && chmod +x /usr/local/bin/apache2-foreground
# Replace the php version placeholder in the entry point, with our php version
RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh
# Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable
RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \
install ./.docker/apache2-foreground /usr/local/bin
ENTRYPOINT ["partdb-entrypoint.sh"]
CMD ["apache2-foreground"]
@@ -130,4 +176,4 @@ CMD ["apache2-foreground"]
STOPSIGNAL SIGWINCH
EXPOSE 80
VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"]
VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"]

View File

@@ -1,11 +1,25 @@
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
RUN apt-get update && apt-get -y install curl zip mariadb-client postgresql-client file acl git gettext ca-certificates gnupg \
RUN apt-get update && apt-get -y install \
curl \
ca-certificates \
mariadb-client \
postgresql-client \
file \
acl \
git \
gettext \
gnupg \
zip \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
# Create workdir and set permissions if directory does not exists
RUN mkdir -p /app
WORKDIR /app
# Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get update && apt-get install -y \
nodejs yarn \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
# Install PHP
RUN set -eux; \
@@ -33,15 +47,13 @@ ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN curl -sL https://deb.nodesource.com/setup_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
ENV COMPOSER_ALLOW_SUPERUSER=1
#COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create workdir and set permissions if directory does not exists
WORKDIR /app
# prevent the reinstallation of vendors at every changes in the source code
COPY --link composer.* symfony.* ./
RUN set -eux; \
@@ -58,7 +70,10 @@ RUN set -eux; \
composer run-script --no-dev post-install-cmd; \
chmod +x bin/console; sync;
RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
RUN yarn install --network-timeout 600000 && \
yarn build && \
yarn cache clean && \
rm -rf node_modules/
# Use docker env to output logs to stdout
ENV APP_ENV=docker
@@ -83,4 +98,4 @@ ENV XDG_DATA_HOME /data
EXPOSE 80
EXPOSE 443
EXPOSE 443/udp
EXPOSE 2019
EXPOSE 2019

View File

@@ -1 +1 @@
1.13.2
1.14.4

View File

@@ -186,5 +186,15 @@ export default class extends Controller {
];
},
});
//Try to find the input field and register a defocus handler. This is necessarry, as by default the autocomplete
//lib has problems when multiple inputs are present on the page. (see https://github.com/algolia/autocomplete/issues/1216)
const inputs = this.element.getElementsByClassName('aa-Input');
for (const input of inputs) {
input.addEventListener('blur', () => {
this._autocomplete.setIsOpen(false);
});
}
}
}

View File

@@ -24,7 +24,6 @@ import {Controller} from "@hotwired/stimulus";
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
export default class extends Controller {
_tomSelect;
@@ -58,7 +57,21 @@ 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 '<div class="create"><i class="fa-solid fa-plus fa-fw"></i>&nbsp;<strong>' + escape(data.input) + '</strong>&hellip;&nbsp;' +
'<small class="text-muted float-end">(' + addHint +')</small>' +
'</div>';
@@ -76,6 +89,22 @@ export default class extends Controller {
}
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,

View File

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

View File

@@ -108,8 +108,8 @@ body {
.back-to-top {
cursor: pointer;
position: fixed;
bottom: 20px;
right: 20px;
bottom: 60px;
right: 40px;
display:none;
z-index: 1030;
}

View File

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

View File

@@ -21,6 +21,7 @@
import {Dropdown} from "bootstrap";
import ClipboardJS from "clipboard";
import {Modal} from "bootstrap";
class RegisterEventHelper {
constructor() {
@@ -31,9 +32,11 @@ class RegisterEventHelper {
//Initialize ClipboardJS
this.registerLoadHandler(() => {
new ClipboardJS('.btn');
})
});
this.registerModalDropRemovalOnFormSubmit();
}
registerModalDropRemovalOnFormSubmit() {
@@ -43,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 = '';
}
});
}

View File

@@ -43,7 +43,6 @@
"omines/datatables-bundle": "^0.8.0",
"paragonie/sodium_compat": "^1.21",
"part-db/label-fonts": "^1.0",
"php-translation/symfony-bundle": "^0.14.0",
"phpdocumentor/reflection-docblock": "^5.2",
"phpstan/phpdoc-parser": "^1.23",
"runtime/frankenphp-symfony": "^0.2.0",
@@ -99,6 +98,7 @@
"dama/doctrine-test-bundle": "^v8.0.0",
"doctrine/doctrine-fixtures-bundle": "^3.2",
"ekino/phpstan-banned-code": "^v1.0.0",
"jbtronics/translation-editor-bundle": "^1.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.4.7",
"phpstan/phpstan-doctrine": "^1.2.11",

2582
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@ 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],
@@ -32,4 +31,5 @@ return [
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],
];

View File

@@ -10,13 +10,12 @@ datatables:
options:
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
#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>>>"
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

View File

@@ -1,5 +0,0 @@
translation:
symfony_profiler:
enabled: true
webui:
enabled: true

View File

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

View File

@@ -11,7 +11,7 @@ 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', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh'] # 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

View File

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

View File

@@ -0,0 +1,3 @@
when@dev:
translation_editor:
resource: '@JbtronicsTranslationEditorBundle/config/routes.php'

View File

@@ -1,3 +0,0 @@
_translation_edit_in_place:
resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yaml'
prefix: /admin

View File

@@ -306,6 +306,16 @@ services:
$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
####################################################################################################################
@@ -400,4 +410,4 @@ when@test:
arguments:
- '@doctrine.fixtures.loader'
- '@doctrine'
- { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' }
- { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' }

View File

@@ -32,11 +32,16 @@ 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://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<TABLE_NAME>` here
(e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`). For SQLite use the following format to specify the
* `DATABASE_URL`: Configures the database which Part-DB uses:
* For MySQL (or MariaDB) use a string in the form of `mysql://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<TABLE_NAME>` 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.
@@ -86,6 +91,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
* `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
@@ -243,4 +252,4 @@ The following options are available:
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.
* `partdb.available_themes`: The list of available themes a user can choose from.

View File

@@ -158,7 +158,7 @@ services:
container_name: partdb_database
image: mysql:8.0
restart: unless-stopped
command: --default-authentication-plugin=mysql_native_password
command: --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=1
environment:
# Change this Password
MYSQL_ROOT_PASSWORD: SECRET_ROOT_PASSWORD

View File

@@ -212,6 +212,26 @@ An API key is not required, it is enough to enable the provider using the follow
* `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.
### Custom provider
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long

View File

@@ -12,6 +12,7 @@ parameters:
- src/Doctrine/Purger/*
- src/DataTables/Adapters/TwoStepORMAdapter.php
- src/Form/Fixes/*
- src/Translation/Fixes/*

View File

@@ -0,0 +1,102 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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(
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'),
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'),
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'),
$expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'),
);
$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',
'openapi' => [
'example' => '',
'allowReserved' => false,// if true, query parameters will be not percent-encoded
'allowEmptyValue' => true,
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
],
];
}
return $description;
}
}

View File

@@ -83,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.');

View File

@@ -35,6 +35,7 @@ use App\Entity\LabelSystem\LabelProcessMode;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AbstractParameter;
use App\Exceptions\AttachmentDownloadException;
use App\Exceptions\TwigModeException;
use App\Form\AdminPages\ImportType;
use App\Form\AdminPages\MassCreationForm;
use App\Repository\AbstractPartsContainingRepository;
@@ -53,6 +54,7 @@ use InvalidArgumentException;
use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
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;
@@ -211,7 +213,12 @@ 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 */

View File

@@ -52,11 +52,11 @@ class AttachmentFileController extends AbstractController
}
if ($attachment->isExternal()) {
throw new RuntimeException('You can not download external attachments!');
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
}
if (!$helper->isFileExisting($attachment)) {
throw new RuntimeException('The file associated with the attachment is not existing!');
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
}
$file_path = $helper->toAbsoluteFilePath($attachment);
@@ -81,11 +81,11 @@ class AttachmentFileController extends AbstractController
}
if ($attachment->isExternal()) {
throw new RuntimeException('You can not download external attachments!');
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
}
if (!$helper->isFileExisting($attachment)) {
throw new RuntimeException('The file associated with the attachment is not existing!');
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
}
$file_path = $helper->toAbsoluteFilePath($attachment);

View File

@@ -117,7 +117,7 @@ class LabelController extends AbstractController
$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');

View File

@@ -61,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.gdpr_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'),

View File

@@ -34,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;
@@ -84,6 +85,11 @@ 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',
'orderField' => 'NATSORT(attachment.name)',

View File

@@ -137,7 +137,7 @@ class EntityConstraint extends AbstractConstraint
}
//We need to handle null values differently, as they can not be compared with == or !=
if (!$this->value instanceof AbstractDBElement) {
if ($this->value === null) {
if($this->operator === '=' || $this->operator === 'INCLUDING_CHILDREN') {
$queryBuilder->andWhere(sprintf("%s IS NULL", $this->property));
return;
@@ -152,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
@@ -168,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 {

View File

@@ -56,8 +56,14 @@ trait FilterTrait
/**
* Adds a simple constraint in the form of (property OPERATOR value) (e.g. "part.name = :name") to the given query builder.
* @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, mixed $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);
@@ -65,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 {

View File

@@ -85,6 +85,9 @@ class TagsConstraint extends AbstractConstraint
*/
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();

View File

@@ -137,7 +137,8 @@ final class PartsDataTable implements DataTableTypeInterface
])
->add('storelocation', TextColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'),
'orderField' => 'NATSORT(_storelocations.name)',
//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')

View File

@@ -127,7 +127,7 @@ class SecurityEventLogEntry extends AbstractLogEntry
* Sets the IP address used to log in the user.
*
* @param string $ip the IP address used to log in the user
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
*
* @return $this
*/

View File

@@ -52,7 +52,7 @@ class UserLoginLogEntry extends AbstractLogEntry
* Sets the IP address used to log in the user.
*
* @param string $ip the IP address used to log in the user
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
*
* @return $this
*/

View File

@@ -49,7 +49,7 @@ class UserLogoutLogEntry extends AbstractLogEntry
* Sets the IP address used to log in the user.
*
* @param string $ip the IP address used to log in the user
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
*
* @return $this
*/

View File

@@ -22,6 +22,7 @@ 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;
@@ -97,7 +98,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[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", "tags", "manufacturer_product_number"])]
#[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)]
@@ -118,7 +120,7 @@ class Part extends AttachmentContainingDBElement
/** @var Collection<int, PartParameter>
*/
#[Assert\Valid]
#[Groups(['full', 'part:read', 'part:write'])]
#[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'])]

View File

@@ -102,7 +102,7 @@ use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface
#[ApiFilter(LikeFilter::class, properties: ["name", "aboutMe"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
#[NoLockout]
#[NoLockout(groups: ['permissions:edit'])]
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface,
BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface, SamlUserInterface
{

View File

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

View File

@@ -1,228 +0,0 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Form\Fixes;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Same as the default NumberToLocalizedStringTransformer, but with a fix for the decimal separator.
* See https://github.com/symfony/symfony/pull/57861
*/
class FixedNumberToLocalizedStringTransformer implements DataTransformerInterface
{
protected $grouping;
protected $roundingMode;
private ?int $scale;
private ?string $locale;
public function __construct(?int $scale = null, ?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, ?string $locale = null)
{
$this->scale = $scale;
$this->grouping = $grouping ?? false;
$this->roundingMode = $roundingMode ?? \NumberFormatter::ROUND_HALFUP;
$this->locale = $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.');
}
$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;
}
/**
* Transforms a localized number into an integer or float.
*
* @param string $value The localized value
*
* @throws TransformationFailedException if the given value is not a string
* or if the value cannot be transformed
*/
public function reverseTransform(mixed $value): int|float|null
{
if (null !== $value && !\is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
if (null === $value || '' === $value) {
return null;
}
if (\in_array($value, ['NaN', 'NAN', 'nan'], true)) {
throw new TransformationFailedException('"NaN" is not a valid number.');
}
$position = 0;
$formatter = $this->getNumberFormatter();
$groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
$decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) {
$value = str_replace('.', $decSep, $value);
}
if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) {
$value = str_replace(',', $decSep, $value);
}
//If the value is in exponential notation with a negative exponent, we end up with a float value too
if (str_contains($value, $decSep) || stripos($value, 'e-') !== false) {
$type = \NumberFormatter::TYPE_DOUBLE;
} else {
$type = \PHP_INT_SIZE === 8
? \NumberFormatter::TYPE_INT64
: \NumberFormatter::TYPE_INT32;
}
$result = $formatter->parse($value, $type, $position);
if (intl_is_failure($formatter->getErrorCode())) {
throw new TransformationFailedException($formatter->getErrorMessage());
}
if ($result >= \PHP_INT_MAX || $result <= -\PHP_INT_MAX) {
throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like.');
}
$result = $this->castParsedValue($result);
if (false !== $encoding = mb_detect_encoding($value, null, true)) {
$length = mb_strlen($value, $encoding);
$remainder = mb_substr($value, $position, $length, $encoding);
} else {
$length = \strlen($value);
$remainder = substr($value, $position, $length);
}
// After parsing, position holds the index of the character where the
// parsing stopped
if ($position < $length) {
// Check if there are unrecognized characters at the end of the
// number (excluding whitespace characters)
$remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0");
if ('' !== $remainder) {
throw new TransformationFailedException(sprintf('The number contains unrecognized characters: "%s".', $remainder));
}
}
// NumberFormatter::parse() does not round
return $this->round($result);
}
/**
* Returns a preconfigured \NumberFormatter instance.
*/
protected function getNumberFormatter(): \NumberFormatter
{
$formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::DECIMAL);
if (null !== $this->scale) {
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
}
$formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping);
return $formatter;
}
/**
* @internal
*/
protected function castParsedValue(int|float $value): int|float
{
if (\is_int($value) && $value === (int) $float = (float) $value) {
return $float;
}
return $value;
}
/**
* Rounds a number according to the configured scale and rounding mode.
*/
private function round(int|float $number): int|float
{
if (null !== $this->scale && null !== $this->roundingMode) {
// shift number to maintain the correct scale during rounding
$roundingCoef = 10 ** $this->scale;
// string representation to avoid rounding errors, similar to bcmul()
$number = (string) ($number * $roundingCoef);
switch ($this->roundingMode) {
case \NumberFormatter::ROUND_CEILING:
$number = ceil($number);
break;
case \NumberFormatter::ROUND_FLOOR:
$number = floor($number);
break;
case \NumberFormatter::ROUND_UP:
$number = $number > 0 ? ceil($number) : floor($number);
break;
case \NumberFormatter::ROUND_DOWN:
$number = $number > 0 ? floor($number) : ceil($number);
break;
case \NumberFormatter::ROUND_HALFEVEN:
$number = round($number, 0, \PHP_ROUND_HALF_EVEN);
break;
case \NumberFormatter::ROUND_HALFUP:
$number = round($number, 0, \PHP_ROUND_HALF_UP);
break;
case \NumberFormatter::ROUND_HALFDOWN:
$number = round($number, 0, \PHP_ROUND_HALF_DOWN);
break;
}
$number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
}
return $number;
}
}

View File

@@ -102,7 +102,7 @@ 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', ExponentialNumberType::class, [
@@ -113,7 +113,7 @@ 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', ExponentialNumberType::class, [
@@ -124,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, [

View File

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

View File

@@ -27,6 +27,7 @@ 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
@@ -38,6 +39,14 @@ class ExponentialNumberType extends AbstractType
return NumberType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
//We want to allow the full precision of the number, so disable rounding
'scale' => null,
]);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->resetViewTransformers();

View File

@@ -23,21 +23,22 @@ declare(strict_types=1);
namespace App\Form\Type\Helper;
use App\Form\Fixes\FixedNumberToLocalizedStringTransformer;
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 FixedNumberToLocalizedStringTransformer
class ExponentialNumberTransformer extends NumberToLocalizedStringTransformer
{
public function __construct(
protected ?int $scale = null,
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);
}
@@ -85,12 +86,28 @@ class ExponentialNumberTransformer extends FixedNumberToLocalizedStringTransform
$formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::SCIENTIFIC);
if (null !== $this->scale) {
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $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;
}
}

View File

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

View File

@@ -36,6 +36,9 @@ class FilenameSanatizer
*/
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

View File

@@ -44,6 +44,12 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/
class EntityImporter
{
/**
* 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)
{
}
@@ -65,6 +71,9 @@ class EntityImporter
*/
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);
@@ -159,6 +168,9 @@ class EntityImporter
*/
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);

View File

@@ -45,6 +45,14 @@ class DigikeyProvider implements InfoProviderInterface
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,
@@ -214,7 +222,12 @@ class DigikeyProvider implements InfoProviderInterface
continue;
}
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']);
//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['Parameter'], value_text: $parameter['Value']);
} else { //Otherwise try to parse it as a numerical value
$results[] = ParameterDTO::parseValueIncludingUnit($parameter['Parameter'], $parameter['Value']);
}
}
return $results;

View File

@@ -102,11 +102,11 @@ class LCSCProvider implements InfoProviderInterface
'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html'
],
]);
if (preg_match('/(pdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) {
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->pdfUrl;
$url = $jsonObj->previewPdfUrl;
}
}
return $url;

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,7 @@ 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;
@@ -183,6 +184,7 @@ final class SandboxedTwigFactory
$twig->addExtension(new IntlExtension());
$twig->addExtension(new MarkdownExtension());
$twig->addExtension(new StringExtension());
$twig->addExtension(new HtmlExtension());
//Add Part-DB specific extension
$twig->addExtension($this->formatExtension);

View File

@@ -216,7 +216,10 @@ class TimeTravel
$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)) {
$data = BigDecimal::of($data);
@@ -224,7 +227,12 @@ class TimeTravel
if (!$data instanceof \DateTimeInterface
&& (in_array($metadata->getFieldMapping($field)->type,
[Types::DATETIME_IMMUTABLE, Types::DATETIME_IMMUTABLE, Types::DATE_MUTABLE, Types::DATETIME_IMMUTABLE], true))) {
[
Types::DATETIME_IMMUTABLE,
Types::DATETIME_IMMUTABLE,
Types::DATE_MUTABLE,
Types::DATETIME_IMMUTABLE
], true))) {
$data = $this->dateTimeDecode($data, $metadata->getFieldMapping($field)->type);
}
@@ -267,9 +275,11 @@ class TimeTravel
$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
@@ -281,6 +291,6 @@ class TimeTravel
$new_value = $enum_class::from($new_value);
}
$property->setValue($element, $new_value);
$property->setValue($target_element, $new_value);
}
}

View File

@@ -0,0 +1,242 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -0,0 +1,262 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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, '<?xml');
}
}

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
*/
namespace App\Twig;
use Symfony\Component\HttpFoundation\Request;
use Twig\TwigFunction;
use App\Services\LogSystem\EventCommentNeededHelper;
use Twig\Extension\AbstractExtension;
@@ -38,6 +39,22 @@ final class MiscExtension extends AbstractExtension
new TwigFunction('event_comment_needed',
fn(string $operation_type) => $this->eventCommentNeededHelper->isCommentNeeded($operation_type)
),
new TwigFunction('uri_without_host', $this->uri_without_host(...))
];
}
/**
* 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;
}
}

View File

@@ -40,16 +40,6 @@
"./config/packages/dama_doctrine_test_bundle.yaml"
]
},
"doctrine/annotations": {
"version": "1.14",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.10",
"ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05"
},
"files": []
},
"doctrine/cache": {
"version": "v1.8.0"
},
@@ -167,6 +157,9 @@
"jbtronics/dompdf-font-loader-bundle": {
"version": "v1.1.1"
},
"jbtronics/translation-editor-bundle": {
"version": "v1.0"
},
"knpuniversity/oauth2-client-bundle": {
"version": "2.15",
"recipe": {
@@ -237,9 +230,6 @@
"nikolaposa/version": {
"version": "2.2.2"
},
"nyholm/nsa": {
"version": "1.1.0"
},
"nyholm/psr7": {
"version": "1.0",
"recipe": {
@@ -291,30 +281,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"
},
@@ -634,9 +600,6 @@
"symfony/polyfill-mbstring": {
"version": "v1.10.0"
},
"symfony/polyfill-php72": {
"version": "v1.10.0"
},
"symfony/polyfill-php80": {
"version": "v1.17.0"
},

View File

@@ -36,7 +36,7 @@
{% if entity.buildPart %}
<span class="form-control-static"><a href="{{ entity_url(entity.buildPart) }}">{{ entity.buildPart.name }}</a></span>
{% else %}
<a href="{{ path('part_new_build_part', {"project_id": entity.id , "_redirect": app.request.baseUrl ~ app.request.requestUri}) }}"
<a href="{{ path('part_new_build_part', {"project_id": entity.id , "_redirect": uri_without_host(app.request)}) }}"
class="btn btn-outline-success">{% trans %}project.edit.associated_build_part.add{% endtrans %}</a>
{% endif %}
<p class="text-muted">{% trans %}project.edit.associated_build.hint{% endtrans %}</p>

View File

@@ -111,7 +111,7 @@
{# Back to top buton #}
<!-- Back to top button -->
<button id="back-to-top" class="btn btn-primary back-to-top" role="button" title="{% trans %}back_to_top{% endtrans %}"
<button id="back-to-top" class="btn btn-primary back-to-top btn-sm" role="button" title="{% trans %}back_to_top{% endtrans %}"
{{ stimulus_controller('common/back_to_top') }} {{ stimulus_action('common/back_to_top', 'backToTop') }}>
<i class="fas fa-angle-up fa-fw"></i>
</button>

View File

@@ -1,5 +1,5 @@
{% macro datatable(datatable, controller = 'elements/datatables/datatables', state_save_tag = null) %}
<div {{ stimulus_controller(controller, {"stateSaveTag": state_save_tag}) }} data-dt-settings='{{ datatable_settings(datatable)|escape('html_attr') }}' data-dt-url="{{ app.request.baseUrl ~ app.request.requestUri }}">
<div {{ stimulus_controller(controller, {"stateSaveTag": state_save_tag}) }} data-dt-settings='{{ datatable_settings(datatable)|escape('html_attr') }}' data-dt-url="{{ uri_without_host(app.request) }}">
<div {{ stimulus_target(controller, 'dt') }}>
<div class="card-body">
<div class="card">
@@ -20,12 +20,12 @@
{% macro partsDatatableWithForm(datatable, state_save_tag = 'parts') %}
<form method="post" action="{{ path("table_action") }}"
{# The app.request.baseUrl here is important or it wont work behind a reverse proxy with subfolder #}
{{ stimulus_controller('elements/datatables/parts', {"stateSaveTag": state_save_tag}) }} data-dt-settings='{{ datatable_settings(datatable)|escape('html_attr') }}' data-dt-url="{{ app.request.baseUrl ~ app.request.requestUri }}"
{{ stimulus_controller('elements/datatables/parts', {"stateSaveTag": state_save_tag}) }} data-dt-settings='{{ datatable_settings(datatable)|escape('html_attr') }}' data-dt-url="{{ uri_without_host(app.request) }}"
{{ stimulus_action('elements/datatables/parts', 'confirmDeletionAtSubmit') }} data-delete-title="{% trans %}part_list.action.delete-title{% endtrans %}"
data-delete-message="{% trans %}part_list.action.delete-message{% endtrans %}">
<input type="hidden" name="_token" value="{{ csrf_token('table_action') }}">
<input type="hidden" name="redirect_back" value="{{ app.request.baseUrl ~ app.request.requestUri }}">
<input type="hidden" name="redirect_back" value="{{ uri_without_host(app.request) }}">
<input type="hidden" name="ids" {{ stimulus_target('elements/datatables/parts', 'selectIDs') }} value="">

View File

@@ -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 %}">
<input type="hidden" name="redirect_back" value="{{ app.request.baseUrl ~ app.request.requestUri }}">
<input type="hidden" name="redirect_back" value="{{ uri_without_host(app.request) }}">
{{ datatables.logDataTable(datatable, tag) }}
</form>

View File

@@ -1,7 +1,7 @@
{% macro settings_drodown(show_label_instead_icon = true) %}
<div class="dropdown">
<button class="btn dropdown-toggle my-2" type="button" id="navbar-search-options" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="false">
<button class="btn dropdown-toggle my-2" type="button" id="navbar-search-options" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="true">
{% if show_label_instead_icon %}{% trans %}search.options.label{% endtrans %}{% else %}<i class="fa-solid fa-gear"></i>{% endif %}
<span class="caret"></span>
</button>

View File

@@ -60,11 +60,11 @@
<a class="link-external" rel="noopener" target="_blank" href="https://github.com/jbtronics/">Jan Böhmer</a>
</strong>. <br> Part-DB is published under the <strong>GNU Affero General Public License v3.0 (or later)</strong>, so it comes with <strong>ABSOLUTELY NO WARRANTY</strong>.
This is free software, and you are welcome to redistribute it under certain conditions.
Click <a href="https://raw.githubusercontent.com/Part-DB/Part-DB-symfony/master/LICENSE" class="link-external" rel="noopener" target="_blank">here</a> for details.<br>
Click <a href="https://raw.githubusercontent.com/Part-DB/Part-DB-server/master/LICENSE" class="link-external" rel="noopener" target="_blank">here</a> for details.<br>
</p>
<strong><i class="fab fa-github fa-fw"></i> {% trans %}homepage.github.caption{% endtrans %}:</strong> {% trans with {'%href%': 'https://github.com/Part-DB/Part-DB-symfony'}%}homepage.github.text{% endtrans %}<br>
<strong><i class="fab fa-github fa-fw"></i> {% trans %}homepage.github.caption{% endtrans %}:</strong> {% trans with {'%href%': 'https://github.com/Part-DB/Part-DB-server'}%}homepage.github.text{% endtrans %}<br>
<strong><i class="fas fa-question fa-fw"></i> {% trans %}homepage.help.caption{% endtrans %}:</strong> {% trans with {'%href%': 'https://docs.part-db.de/'}%}homepage.help.text{% endtrans %}<br>
<strong><i class="fas fa-comments fa-fw"></i> {% trans %}homepage.forum.caption{% endtrans %}:</strong> {% trans with {'%href%': 'https://github.com/Part-DB/Part-DB-symfony/discussions'}%}homepage.forum.text{% endtrans %}<br>
<strong><i class="fas fa-comments fa-fw"></i> {% trans %}homepage.forum.caption{% endtrans %}:</strong> {% trans with {'%href%': 'https://github.com/Part-DB/Part-DB-server/discussions'}%}homepage.forum.text{% endtrans %}<br>
</div>
</div>

View File

@@ -38,81 +38,90 @@
{{ form_end(form) }}
{% if results is not null %}
<table class="table table-striped table-hover">
<thead>
<tr>
<th></th>
<th>{% trans %}name.label{% endtrans %} / {% trans %}part.table.mpn{% endtrans %}</th>
<th>{% trans %}description.label{% endtrans %} / {% trans %}category.label{% endtrans %}</th>
<th>{% trans %}manufacturer.label{% endtrans %} / {% trans %}footprint.label{% endtrans %}</th>
<th>{% trans %}part.table.manufacturingStatus{% endtrans %}</th>
<th>{% trans %}info_providers.table.provider.label{% endtrans %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for result in results %}
{% if results|length > 0 %}
<b>{% trans with {'%number%': results|length} %}info_providers.search.number_of_results{% endtrans %}</b>:
<table class="table table-striped table-hover">
<thead>
<tr>
<td>
<img src="{{ result.preview_image_url }}" data-thumbnail="{{ result.preview_image_url }}"
class="hoverpic" style="max-width: 45px;" {{ stimulus_controller('elements/hoverpic') }}>
</td>
<td>
{% if result.provider_url is not null %}
<a href="{{ result.provider_url }}" target="_blank" rel="noopener">{{ result.name }}</a>
{% else %}
{{ result.name }}
{% endif %}
{% if result.mpn is not null %}
<br>
<small class="text-muted" title="{% trans %}part.table.mpn{% endtrans %}">{{ result.mpn }}</small>
{% endif %}
</td>
<td>
{{ result.description }}
{% if result.category is not null %}
<br>
<small class="text-muted">{{ result.category }}</small>
{% endif %}
</td>
<td>
{{ result.manufacturer ?? '' }}
{% if result.footprint is not null %}
<br>
<small class="text-muted">{{ result.footprint }}</small>
{% endif %}
</td>
<td>{{ helper.m_status_to_badge(result.manufacturing_status) }}</td>
<td>
{% if result.provider_url %}
<a href="{{ result.provider_url }}" target="_blank" rel="noopener">
{{ info_provider_label(result.provider_key)|default(result.provider_key) }}
</a>
{% else %}
{{ info_provider_label(result.provider_key)|default(result.provider_key) }}
{% endif %}
<br>
<small class="text-muted">{{ result.provider_id }}</small>
<td>
{% if update_target %} {# We update an existing part #}
{% set href = path('info_providers_update_part',
{'providerKey': result.provider_key, 'providerId': result.provider_id, 'id': update_target.iD}) %}
{% else %} {# Create a fresh part #}
{% set href = path('info_providers_create_part',
{'providerKey': result.provider_key, 'providerId': result.provider_id}) %}
{% endif %}
<a class="btn btn-primary" href="{{ href }}"
target="_blank" title="{% trans %}part.create.btn{% endtrans %}">
<i class="fa-solid fa-plus-square"></i>
</a>
</td>
<th></th>
<th>{% trans %}name.label{% endtrans %} / {% trans %}part.table.mpn{% endtrans %}</th>
<th>{% trans %}description.label{% endtrans %} / {% trans %}category.label{% endtrans %}</th>
<th>{% trans %}manufacturer.label{% endtrans %} / {% trans %}footprint.label{% endtrans %}</th>
<th>{% trans %}part.table.manufacturingStatus{% endtrans %}</th>
<th>{% trans %}info_providers.table.provider.label{% endtrans %}</th>
<th></th>
</tr>
{% endfor %}
</thead>
<tbody>
{% for result in results %}
<tr>
<td>
<img src="{{ result.preview_image_url }}" data-thumbnail="{{ result.preview_image_url }}"
class="hoverpic" style="max-width: 45px;" {{ stimulus_controller('elements/hoverpic') }}>
</td>
<td>
{% if result.provider_url is not null %}
<a href="{{ result.provider_url }}" target="_blank" rel="noopener">{{ result.name }}</a>
{% else %}
{{ result.name }}
{% endif %}
</tbody>
</table>
{% if result.mpn is not null %}
<br>
<small class="text-muted" title="{% trans %}part.table.mpn{% endtrans %}">{{ result.mpn }}</small>
{% endif %}
</td>
<td>
{{ result.description }}
{% if result.category is not null %}
<br>
<small class="text-muted">{{ result.category }}</small>
{% endif %}
</td>
<td>
{{ result.manufacturer ?? '' }}
{% if result.footprint is not null %}
<br>
<small class="text-muted">{{ result.footprint }}</small>
{% endif %}
</td>
<td>{{ helper.m_status_to_badge(result.manufacturing_status) }}</td>
<td>
{% if result.provider_url %}
<a href="{{ result.provider_url }}" target="_blank" rel="noopener">
{{ info_provider_label(result.provider_key)|default(result.provider_key) }}
</a>
{% else %}
{{ info_provider_label(result.provider_key)|default(result.provider_key) }}
{% endif %}
<br>
<small class="text-muted">{{ result.provider_id }}</small>
<td>
{% if update_target %} {# We update an existing part #}
{% set href = path('info_providers_update_part',
{'providerKey': result.provider_key, 'providerId': result.provider_id, 'id': update_target.iD}) %}
{% else %} {# Create a fresh part #}
{% set href = path('info_providers_create_part',
{'providerKey': result.provider_key, 'providerId': result.provider_id}) %}
{% endif %}
<a class="btn btn-primary" href="{{ href }}"
target="_blank" title="{% trans %}part.create.btn{% endtrans %}">
<i class="fa-solid fa-plus-square"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info" role="alert">
{% trans %}info_providers.search.no_results{% endtrans %}
</div>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -22,7 +22,7 @@
data-delete-title="{% trans %}log.undo.confirm_title{% endtrans %}"
data-delete-message="{% trans %}log.undo.confirm_message{% endtrans %}">
<input type="hidden" name="redirect_back" value="{{ app.request.baseUrl ~ app.request.requestUri }}">
<input type="hidden" name="redirect_back" value="{{ uri_without_host(app.request) }}">
<div class="btn-group btn-group-sm" role="group">
<button type="submit" class="btn btn-outline-secondary" name="undo" value="{{ entry.id }}" {% if disabled %}disabled{% endif %}>

View File

@@ -1,96 +1,131 @@
{% import "helper.twig" as helper %}
<div class="row">
<div class="col-md-3 col-lg-4 col-3 mt-auto mb-auto">
{% include "parts/info/_picture.html.twig" %}
</div>
<div class="col-md-9 col-lg-8 col-7">
<h5 class="text-muted pt-2" title="{% trans %}manufacturer.label{% endtrans %}">
{% if part.manufacturer %}
{% if part.manufacturer.id is not null %}
<a href="{{ entity_url(part.manufacturer, 'list_parts') }}">{{ part.manufacturer.name}}</a>
{% if part.manufacturer or part.manufacturerProductUrl or part.manufacturerProductNumber %}
<h5 class="text-muted pt-2" title="{% trans %}manufacturer.label{% endtrans %}">
{% if part.manufacturer %}
{% if part.manufacturer.id is not null %}
<a href="{{ entity_url(part.manufacturer, 'list_parts') }}">{{ part.manufacturer.name}}</a>
{% else %}
{{ part.manufacturer.name }}
{% endif %}
{% endif %}
{% if part.manufacturerProductUrl %}
<small>
<a class="link-external" href="{{ part.manufacturerProductUrl }}" rel="noopener" target="_blank">
{% if part.manufacturerProductNumber is not empty %}
{{ part.manufacturerProductNumber }}
{% else %}
{{ part.manufacturer.name }}
<i>{{ part.name }}</i>
{% endif %}
{% endif %}
{% if part.manufacturerProductUrl %}
<small>
<a class="link-external" href="{{ part.manufacturerProductUrl }}" rel="noopener" target="_blank">
{% if part.manufacturerProductNumber is not empty %}
{{ part.manufacturerProductNumber }}
{% else %}
<i>{{ part.name }}</i>
{% endif %}
</a>
</small>
{% else %}
<small>{{ part.manufacturerProductNumber }}</small>
{% endif %}
</h5>
<h3 class="w-fit" title="{% trans %}name.label{% endtrans %}">{{ part.name }}
{# You need edit permission to use the edit button #}
{% if timeTravel is not null %}
<a href="{{ entity_url(part, 'info') }}"><i title="{% trans %}part.back_to_info{% endtrans %}" class="fas fa-fw fa-arrow-circle-left"></i></a>
{% elseif is_granted('edit', part) %}
<a href="{{ entity_url(part, 'edit') }}"><i class="fas fa-fw fa-sm fa-edit"></i></a>
{% endif %}
</h3>
<h6 class="text-muted w-fit" title="{% trans %}description.label{% endtrans %}"><span>{{ part.description|format_markdown(true) }}</span></h6>
<h6 class="">
<i class="fas fa-tag fa-fw" title="{% trans %}category.label{% endtrans %}"></i>
<span class="text-muted">{{ helper.structural_entity_link(part.category) }}</span>
</h6>
<h6><i class="fas fa-shapes fa-fw"></i>
<span class="{% if part.notEnoughInstock and not part.amountUnknown %}text-danger font-weight-bold{% else %}text-muted{% endif %}">
{% if not part.amountUnknown %}
{# For known instock we can just show the label as normal #}
<span title="{% trans %}instock.label{% endtrans %}">{{ part.amountSum | format_amount(part.partUnit) }}</span>
{% else %}
{% if part.amountSum == 0.0 %}
<b title="{% trans %}part_lots.instock_unknown{% endtrans %}">?</b>
{% else %}
<span title="{% trans %}part_lots.instock_unknown{% endtrans %}">≥{{ part.amountSum | format_amount(part.partUnit) }}</span>
{% endif %}
{% endif %}
{% if part.expiredAmountSum > 0 %}
<span title="{% trans %}part_lots.is_expired{% endtrans %}" class="text-muted">(+{{ part.expiredAmountSum }})</span>
{% endif %}
/
<span title="{% trans %}mininstock.label{% endtrans %}">{{ part.minAmount | format_amount(part.partUnit) }}</span>
</span>
{% if part.notEnoughInstock %}
<span class="badge badge-warning bg-warning rounded-pill"><i class="fa-solid fa-less-than-equal"></i>&nbsp;{% trans %}part.info.amount.less_than_desired{% endtrans %}</span>
{% endif %}
</h6>
<h6 class="">
<i class="fas fa-microchip fa-fw" title="{% trans %}footprint.label{% endtrans %}"></i>
<span class="text-muted">{{ helper.structural_entity_link(part.footprint) }}</span>
</h6>
</a>
</small>
{% elseif part.manufacturerProductNumber %}
<small>{{ part.manufacturerProductNumber }}</small>
{% endif %}
</h5>
{% endif %}
{% set min_order_amount = pricedetail_helper.minOrderAmount(part) %}
{% set max_order_amount = pricedetail_helper.maxDiscountAmount(part) %}
{% set max_order_price = pricedetail_helper.calculateAvgPrice(part, max_order_amount, app.user.currency ?? null) %}
{% set min_order_price = pricedetail_helper.calculateAvgPrice(part, min_order_amount, app.user.currency ?? null ) %}
{% if max_order_price is not null %}
<h6>
<h3 class="w-fit" title="{% trans %}name.label{% endtrans %}">
{{ part.name }}
{# You need edit permission to use the edit button #}
{% if timeTravel is not null %}
<a href="{{ entity_url(part, 'info') }}"><i title="{% trans %}part.back_to_info{% endtrans %}" class="fas fa-fw fa-arrow-circle-left"></i></a>
{% elseif is_granted('edit', part) %}
<a href="{{ entity_url(part, 'edit') }}"><i class="fas fa-fw fa-sm fa-edit"></i></a>
{% endif %}
</h3>
{# Slighlty highlight every text in this block over normal text (similar to h5) #}
<dl style="font-weight: 500;">
<div class="">
<dt>
<span class="visually-hidden">{% trans %}description.label{% endtrans %}</span>
</dt>
<dd class="d-inline">
<span class="text-muted w-fit" title="{% trans %}description.label{% endtrans %}">{{ part.description|format_markdown(true) }}</span>
</dd>
</div>
<div>
<dt class="d-inline-block">
<span class="visually-hidden">{% trans %}category.label{% endtrans %}</span>
<i class="fas fa-tag fa-fw" title="{% trans %}category.label{% endtrans %}"></i>
</dt>
<dd class="d-inline">
<span class="text-muted">{{ helper.structural_entity_link(part.category) }}</span>
</dd>
</div>
<div>
<dt class="d-inline-block">
<span class="visually-hidden">{% trans %}part.part_lots.label{% endtrans %}</span>
<i class="fas fa-shapes fa-fw"></i>
</dt>
<dd class="d-inline">
<span class="{% if part.notEnoughInstock and not part.amountUnknown %}text-danger font-weight-bold{% else %}text-muted{% endif %}">
{% if not part.amountUnknown %}
{# For known instock we can just show the label as normal #}
<span title="{% trans %}instock.label{% endtrans %}">{{ part.amountSum | format_amount(part.partUnit) }}</span>
{% else %}
{% if part.amountSum == 0.0 %}
<b title="{% trans %}part_lots.instock_unknown{% endtrans %}">?</b>
{% else %}
<span title="{% trans %}part_lots.instock_unknown{% endtrans %}">≥{{ part.amountSum | format_amount(part.partUnit) }}</span>
{% endif %}
{% endif %}
{% if part.expiredAmountSum > 0 %}
<span title="{% trans %}part_lots.is_expired{% endtrans %}" class="text-muted">(+{{ part.expiredAmountSum }})</span>
{% endif %}
/
<span title="{% trans %}mininstock.label{% endtrans %}">{{ part.minAmount | format_amount(part.partUnit) }}</span>
</span>
{% if part.notEnoughInstock %}
<span class="badge badge-warning bg-warning rounded-pill"><i class="fa-solid fa-less-than-equal"></i>&nbsp;{% trans %}part.info.amount.less_than_desired{% endtrans %}</span>
{% endif %}
</dd>
</div>
<div>
<dt class="d-inline-block">
<span class="visually-hidden">{% trans %}footprint.label{% endtrans %}</span>
<i class="fas fa-microchip fa-fw" title="{% trans %}footprint.label{% endtrans %}"></i>
</dt>
<dd class="d-inline">
<span class="text-muted">{{ helper.structural_entity_link(part.footprint) }}</span>
</dd>
</div>
{% set min_order_amount = pricedetail_helper.minOrderAmount(part) %}
{% set max_order_amount = pricedetail_helper.maxDiscountAmount(part) %}
{% set max_order_price = pricedetail_helper.calculateAvgPrice(part, max_order_amount, app.user.currency ?? null) %}
{% set min_order_price = pricedetail_helper.calculateAvgPrice(part, min_order_amount, app.user.currency ?? null ) %}
{% if max_order_price is not null %}
<div>
<dt class="d-inline-block">
<i class="fas fa-money-bill-alt fa-fw"></i>
</dt>
<dd class="d-inline">
<span class="text-muted">
<span title="{% trans %}part.avg_price.label{% endtrans %} {{ max_order_amount | format_amount(part.partUnit) }}">{{ max_order_price | format_money(app.user.currency ?? null) }}</span>
{% if min_order_price is not null and min_order_amount < max_order_amount %}
<span> - </span>
<span title="{% trans %}part.avg_price.label{% endtrans %} {{ min_order_amount | format_amount(part.partUnit) }}">{% if max_order_price is not null %}{{ min_order_price | format_money(app.user.currency ?? null) }}{% else %}???{% endif %}</span>
{% endif %}
</span>
</h6>
{% endif %}
{#
{% if part.comment != "" %}
<h6 title="{% trans %}comment.label{% endtrans %}">
<i class="fas fa-comment-alt fa-fw"></i>
<div class="d-inline-flex">
<span class="text-muted">{{ part.comment|nl2br }}</span>
</div>
</h6>
{% endif %} #}
</div>
</div>
</span>
</dd>
</div>
{% endif %}
{# {% if part.comment != "" %}
<div>
<dt class="d-inline-block">
<span class="visually-hidden">{% trans %}comment.label{% endtrans %}</span>
<i class="fas fa-comment-alt fa-fw" title="{% trans %}comment.label{% endtrans %}"></i>
</dt>
<dd class="d-inline">
<span class="text-muted">>{{ part.comment|nl2br }}</span>
</dd>
</div>
{% endif %} #}
</dl>

View File

@@ -7,7 +7,7 @@
</button>
{% endif %}
<div class="modal fade" id="merge-modal" tabindex="-1" aria-labelledby="merge-modal-title" tabindex="-1" aria-hidden="true" {{ stimulus_controller('pages/part_withdraw_modal') }}>
<div class="modal fade" id="merge-modal" tabindex="-1" aria-labelledby="merge-modal-title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content" {{ stimulus_controller('pages/part_merge_modal', {'targetId': part.iD }) }}>
<div class="modal-header">
@@ -17,7 +17,7 @@
<div class="modal-body">
{# non visible form elements #}
<input type="hidden" name="lot_id" value="">
<input type="hidden" name="_redirect" value="{{ app.request.baseUrl ~ app.request.requestUri }}">
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
<div class="row mb-2">
<label class="form-label">

View File

@@ -29,7 +29,7 @@
</table>
<a class="btn btn-success" {% if not is_granted('@projects.edit') %}disabled{% endif %}
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": app.request.baseUrl ~ app.request.requestUri}) }}">
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": uri_without_host(app.request)}) }}">
<i class="fa-solid fa-magnifying-glass-plus fa-fw"></i>
{% trans %}part.info.add_part_to_project{% endtrans %}
</a>

View File

@@ -69,7 +69,7 @@
{{ dropdown.profile_dropdown('part', part.id) }}
<a class="btn btn-success mt-2" {% if not is_granted('@projects.edit') %}disabled{% endif %}
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": app.request.baseUrl ~ app.request.requestUri}) }}">
href="{{ path('project_add_parts_no_id', {"parts": part.id, "_redirect": uri_without_host(app.request)}) }}">
<i class="fa-solid fa-magnifying-glass-plus fa-fw"></i>
{% trans %}part.info.add_part_to_project{% endtrans %}
</a>

View File

@@ -15,7 +15,7 @@
<input type="hidden" name="lot_id" value="">
<input type="hidden" name="action" value="">
<input type="hidden" name="_csfr" value="{{ csrf_token('part_withraw' ~ part.iD) }}">
<input type="hidden" name="_redirect" value="{{ app.request.baseUrl ~ app.request.requestUri }}">
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
<div class="row mb-2">
<label class="col-form-label col-sm-3">

View File

@@ -15,174 +15,177 @@
{% endblock %}
{% block card_title %}
<i class="fa {{ part.favorite ? 'fa-star' : 'fa-info-circle'}} fa-fw" aria-hidden="true"></i>
{% trans %}part.info.title{% endtrans %} <b>"{{ part.name }}"</b>
{% if timeTravel != null %}
<i>({{ timeTravel | format_datetime('short') }})</i>
{% endif %}
{% if part.projectBuildPart %}
(<i>{{ entity_type_label(part.builtProject) }}</i>: <a class="text-white" href="{{ entity_url(part.builtProject) }}">{{ part.builtProject.name }}</a>)
{% endif %}
<div class="float-end">
<span>
<i class="fa {{ part.favorite ? 'fa-star' : 'fa-info-circle'}} fa-fw" aria-hidden="true"></i>
{% trans %}part.info.title{% endtrans %} <b>"{{ part.name }}"</b>
{% if timeTravel != null %}
<i>({{ timeTravel | format_datetime('short') }})</i>
{% endif %}
{% if part.projectBuildPart %}
(<i>{{ entity_type_label(part.builtProject) }}</i>: <a class="text-white" href="{{ entity_url(part.builtProject) }}">{{ part.builtProject.name }}</a>)
{% endif %}
</span>
<span class="float-end">
{% trans %}id.label{% endtrans %}: {{ part.id }} {% if part.ipn is not empty %}(<i>{{ part.ipn }}</i>){% endif %}
</div>
</span>
{% endblock %}
{% block card_content %}
<div class="row">
<div class="col-md-9">
<div class="col col-md-3 mt-auto mb-auto">
{% include "parts/info/_picture.html.twig" %}
</div>
<div class="col-12 col-md-9 col-lg-6">
{% include "parts/info/_main_infos.html.twig" %}
</div>
<div class="col-md-3 offset-md-0 col-9 offset-3">
<div class="col offset-md-3 offset-lg-0">
{% include "parts/info/_sidebar.html.twig" %}
</div>
</div>
<div class="">
<div class="">
<ul class="nav nav-tabs" id="partTab" role="tablist">
<ul class="nav nav-tabs" id="partTab" role="tablist">
<li class="nav-item">
<a class="nav-link {% if part.partLots %}active{% endif %}" id="part_lots-tab" data-bs-toggle="tab"
href="#part_lots" role="tab">
<i class="fas fa-box fa-fw"></i>
{% trans %}part.part_lots.label{% endtrans %}
<span class="badge bg-secondary">{{ part.partLots | length }}</span>
</a>
</li>
{% if part.comment is not empty %}
<li class="nav-item">
<a class="nav-link {% if part.partLots %}active{% endif %}" id="part_lots-tab" data-bs-toggle="tab"
href="#part_lots" role="tab">
<i class="fas fa-box fa-fw"></i>
{% trans %}part.part_lots.label{% endtrans %}
<span class="badge bg-secondary">{{ part.partLots | length }}</span>
<a class="nav-link" id="comment-tab" data-bs-toggle="tab"
href="#comment" role="tab">
<i class="fas fa-sticky-note fa-fw"></i>
{% trans %}comment.label{% endtrans %}
</a>
</li>
{% if part.comment is not empty %}
<li class="nav-item">
<a class="nav-link" id="comment-tab" data-bs-toggle="tab"
href="#comment" role="tab">
<i class="fas fa-sticky-note fa-fw"></i>
{% trans %}comment.label{% endtrans %}
</a>
</li>
{% endif %}
{% if part.parameters is not empty or description_params is not empty or comment_params is not empty %}
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" role="tab" href="#specifications">
<i class="fas fa-atlas fa-fw"></i>
{% trans %}part.info.specifications{% endtrans %}
<span class="badge bg-secondary">{{ part.parameters | length }}</span>
</a>
</li>
{% endif %}
{% if part.attachments is not empty %}
<li class="nav-item">
<a class="nav-link" id="attachment-tab" data-bs-toggle="tab"
href="#attachments" role="tab">
<i class="fas fa-paperclip fa-fw"></i>
{% trans %}attachment.labelp{% endtrans %}
<span class="badge bg-secondary">{{ part.attachments | length }}</span>
</a>
</li>
{% endif %}
{% if part.orderdetails is not empty %}
<li class="nav-item">
<a class="nav-link" id="supplier-tab" data-bs-toggle="tab" href="#suppliers" role="tab">
<i class="fas fa-shopping-cart fa-fw"></i>
{% trans %}vendor.partinfo.shopping_infos{% endtrans %}
<span class="badge bg-secondary">{{ part.orderdetails | length }}</span>
</a>
</li>
{% endif %}
{% if part.associatedPartsAll is not empty %}
<li class="nav-item">
<a class="nav-link" id="associations-tab" data-bs-toggle="tab" href="#associations" role="tab">
<i class="fas fas fa-circle-nodes fa-fw fa-fw"></i>
{% trans %}part.edit.tab.associations{% endtrans %}
<span class="badge bg-secondary">{{ part.associatedPartsAll | length }}</span>
</a>
</li>
{% endif %}
<li class="nav-item {% if datatable is null %}not-allowed{% endif %}">
<a class="nav-link {% if datatable is null %}disabled{% endif %}" id="history-tab" data-bs-toggle="tab" href="#history" role="tab">
<i class="fas fa-history"></i>
{% trans %}vendor.partinfo.history{% endtrans %}
</a>
</li>
{% if part.projectBomEntries is not empty %}
<li class="nav-item">
<a class="nav-link" id="projects-tab" data-bs-toggle="tab" href="#projects" role="tab">
<i class="fas fa-archive fa-fw"></i>
{% trans %}project.labelp{% endtrans %}
<span class="badge bg-secondary">{{ part.projectBomEntries | length }}</span>
</a>
</li>
{% endif %}
{% endif %}
{% if part.parameters is not empty or description_params is not empty or comment_params is not empty %}
<li class="nav-item">
<a class="nav-link" id="tools-tab" data-bs-toggle="tab" href="#tools" role="tab">
<i class="fas fa-tools"></i>
{% trans %}tools.label{% endtrans %}
<a class="nav-link" data-bs-toggle="tab" role="tab" href="#specifications">
<i class="fas fa-atlas fa-fw"></i>
{% trans %}part.info.specifications{% endtrans %}
<span class="badge bg-secondary">{{ part.parameters | length }}</span>
</a>
</li>
{% endif %}
{% if part.attachments is not empty %}
<li class="nav-item">
<a class="nav-link" id="extended_info-tab" data-bs-toggle="tab" href="#extended_info" role="tab">
<i class="fas fa-clipboard-list"></i>
{% trans %}extended_info.label{% endtrans %}
<a class="nav-link" id="attachment-tab" data-bs-toggle="tab"
href="#attachments" role="tab">
<i class="fas fa-paperclip fa-fw"></i>
{% trans %}attachment.labelp{% endtrans %}
<span class="badge bg-secondary">{{ part.attachments | length }}</span>
</a>
</li>
{% endif %}
{% if part.orderdetails is not empty %}
<li class="nav-item">
<a class="nav-link" id="supplier-tab" data-bs-toggle="tab" href="#suppliers" role="tab">
<i class="fas fa-shopping-cart fa-fw"></i>
{% trans %}vendor.partinfo.shopping_infos{% endtrans %}
<span class="badge bg-secondary">{{ part.orderdetails | length }}</span>
</a>
</li>
{% endif %}
</ul>
<div class="tab-content" id="myTabContent">
{% if part.comment is not empty %}
<div class="tab-pane fade show" id="comment" role="tabpanel" aria-labelledby="home-tab">
<div class="container-fluid mt-2 latex" data-controller="common--latex">
{{ part.comment|format_markdown }}
</div>
{% if part.associatedPartsAll is not empty %}
<li class="nav-item">
<a class="nav-link" id="associations-tab" data-bs-toggle="tab" href="#associations" role="tab">
<i class="fas fas fa-circle-nodes fa-fw fa-fw"></i>
{% trans %}part.edit.tab.associations{% endtrans %}
<span class="badge bg-secondary">{{ part.associatedPartsAll | length }}</span>
</a>
</li>
{% endif %}
<li class="nav-item {% if datatable is null %}not-allowed{% endif %}">
<a class="nav-link {% if datatable is null %}disabled{% endif %}" id="history-tab" data-bs-toggle="tab" href="#history" role="tab">
<i class="fas fa-history"></i>
{% trans %}vendor.partinfo.history{% endtrans %}
</a>
</li>
{% if part.projectBomEntries is not empty %}
<li class="nav-item">
<a class="nav-link" id="projects-tab" data-bs-toggle="tab" href="#projects" role="tab">
<i class="fas fa-archive fa-fw"></i>
{% trans %}project.labelp{% endtrans %}
<span class="badge bg-secondary">{{ part.projectBomEntries | length }}</span>
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" id="tools-tab" data-bs-toggle="tab" href="#tools" role="tab">
<i class="fas fa-tools"></i>
{% trans %}tools.label{% endtrans %}
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="extended_info-tab" data-bs-toggle="tab" href="#extended_info" role="tab">
<i class="fas fa-clipboard-list"></i>
{% trans %}extended_info.label{% endtrans %}
</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
{% if part.comment is not empty %}
<div class="tab-pane fade show" id="comment" role="tabpanel" aria-labelledby="home-tab">
<div class="container-fluid mt-2 latex" data-controller="common--latex">
{{ part.comment|format_markdown }}
</div>
{% endif %}
<div class="tab-pane fade show active" id="part_lots" role="tabpanel" aria-labelledby="part_lots-tab">
{% include "parts/info/_part_lots.html.twig" %}
</div>
{% endif %}
{% if part.attachments is not empty %}
<div class="tab-pane fade" id="attachments" role="tabpanel" aria-labelledby="attachment-tab">
{% include "parts/info/_attachments_info.html.twig" %}
</div>
{% endif %}
<div class="tab-pane fade show active" id="part_lots" role="tabpanel" aria-labelledby="part_lots-tab">
{% include "parts/info/_part_lots.html.twig" %}
</div>
{% if part.orderdetails is not empty %}
<div class="tab-pane fade" id="suppliers" role="tabpanel" aria-labelledby="supplier-tab">
{% include "parts/info/_order_infos.html.twig" %}
{% if part.attachments is not empty %}
<div class="tab-pane fade" id="attachments" role="tabpanel" aria-labelledby="attachment-tab">
{% include "parts/info/_attachments_info.html.twig" %}
</div>
{% endif %}
{% endif %}
{% if part.associatedPartsAll is not empty %}
<div class="tab-pane fade" id="associations" role="tabpanel" aria-labelledby="associations-tab">
{% include "parts/info/_associations.html.twig" %}
</div>
{% endif %}
{% if part.orderdetails is not empty %}
<div class="tab-pane fade" id="suppliers" role="tabpanel" aria-labelledby="supplier-tab">
{% include "parts/info/_order_infos.html.twig" %}
</div>
{% endif %}
<div class="tab-pane fade" id="projects" role="tabpanel" aria-labelledby="projects-tab">
{% include "parts/info/_projects.html.twig" %}
{% if part.associatedPartsAll is not empty %}
<div class="tab-pane fade" id="associations" role="tabpanel" aria-labelledby="associations-tab">
{% include "parts/info/_associations.html.twig" %}
</div>
{% endif %}
<div class="tab-pane fade" id="history" role="tabpanel" aria-labelledby="history-tab">
{% include "parts/info/_history.html.twig" %}
<div class="tab-pane fade" id="projects" role="tabpanel" aria-labelledby="projects-tab">
{% include "parts/info/_projects.html.twig" %}
</div>
<div class="tab-pane fade" id="history" role="tabpanel" aria-labelledby="history-tab">
{% include "parts/info/_history.html.twig" %}
</div>
<div class="tab-pane fade" id="tools" role="tabpanel" aria-labelledby="tools-tab">
{% include "parts/info/_tools.html.twig" %}
</div>
{% if part.parameters is not empty or description_params is not empty or comment_params is not empty %}
<div class="tab-pane fade" id="specifications" role="tabpanel" aria-labelledby="tools-tab">
{% include "parts/info/_specifications.html.twig" %}
</div>
{% endif %}
<div class="tab-pane fade" id="tools" role="tabpanel" aria-labelledby="tools-tab">
{% include "parts/info/_tools.html.twig" %}
</div>
<div class="tab-pane fade" id="extended_info" role="tabpanel" aria-labelledby="extended_info-tab">
{% if part.parameters is not empty or description_params is not empty or comment_params is not empty %}
<div class="tab-pane fade" id="specifications" role="tabpanel" aria-labelledby="tools-tab">
{% include "parts/info/_specifications.html.twig" %}
</div>
{% endif %}
{% include "parts/info/_extended_infos.html.twig" %}
<div class="tab-pane fade" id="extended_info" role="tabpanel" aria-labelledby="extended_info-tab">
{% include "parts/info/_extended_infos.html.twig" %}
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -50,7 +50,7 @@
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" disabled {% if searchFilter.ordernr %}checked{% endif %}>
<label for="search_supplierpartnr" class="form-check-label justify-content-start">{% trans %}ordernumber.label.short{% endtrans %}</label>
<label for="search_supplierpartnr" class="form-check-label justify-content-start">{% trans %}orderdetails.edit.supplierpartnr{% endtrans %}</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" disabled {% if searchFilter.supplier %}checked{% endif %}>

View File

@@ -2,7 +2,7 @@
<div class="btn-group mb-2 mt-2">
<a class="btn btn-success" {% if not is_granted('@projects.edit') %}disabled{% endif %}
href="{{ path('project_add_parts', {"id": project.id, "_redirect": app.request.baseUrl ~ app.request.requestUri}) }}">
href="{{ path('project_add_parts', {"id": project.id, "_redirect": uri_without_host(app.request)}) }}">
<i class="fa-solid fa-square-plus fa-fw"></i>
{% trans %}project.info.bom_add_parts{% endtrans %}
</a>

View File

@@ -28,7 +28,7 @@
<div class="col-4">
<div class="input-group mb-3">
<input type="number" min="1" class="form-control" placeholder="{% trans %}project.builds.number_of_builds{% endtrans %}" name="n" required>
<input type="hidden" name="_redirect" value="{{ app.request.baseUrl ~ app.request.requestUri }}">
<input type="hidden" name="_redirect" value="{{ uri_without_host(app.request) }}">
<button class="btn btn-outline-secondary" type="submit" id="button-addon2">{% trans %}project.build.btn_build{% endtrans %}</button>
</div>
</div>

View File

@@ -11,7 +11,7 @@
</tr>
<tr>
<td>Symfony environment</td>
<td>{{ enviroment }} (Debug: {{ helper.boolean_badge(is_debug) }})</td>
<td>{{ environment }} (Debug: {{ helper.boolean_badge(is_debug) }})</td>
</tr>
<tr>
<td>Part-DB Instance name</td>
@@ -42,8 +42,8 @@
<td>{{ helper.boolean_badge(demo_mode) }}</td>
</tr>
<tr>
<td>GPDR Compliance Mode</td>
<td>{{ helper.boolean_badge(gpdr_compliance) }}</td>
<td>GDPR Compliance Mode</td>
<td>{{ helper.boolean_badge(gdpr_compliance) }}</td>
</tr>
<tr class="table-info">
<td colspan="2"><b>Users</b></td>

View File

@@ -18,34 +18,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Tests\Exceptions;
use App\Exceptions\TwigModeException;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Twig\Error\Error;
namespace App\Form\Fixes;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
class FixNumberType extends AbstractTypeExtension
class TwigModeExceptionTest extends KernelTestCase
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
//Remove existing view transformers
$builder->resetViewTransformers();
private string $projectPath;
//And add our fixed version
$builder->addViewTransformer(new FixedNumberToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
$options['rounding_mode'],
$options['html5'] ? 'en' : null
));
public function setUp(): void
{
self::bootKernel();
$this->projectPath = self::getContainer()->getParameter('kernel.project_dir');
}
public static function getExtendedTypes(): iterable
public function testGetSafeMessage(): void
{
return [NumberType::class];
$testException = new Error("Error at : " . $this->projectPath . "/src/dir/path/file.php");
$twigModeException = new TwigModeException($testException);
$this->assertSame("Error at : " . $this->projectPath . "/src/dir/path/file.php", $testException->getMessage());
$this->assertSame("Error at : [Part-DB Root Folder]/src/dir/path/file.php", $twigModeException->getSafeMessage());
}
}
}

View File

@@ -80,10 +80,10 @@ class SandboxedTwigFactoryTest extends WebTestCase
'];
yield ['
{{ location.isRoot}} {{ location.isChildOf(location) }} {{ location.comment }} {{ location.level }}
{{ location.fullPath }} {% set arr = location.pathArray %} {% set child = location.children %} {{location.childrenNotSelectable}}
{{ location.fullPath }} {% set arr = location.pathArray %} {% set child = location.children %} {{location.notSelectable}}
'];
yield ['
{{ part.reviewNeeded }} {{ part.tags }} {{ part.mass }}
{{ part.needsReview }} {{ part.tags }} {{ part.mass }}
'];
yield ['
{{ entity_type(part) is object }}

View File

@@ -11196,7 +11196,7 @@ Element 3</target>
<unit id="9jOklgS" name="log.user_login.ip_anonymize_hint">
<segment state="translated">
<source>log.user_login.ip_anonymize_hint</source>
<target>Pokud poslední číslice IP adresy chybí, je povolen režim GPDR, ve kterém jsou IP adresy anynomizovány.</target>
<target>Pokud poslední číslice IP adresy chybí, je povolen režim GDPR, ve kterém jsou IP adresy anynomizovány.</target>
</segment>
</unit>
<unit id="kaMyDVi" name="log.user_not_allowed.unauthorized_access_attempt_to">

View File

@@ -11201,7 +11201,7 @@ Oversættelsen
<unit id="9jOklgS" name="log.user_login.ip_anonymize_hint">
<segment state="translated">
<source>log.user_login.ip_anonymize_hint</source>
<target>Hvis de sidste cifre i IP-adressen mangler, aktiveres DSGV-tilstanden, hvor IP-adresserne anonymiseres.</target>
<target>Hvis de sidste cifre i IP-adressen mangler, aktiveres databeskyttelsesforordningen mode, hvor IP-adresserne anonymiseres.</target>
</segment>
</unit>
<unit id="kaMyDVi" name="log.user_not_allowed.unauthorized_access_attempt_to">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

12227
translations/messages.pl.xlf Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,13 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
<file id="security.en">
<unit id="aazoCks" name="user.login_error.user_disabled">
<segment>
<segment state="translated">
<source>user.login_error.user_disabled</source>
<target>Your account is disabled! Contact an administrator if you think this is wrong.</target>
</segment>
</unit>
<unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml">
<segment>
<segment state="translated">
<source>saml.error.cannot_login_local_user_per_saml</source>
<target>You cannot login as local user via SSO! Use your local user password instead.</target>
</segment>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
<file id="validators.en">
<unit id="xevSdCK" name="part.master_attachment.must_be_picture">
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentContainingDBElement.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
@@ -42,7 +42,7 @@
<target>Der Vorschauanhang muss ein gültiges Bild sein!</target>
</segment>
</unit>
<unit id="VJHTkxx" name="structural.entity.unique_name">
<unit id="v8HkcJB" name="structural.entity.unique_name">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Base\AbstractCompany.php:0</note>
@@ -87,7 +87,7 @@
<target>Es kann auf jeder Ebene nur ein Objekt mit dem gleichem Namen geben!</target>
</segment>
</unit>
<unit id="3ODUtpU" name="parameters.validator.min_lesser_typical">
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -107,7 +107,7 @@
<target>Wert muss kleiner oder gleich als der typische Wert sein ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="jDBA_WW" name="parameters.validator.min_lesser_max">
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -127,7 +127,7 @@
<target>Wert muss kleiner als der Maximalwert sein ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="ygK_e_X" name="parameters.validator.max_greater_typical">
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -147,7 +147,7 @@
<target>Wert muss größer oder gleich dem typischen Wert sein ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="isXL.ie" name="validator.user.username_already_used">
<unit id="P41193Y" name="validator.user.username_already_used">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -157,7 +157,7 @@
<target>Es existiert bereits ein Benutzer mit diesem Namen.</target>
</segment>
</unit>
<unit id="NcM463r" name="user.invalid_username">
<unit id="EKPQiyf" name="user.invalid_username">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -167,7 +167,7 @@
<target>Der Benutzername darf nur Buchstaben, Zahlen, Unterstriche, Punkte, Plus- oder Minuszeichen enthalten.</target>
</segment>
</unit>
<unit id="lZvhKYu" name="validator.noneofitschild.self">
<unit id="_v.DMg." name="validator.noneofitschild.self">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
@@ -176,176 +176,188 @@
<target>Ein Element kann nicht sein eigenenes übergeordnetes Element sein!</target>
</segment>
</unit>
<unit id="pr07aV4" name="validator.noneofitschild.children">
<unit id="W90LyFQ" name="validator.noneofitschild.children">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="final">
<segment state="translated">
<source>validator.noneofitschild.children</source>
<target>Ein Kindelement kann nicht das übergeordnete Element sein!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<unit id="GAUS.LK" name="validator.select_valid_category">
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Bitte wählen Sie eine gültige Kategorie.</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<unit id="h6qELde" name="validator.part_lot.only_existing">
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Der Lagerort wurde als "nur bestehende Teile" markiert, daher können keine neuen Teile hinzugefügt werden.</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<unit id="Prriyy0" name="validator.part_lot.location_full.no_increase">
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Lagerort ist voll. Bestand kann nicht erhöht werden (neuer Wert muss kleiner sein als {{old_amount}}).</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<segment state="final">
<unit id="eeEjB4s" name="validator.part_lot.location_full">
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>Der Lagerort ist voll, daher können keine neue Teile hinzugefügt werden.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<segment state="final">
<unit id="2yWi8eP" name="validator.part_lot.single_part">
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>Der Lagerort wurde als "Nur ein Bauteil" markiert, daher kann kein neues Bauteil hinzugefügt werden.</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<unit id="A.TFhbb" name="validator.attachment.must_not_be_null">
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>Sie müssen ein Dateitypen auswählen!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>Sie müssen einen Lieferanten auswählen!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>Um SI-Prefixe zu aktivieren, müssen Sie einen Einheitensymbol setzen!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>Die Internal Part Number (IPN) muss einzigartig sein. Der Wert {{value}} wird bereits benutzt!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>Sie müssen ein Bauteil auswählen, oder einen Namen für ein nicht-Bauteil BOM-Eintrag setzen!</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>Es gibt bereits einen BOM Eintrag mit diesem Namen!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<unit id="jB3B50E" name="project.bom_entry.part_already_in_bom">
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>Dieses Bauteil existiert bereits in der BOM!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>Die Anzahl der Bestückungsnamen muss mit der Menge der zu bestückenden Bauteile übereinstimmen!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>Die BOM eines Projektes kann nicht das eigene Produktionsbauteil enthalten!</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>Die Projekt-BOM muss alle Produktionsbauteile der Unterprojekte enthalten. Bauteil %part_name% des Projektes %project_name% fehlt!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Sie können keinen Preis für Bauteil-BOM-Einträge definieren. Definieren Sie die Preise stattdessen auf dem Bauteil.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>Sie haben mehr zur Entnahme ausgewählt als notwendig. Entfernen Sie die überflüssige Anzahl.</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>Sie haben weniger zur Entnahme ausgewählt, als zum Bau notwendig ist! Fügen Sie mehr hinzu.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<unit id="yZGS8uZ" name="part.name.must_match_category_regex">
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>Der Bauteilename entspricht nicht dem regulären Ausdruck, der von der Kategorie vorgegeben wurde: %regex%</target>
</segment>
</unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<unit id="Q8wP5Jd" name="validator.attachment.name_not_blank">
<segment state="translated">
<source>validator.attachment.name_not_blank</source>
<target>Wählen Sie einen Wert, oder laden Sie eine Datei hoch, um dessen Dateiname automatisch als Namen für diesen Anhang zu nutzen.</target>
</segment>
</unit>
<unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner">
<unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated">
<source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>Der Besitzer dieses Bauteilebestandes und des gewählten Lagerortes müssen übereinstimmen (%owner_name%)!</target>
</segment>
</unit>
<unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous">
<unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated">
<source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>Der Eigentümer darf nicht der anonymous Benutzer sein!</target>
</segment>
</unit>
<unit id="N8aA0Uh" name="validator.part_association.must_set_an_value_if_type_is_other">
<unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated">
<source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>Wenn die Art der Verknüpfung auf "Andere" gesetzt wurde, müssen Sie einen beschreibenden Wert setzen!</target>
</segment>
</unit>
<unit id="9VYNZ4v" name="validator.part_association.part_cannot_be_associated_with_itself">
<unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated">
<source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>Ein Bauteil kann nicht mit sich selbst verknüpft werden!</target>
</segment>
</unit>
<unit id="csc1PNn" name="validator.part_association.already_exists">
<unit id="q5Ej6Xm" name="validator.part_association.already_exists">
<segment state="translated">
<source>validator.part_association.already_exists</source>
<target>Eine Verknüpfung mit diesem Bauteil existiert bereits!</target>
</segment>
</unit>
<unit id="sfW4NYE" name="validator.part_lot.vendor_barcode_must_be_unique">
<unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated">
<source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>Dieser Lieferanten Barcode Wert wird bereits bei einem anderen Bestand verwendet. Der Barcode muss eindeutig sein!</target>
</segment>
</unit>
<unit id="o1qmPUm" name="validator.year_2038_bug_on_32bit">
<unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit">
<segment state="translated">
<source>validator.year_2038_bug_on_32bit</source>
<target>Aufgrund technischer Beschränkungen ist es nicht möglich, ein Datum nach dem 19.01.2038 auf 32-Bit Systemen auszuwählen!</target>
</segment>
</unit>
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>Der gegebene Bereich ist nicht gültig!</target>
</segment>
</unit>
<unit id="m4gp2P_" name="validator.google_code.wrong_code">
<segment state="translated">
<source>validator.google_code.wrong_code</source>
<target>Ungültiger Code. Überprüfen Sie, dass die Authenticator App korrekt eingerichtet ist und dass der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben.</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
<file id="validators.en">
<unit id="xevSdCK" name="part.master_attachment.must_be_picture">
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentContainingDBElement.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
@@ -37,12 +37,12 @@
<note priority="1">Part-DB1\src\Entity\UserSystem\Group.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>part.master_attachment.must_be_picture</source>
<target>The preview attachment must be a valid picture!</target>
</segment>
</unit>
<unit id="VJHTkxx" name="structural.entity.unique_name">
<unit id="v8HkcJB" name="structural.entity.unique_name">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Base\AbstractCompany.php:0</note>
@@ -82,12 +82,12 @@
<note priority="1">src\Entity\StructuralDBElement.php:0</note>
<note priority="1">src\Entity\Supplier.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>structural.entity.unique_name</source>
<target>An element with this name already exists on this level!</target>
</segment>
</unit>
<unit id="3ODUtpU" name="parameters.validator.min_lesser_typical">
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -102,12 +102,12 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_typical</source>
<target>Value must be lesser or equal the the typical value ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="jDBA_WW" name="parameters.validator.min_lesser_max">
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -122,12 +122,12 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.min_lesser_max</source>
<target>Value must be lesser than the maximum value ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="ygK_e_X" name="parameters.validator.max_greater_typical">
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -142,216 +142,222 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>parameters.validator.max_greater_typical</source>
<target>Value must be greater or equal than the typical value ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="isXL.ie" name="validator.user.username_already_used">
<unit id="P41193Y" name="validator.user.username_already_used">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>validator.user.username_already_used</source>
<target>A user with this name is already exisiting</target>
</segment>
</unit>
<unit id="NcM463r" name="user.invalid_username">
<unit id="EKPQiyf" name="user.invalid_username">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes>
<segment>
<segment state="translated">
<source>user.invalid_username</source>
<target>The username must contain only letters, numbers, underscores, dots, pluses or minuses!</target>
</segment>
</unit>
<unit id="lZvhKYu" name="validator.noneofitschild.self">
<unit id="_v.DMg." name="validator.noneofitschild.self">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="translated">
<source>validator.noneofitschild.self</source>
<target>An element can not be its own parent!</target>
</segment>
</unit>
<unit id="pr07aV4" name="validator.noneofitschild.children">
<unit id="W90LyFQ" name="validator.noneofitschild.children">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment>
<segment state="translated">
<source>validator.noneofitschild.children</source>
<target>You can not assign children element as parent (This would cause loops)!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<segment>
<unit id="GAUS.LK" name="validator.select_valid_category">
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Please select a valid category!</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<segment>
<unit id="h6qELde" name="validator.part_lot.only_existing">
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Can not add new parts to this location as it is marked as "Only Existing"</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<segment>
<unit id="Prriyy0" name="validator.part_lot.location_full.no_increase">
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Location is full. Amount can not be increased (new value must be smaller than {{ old_amount }}).</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<segment>
<unit id="eeEjB4s" name="validator.part_lot.location_full">
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>Location is full. Can not add new parts to it.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<segment>
<unit id="2yWi8eP" name="validator.part_lot.single_part">
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>This location can only contain a single part and it is already full!</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<segment>
<unit id="A.TFhbb" name="validator.attachment.must_not_be_null">
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>You must select an attachment type!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<segment>
<unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>You must select an supplier!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment>
<unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>To enable SI prefixes, you have to set a unit symbol!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<segment>
<unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>The internal part number must be unique. {{ value }} is already in use!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<segment>
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>You have to choose a part for a part BOM entry or set a name for a non-part BOM entry.</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<segment>
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>There is already an BOM entry with this name!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<segment>
<unit id="jB3B50E" name="project.bom_entry.part_already_in_bom">
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>This part already exists in the BOM!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<segment>
<unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>The number of mountnames has to match the BOMs quantity!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<segment>
<unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>You can not add a project's own builds part to the BOM.</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<segment>
<unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<segment>
<unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Prices are not allowed on BOM entries associated with a part. Define the price on the part instead.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<segment>
<unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>You have selected more quantity to withdraw than needed! Remove unnecessary quantity.</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<segment>
<unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>You have selected less quantity to withdraw than needed for the build! Add additional quantity.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<segment>
<unit id="yZGS8uZ" name="part.name.must_match_category_regex">
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>The part name does not match the regular expression stated by the category: %regex%</target>
</segment>
</unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<segment>
<unit id="Q8wP5Jd" name="validator.attachment.name_not_blank">
<segment state="translated">
<source>validator.attachment.name_not_blank</source>
<target>Set a value here, or upload a file to automatically use its filename as name for the attachment.</target>
</segment>
</unit>
<unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment>
<unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated">
<source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>The owner of this lot must match the owner of the selected storage location (%owner_name%)!</target>
</segment>
</unit>
<unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous">
<segment>
<unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated">
<source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>A lot owner must not be the anonymous user!</target>
</segment>
</unit>
<unit id="N8aA0Uh" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment>
<unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated">
<source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>If you set the type to "other", then you have to set a descriptive value for it!</target>
</segment>
</unit>
<unit id="9VYNZ4v" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment>
<unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated">
<source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>A part can not be associated with itself!</target>
</segment>
</unit>
<unit id="csc1PNn" name="validator.part_association.already_exists">
<segment>
<unit id="q5Ej6Xm" name="validator.part_association.already_exists">
<segment state="translated">
<source>validator.part_association.already_exists</source>
<target>The association with this part already exists!</target>
</segment>
</unit>
<unit id="sfW4NYE" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment>
<unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated">
<source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>This vendor barcode value was already used in another lot. The barcode must be unique!</target>
</segment>
</unit>
<unit id="o1qmPUm" name="validator.year_2038_bug_on_32bit">
<segment>
<unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit">
<segment state="translated">
<source>validator.year_2038_bug_on_32bit</source>
<target>Due to technical limitations, it is not possible to select dates after the 2038-01-19 on 32-bit systems!</target>
</segment>
</unit>
<unit id="iXcU7ce" name="validator.invalid_range">
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>The given range is not valid!</target>
</segment>
</unit>
<unit id="m4gp2P_" name="validator.google_code.wrong_code">
<segment state="translated">
<source>validator.google_code.wrong_code</source>
<target>Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly.</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="it">
<file id="validators.en">
<unit id="xevSdCK" name="part.master_attachment.must_be_picture">
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentContainingDBElement.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
@@ -42,7 +42,7 @@
<target>L'anteprima di un allegato deve essere un'immagine valida!</target>
</segment>
</unit>
<unit id="VJHTkxx" name="structural.entity.unique_name">
<unit id="v8HkcJB" name="structural.entity.unique_name">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Base\AbstractCompany.php:0</note>
@@ -87,7 +87,7 @@
<target>Un elemento con questo nome esiste già a questo livello!</target>
</segment>
</unit>
<unit id="3ODUtpU" name="parameters.validator.min_lesser_typical">
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -107,7 +107,7 @@
<target>Il valore deve essere inferiore o uguale al valore tipico ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="jDBA_WW" name="parameters.validator.min_lesser_max">
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -127,7 +127,7 @@
<target>Il valore deve essere inferiore al valore massimo ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="ygK_e_X" name="parameters.validator.max_greater_typical">
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -147,7 +147,7 @@
<target>Il valore deve essere maggiore o uguale al valore tipico ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="isXL.ie" name="validator.user.username_already_used">
<unit id="P41193Y" name="validator.user.username_already_used">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -157,7 +157,7 @@
<target>Esiste già un utente con questo nome</target>
</segment>
</unit>
<unit id="NcM463r" name="user.invalid_username">
<unit id="EKPQiyf" name="user.invalid_username">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -167,7 +167,7 @@
<target>Il nome utente deve contenere solo lettere, numeri, trattini bassi, punti, più o meno!</target>
</segment>
</unit>
<unit id="lZvhKYu" name="validator.noneofitschild.self">
<unit id="_v.DMg." name="validator.noneofitschild.self">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
@@ -176,7 +176,7 @@
<target>Un elemento non può essere il proprio elemento padre!</target>
</segment>
</unit>
<unit id="pr07aV4" name="validator.noneofitschild.children">
<unit id="W90LyFQ" name="validator.noneofitschild.children">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
@@ -185,167 +185,179 @@
<target>Un elemento figlio non può essere anche elemento padre!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<unit id="GAUS.LK" name="validator.select_valid_category">
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Selezionare una categoria valida.</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<unit id="h6qELde" name="validator.part_lot.only_existing">
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Questa ubicazione è stata contrassegnata come "solo parti esistenti", quindi non è possibile aggiungere nuove parti.</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<unit id="Prriyy0" name="validator.part_lot.location_full.no_increase">
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Questa ubicazione è piena. La quantità non può essere superiore a {{old_amount}}.</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<unit id="eeEjB4s" name="validator.part_lot.location_full">
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>Questa ubicazione è piena, non è possibile aggiungere nuovi componenti.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<unit id="2yWi8eP" name="validator.part_lot.single_part">
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>L'ubicazione è stata contrassegnata come "singolo componente", quindi non vi si possono aggiungere componenti.</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<unit id="A.TFhbb" name="validator.attachment.must_not_be_null">
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>Bisogna selezionare un tipo di file!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>Bisogna selezionare un fornitore!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>Per attivare i prefissi SI, è necessario impostare un simbolo di unità!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>Il codice interno (IPN) deve essere univoco. Il valore {{value}} è già in uso!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>È necessario selezionare un componente o assegnare un nome ad una voce BOM che non indica un componente!</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>Esiste già una voce BOM con questo nome!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<unit id="jB3B50E" name="project.bom_entry.part_already_in_bom">
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>Questo componente esiste già nella BOM!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>La quantità dei nomi delle parti deve coincidere con la quantità prevista in BOM!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>Non è possibile aggiungere un componente di produzione interno del progetto alla lista dei materiali (BOM).</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>Il progetto BOM (lista dei materiali) deve contenere tutti i componenti di produzione dei sottoprogetti. Manca il componente %part_name% del progetto %project_name%!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Non è possibile definire un prezzo per le voci BOM (lista dei materiali). Definisci invece i prezzi nella scheda del componente.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>E' stato selezionato più del necessario per il prelievo. Rimuovere la quantità superflua.</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>E' stato selezionato meno del necessario per la costruzione! Aggiungere la quantità necessaria.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<unit id="yZGS8uZ" name="part.name.must_match_category_regex">
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>Il nome del componente non corrisponde all'espressione regolare specificata dalla categoria: %regex%</target>
</segment>
</unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<unit id="Q8wP5Jd" name="validator.attachment.name_not_blank">
<segment state="translated">
<source>validator.attachment.name_not_blank</source>
<target>Seleziona un valore, o carica un file per usare automaticamente il suo nome di file come nome per quell'allegato.</target>
</segment>
</unit>
<unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner">
<unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated">
<source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>Il proprietario di questo stock di componenti e quello dell'ubicazione scelta devono corrispondere (%owner_name%)!</target>
</segment>
</unit>
<unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous">
<unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated">
<source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>Il proprietario non può essere un utente anonimo!</target>
</segment>
</unit>
<unit id="N8aA0Uh" name="validator.part_association.must_set_an_value_if_type_is_other">
<unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated">
<source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>Se si imposta il tipo su "altro", è necessario definirne un valore descrittivo.</target>
</segment>
</unit>
<unit id="9VYNZ4v" name="validator.part_association.part_cannot_be_associated_with_itself">
<unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated">
<source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>Non è possibile associare un componente a se stesso.</target>
</segment>
</unit>
<unit id="csc1PNn" name="validator.part_association.already_exists">
<unit id="q5Ej6Xm" name="validator.part_association.already_exists">
<segment state="translated">
<source>validator.part_association.already_exists</source>
<target>L'associazione con questo componente esiste già.</target>
</segment>
</unit>
<unit id="sfW4NYE" name="validator.part_lot.vendor_barcode_must_be_unique">
<unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated">
<source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>Il valore del codice a barre di questo fornitore è già stato utilizzato in un altro lotto. Il codice a barre deve essere unico.</target>
</segment>
</unit>
<unit id="o1qmPUm" name="validator.year_2038_bug_on_32bit">
<unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit">
<segment state="translated">
<source>validator.year_2038_bug_on_32bit</source>
<target>A causa di limitazioni tecniche, non è possibile selezionare date successive al 19-01-2038 su sistemi a 32 bit!</target>
</segment>
</unit>
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>L'intervallo indicato non è valido!</target>
</segment>
</unit>
<unit id="m4gp2P_" name="validator.google_code.wrong_code">
<segment state="translated">
<source>validator.google_code.wrong_code</source>
<target>Codice non valido. Controlla che la tua app di autenticazione sia impostata correttamente e che sia il server che il dispositivo di autenticazione abbiano l'ora impostata correttamente.</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="pl">
<file id="validators.en">
<unit id="xevSdCK" name="part.master_attachment.must_be_picture">
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentContainingDBElement.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
@@ -42,7 +42,7 @@
<target>Załącznik podglądowy musi zawierać prawidłowe zdjęcie!</target>
</segment>
</unit>
<unit id="VJHTkxx" name="structural.entity.unique_name">
<unit id="v8HkcJB" name="structural.entity.unique_name">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Base\AbstractCompany.php:0</note>
@@ -87,7 +87,7 @@
<target>Element o tej nazwie już istnieje na tym poziomie!</target>
</segment>
</unit>
<unit id="3ODUtpU" name="parameters.validator.min_lesser_typical">
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -107,7 +107,7 @@
<target>Wartość musi być mniejsza lub równa wartości nominalnej ({{compare_value }}).</target>
</segment>
</unit>
<unit id="jDBA_WW" name="parameters.validator.min_lesser_max">
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -127,7 +127,7 @@
<target>Wartość musi być mniejsza niż wartość maksymalna ({{ compare_value }}).</target>
</segment>
</unit>
<unit id="ygK_e_X" name="parameters.validator.max_greater_typical">
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -147,7 +147,7 @@
<target>Wartość musi być większa lub równa wartości nominalnej ({{ compare_value }}).</target>
</segment>
</unit>
<unit id="isXL.ie" name="validator.user.username_already_used">
<unit id="P41193Y" name="validator.user.username_already_used">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -157,7 +157,7 @@
<target>Użytkownik o tej nazwie już istnieje</target>
</segment>
</unit>
<unit id="NcM463r" name="user.invalid_username">
<unit id="EKPQiyf" name="user.invalid_username">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -167,7 +167,7 @@
<target>Nazwa użytkownika może zawierać wyłącznie litery, cyfry, podkreślenia, kropki, plusy i minusy!</target>
</segment>
</unit>
<unit id="lZvhKYu" name="validator.noneofitschild.self">
<unit id="_v.DMg." name="validator.noneofitschild.self">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
@@ -176,7 +176,7 @@
<target>Element nie może być swoim własnym elementem nadrzędnym!</target>
</segment>
</unit>
<unit id="pr07aV4" name="validator.noneofitschild.children">
<unit id="W90LyFQ" name="validator.noneofitschild.children">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
@@ -185,167 +185,179 @@
<target>Nie możesz przypisać elementu podrzędnego jako elementu nadrzędnego (spowodowałoby to pętle)!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<unit id="GAUS.LK" name="validator.select_valid_category">
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Proszę wybrać prawidłową kategorię!</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<unit id="h6qELde" name="validator.part_lot.only_existing">
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Nie można dodać nowych części do tej lokalizacji, ponieważ jest ona oznaczona jako „Tylko istniejące”</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<unit id="Prriyy0" name="validator.part_lot.location_full.no_increase">
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Lokalizacja jest pełna. Ilości nie można zwiększyć (nowa wartość musi być mniejsza niż {{ old_amount }}).</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<unit id="eeEjB4s" name="validator.part_lot.location_full">
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>Lokalizacja jest pełna. Nie można do niego dodawać nowych części.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<unit id="2yWi8eP" name="validator.part_lot.single_part">
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>Ta lokalizacja może zawierać tylko jedną część i jest już pełna!</target>
<target>Miejsce przechowywania zostało oznaczone jako „Tylko jeden komponent”, więc nie można dodać nowego komponentu.</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<unit id="A.TFhbb" name="validator.attachment.must_not_be_null">
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>Musisz wybrać typ załącznika!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>Musisz wybrać dostawcę!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>Aby włączyć przedrostki SI, musisz ustawić symbol jednostki!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>Wewnętrzny numer części musi być unikalny. {{value }} jest już w użyciu!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>Należy wybrać część dla wpisu BOM części lub ustawić nazwę dla wpisu BOM niebędącego częścią.</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>Istnieje już pozycja BOM o tej nazwie!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<unit id="jB3B50E" name="project.bom_entry.part_already_in_bom">
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>Ta część już istnieje w BOM-ie!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>Ta część już istnieje w BOM-ie! Liczba nazw montowań musi odpowiadać liczbie BOM-ów!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>Do zestawienia komponentów nie można dodać własnej części konstrukcyjnej projektu.</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>BOM projektu musi zawierać wszystkie komponenty produkcyjne podprojektów. Brakuje komponentu %part_name% projektu %project_name%!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Ceny nie są dozwolone we wpisach BOM powiązanych z częścią. Zamiast tego zdefiniuj cenę części.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>Wybrałeś większą ilość, niż jest to konieczne! Usuń niepotrzebną ilość.</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>Wybrałeś mniejszą ilość do pobrania, niż jest to potrzebne do kompilacji! Dodaj dodatkową ilość.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<unit id="yZGS8uZ" name="part.name.must_match_category_regex">
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>Nazwa części nie pasuje do wyrażenia regularnego określonego w kategorii: %regex%</target>
</segment>
</unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<unit id="Q8wP5Jd" name="validator.attachment.name_not_blank">
<segment state="translated">
<source>validator.attachment.name_not_blank</source>
<target>Ustaw tutaj wartość lub prześlij plik, aby automatycznie użyć jego nazwy jako nazwy załącznika.</target>
</segment>
</unit>
<unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner">
<unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated">
<source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>Właściciel tego zestawu komponentów i wybrana lokalizacja przechowywania muszą być zgodne (%owner_name%)!</target>
</segment>
</unit>
<unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous">
<unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated">
<source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>Właściciel nie może być anonimowym użytkownikiem!</target>
</segment>
</unit>
<unit id="N8aA0Uh" name="validator.part_association.must_set_an_value_if_type_is_other">
<unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated">
<source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>Jeśli ustawisz typ na „inny”, musisz ustawić dla niego wartość opisową!</target>
</segment>
</unit>
<unit id="9VYNZ4v" name="validator.part_association.part_cannot_be_associated_with_itself">
<unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated">
<source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>Część nie może być powiązana sama ze sobą!</target>
</segment>
</unit>
<unit id="csc1PNn" name="validator.part_association.already_exists">
<unit id="q5Ej6Xm" name="validator.part_association.already_exists">
<segment state="translated">
<source>validator.part_association.already_exists</source>
<target>Powiązanie z tą częścią już istnieje!</target>
</segment>
</unit>
<unit id="sfW4NYE" name="validator.part_lot.vendor_barcode_must_be_unique">
<unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated">
<source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>Ta wartość kodu kreskowego dostawcy jest już używana w innym magazynie. Kod kreskowy musi być unikalny!</target>
</segment>
</unit>
<unit id="o1qmPUm" name="validator.year_2038_bug_on_32bit">
<unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit">
<segment state="translated">
<source>validator.year_2038_bug_on_32bit</source>
<target>Ze względu na ograniczenia techniczne nie jest możliwe wybranie daty po 19 stycznia 2038 w systemach 32-bitowych!</target>
</segment>
</unit>
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>Podany zakres jest nieprawidłowy!</target>
</segment>
</unit>
<unit id="m4gp2P_" name="validator.google_code.wrong_code">
<segment state="translated">
<source>validator.google_code.wrong_code</source>
<target>Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas.</target>
</segment>
</unit>
</file>
</xliff>

2394
yarn.lock

File diff suppressed because it is too large Load Diff