mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-01 04:55:10 +01:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0234463b68 | ||
|
|
ef412eef92 | ||
|
|
76ebd22eab | ||
|
|
5b0ca8e346 | ||
|
|
0b6b10c27b | ||
|
|
6225d2c9b3 | ||
|
|
01fc6524a4 | ||
|
|
2575e6a160 | ||
|
|
484ba5ebd7 | ||
|
|
b42d98e9f8 | ||
|
|
65b2f045ac | ||
|
|
5e76451d46 | ||
|
|
a873ad3316 | ||
|
|
b1e03f49ee | ||
|
|
011e23f8e6 | ||
|
|
646cd8cf22 | ||
|
|
52ac8a70d5 | ||
|
|
e020334b73 | ||
|
|
7698e83f0b | ||
|
|
dd56f5e0c8 | ||
|
|
92c32eef74 | ||
|
|
08770c7dc5 | ||
|
|
808a94e4df | ||
|
|
490086d531 | ||
|
|
2ef3fbb81b | ||
|
|
7d834ac8d7 | ||
|
|
15ad0ec9c0 | ||
|
|
f0b78e8b2c | ||
|
|
e616faa76f | ||
|
|
8159f4d8ee | ||
|
|
021c576468 | ||
|
|
1b2339a82c | ||
|
|
2b6bb3f773 | ||
|
|
abc5c61a06 | ||
|
|
7145bce605 | ||
|
|
bb92e5e9ee | ||
|
|
0c47aa226c | ||
|
|
76e945bbbd | ||
|
|
4a6ec2581d | ||
|
|
3d75bf5f9f | ||
|
|
c27648b89b | ||
|
|
ccf67c0662 | ||
|
|
ca116cae91 | ||
|
|
a29d933f99 | ||
|
|
49acf3e0cf | ||
|
|
234b5abb96 | ||
|
|
839bcf91d6 | ||
|
|
58ed57fab7 | ||
|
|
fa42997733 | ||
|
|
ac416141d0 | ||
|
|
c629a85b14 | ||
|
|
7ccfea208f | ||
|
|
f3c802bcff | ||
|
|
574583bd6a | ||
|
|
84c54d0b25 | ||
|
|
86d3f87694 | ||
|
|
ddd7252051 | ||
|
|
b4e8136618 | ||
|
|
c2638991f2 | ||
|
|
8554be9abd | ||
|
|
87a518703f | ||
|
|
dd03ca943d | ||
|
|
6997861811 | ||
|
|
1cc1530b20 | ||
|
|
98597fb3aa | ||
|
|
283a445198 | ||
|
|
7db44f0ec5 | ||
|
|
abb5395cae | ||
|
|
8c8b44baef | ||
|
|
7366a33fe5 | ||
|
|
ad02d7e525 | ||
|
|
b5a0189f29 | ||
|
|
756152dd68 | ||
|
|
173a8ee680 | ||
|
|
b99777cde1 | ||
|
|
8193e7a68e | ||
|
|
f18c024daa | ||
|
|
f6577a8f33 | ||
|
|
7fc3153dde | ||
|
|
5231dbd6e7 | ||
|
|
77671550a7 | ||
|
|
e231404128 | ||
|
|
6650e2da3d | ||
|
|
fd521acaa4 |
@@ -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
|
||||
|
||||
@@ -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
34
.env
@@ -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
|
||||
##################################################################################
|
||||
|
||||
186
Dockerfile
186
Dockerfile
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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> <strong>' + escape(data.input) + '</strong>… ' +
|
||||
'<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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
2582
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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],
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
translation:
|
||||
symfony_profiler:
|
||||
enabled: true
|
||||
webui:
|
||||
enabled: true
|
||||
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
3
config/routes/jbtronics_translation_editor.yaml
Normal file
3
config/routes/jbtronics_translation_editor.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
when@dev:
|
||||
translation_editor:
|
||||
resource: '@JbtronicsTranslationEditorBundle/config/routes.php'
|
||||
@@ -1,3 +0,0 @@
|
||||
_translation_edit_in_place:
|
||||
resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yaml'
|
||||
prefix: /admin
|
||||
@@ -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' }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ parameters:
|
||||
- src/Doctrine/Purger/*
|
||||
- src/DataTables/Adapters/TwoStepORMAdapter.php
|
||||
- src/Form/Fixes/*
|
||||
- src/Translation/Fixes/*
|
||||
|
||||
|
||||
|
||||
|
||||
102
src/ApiPlatform/Filter/TagFilter.php
Normal file
102
src/ApiPlatform/Filter/TagFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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'])]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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, [
|
||||
|
||||
@@ -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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
1476
src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php
Normal file
1476
src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
242
src/Translation/Fixes/SegmentAwareXliffFileDumper.php
Normal file
242
src/Translation/Fixes/SegmentAwareXliffFileDumper.php
Normal 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);
|
||||
}
|
||||
}
|
||||
262
src/Translation/Fixes/SegmentAwareXliffFileLoader.php
Normal file
262
src/Translation/Fixes/SegmentAwareXliffFileLoader.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
43
symfony.lock
43
symfony.lock
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="">
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}>
|
||||
|
||||
@@ -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> {% 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> {% 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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
12227
translations/messages.pl.xlf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user