mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Merge branch 'master' into settings-bundle
This commit is contained in:
commit
8750573724
191 changed files with 27745 additions and 12133 deletions
|
@ -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
|
@ -146,6 +146,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
|
||||
##################################################################################
|
||||
|
|
0
.env.dev
Normal file
0
.env.dev
Normal file
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: [ '8.1', '8.2', '8.3' ]
|
||||
php-versions: [ '8.1', '8.2', '8.3', '8.4' ]
|
||||
db-type: [ 'mysql', 'sqlite', 'postgres' ]
|
||||
|
||||
env:
|
||||
|
@ -126,7 +126,7 @@ jobs:
|
|||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
env_vars: PHP_VERSION,DB_TYPE
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
187
Dockerfile
187
Dockerfile
|
@ -1,22 +1,64 @@
|
|||
FROM debian:bullseye-slim
|
||||
ARG BASE_IMAGE=debian:bookworm-slim
|
||||
ARG PHP_VERSION=8.3
|
||||
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
ARG PHP_VERSION
|
||||
|
||||
# Install needed dependencies for PHP build
|
||||
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
|
||||
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
|
||||
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && apt-get -y install apt-transport-https lsb-release ca-certificates curl zip 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,87 @@ 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
|
||||
log_limit=8096
|
||||
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 +164,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 +177,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
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.13.2
|
||||
1.15.1
|
||||
|
|
|
@ -53,6 +53,7 @@ export default class extends Controller {
|
|||
|
||||
const config = {
|
||||
language: language,
|
||||
licenseKey: "GPL",
|
||||
}
|
||||
|
||||
const watchdog = new EditorWatchdog();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import {Controller} from "@hotwired/stimulus";
|
||||
//import * as ZXing from "@zxing/library";
|
||||
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "html5-qrcode";
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
@ -50,7 +50,7 @@ export default class extends Controller {
|
|||
});
|
||||
|
||||
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
||||
fps: 2,
|
||||
fps: 10,
|
||||
qrbox: qrboxFunction,
|
||||
experimentalFeatures: {
|
||||
//This option improves reading quality on android chrome
|
||||
|
@ -61,6 +61,11 @@ export default class extends Controller {
|
|||
this._scanner.render(this.onScanSuccess.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._scanner.pause();
|
||||
this._scanner.clear();
|
||||
}
|
||||
|
||||
onScanSuccess(decodedText, decodedResult) {
|
||||
//Put our decoded Text into the input box
|
||||
document.getElementById('scan_dialog_input').value = decodedText;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -24,9 +24,8 @@
|
|||
/** Should be the same settings, as in label_style.css */
|
||||
.ck-html-label .ck-content {
|
||||
font-family: "DejaVu Sans Mono", monospace;
|
||||
font-size: 12px;
|
||||
font-size: 12pt;
|
||||
line-height: 1.0;
|
||||
font-size-adjust: 1.5;
|
||||
}
|
||||
|
||||
.ck-html-label .ck-content p {
|
||||
|
|
|
@ -44,4 +44,18 @@ import "./register_events";
|
|||
import "./tristate_checkboxes";
|
||||
|
||||
//Define jquery globally
|
||||
window.$ = window.jQuery = require("jquery")
|
||||
window.$ = window.jQuery = require("jquery");
|
||||
|
||||
//Use the local WASM file for the ZXing library
|
||||
import {
|
||||
setZXingModuleOverrides,
|
||||
} from "barcode-detector/pure";
|
||||
import wasmFile from "../../node_modules/zxing-wasm/dist/reader/zxing_reader.wasm";
|
||||
setZXingModuleOverrides({
|
||||
locateFile: (path, prefix) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return wasmFile;
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
});
|
|
@ -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 = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
//Increase xdebug.max_nesting_level to 1000 if required (see issue #411)
|
||||
//Check if xdebug extension is active, and xdebug.max_nesting_level is set to 256 or lower
|
||||
if (extension_loaded('xdebug') && ((int) ini_get('xdebug.max_nesting_level')) <= 256) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"name": "part-db/part-db-server",
|
||||
"type": "project",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
|
@ -16,7 +17,7 @@
|
|||
"brick/math": "0.12.1 as 0.11.0",
|
||||
"composer/ca-bundle": "^1.3",
|
||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
||||
"doctrine/data-fixtures": "^1.6.6",
|
||||
"doctrine/data-fixtures": "^2.0.0",
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
"doctrine/doctrine-bundle": "^2.0",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
|
@ -40,13 +41,9 @@
|
|||
"nelmio/cors-bundle": "^2.3",
|
||||
"nelmio/security-bundle": "^3.0",
|
||||
"nyholm/psr7": "^1.1",
|
||||
"ocramius/proxy-manager": "2.2.*",
|
||||
"omines/datatables-bundle": "^0.8.0",
|
||||
"omines/datatables-bundle": "^0.9.1",
|
||||
"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",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^6.8.0",
|
||||
|
@ -71,7 +68,6 @@
|
|||
"symfony/process": "6.4.*",
|
||||
"symfony/property-access": "6.4.*",
|
||||
"symfony/property-info": "6.4.*",
|
||||
"symfony/proxy-manager-bridge": "6.4.*",
|
||||
"symfony/rate-limiter": "6.4.*",
|
||||
"symfony/runtime": "6.4.*",
|
||||
"symfony/security-bundle": "6.4.*",
|
||||
|
@ -93,20 +89,20 @@
|
|||
"twig/intl-extra": "^3.8",
|
||||
"twig/markdown-extra": "^3.8",
|
||||
"twig/string-extra": "^3.8",
|
||||
"web-auth/webauthn-symfony-bundle": "^4.0.0",
|
||||
"webmozart/assert": "^1.4"
|
||||
"web-auth/webauthn-symfony-bundle": "^4.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^v8.0.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"ekino/phpstan-banned-code": "^v1.0.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.0.0",
|
||||
"ekino/phpstan-banned-code": "^v3.0.0",
|
||||
"jbtronics/translation-editor-bundle": "^1.0",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^1.4.7",
|
||||
"phpstan/phpstan-doctrine": "^1.2.11",
|
||||
"phpstan/phpstan-strict-rules": "^1.5",
|
||||
"phpstan/phpstan-symfony": "^1.1.7",
|
||||
"phpstan/phpstan": "^2.0.4",
|
||||
"phpstan/phpstan-doctrine": "^2.0.1",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.1",
|
||||
"phpstan/phpstan-symfony": "^2.0.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"rector/rector": "^1.1.1",
|
||||
"rector/rector": "^2.0.4",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
|
|
3210
composer.lock
generated
3210
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],
|
||||
|
@ -33,4 +32,5 @@ return [
|
|||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true],
|
||||
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
|
||||
];
|
||||
|
|
|
@ -9,14 +9,13 @@ datatables:
|
|||
# Set options, as documented at https://datatables.net/reference/option/
|
||||
options:
|
||||
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
|
||||
pageLength: 50 # 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>>>"
|
||||
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
||||
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
|
||||
<'card'
|
||||
rt
|
||||
<'card-footer card-footer-table text-muted' i >
|
||||
>
|
||||
<'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>"
|
||||
pagingType: 'simple_numbers'
|
||||
searching: true
|
||||
stateSave: true
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
translation:
|
||||
symfony_profiler:
|
||||
enabled: true
|
||||
webui:
|
||||
enabled: true
|
|
@ -57,6 +57,7 @@ doctrine:
|
|||
field2: App\Doctrine\Functions\Field2
|
||||
natsort: App\Doctrine\Functions\Natsort
|
||||
array_position: App\Doctrine\Functions\ArrayPosition
|
||||
ilike: App\Doctrine\Functions\ILike
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
|
|
@ -50,7 +50,6 @@ when@prod:
|
|||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
@ -74,7 +73,6 @@ when@docker:
|
|||
type: stream
|
||||
path: "php://stderr"
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
|
|
@ -51,12 +51,16 @@ nelmio_security:
|
|||
img-src:
|
||||
- '*'
|
||||
- 'data:'
|
||||
# Required for be able to load pictures in the QR code scanner
|
||||
- 'blob:'
|
||||
style-src:
|
||||
- 'self'
|
||||
- 'unsafe-inline'
|
||||
- 'data:'
|
||||
script-src:
|
||||
- 'self'
|
||||
# Required for loading the Wasm for the barcode scanner:
|
||||
- 'wasm-unsafe-eval'
|
||||
object-src:
|
||||
- 'self'
|
||||
- 'data:'
|
||||
|
|
|
@ -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]
|
|
@ -8,7 +8,7 @@ parameters:
|
|||
|
||||
# This is used as workaround for places where we can not access the settings directly (like the 2FA application names)
|
||||
partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage)
|
||||
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.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
|
|
@ -216,6 +216,16 @@ services:
|
|||
$onlyAuthorizedSellers: '%env(bool:PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS)%'
|
||||
|
||||
|
||||
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
|
||||
####################################################################################################################
|
||||
|
@ -292,4 +302,4 @@ when@test:
|
|||
arguments:
|
||||
- '@doctrine.fixtures.loader'
|
||||
- '@doctrine'
|
||||
- { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' }
|
||||
- { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' }
|
||||
|
|
|
@ -23,7 +23,7 @@ each other so that it does not matter which one of your 1000 things of Part you
|
|||
A part entity has many fields, which can be used to describe it better. Most of the fields are optional:
|
||||
|
||||
* **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a
|
||||
name you thought of yourself. The name have to be unique in a single category.
|
||||
name you thought of yourself. Each name needs to be unique and must exist in a single category.
|
||||
* **Description**: A short (single-line) description of what this part is/does. For longer information, you should use
|
||||
the comment field or the specifications
|
||||
* **Category** (Required): The category (see there) to which this part belongs to.
|
||||
|
@ -239,4 +239,4 @@ replaced with data for the actual thing.
|
|||
|
||||
You do not have to define a label profile to generate labels (you can just set the settings on the fly in the label
|
||||
dialog), however, if you want to generate many labels, it is recommended to save the settings as a label profile, to save
|
||||
it for later usage. This ensures that all generated labels look the same.
|
||||
it for later usage. This ensures that all generated labels look the same.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -6,4 +6,6 @@ has_children: true
|
|||
---
|
||||
|
||||
# Installation
|
||||
Below you can find some guides to install Part-DB.
|
||||
Below you can find some guides to install Part-DB.
|
||||
|
||||
For the hobbyists without much experience, we recommend the docker installation or direct installation on debian.
|
|
@ -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
|
||||
|
|
42
docs/installation/kubernetes.md
Normal file
42
docs/installation/kubernetes.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
title: Kubernetes / Helm
|
||||
layout: default
|
||||
parent: Installation
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
# Kubernetes / Helm Charts
|
||||
|
||||
If you are using Kubernetes, you can use the [helm charts](https://helm.sh/) provided in this [repository](https://github.com/Part-DB/helm-charts).
|
||||
|
||||
## Usage
|
||||
|
||||
[Helm](https://helm.sh) must be installed to use the charts. Please refer to
|
||||
Helm's [documentation](https://helm.sh/docs) to get started.
|
||||
|
||||
Once Helm has been set up correctly, add the repo as follows:
|
||||
|
||||
`helm repo add part-db https://part-db.github.io/helm-charts`
|
||||
|
||||
If you had already added this repo earlier, run `helm repo update` to retrieve
|
||||
the latest versions of the packages. You can then run `helm search repo
|
||||
part-db` to see the charts.
|
||||
|
||||
To install the part-db chart:
|
||||
|
||||
helm install my-part-db part-db/part-db
|
||||
|
||||
To uninstall the chart:
|
||||
|
||||
helm delete my-part-db
|
||||
|
||||
This repository is also available at [ArtifactHUB](https://artifacthub.io/packages/search?repo=part-db).
|
||||
|
||||
## Configuration
|
||||
|
||||
See the README in the [chart directory](https://github.com/Part-DB/helm-charts/tree/main/charts/part-db) for more
|
||||
information on the available configuration options.
|
||||
|
||||
## Bugreports
|
||||
|
||||
If you find issues related to the helm charts, please open an issue in the [helm-charts repository](https://github.com/Part-DB/helm-charts).
|
31
docs/installation/proxmox.md
Normal file
31
docs/installation/proxmox.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: Proxmox VE LXC
|
||||
layout: default
|
||||
parent: Installation
|
||||
nav_order: 6
|
||||
---
|
||||
|
||||
# Proxmox VE LXC
|
||||
|
||||
{: .warning }
|
||||
> The proxmox VE LXC script for Part-DB is developed and maintained by [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/)
|
||||
> and not by the Part-DB developers. Keep in mind that the script is not officially supported by the Part-DB developers.
|
||||
|
||||
If you are using Proxmox VE you can use the scripts provided by [Proxmox VE Helper-Scripts community](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db)
|
||||
to easily install Part-DB in a LXC container.
|
||||
|
||||
## Usage
|
||||
|
||||
To create a new LXC container with Part-DB, you can use the following command in the Proxmox VE shell:
|
||||
|
||||
```bash
|
||||
bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/ct/part-db.sh)"
|
||||
```
|
||||
|
||||
The same command can be used to update an existing Part-DB container.
|
||||
|
||||
See the [helper script website](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db) for more information.
|
||||
|
||||
## Bugreports
|
||||
|
||||
If you find issues related to the proxmox VE LXC script, please open an issue in the [Proxmox VE Helper-Scripts repository](https://github.com/community-scripts/ProxmoxVE).
|
|
@ -107,7 +107,7 @@ The following env configuration options are available:
|
|||
default: `EUR`). If an offer is only available in a certain currency,
|
||||
Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert
|
||||
it to your preferred currency.
|
||||
* `PROVIDER_OCOTPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code,
|
||||
* `PROVIDER_OCTOPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code,
|
||||
default: `DE`). To get the correct prices, you have to set this and the currency setting to the correct value.
|
||||
* `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This
|
||||
affects how quickly your monthly limit is used up.
|
||||
|
@ -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
|
||||
|
|
81
package.json
81
package.json
|
@ -9,7 +9,7 @@
|
|||
"@symfony/stimulus-bridge": "^3.2.0",
|
||||
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
||||
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
|
||||
"@symfony/webpack-encore": "^4.1.0",
|
||||
"@symfony/webpack-encore": "^5.0.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"core-js": "^3.23.0",
|
||||
"intl-messageformat": "^10.2.5",
|
||||
|
@ -18,7 +18,7 @@
|
|||
"regenerator-runtime": "^0.13.9",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-bundle-analyzer": "^4.3.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-cli": "^5.1.0",
|
||||
"webpack-notifier": "^1.15.0"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
@ -33,50 +33,52 @@
|
|||
"@algolia/autocomplete-js": "^1.17.0",
|
||||
"@algolia/autocomplete-plugin-recent-searches": "^1.17.0",
|
||||
"@algolia/autocomplete-theme-classic": "^1.17.0",
|
||||
"@ckeditor/ckeditor5-alignment": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-autoformat": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-block-quote": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-code-block": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-dev-translations": "^39.1.0",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^39.1.0",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-essentials": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-font": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-heading": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-highlight": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-html-embed": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-html-support": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-image": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-indent": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-link": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-list": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-media-embed": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-paragraph": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-remove-format": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-source-editing": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-special-characters": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-table": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-upload": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-watchdog": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-word-count": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-alignment": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-autoformat": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-block-quote": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-code-block": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-dev-translations": "^43.0.1",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^43.0.1",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-essentials": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-font": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-heading": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-highlight": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-html-embed": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-html-support": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-image": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-indent": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-link": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-list": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-media-embed": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-paragraph": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-remove-format": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-source-editing": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-special-characters": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-table": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-upload": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-watchdog": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-word-count": "^44.0.0",
|
||||
"@jbtronics/bs-treeview": "^1.0.1",
|
||||
"@part-db/html5-qrcode": "^3.1.0",
|
||||
"@zxcvbn-ts/core": "^3.0.2",
|
||||
"@zxcvbn-ts/language-common": "^3.0.3",
|
||||
"@zxcvbn-ts/language-de": "^3.0.1",
|
||||
"@zxcvbn-ts/language-en": "^3.0.1",
|
||||
"@zxcvbn-ts/language-fr": "^3.0.1",
|
||||
"@zxcvbn-ts/language-ja": "^3.0.1",
|
||||
"barcode-detector": "^2.3.1",
|
||||
"bootbox": "^6.0.0",
|
||||
"bootswatch": "^5.1.3",
|
||||
"bs-custom-file-input": "^1.3.4",
|
||||
"clipboard": "^2.0.4",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"compression-webpack-plugin": "^11.1.0",
|
||||
"datatables.net": "^2.0.0",
|
||||
"datatables.net-bs5": "^2.0.0",
|
||||
"datatables.net-buttons-bs5": "^3.0.0",
|
||||
|
@ -86,18 +88,17 @@
|
|||
"datatables.net-select-bs5": "^2.0.0",
|
||||
"dompurify": "^3.0.3",
|
||||
"emoji.json": "^15.0.0",
|
||||
"exports-loader": "^3.0.0",
|
||||
"html5-qrcode": "^2.2.1",
|
||||
"exports-loader": "^5.0.0",
|
||||
"json-formatter-js": "^2.3.4",
|
||||
"jszip": "^3.2.0",
|
||||
"katex": "^0.16.0",
|
||||
"marked": "^12.0.0",
|
||||
"marked-gfm-heading-id": "^3.0.4",
|
||||
"marked": "^15.0.4",
|
||||
"marked-gfm-heading-id": "^4.1.1",
|
||||
"marked-mangle": "^1.0.1",
|
||||
"pdfmake": "^0.2.2",
|
||||
"stimulus-use": "^0.52.0",
|
||||
"tom-select": "^2.1.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.0.2"
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ parameters:
|
|||
- src/Doctrine/Purger/*
|
||||
- src/DataTables/Adapters/TwoStepORMAdapter.php
|
||||
- src/Form/Fixes/*
|
||||
- src/Translation/Fixes/*
|
||||
|
||||
|
||||
|
||||
|
@ -19,7 +20,7 @@ parameters:
|
|||
treatPhpDocTypesAsCertain: false
|
||||
|
||||
symfony:
|
||||
container_xml_path: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml'
|
||||
containerXmlPath: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml'
|
||||
|
||||
doctrine:
|
||||
objectManagerLoader: tests/object-manager.php
|
||||
|
@ -29,11 +30,6 @@ parameters:
|
|||
|
||||
checkFunctionNameCase: false
|
||||
|
||||
checkAlwaysTrueInstanceof: false
|
||||
checkAlwaysTrueCheckTypeFunctionCall: false
|
||||
checkAlwaysTrueStrictComparison: false
|
||||
reportAlwaysTrueInLastCondition: false
|
||||
|
||||
reportMaybesInPropertyPhpDocTypes: false
|
||||
reportMaybesInMethodSignatures: false
|
||||
|
||||
|
@ -42,14 +38,14 @@ parameters:
|
|||
booleansInConditions: false
|
||||
uselessCast: false
|
||||
requireParentConstructorCall: true
|
||||
disallowedConstructs: false
|
||||
overwriteVariablesWithLoop: false
|
||||
closureUsesThis: false
|
||||
matchingInheritedMethodNames: true
|
||||
numericOperandsInArithmeticOperators: true
|
||||
strictCalls: true
|
||||
switchConditionsMatchingType: false
|
||||
noVariableVariables: false
|
||||
disallowedEmpty: false
|
||||
disallowedShortTernary: false
|
||||
|
||||
ignoreErrors:
|
||||
# Ignore errors caused by complex mapping with AbstractStructuralDBElement
|
||||
|
@ -61,4 +57,7 @@ parameters:
|
|||
- '#Part::getParameters\(\) should return .*AbstractParameter#'
|
||||
|
||||
# Ignore doctrine type mapping mismatch
|
||||
- '#Property .* type mapping mismatch: property can contain .* but database expects .*#'
|
||||
- '#Property .* type mapping mismatch: property can contain .* but database expects .*#'
|
||||
|
||||
# Ignore error of unused WithPermPresetsTrait, as it is used in the migrations which are not analyzed by Phpstan
|
||||
- '#Trait App\\Migration\\WithPermPresetsTrait is used zero times and is not analysed#'
|
||||
|
|
|
@ -50,7 +50,7 @@ final class LikeFilter extends AbstractFilter
|
|||
}
|
||||
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
|
||||
$queryBuilder
|
||||
->andWhere(sprintf('o.%s LIKE :%s', $property, $parameterName))
|
||||
->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName))
|
||||
->setParameter($parameterName, $value);
|
||||
}
|
||||
|
||||
|
|
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(
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE',
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE',
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE',
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE',
|
||||
);
|
||||
|
||||
$queryBuilder->andWhere($tmp);
|
||||
|
||||
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $value . ',%');
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $value);
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_3', $value . ',%');
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_4', $value);
|
||||
}
|
||||
|
||||
public function getDescription(string $resourceClass): array
|
||||
{
|
||||
if (!$this->properties) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$description = [];
|
||||
foreach (array_keys($this->properties) as $property) {
|
||||
$description[(string)$property] = [
|
||||
'property' => $property,
|
||||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'description' => 'Filter for tags of a part',
|
||||
'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;
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@ class CheckRequirementsCommand extends Command
|
|||
//Checking 32-bit system
|
||||
if (PHP_INT_SIZE === 4) {
|
||||
$io->warning('You are using a 32-bit system. You will have problems with working with dates after the year 2038, therefore a 64-bit system is recommended.');
|
||||
} elseif (PHP_INT_SIZE === 8) {
|
||||
} elseif (PHP_INT_SIZE === 8) { //@phpstan-ignore-line //PHP_INT_SIZE is always 4 or 8
|
||||
if (!$only_issues) {
|
||||
$io->success('You are using a 64-bit system.');
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ class ConvertBBCodeCommand extends Command
|
|||
|
||||
/**
|
||||
* Returns a list which entities and which properties need to be checked.
|
||||
* @return array<class-string<AbstractNamedDBElement>, string[]>
|
||||
*/
|
||||
protected function getTargetsLists(): array
|
||||
{
|
||||
|
@ -109,7 +110,6 @@ class ConvertBBCodeCommand extends Command
|
|||
$class
|
||||
));
|
||||
//Determine which entities of this type we need to modify
|
||||
/** @var EntityRepository $repo */
|
||||
$repo = $this->em->getRepository($class);
|
||||
$qb = $repo->createQueryBuilder('e')
|
||||
->select('e');
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -206,12 +206,15 @@ class UsersPermissionsCommand extends Command
|
|||
return '<fg=green>Allow</>';
|
||||
} elseif ($permission_value === false) {
|
||||
return '<fg=red>Disallow</>';
|
||||
} elseif ($permission_value === null && !$inherit) {
|
||||
}
|
||||
// Permission value is null by this point
|
||||
elseif (!$inherit) {
|
||||
return '<fg=blue>Inherit</>';
|
||||
} elseif ($permission_value === null && $inherit) {
|
||||
} elseif ($inherit) {
|
||||
return '<fg=red>Disallow (Inherited)</>';
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line This line is never reached, but PHPstorm complains otherwise
|
||||
return '???';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,10 +213,14 @@ abstract class BaseAdminController extends AbstractController
|
|||
//Show preview for LabelProfile if needed.
|
||||
if ($entity instanceof LabelProfile) {
|
||||
$example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement());
|
||||
$pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
|
||||
$pdf_data = null;
|
||||
try {
|
||||
$pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
|
||||
} catch (TwigModeException $exception) {
|
||||
$form->get('options')->get('lines')->addError(new FormError($exception->getSafeMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/** @var AbstractPartsContainingRepository $repo */
|
||||
$repo = $this->entityManager->getRepository($this->entity_class);
|
||||
|
||||
return $this->render($this->twig_template, [
|
||||
|
@ -390,7 +396,7 @@ abstract class BaseAdminController extends AbstractController
|
|||
{
|
||||
if ($entity instanceof AbstractPartsContainingDBElement) {
|
||||
/** @var AbstractPartsContainingRepository $repo */
|
||||
$repo = $this->entityManager->getRepository($this->entity_class);
|
||||
$repo = $this->entityManager->getRepository($this->entity_class); //@phpstan-ignore-line
|
||||
if ($repo->getPartsCount($entity) > 0) {
|
||||
$this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()]));
|
||||
|
||||
|
|
|
@ -53,11 +53,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);
|
||||
|
@ -82,11 +82,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);
|
||||
|
|
|
@ -23,10 +23,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Form\InfoProviderSystem\PartSearchType;
|
||||
use App\Services\InfoProviderSystem\ExistingPartFinder;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -42,7 +45,9 @@ class InfoProviderController extends AbstractController
|
|||
{
|
||||
|
||||
public function __construct(private readonly ProviderRegistry $providerRegistry,
|
||||
private readonly PartInfoRetriever $infoRetriever)
|
||||
private readonly PartInfoRetriever $infoRetriever,
|
||||
private readonly ExistingPartFinder $existingPartFinder
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -72,21 +77,49 @@ class InfoProviderController extends AbstractController
|
|||
//When we are updating a part, use its name as keyword, to make searching easier
|
||||
//However we can only do this, if the form was not submitted yet
|
||||
if ($update_target !== null && !$form->isSubmitted()) {
|
||||
$form->get('keyword')->setData($update_target->getName());
|
||||
//Use the provider reference if available, otherwise use the manufacturer product number
|
||||
$keyword = $update_target->getProviderReference()->getProviderId() ?? $update_target->getManufacturerProductNumber();
|
||||
//Or the name if both are not available
|
||||
if ($keyword === "") {
|
||||
$keyword = $update_target->getName();
|
||||
}
|
||||
|
||||
$form->get('keyword')->setData($keyword);
|
||||
|
||||
//If we are updating a part, which already has a provider, preselect that provider in the form
|
||||
if ($update_target->getProviderReference()->getProviderKey() !== null) {
|
||||
try {
|
||||
$form->get('providers')->setData([$this->providerRegistry->getProviderByKey($update_target->getProviderReference()->getProviderKey())]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
//If the provider is not found, just ignore it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$keyword = $form->get('keyword')->getData();
|
||||
$providers = $form->get('providers')->getData();
|
||||
|
||||
$dtos = [];
|
||||
|
||||
try {
|
||||
$results = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
|
||||
$dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
|
||||
} catch (ClientException $e) {
|
||||
$this->addFlash('error', t('info_providers.search.error.client_exception'));
|
||||
$this->addFlash('error',$e->getMessage());
|
||||
//Log the exception
|
||||
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
|
||||
// modify the array to an array of arrays that has a field for a matching local Part
|
||||
// the advantage to use that format even when we don't look for local parts is that we
|
||||
// always work with the same interface
|
||||
$results = array_map(function ($result) {return ['dto' => $result, 'localPart' => null];}, $dtos);
|
||||
if(!$update_target) {
|
||||
foreach ($results as $index => $result) {
|
||||
$results[$index]['localPart'] = $this->existingPartFinder->findFirstExisting($result['dto']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('info_providers/search/part_search.html.twig', [
|
||||
|
|
|
@ -108,8 +108,31 @@ class LabelController extends AbstractController
|
|||
$pdf_data = null;
|
||||
$filename = 'invalid.pdf';
|
||||
|
||||
//Generate PDF either when the form is submitted and valid, or the form was not submit yet, and generate is set
|
||||
if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && $profile instanceof LabelProfile)) {
|
||||
|
||||
//Check if the label should be saved as profile
|
||||
if ($form->get('save_profile')->isClicked() && $this->isGranted('@labels.create_profiles')) { //@phpstan-ignore-line Phpstan does not recognize the isClicked method
|
||||
//Retrieve the profile name from the form
|
||||
$new_name = $form->get('save_profile_name')->getData();
|
||||
//ensure that the name is not empty
|
||||
if ($new_name === '' || $new_name === null) {
|
||||
$form->get('save_profile_name')->addError(new FormError($this->translator->trans('label_generator.profile_name_empty')));
|
||||
goto render;
|
||||
}
|
||||
|
||||
$profile = new LabelProfile();
|
||||
$profile->setName($form->get('save_profile_name')->getData());
|
||||
$profile->setOptions($form_options);
|
||||
$this->em->persist($profile);
|
||||
$this->em->flush();
|
||||
$this->addFlash('success', 'label_generator.profile_saved');
|
||||
|
||||
return $this->redirectToRoute('label_dialog_profile', [
|
||||
'profile' => $profile->getID(),
|
||||
'target_id' => (string) $form->get('target_id')->getData()
|
||||
]);
|
||||
}
|
||||
|
||||
$target_id = (string) $form->get('target_id')->getData();
|
||||
$targets = $this->findObjects($form_options->getSupportedElement(), $target_id);
|
||||
if ($targets !== []) {
|
||||
|
@ -117,7 +140,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');
|
||||
|
@ -132,6 +155,7 @@ class LabelController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
render:
|
||||
return $this->render('label_system/dialog.html.twig', [
|
||||
'form' => $form,
|
||||
'pdf_data' => $pdf_data,
|
||||
|
@ -152,7 +176,7 @@ class LabelController extends AbstractController
|
|||
{
|
||||
$id_array = $this->rangeParser->parse($ids);
|
||||
|
||||
/** @var DBElementRepository $repo */
|
||||
/** @var DBElementRepository<AbstractDBElement> $repo */
|
||||
$repo = $this->em->getRepository($type->getEntityClass());
|
||||
|
||||
return $repo->getElementsFromIDArray($id_array);
|
||||
|
|
|
@ -229,6 +229,10 @@ class PartController extends AbstractController
|
|||
$dto = $infoRetriever->getDetails($providerKey, $providerId);
|
||||
$new_part = $infoRetriever->dtoToPart($dto);
|
||||
|
||||
if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) {
|
||||
$this->addFlash('warning', t("part.create_from_info_provider.no_category_yet"));
|
||||
}
|
||||
|
||||
return $this->renderPartForm('new', $request, $new_part, [
|
||||
'info_provider_dto' => $dto,
|
||||
]);
|
||||
|
|
|
@ -66,6 +66,7 @@ class PartListsController extends AbstractController
|
|||
$ids = $request->request->get('ids');
|
||||
$action = $request->request->get('action');
|
||||
$target = $request->request->get('target');
|
||||
$redirectResponse = null;
|
||||
|
||||
if (!$this->isCsrfTokenValid('table_action', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'csfr_invalid');
|
||||
|
@ -86,7 +87,7 @@ class PartListsController extends AbstractController
|
|||
}
|
||||
|
||||
//If the action handler returned a response, we use it, otherwise we redirect back to the previous page.
|
||||
if (isset($redirectResponse) && $redirectResponse instanceof Response) {
|
||||
if ($redirectResponse !== null) {
|
||||
return $redirectResponse;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,10 +42,10 @@ declare(strict_types=1);
|
|||
namespace App\Controller;
|
||||
|
||||
use App\Form\LabelSystem\ScanDialogType;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeScanHelper;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeRedirector;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeScanResult;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -77,13 +77,21 @@ class ScanController extends AbstractController
|
|||
$mode = $form['mode']->getData();
|
||||
}
|
||||
|
||||
$infoModeData = null;
|
||||
|
||||
if ($input !== null) {
|
||||
try {
|
||||
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
||||
try {
|
||||
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||
} catch (EntityNotFoundException) {
|
||||
$this->addFlash('success', 'scan.qr_not_found');
|
||||
//Perform a redirect if the info mode is not enabled
|
||||
if (!$form['info_mode']->getData()) {
|
||||
try {
|
||||
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||
} catch (EntityNotFoundException) {
|
||||
$this->addFlash('success', 'scan.qr_not_found');
|
||||
}
|
||||
} else { //Otherwise retrieve infoModeData
|
||||
$infoModeData = $scan_result->getDecodedForInfoMode();
|
||||
|
||||
}
|
||||
} catch (InvalidArgumentException) {
|
||||
$this->addFlash('error', 'scan.format_unknown');
|
||||
|
@ -92,6 +100,7 @@ class ScanController extends AbstractController
|
|||
|
||||
return $this->render('label_system/scanner/scanner.html.twig', [
|
||||
'form' => $form,
|
||||
'infoModeData' => $infoModeData,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -109,7 +118,7 @@ class ScanController extends AbstractController
|
|||
throw new InvalidArgumentException('Unknown type: '.$type);
|
||||
}
|
||||
//Construct the scan result manually, as we don't have a barcode here
|
||||
$scan_result = new BarcodeScanResult(
|
||||
$scan_result = new LocalBarcodeScanResult(
|
||||
target_type: BarcodeScanHelper::QR_TYPE_MAP[$type],
|
||||
target_id: $id,
|
||||
//The routes are only used on the internal generated QR codes
|
||||
|
|
|
@ -63,10 +63,10 @@ class ToolsController extends AbstractController
|
|||
'default_theme' => $settings->system->customization->theme,
|
||||
'enabled_locales' => $this->getParameter('partdb.locale_menu'),
|
||||
'demo_mode' => $this->getParameter('partdb.demo_mode'),
|
||||
'gpdr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
|
||||
'use_gravatar' => $settings->system->privacy->useGravatar,
|
||||
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
|
||||
'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'),
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Category;
|
||||
|
@ -92,7 +93,7 @@ class TypeaheadController extends AbstractController
|
|||
|
||||
/**
|
||||
* This function map the parameter type to the class, so we can access its repository
|
||||
* @return class-string
|
||||
* @return class-string<AbstractParameter>
|
||||
*/
|
||||
private function typeToParameterClass(string $type): string
|
||||
{
|
||||
|
@ -155,7 +156,7 @@ class TypeaheadController extends AbstractController
|
|||
//Ensure user has the correct permissions
|
||||
$this->denyAccessUnlessGranted('read', $test_obj);
|
||||
|
||||
/** @var ParameterRepository $repository */
|
||||
/** @var ParameterRepository<AbstractParameter> $repository */
|
||||
$repository = $entityManager->getRepository($class);
|
||||
|
||||
$data = $repository->autocompleteParamName($query);
|
||||
|
|
|
@ -240,7 +240,10 @@ class UserSettingsController extends AbstractController
|
|||
$page_need_reload = true;
|
||||
}
|
||||
|
||||
/** @var Form $form We need a form implementation for the next calls */
|
||||
if (!$form instanceof Form) {
|
||||
throw new RuntimeException('Form is not an instance of Form, so we cannot retrieve the clicked button!');
|
||||
}
|
||||
|
||||
//Remove the avatar attachment from the user if requested
|
||||
if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) {
|
||||
$em->remove($user->getMasterPictureAttachment());
|
||||
|
|
|
@ -41,7 +41,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface
|
|||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
/** @var User $admin_user */
|
||||
$admin_user = $this->getReference(UserFixtures::ADMIN);
|
||||
$admin_user = $this->getReference(UserFixtures::ADMIN, User::class);
|
||||
|
||||
$read_only_token = new ApiToken();
|
||||
$read_only_token->setUser($admin_user);
|
||||
|
|
|
@ -35,7 +35,7 @@ use Doctrine\Persistence\ObjectManager;
|
|||
class LogEntryFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$this->createCategoryEntries($manager);
|
||||
$this->createDeletedCategory($manager);
|
||||
|
|
|
@ -106,7 +106,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
$partLot2->setComment('Test');
|
||||
$partLot2->setNeedsRefill(true);
|
||||
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
|
||||
$partLot2->setVendorBarcode('lot2_vendor_barcode');
|
||||
$partLot2->setUserBarcode('lot2_vendor_barcode');
|
||||
$part->addPartLot($partLot2);
|
||||
|
||||
$orderdetail = new Orderdetail();
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
|
@ -41,7 +42,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
|
|||
{
|
||||
$anonymous = new User();
|
||||
$anonymous->setName('anonymous');
|
||||
$anonymous->setGroup($this->getReference(GroupFixtures::READONLY));
|
||||
$anonymous->setGroup($this->getReference(GroupFixtures::READONLY, Group::class));
|
||||
$anonymous->setNeedPwChange(false);
|
||||
$anonymous->setPassword($this->encoder->hashPassword($anonymous, 'test'));
|
||||
$manager->persist($anonymous);
|
||||
|
@ -50,7 +51,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
|
|||
$admin->setName('admin');
|
||||
$admin->setPassword($this->encoder->hashPassword($admin, 'test'));
|
||||
$admin->setNeedPwChange(false);
|
||||
$admin->setGroup($this->getReference(GroupFixtures::ADMINS));
|
||||
$admin->setGroup($this->getReference(GroupFixtures::ADMINS, Group::class));
|
||||
$manager->persist($admin);
|
||||
$this->addReference(self::ADMIN, $admin);
|
||||
|
||||
|
@ -60,7 +61,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
|
|||
$user->setEmail('user@invalid.invalid');
|
||||
$user->setFirstName('Test')->setLastName('User');
|
||||
$user->setPassword($this->encoder->hashPassword($user, 'test'));
|
||||
$user->setGroup($this->getReference(GroupFixtures::USERS));
|
||||
$user->setGroup($this->getReference(GroupFixtures::USERS, Group::class));
|
||||
$manager->persist($user);
|
||||
|
||||
$noread = new User();
|
||||
|
|
|
@ -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,15 +85,18 @@ 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();
|
||||
|
||||
$tmp = $expr->orX(
|
||||
$expr->like($this->property, ':' . $tag_identifier_prefix . '_1'),
|
||||
$expr->like($this->property, ':' . $tag_identifier_prefix . '_2'),
|
||||
$expr->like($this->property, ':' . $tag_identifier_prefix . '_3'),
|
||||
$expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'),
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_1) = TRUE',
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_2) = TRUE',
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_3) = TRUE',
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_4) = TRUE',
|
||||
);
|
||||
|
||||
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
|
||||
|
@ -130,6 +133,7 @@ class TagsConstraint extends AbstractConstraint
|
|||
return;
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line Keep this check to ensure that everything has the same structure even if we add a new operator
|
||||
if ($this->operator === 'NONE') {
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions)));
|
||||
return;
|
||||
|
|
|
@ -107,7 +107,8 @@ class TextConstraint extends AbstractConstraint
|
|||
}
|
||||
|
||||
if ($like_value !== null) {
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value);
|
||||
$queryBuilder->andWhere(sprintf('ILIKE(%s, :%s) = TRUE', $this->property, $this->identifier));
|
||||
$queryBuilder->setParameter($this->identifier, $like_value);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ declare(strict_types=1);
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace App\DataTables\Filters;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class PartSearchFilter implements FilterInterface
|
||||
|
@ -132,15 +131,15 @@ class PartSearchFilter implements FilterInterface
|
|||
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
|
||||
}
|
||||
|
||||
return sprintf("%s LIKE :search_query", $field);
|
||||
return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
|
||||
}, $fields_to_search);
|
||||
|
||||
//Add Or concatation of the expressions to our query
|
||||
//Add Or concatenation of the expressions to our query
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->orX(...$expressions)
|
||||
);
|
||||
|
||||
//For regex we pass the query as is, for like we add % to the start and end as wildcards
|
||||
//For regex, we pass the query as is, for like we add % to the start and end as wildcards
|
||||
if ($this->regex) {
|
||||
$queryBuilder->setParameter('search_query', $this->keyword);
|
||||
} else {
|
||||
|
|
|
@ -138,7 +138,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')
|
||||
|
||||
|
|
|
@ -87,16 +87,14 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
if(!$context->getPart() instanceof Part) {
|
||||
return htmlspecialchars((string) $context->getName());
|
||||
}
|
||||
if($context->getPart() instanceof Part) {
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
throw new \RuntimeException('This should never happen!');
|
||||
//Part exists if we reach this point
|
||||
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
}
|
||||
return $tmp;
|
||||
},
|
||||
])
|
||||
->add('ipn', TextColumn::class, [
|
||||
|
|
71
src/Doctrine/Functions/ILike.php
Normal file
71
src/Doctrine/Functions/ILike.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?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\Doctrine\Functions;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
/**
|
||||
* A platform invariant version of the case-insensitive LIKE operation.
|
||||
* On MySQL and SQLite this is the normal LIKE, but on PostgreSQL it is the ILIKE operator.
|
||||
*/
|
||||
class ILike extends FunctionNode
|
||||
{
|
||||
|
||||
public $value = null;
|
||||
|
||||
public $expr = null;
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->value = $parser->StringPrimary();
|
||||
$parser->match(TokenType::T_COMMA);
|
||||
$this->expr = $parser->StringExpression();
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
||||
|
||||
//
|
||||
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
|
||||
$operator = 'LIKE';
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
//Use the case-insensitive operator, to have the same behavior as MySQL
|
||||
$operator = 'ILIKE';
|
||||
} else {
|
||||
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
|
||||
}
|
||||
|
||||
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
|
||||
}
|
||||
}
|
|
@ -44,15 +44,13 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
|
|||
$native_connection = $connection->getNativeConnection();
|
||||
|
||||
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
|
||||
if($native_connection instanceof \PDO && method_exists($native_connection, 'sqliteCreateFunction' )) {
|
||||
if($native_connection instanceof \PDO) {
|
||||
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
||||
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
|
||||
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
|
||||
|
||||
//Create a new collation for natural sorting
|
||||
if (method_exists($native_connection, 'sqliteCreateCollation')) {
|
||||
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
|
||||
}
|
||||
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class DoNotUsePurgerFactory implements PurgerFactory
|
|||
throw new \LogicException('Do not use doctrine:fixtures:load directly. Use partdb:fixtures:load instead!');
|
||||
}
|
||||
|
||||
public function setEntityManager(EntityManagerInterface $em)
|
||||
public function setEntityManager(EntityManagerInterface $em): void
|
||||
{
|
||||
// TODO: Implement setEntityManager() method.
|
||||
}
|
||||
|
|
|
@ -531,7 +531,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
$url = str_replace(' ', '%20', $url);
|
||||
|
||||
//Only set if the URL is not empty
|
||||
if ($url !== null && $url !== '') {
|
||||
if ($url !== '') {
|
||||
if (str_contains($url, '%BASE%') || str_contains($url, '%MEDIA%')) {
|
||||
throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
/**
|
||||
* @template-covariant AT of Attachment
|
||||
* @template AT of Attachment
|
||||
*/
|
||||
#[ORM\MappedSuperclass(repositoryClass: AttachmentContainingDBElementRepository::class)]
|
||||
abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface
|
||||
|
|
|
@ -33,8 +33,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
/**
|
||||
* This abstract class is used for companies like suppliers or manufacturers.
|
||||
*
|
||||
* @template-covariant AT of Attachment
|
||||
* @template-covariant PT of AbstractParameter
|
||||
* @template AT of Attachment
|
||||
* @template PT of AbstractParameter
|
||||
* @extends AbstractPartsContainingDBElement<AT, PT>
|
||||
*/
|
||||
#[ORM\MappedSuperclass]
|
||||
|
|
|
@ -31,8 +31,8 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
/**
|
||||
* @template-covariant AT of Attachment
|
||||
* @template-covariant PT of AbstractParameter
|
||||
* @template AT of Attachment
|
||||
* @template PT of AbstractParameter
|
||||
* @extends AbstractStructuralDBElement<AT, PT>
|
||||
*/
|
||||
#[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)]
|
||||
|
|
|
@ -53,8 +53,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||
*
|
||||
* @see \App\Tests\Entity\Base\AbstractStructuralDBElementTest
|
||||
*
|
||||
* @template-covariant AT of Attachment
|
||||
* @template-covariant PT of AbstractParameter
|
||||
* @template AT of Attachment
|
||||
* @template PT of AbstractParameter
|
||||
* @template-use ParametersTrait<PT>
|
||||
* @extends AttachmentContainingDBElement<AT>
|
||||
* @uses ParametersTrait<PT>
|
||||
|
|
|
@ -22,6 +22,8 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\Entity\LabelSystem;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
|
@ -34,7 +36,7 @@ enum LabelSupportedElement: string
|
|||
|
||||
/**
|
||||
* Returns the entity class for the given element type
|
||||
* @return string
|
||||
* @return class-string<AbstractDBElement>
|
||||
*/
|
||||
public function getEntityClass(): string
|
||||
{
|
||||
|
|
|
@ -44,9 +44,9 @@ namespace App\Entity\LogSystem;
|
|||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Events\SecurityEvents;
|
||||
use App\Helpers\IPAnonymizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
/**
|
||||
* This log entry is created when something security related to a user happens.
|
||||
|
@ -127,14 +127,14 @@ 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
|
||||
*/
|
||||
public function setIPAddress(string $ip, bool $anonymize = true): self
|
||||
{
|
||||
if ($anonymize) {
|
||||
$ip = IpUtils::anonymize($ip);
|
||||
$ip = IPAnonymizer::anonymize($ip);
|
||||
}
|
||||
$this->extra['i'] = $ip;
|
||||
|
||||
|
|
|
@ -22,8 +22,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Helpers\IPAnonymizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
|
||||
/**
|
||||
* This log entry is created when a user logs in.
|
||||
|
@ -52,14 +53,14 @@ 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
|
||||
*/
|
||||
public function setIPAddress(string $ip, bool $anonymize = true): self
|
||||
{
|
||||
if ($anonymize) {
|
||||
$ip = IpUtils::anonymize($ip);
|
||||
$ip = IPAnonymizer::anonymize($ip);
|
||||
}
|
||||
|
||||
$this->extra['i'] = $ip;
|
||||
|
|
|
@ -22,8 +22,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Helpers\IPAnonymizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
#[ORM\Entity]
|
||||
class UserLogoutLogEntry extends AbstractLogEntry
|
||||
|
@ -49,14 +49,14 @@ 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
|
||||
*/
|
||||
public function setIPAddress(string $ip, bool $anonymize = true): self
|
||||
{
|
||||
if ($anonymize) {
|
||||
$ip = IpUtils::anonymize($ip);
|
||||
$ip = IPAnonymizer::anonymize($ip);
|
||||
}
|
||||
|
||||
$this->extra['i'] = $ip;
|
||||
|
|
|
@ -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'])]
|
||||
|
|
|
@ -68,7 +68,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
|
||||
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
|
||||
#[ValidPartLot]
|
||||
#[UniqueEntity(['vendor_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
|
||||
#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: 'is_granted("read", object)'),
|
||||
|
@ -166,10 +166,10 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
|||
/**
|
||||
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
|
||||
*/
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
#[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)]
|
||||
#[Groups(['part_lot:read', 'part_lot:write'])]
|
||||
#[Length(max: 255)]
|
||||
protected ?string $vendor_barcode = null;
|
||||
protected ?string $user_barcode = null;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
|
@ -185,7 +185,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
|||
*
|
||||
* @return bool|null True, if the part lot is expired. Returns null, if no expiration date was set.
|
||||
*
|
||||
* @throws Exception If an error with the DateTime occurs
|
||||
*/
|
||||
public function isExpired(): ?bool
|
||||
{
|
||||
|
@ -376,19 +375,19 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
|||
* null if no barcode is set.
|
||||
* @return string|null
|
||||
*/
|
||||
public function getVendorBarcode(): ?string
|
||||
public function getUserBarcode(): ?string
|
||||
{
|
||||
return $this->vendor_barcode;
|
||||
return $this->user_barcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor).
|
||||
* @param string|null $vendor_barcode
|
||||
* @param string|null $user_barcode
|
||||
* @return $this
|
||||
*/
|
||||
public function setVendorBarcode(?string $vendor_barcode): PartLot
|
||||
public function setUserBarcode(?string $user_barcode): PartLot
|
||||
{
|
||||
$this->vendor_barcode = $vendor_barcode;
|
||||
$this->user_barcode = $user_barcode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class Supplier extends AbstractCompany
|
|||
protected ?AbstractStructuralDBElement $parent = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Orderdetail>|Orderdetail[]
|
||||
* @var Collection<int, Orderdetail>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Orderdetail::class)]
|
||||
protected Collection $orderdetails;
|
||||
|
|
|
@ -333,7 +333,6 @@ class Project extends AbstractStructuralDBElement
|
|||
{
|
||||
//If this project has subprojects, and these have builds part, they must be included in the BOM
|
||||
foreach ($this->getChildren() as $child) {
|
||||
/** @var $child Project */
|
||||
if (!$child->getBuildPart() instanceof Part) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
@ -256,7 +256,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
protected ?string $password = null;
|
||||
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Regex('/^[\w\.\+\-\$]+$/', message: 'user.invalid_username')]
|
||||
#[Assert\Regex('/^[\w\.\+\-\$]+[\w\.\+\-\$\@]*$/', message: 'user.invalid_username')]
|
||||
#[Groups(['user:read'])]
|
||||
protected string $name = '';
|
||||
|
||||
|
@ -893,8 +893,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
* @param string[] $codes An array containing the backup codes
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws Exception If an error with the datetime occurs
|
||||
*/
|
||||
public function setBackupCodes(array $codes): self
|
||||
{
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
|
||||
|
||||
/**
|
||||
* This class fixes the wrong pathes generated by webpack using the auto publicPath mode.
|
||||
* Basically it replaces the wrong /auto/ part of the path with the correct /build/ in all encore entrypoints.
|
||||
*/
|
||||
class WebpackAutoPathSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
RenderAssetTagEvent::class => 'onRenderAssetTag'
|
||||
];
|
||||
}
|
||||
|
||||
public function onRenderAssetTag(RenderAssetTagEvent $event): void
|
||||
{
|
||||
if ($event->isScriptTag()) {
|
||||
$event->setAttribute('src', $this->resolveAuto($event->getUrl()));
|
||||
}
|
||||
if ($event->isLinkTag()) {
|
||||
$event->setAttribute('href', $this->resolveAuto($event->getUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveAuto(string $path): string
|
||||
{
|
||||
//Replace the first occurence of /auto/ with /build/ to get the correct path
|
||||
return preg_replace('/\/auto\//', '/build/', $path, 1);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -71,6 +71,22 @@ class LabelDialogType extends AbstractType
|
|||
'label' => false,
|
||||
'disabled' => !$this->security->isGranted('@labels.edit_options') || $options['disable_options'],
|
||||
]);
|
||||
|
||||
$builder->add('save_profile_name', TextType::class, [
|
||||
'required' => false,
|
||||
'attr' =>[
|
||||
'placeholder' => 'label_generator.save_profile_name',
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->add('save_profile', SubmitType::class, [
|
||||
'label' => 'label_generator.save_profile',
|
||||
'disabled' => !$this->security->isGranted('@labels.create_profiles'),
|
||||
'attr' => [
|
||||
'class' => 'btn btn-outline-success'
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->add('update', SubmitType::class, [
|
||||
'label' => 'label_generator.update',
|
||||
]);
|
||||
|
|
|
@ -41,8 +41,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Form\LabelSystem;
|
||||
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
|
@ -55,6 +56,8 @@ class ScanDialogType extends AbstractType
|
|||
{
|
||||
$builder->add('input', TextType::class, [
|
||||
'label' => 'scan_dialog.input',
|
||||
//Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters
|
||||
'trim' => false,
|
||||
'attr' => [
|
||||
'autofocus' => true,
|
||||
'id' => 'scan_dialog_input',
|
||||
|
@ -71,9 +74,14 @@ class ScanDialogType extends AbstractType
|
|||
null => 'scan_dialog.mode.auto',
|
||||
BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal',
|
||||
BarcodeSourceType::IPN => 'scan_dialog.mode.ipn',
|
||||
BarcodeSourceType::VENDOR => 'scan_dialog.mode.vendor',
|
||||
BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
|
||||
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp'
|
||||
},
|
||||
]);
|
||||
|
||||
$builder->add('info_mode', CheckboxType::class, [
|
||||
'label' => 'scan_dialog.info_mode',
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -102,6 +102,8 @@ class PartBaseType extends AbstractType
|
|||
'dto_value' => $dto?->category,
|
||||
'label' => 'part.edit.category',
|
||||
'disable_not_selectable' => true,
|
||||
//Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity)
|
||||
'required' => !$new_part,
|
||||
])
|
||||
->add('footprint', StructuralEntityType::class, [
|
||||
'class' => Footprint::class,
|
||||
|
|
|
@ -103,10 +103,12 @@ class PartLotType extends AbstractType
|
|||
'help' => 'part_lot.owner.help',
|
||||
]);
|
||||
|
||||
$builder->add('vendor_barcode', TextType::class, [
|
||||
'label' => 'part_lot.edit.vendor_barcode',
|
||||
$builder->add('user_barcode', TextType::class, [
|
||||
'label' => 'part_lot.edit.user_barcode',
|
||||
'help' => 'part_lot.edit.vendor_barcode.help',
|
||||
'required' => false,
|
||||
//Do not remove whitespace chars on the beginning and end of the string
|
||||
'trim' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +39,15 @@ class ExponentialNumberType extends AbstractType
|
|||
return NumberType::class;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
//We want to allow the full precision of the number, so disable rounding
|
||||
'scale' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->resetViewTransformers();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ class StructuralEntityChoiceHelper
|
|||
|
||||
/**
|
||||
* Generates the choice attributes for the given AbstractStructuralDBElement.
|
||||
* @return array|string[]
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function generateChoiceAttr(AbstractNamedDBElement $choice, Options|array $options): array
|
||||
{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue