Merge branch 'master' into settings-bundle

This commit is contained in:
Jan Böhmer 2025-01-17 22:06:18 +01:00
commit 8750573724
191 changed files with 27745 additions and 12133 deletions

View file

@ -1,51 +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/>.
*/
declare(strict_types=1);
namespace App\Form\Fixes;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
class FixNumberType extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
//Remove existing view transformers
$builder->resetViewTransformers();
//And add our fixed version
$builder->addViewTransformer(new FixedNumberToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
$options['rounding_mode'],
$options['html5'] ? 'en' : null
));
}
public static function getExtendedTypes(): iterable
{
return [NumberType::class];
}
}

View file

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

View file

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

View file

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

View file

@ -102,7 +102,7 @@ class ParameterType extends AbstractType
'step' => 'any',
'placeholder' => 'parameters.max.placeholder',
'class' => 'form-control-sm',
'style' => 'max-width: 12ch;',
'style' => 'max-width: 25ch;',
],
]);
$builder->add('value_min', ExponentialNumberType::class, [
@ -113,7 +113,7 @@ class ParameterType extends AbstractType
'step' => 'any',
'placeholder' => 'parameters.min.placeholder',
'class' => 'form-control-sm',
'style' => 'max-width: 12ch;',
'style' => 'max-width: 25ch;',
],
]);
$builder->add('value_typical', ExponentialNumberType::class, [
@ -124,7 +124,7 @@ class ParameterType extends AbstractType
'step' => 'any',
'placeholder' => 'parameters.typical.placeholder',
'class' => 'form-control-sm',
'style' => 'max-width: 12ch;',
'style' => 'max-width: 25ch;',
],
]);
$builder->add('unit', TextType::class, [

View file

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

View file

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

View file

@ -53,6 +53,7 @@ class TFAGoogleSettingsType extends AbstractType
'google_confirmation',
TextType::class,
[
'label' => 'tfa.check.code.confirmation',
'mapped' => false,
'attr' => [
'maxlength' => '6',
@ -60,7 +61,7 @@ class TFAGoogleSettingsType extends AbstractType
'pattern' => '\d*',
'autocomplete' => 'off',
],
'constraints' => [new ValidGoogleAuthCode()],
'constraints' => [new ValidGoogleAuthCode(groups: ["google_authenticator"])],
]
);
@ -92,6 +93,7 @@ class TFAGoogleSettingsType extends AbstractType
{
$resolver->setDefaults([
'data_class' => User::class,
'validation_groups' => ['google_authenticator'],
]);
}
}

View file

@ -27,6 +27,7 @@ use App\Form\Type\Helper\ExponentialNumberTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Similar to the NumberType, but formats small values in scienfitic notation instead of rounding it to 0, like NumberType
@ -38,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();

View file

@ -23,21 +23,22 @@ declare(strict_types=1);
namespace App\Form\Type\Helper;
use App\Form\Fixes\FixedNumberToLocalizedStringTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
/**
* This transformer formats small values in scienfitic notation instead of rounding it to 0, like the default
* NumberFormatter.
*/
class ExponentialNumberTransformer extends FixedNumberToLocalizedStringTransformer
class ExponentialNumberTransformer extends NumberToLocalizedStringTransformer
{
public function __construct(
protected ?int $scale = null,
private ?int $scale = null,
?bool $grouping = false,
?int $roundingMode = \NumberFormatter::ROUND_HALFUP,
protected ?string $locale = null
) {
//Set scale to null, to disable rounding of values
parent::__construct($scale, $grouping, $roundingMode, $locale);
}
@ -85,12 +86,28 @@ class ExponentialNumberTransformer extends FixedNumberToLocalizedStringTransform
$formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::SCIENTIFIC);
if (null !== $this->scale) {
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
$formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $this->scale);
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
}
$formatter->setAttribute(\NumberFormatter::GROUPING_USED, (int) $this->grouping);
return $formatter;
}
protected function getNumberFormatter(): \NumberFormatter
{
$formatter = parent::getNumberFormatter();
//Unset the fraction digits, as we don't want to round the number
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, 0);
if (null !== $this->scale) {
$formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $this->scale);
} else {
$formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, 100);
}
return $formatter;
}
}

View file

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

View file

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace App\Form\Type\Helper;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Repository\StructuralDBElementRepository;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
@ -33,6 +34,9 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @template T of AbstractStructuralDBElement
*/
class StructuralEntityChoiceLoader extends AbstractChoiceLoader
{
private ?string $additional_element = null;
@ -90,10 +94,14 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
}
}
/** @var class-string<T> $class */
$class = $this->options['class'];
/** @var StructuralDBElementRepository $repo */
/** @var StructuralDBElementRepository<T> $repo */
$repo = $this->entityManager->getRepository($class);
$entities = $repo->getNewEntityFromPath($value, '->');
$results = [];

View file

@ -99,7 +99,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
*
* @return mixed The value in the transformed representation
*
* @throws TransformationFailedException when the transformation fails
*/
public function transform(mixed $value)
{
@ -142,8 +141,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
* @param mixed $value The value in the transformed representation
*
* @return mixed The value in the original representation
*
* @throws TransformationFailedException when the transformation fails
*/
public function reverseTransform(mixed $value)
{

View file

@ -57,6 +57,8 @@ class UserAdminForm extends AbstractType
parent::configureOptions($resolver); // TODO: Change the autogenerated stub
$resolver->setRequired('attachment_class');
$resolver->setDefault('parameter_class', false);
$resolver->setDefault('validation_groups', ['Default', 'permissions:edit']);
}
public function buildForm(FormBuilderInterface $builder, array $options): void