mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-24 11:59:31 +00:00
Merge tag 'v2.2.1' into order-details
This commit is contained in:
commit
a64dec2985
90 changed files with 16636 additions and 1723 deletions
|
|
@ -25,6 +25,7 @@ namespace App\Controller;
|
|||
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Exceptions\OAuthReconnectRequiredException;
|
||||
use App\Form\InfoProviderSystem\PartSearchType;
|
||||
use App\Services\InfoProviderSystem\ExistingPartFinder;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
|
|
@ -175,8 +176,11 @@ class InfoProviderController extends AbstractController
|
|||
$this->addFlash('error',$e->getMessage());
|
||||
//Log the exception
|
||||
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
|
||||
} catch (OAuthReconnectRequiredException $e) {
|
||||
$this->addFlash('error', t('info_providers.search.error.oauth_reconnect', ['%provider%' => $e->getProviderName()]));
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ use App\Exceptions\InvalidRegexException;
|
|||
use App\Form\Filters\PartFilterType;
|
||||
use App\Services\Parts\PartsTableActionHandler;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use App\Settings\BehaviorSettings\SidebarSettings;
|
||||
use App\Settings\BehaviorSettings\TableSettings;
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -56,11 +57,21 @@ class PartListsController extends AbstractController
|
|||
private readonly NodesListBuilder $nodesListBuilder,
|
||||
private readonly DataTableFactory $dataTableFactory,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly TableSettings $tableSettings
|
||||
private readonly TableSettings $tableSettings,
|
||||
private readonly SidebarSettings $sidebarSettings,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filter operator to use by default (INCLUDING_CHILDREN or =)
|
||||
* @return string
|
||||
*/
|
||||
private function getFilterOperator(): string
|
||||
{
|
||||
return $this->sidebarSettings->dataStructureNodesTableIncludeChildren ? 'INCLUDING_CHILDREN' : '=';
|
||||
}
|
||||
|
||||
#[Route(path: '/table/action', name: 'table_action', methods: ['POST'])]
|
||||
public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response
|
||||
{
|
||||
|
|
@ -203,7 +214,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/category_list.html.twig',
|
||||
function (PartFilter $filter) use ($category) {
|
||||
$filter->category->setOperator('INCLUDING_CHILDREN')->setValue($category);
|
||||
$filter->category->setOperator($this->getFilterOperator())->setValue($category);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('category')->get('value'));
|
||||
}, [
|
||||
|
|
@ -221,7 +232,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/footprint_list.html.twig',
|
||||
function (PartFilter $filter) use ($footprint) {
|
||||
$filter->footprint->setOperator('INCLUDING_CHILDREN')->setValue($footprint);
|
||||
$filter->footprint->setOperator($this->getFilterOperator())->setValue($footprint);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('footprint')->get('value'));
|
||||
}, [
|
||||
|
|
@ -239,7 +250,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/manufacturer_list.html.twig',
|
||||
function (PartFilter $filter) use ($manufacturer) {
|
||||
$filter->manufacturer->setOperator('INCLUDING_CHILDREN')->setValue($manufacturer);
|
||||
$filter->manufacturer->setOperator($this->getFilterOperator())->setValue($manufacturer);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('manufacturer')->get('value'));
|
||||
}, [
|
||||
|
|
@ -257,7 +268,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/store_location_list.html.twig',
|
||||
function (PartFilter $filter) use ($storelocation) {
|
||||
$filter->storelocation->setOperator('INCLUDING_CHILDREN')->setValue($storelocation);
|
||||
$filter->storelocation->setOperator($this->getFilterOperator())->setValue($storelocation);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('storelocation')->get('value'));
|
||||
}, [
|
||||
|
|
@ -275,7 +286,7 @@ class PartListsController extends AbstractController
|
|||
return $this->showListWithFilter($request,
|
||||
'parts/lists/supplier_list.html.twig',
|
||||
function (PartFilter $filter) use ($supplier) {
|
||||
$filter->supplier->setOperator('INCLUDING_CHILDREN')->setValue($supplier);
|
||||
$filter->supplier->setOperator($this->getFilterOperator())->setValue($supplier);
|
||||
}, function (FormInterface $filterForm) {
|
||||
$this->disableFormFieldAfterCreation($filterForm->get('supplier')->get('value'));
|
||||
}, [
|
||||
|
|
|
|||
|
|
@ -96,14 +96,15 @@ class TextConstraint extends AbstractConstraint
|
|||
|
||||
//The CONTAINS, LIKE, STARTS and ENDS operators use the LIKE operator, but we have to build the value string differently
|
||||
$like_value = null;
|
||||
$escaped_value = str_replace(['%', '_'], ['\%', '\_'], $this->value);
|
||||
if ($this->operator === 'LIKE') {
|
||||
$like_value = $this->value;
|
||||
$like_value = $this->value; //Here we do not escape anything, as the user may provide % and _ wildcards
|
||||
} elseif ($this->operator === 'STARTS') {
|
||||
$like_value = $this->value . '%';
|
||||
$like_value = $escaped_value . '%';
|
||||
} elseif ($this->operator === 'ENDS') {
|
||||
$like_value = '%' . $this->value;
|
||||
$like_value = '%' . $escaped_value;
|
||||
} elseif ($this->operator === 'CONTAINS') {
|
||||
$like_value = '%' . $this->value . '%';
|
||||
$like_value = '%' . $escaped_value . '%';
|
||||
}
|
||||
|
||||
if ($like_value !== null) {
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@ class PartSearchFilter implements FilterInterface
|
|||
if ($this->regex) {
|
||||
$queryBuilder->setParameter('search_query', $this->keyword);
|
||||
} else {
|
||||
//Escape % and _ characters in the keyword
|
||||
$this->keyword = str_replace(['%', '_'], ['\%', '\_'], $this->keyword);
|
||||
$queryBuilder->setParameter('search_query', '%' . $this->keyword . '%');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ class ILike extends FunctionNode
|
|||
{
|
||||
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
||||
|
||||
//
|
||||
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
|
||||
$operator = 'LIKE';
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
|
|
@ -66,6 +65,12 @@ class ILike extends FunctionNode
|
|||
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
|
||||
}
|
||||
|
||||
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
|
||||
$escape = "";
|
||||
if ($platform instanceof SQLitePlatform) {
|
||||
//SQLite needs ESCAPE explicitly defined backslash as escape character
|
||||
$escape = " ESCAPE '\\'";
|
||||
}
|
||||
|
||||
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . $escape . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ trait ProjectTrait
|
|||
/**
|
||||
* @var Collection<ProjectBOMEntry> $project_bom_entries
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'part', targetEntity: ProjectBOMEntry::class, cascade: ['remove'], orphanRemoval: true)]
|
||||
#[ORM\OneToMany(targetEntity: ProjectBOMEntry::class, mappedBy: 'part')]
|
||||
protected Collection $project_bom_entries;
|
||||
|
||||
/**
|
||||
|
|
|
|||
59
src/EntityListeners/PartProjectBOMEntryUnlinkListener.php
Normal file
59
src/EntityListeners/PartProjectBOMEntryUnlinkListener.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 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\EntityListeners;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Event\PreRemoveEventArgs;
|
||||
|
||||
/**
|
||||
* If an part is deleted, this listener makes sure that all ProjectBOMEntries that reference this part, are updated
|
||||
* to not reference the part anymore, but instead store the part name in the name field.
|
||||
*/
|
||||
#[AsEntityListener(event: "preRemove", entity: Part::class)]
|
||||
class PartProjectBOMEntryUnlinkListener
|
||||
{
|
||||
public function preRemove(Part $part, PreRemoveEventArgs $event): void
|
||||
{
|
||||
// Iterate over all ProjectBOMEntries that use this part and put the part name into the name field
|
||||
foreach ($part->getProjectBomEntries() as $bom_entry) {
|
||||
$old_name = $bom_entry->getName();
|
||||
if ($old_name === null || trim($old_name) === '') {
|
||||
$bom_entry->setName($part->getName());
|
||||
} else {
|
||||
$bom_entry->setName($old_name . ' (' . $part->getName() . ')');
|
||||
}
|
||||
|
||||
$old_comment = $bom_entry->getComment();
|
||||
if ($old_comment === null || trim($old_comment) === '') {
|
||||
$bom_entry->setComment('Part was deleted: ' . $part->getName());
|
||||
} else {
|
||||
$bom_entry->setComment($old_comment . "\n\n Part was deleted: " . $part->getName());
|
||||
}
|
||||
|
||||
//Remove the part reference
|
||||
$bom_entry->setPart(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Exceptions/OAuthReconnectRequiredException.php
Normal file
48
src/Exceptions/OAuthReconnectRequiredException.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 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\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class OAuthReconnectRequiredException extends \RuntimeException
|
||||
{
|
||||
private string $providerName = "unknown";
|
||||
|
||||
public function __construct(string $message = "You need to reconnect the OAuth connection for this provider!", int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function forProvider(string $providerName): self
|
||||
{
|
||||
$exception = new self("You need to reconnect the OAuth connection for the provider '$providerName'!");
|
||||
$exception->providerName = $providerName;
|
||||
return $exception;
|
||||
}
|
||||
|
||||
public function getProviderName(): string
|
||||
{
|
||||
return $this->providerName;
|
||||
}
|
||||
}
|
||||
57
src/Form/Type/LanguageMenuEntriesType.php
Normal file
57
src/Form/Type/LanguageMenuEntriesType.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 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\Type;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
|
||||
use Symfony\Component\Intl\Languages;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class LanguageMenuEntriesType extends AbstractType
|
||||
{
|
||||
public function __construct(#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return LanguageType::class;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$choices = [];
|
||||
foreach ($this->preferred_languages as $lang_code) {
|
||||
$choices[Languages::getName($lang_code)] = $lang_code;
|
||||
}
|
||||
|
||||
$resolver->setDefaults([
|
||||
'choice_loader' => null,
|
||||
'choices' => $choices,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,11 @@ class SIFormatter
|
|||
*/
|
||||
public function getMagnitude(float $value): int
|
||||
{
|
||||
//Check for zero, as log10(0) is undefined/gives -infinity, which leads to casting issues in PHP8.5+
|
||||
if (0.0 === $value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) floor(log10(abs($value)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ final class DTOtoEntityConverter
|
|||
$attachment = $this->convertFile($image, $image_type);
|
||||
|
||||
$attachments_grouped[$attachment->getName()][] = $attachment;
|
||||
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
|
||||
if (count($attachments_grouped[$attachment->getName()]) > 1) {
|
||||
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()]) + 1) . ')');
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ final class DTOtoEntityConverter
|
|||
$attachment = $this->convertFile($datasheet, $datasheet_type);
|
||||
|
||||
$attachments_grouped[$attachment->getName()][] = $attachment;
|
||||
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
|
||||
if (count($attachments_grouped[$attachment->getName()]) > 1) {
|
||||
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()])) . ')');
|
||||
}
|
||||
|
||||
|
|
@ -357,4 +357,4 @@ final class DTOtoEntityConverter
|
|||
return $tmp;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
|||
namespace App\Services\InfoProviderSystem\Providers;
|
||||
|
||||
use App\Entity\Parts\ManufacturingStatus;
|
||||
use App\Exceptions\OAuthReconnectRequiredException;
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
|
|
@ -117,12 +118,22 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
];
|
||||
|
||||
//$response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [
|
||||
$response = $this->digikeyClient->request('POST', '/products/v4/search/keyword', [
|
||||
'json' => $request,
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
try {
|
||||
$response = $this->digikeyClient->request('POST', '/products/v4/search/keyword', [
|
||||
'json' => $request,
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
|
||||
$response_array = $response->toArray();
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
//Check if the exception was caused by an invalid or expired token
|
||||
if (str_contains($exception->getMessage(), 'access_token')) {
|
||||
throw OAuthReconnectRequiredException::forProvider($this->getProviderKey());
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$response_array = $response->toArray();
|
||||
|
||||
|
||||
$result = [];
|
||||
|
|
@ -150,9 +161,18 @@ class DigikeyProvider implements InfoProviderInterface
|
|||
|
||||
public function getDetails(string $id): PartDetailDTO
|
||||
{
|
||||
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
try {
|
||||
$response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [
|
||||
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
|
||||
]);
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
//Check if the exception was caused by an invalid or expired token
|
||||
if (str_contains($exception->getMessage(), 'access_token')) {
|
||||
throw OAuthReconnectRequiredException::forProvider($this->getProviderKey());
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$response_array = $response->toArray();
|
||||
$product = $response_array['Product'];
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ use App\Services\Parts\PartLotWithdrawAddHelper;
|
|||
/**
|
||||
* @see \App\Tests\Services\ProjectSystem\ProjectBuildHelperTest
|
||||
*/
|
||||
class ProjectBuildHelper
|
||||
final readonly class ProjectBuildHelper
|
||||
{
|
||||
public function __construct(private readonly PartLotWithdrawAddHelper $withdraw_add_helper)
|
||||
public function __construct(private PartLotWithdrawAddHelper $withdraw_add_helper)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -63,20 +63,37 @@ class ProjectBuildHelper
|
|||
*/
|
||||
public function getMaximumBuildableCount(Project $project): int
|
||||
{
|
||||
$bom_entries = $project->getBomEntries();
|
||||
if ($bom_entries->isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
$maximum_buildable_count = PHP_INT_MAX;
|
||||
foreach ($project->getBomEntries() as $bom_entry) {
|
||||
foreach ($bom_entries as $bom_entry) {
|
||||
//Skip BOM entries without a part (as we can not determine that)
|
||||
if (!$bom_entry->isPartBomEntry()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//The maximum buildable count for the whole project is the minimum of all BOM entries
|
||||
$maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry));
|
||||
}
|
||||
|
||||
return $maximum_buildable_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum buildable amount of the given project as string, based on the stock of the used parts in the BOM.
|
||||
* If the maximum buildable count is infinite, the string '∞' is returned.
|
||||
* @param Project $project
|
||||
* @return string
|
||||
*/
|
||||
public function getMaximumBuildableCountAsString(Project $project): string
|
||||
{
|
||||
$max_count = $this->getMaximumBuildableCount($project);
|
||||
if ($max_count === PHP_INT_MAX) {
|
||||
return '∞';
|
||||
}
|
||||
return (string) $max_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given project can be built with the current stock.
|
||||
* This means that the maximum buildable count is greater or equal than the requested $number_of_projects
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace App\Settings;
|
|||
use App\Settings\BehaviorSettings\BehaviorSettings;
|
||||
use App\Settings\InfoProviderSystem\InfoProviderSettings;
|
||||
use App\Settings\MiscSettings\MiscSettings;
|
||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||
use App\Settings\SystemSettings\SystemSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\EmbeddedSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
|
|
@ -49,4 +49,4 @@ class AppSettings
|
|||
|
||||
#[EmbeddedSettings()]
|
||||
public ?MiscSettings $miscSettings = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ namespace App\Settings\BehaviorSettings;
|
|||
use Jbtronics\SettingsBundle\Settings\EmbeddedSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
|
||||
#[Settings]
|
||||
#[Settings(label: new TM("settings.behavior"))]
|
||||
class BehaviorSettings
|
||||
{
|
||||
use SettingsTrait;
|
||||
|
|
@ -40,4 +41,4 @@ class BehaviorSettings
|
|||
|
||||
#[EmbeddedSettings]
|
||||
public ?PartInfoSettings $partInfo = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,4 +73,11 @@ class SidebarSettings
|
|||
*/
|
||||
#[SettingsParameter(label: new TM("settings.behavior.sidebar.rootNodeRedirectsToNewEntity"))]
|
||||
public bool $rootNodeRedirectsToNewEntity = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool Whether to include child nodes in the data structure nodes table, or only show the selected node's parts.
|
||||
*/
|
||||
#[SettingsParameter(label: new TM("settings.behavior.sidebar.data_structure_nodes_table_include_children"),
|
||||
description: new TM("settings.behavior.sidebar.data_structure_nodes_table_include_children.help"))]
|
||||
public bool $dataStructureNodesTableIncludeChildren = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ use Jbtronics\SettingsBundle\Settings\EmbeddedSettings;
|
|||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
|
||||
#[Settings()]
|
||||
#[Settings(label: new TM("settings.ips"))]
|
||||
class InfoProviderSettings
|
||||
{
|
||||
use SettingsTrait;
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ namespace App\Settings\MiscSettings;
|
|||
|
||||
use Jbtronics\SettingsBundle\Settings\EmbeddedSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
|
||||
#[Settings]
|
||||
#[Settings(label: new TM("settings.misc"))]
|
||||
class MiscSettings
|
||||
{
|
||||
#[EmbeddedSettings]
|
||||
|
|
@ -34,4 +35,4 @@ class MiscSettings
|
|||
|
||||
#[EmbeddedSettings]
|
||||
public ?ExchangeRateSettings $exchangeRate = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Settings\SystemSettings;
|
||||
|
||||
use App\Form\Type\LanguageMenuEntriesType;
|
||||
use App\Form\Type\LocaleSelectType;
|
||||
use App\Settings\SettingsIcon;
|
||||
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
|
||||
use Jbtronics\SettingsBundle\ParameterTypes\ArrayType;
|
||||
use Jbtronics\SettingsBundle\ParameterTypes\StringType;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
|
||||
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
|
||||
|
|
@ -60,4 +63,14 @@ class LocalizationSettings
|
|||
envVar: "string:BASE_CURRENCY", envVarMode: EnvVarMode::OVERWRITE
|
||||
)]
|
||||
public string $baseCurrency = 'EUR';
|
||||
}
|
||||
|
||||
#[SettingsParameter(type: ArrayType::class,
|
||||
label: new TM("settings.system.localization.language_menu_entries"),
|
||||
description: new TM("settings.system.localization.language_menu_entries.description"),
|
||||
options: ['type' => StringType::class],
|
||||
formType: LanguageMenuEntriesType::class,
|
||||
formOptions: ['multiple' => true, 'required' => false, 'ordered' => true],
|
||||
)]
|
||||
#[Assert\All([new Assert\Locale()])]
|
||||
public array $languageMenuEntries = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,13 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Settings;
|
||||
namespace App\Settings\SystemSettings;
|
||||
|
||||
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||
use App\Settings\SystemSettings\CustomizationSettings;
|
||||
use App\Settings\SystemSettings\HistorySettings;
|
||||
use App\Settings\SystemSettings\LocalizationSettings;
|
||||
use App\Settings\SystemSettings\PrivacySettings;
|
||||
use Jbtronics\SettingsBundle\Settings\EmbeddedSettings;
|
||||
use Jbtronics\SettingsBundle\Settings\Settings;
|
||||
use Symfony\Component\Translation\TranslatableMessage as TM;
|
||||
|
||||
#[Settings]
|
||||
#[Settings(label: new TM("settings.system"))]
|
||||
class SystemSettings
|
||||
{
|
||||
#[EmbeddedSettings()]
|
||||
|
|
@ -48,4 +44,4 @@ class SystemSettings
|
|||
|
||||
#[EmbeddedSettings()]
|
||||
public ?HistorySettings $history = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ final class FormatExtension extends AbstractExtension
|
|||
public function formatBytes(int $bytes, int $precision = 2): string
|
||||
{
|
||||
$size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
|
||||
$factor = floor((strlen((string) $bytes) - 1) / 3);
|
||||
$factor = (int) floor((strlen((string) $bytes) - 1) / 3);
|
||||
//We use the real (10 based) SI prefix here
|
||||
return sprintf("%.{$precision}f", $bytes / (1000 ** $factor)) . ' ' . @$size[$factor];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue