mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-13 05:39:33 +00:00
Merge remote-tracking branch 'origin/feature/create-assemblies' into feature/all-features
# Conflicts: # .env # config/parameters.yaml # docs/configuration.md # migrations/Version20250304081039.php # migrations/Version20250304154507.php # src/Controller/AdminPages/BaseAdminController.php # src/Controller/ProjectController.php # src/Controller/TypeaheadController.php # src/DataTables/AssemblyBomEntriesDataTable.php # src/DataTables/PartsDataTable.php # src/Entity/AssemblySystem/AssemblyBOMEntry.php # src/Entity/Attachments/Attachment.php # src/Entity/Base/AbstractDBElement.php # src/Entity/LogSystem/CollectionElementDeleted.php # src/Entity/Parameters/AbstractParameter.php # src/Form/AssemblySystem/AssemblyBOMEntryType.php # src/Helpers/Assemblies/AssemblyPartAggregator.php # src/Security/Voter/AttachmentVoter.php # src/Services/AssemblySystem/AssemblyBuildHelper.php # src/Services/ImportExportSystem/BOMImporter.php # src/Services/ImportExportSystem/EntityExporter.php # src/Services/Trees/ToolsTreeBuilder.php # src/Services/Trees/TreeViewGenerator.php # src/Settings/BehaviorSettings/AssemblyBomTableColumns.php # src/Settings/BehaviorSettings/TableSettings.php # src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php # templates/admin/assembly_admin.html.twig # templates/assemblies/build/_form.html.twig # templates/assemblies/import_bom.html.twig # templates/assemblies/info/_info_card.html.twig # templates/assemblies/info/info.html.twig # templates/components/tree_macros.html.twig # templates/form/collection_types_layout_assembly.html.twig # translations/messages.cs.xlf # translations/messages.da.xlf # translations/messages.de.xlf # translations/messages.el.xlf # translations/messages.en.xlf # translations/messages.es.xlf # translations/messages.fr.xlf # translations/messages.it.xlf # translations/messages.ja.xlf # translations/messages.nl.xlf # translations/messages.pl.xlf # translations/messages.ru.xlf # translations/messages.zh.xlf # translations/validators.cs.xlf # translations/validators.en.xlf
This commit is contained in:
commit
f7ec130010
42 changed files with 1272 additions and 2273 deletions
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
namespace App\Controller;
|
||||
|
||||
use App\DataTables\LogDataTable;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Attachments\AttachmentUpload;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
|
|
|
|||
|
|
@ -194,14 +194,14 @@ class ProjectController extends AbstractController
|
|||
|
||||
// For PCB imports, proceed directly
|
||||
$importerResult = $BOMImporter->importFileIntoProject($form->get('file')->getData(), $project, [
|
||||
'type' => $form->get('type')->getData(),
|
||||
'type' => $import_type,
|
||||
]);
|
||||
|
||||
// Validate the project entries
|
||||
$errors = $validator->validateProperty($project, 'bom_entries');
|
||||
|
||||
//If no validation errors occured, save the changes and redirect to edit page
|
||||
if (count ($errors) === 0 && $importerResult->getViolations()->count() === 0) {
|
||||
//If no validation errors occurred, save the changes and redirect to edit page
|
||||
if (count($errors) === 0 && $importerResult->getViolations()->count() === 0) {
|
||||
$entries = $importerResult->getBomEntries();
|
||||
|
||||
$this->addFlash('success', t('project.bom_import.flash.success', ['%count%' => count($entries)]));
|
||||
|
|
|
|||
|
|
@ -162,6 +162,12 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface
|
|||
return $html;
|
||||
},
|
||||
])
|
||||
->add('designator', TextColumn::class, [
|
||||
'label' => 'assembly.bom.designator',
|
||||
'render' => function ($value, AssemblyBOMEntry $context) {
|
||||
return htmlspecialchars($context->getDesignator());
|
||||
},
|
||||
])
|
||||
->add('instockAmount', TextColumn::class, [
|
||||
'label' => 'assembly.bom.instockAmount',
|
||||
'visible' => false,
|
||||
|
|
@ -195,7 +201,7 @@ class AssemblyBomEntriesDataTable implements DataTableTypeInterface
|
|||
$this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->tableSettings->assembliesBomDefaultColumns,
|
||||
"TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS");
|
||||
|
||||
$dataTable->addOrderBy('name', DataTable::SORT_ASCENDING);
|
||||
$dataTable->addOrderBy('name');
|
||||
|
||||
$dataTable->createAdapter(ORMAdapter::class, [
|
||||
'entity' => Attachment::class,
|
||||
|
|
|
|||
|
|
@ -282,34 +282,6 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
]);
|
||||
}
|
||||
|
||||
//Add a assembly column to list where the part is used, when the user has the permission to see the assemblies
|
||||
if ($this->security->isGranted('read', Assembly::class)) {
|
||||
$this->csh->add('assemblies', TextColumn::class, [
|
||||
'label' => $this->translator->trans('assembly.labelp'),
|
||||
'render' => function ($value, Part $context): string {
|
||||
//Only show the first 5 assembly names
|
||||
$assemblies = $context->getAssemblies();
|
||||
$tmp = "";
|
||||
|
||||
$max = 5;
|
||||
|
||||
for ($i = 0; $i < min($max, count($assemblies)); $i++) {
|
||||
$url = $this->urlGenerator->infoURL($assemblies[$i]);
|
||||
$tmp .= sprintf('<a href="%s">%s</a>', $url, htmlspecialchars($assemblies[$i]->getName()));
|
||||
if ($i < count($assemblies) - 1) {
|
||||
$tmp .= ", ";
|
||||
}
|
||||
}
|
||||
|
||||
if (count($assemblies) > $max) {
|
||||
$tmp .= ", + ".(count($assemblies) - $max);
|
||||
}
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
$this->csh
|
||||
->add('edit', IconLinkColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.edit'),
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ use ApiPlatform\OpenApi\Model\Operation;
|
|||
use ApiPlatform\Serializer\Filter\PropertyFilter;
|
||||
use App\ApiPlatform\Filter\LikeFilter;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Repository\DBElementRepository;
|
||||
use App\Validator\Constraints\AssemblySystem\AssemblyCycle;
|
||||
use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry;
|
||||
|
|
@ -86,7 +84,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||
normalizationContext: ['groups' => ['bom_entry:read', 'api:basic:read'], 'openapi_definition_name' => 'Read']
|
||||
)]
|
||||
#[ApiFilter(PropertyFilter::class)]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name", 'mountnames', 'designator', "comment"])]
|
||||
#[ApiFilter(RangeFilter::class, properties: ['quantity'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified', 'quantity'])]
|
||||
class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInterface, TimeStampableInterface
|
||||
|
|
@ -105,6 +103,13 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt
|
|||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
|
||||
protected string $mountnames = '';
|
||||
|
||||
/**
|
||||
* @var string Reference mark on the circuit diagram/PCB
|
||||
*/
|
||||
#[ORM\Column(name: 'designator', type: Types::TEXT)]
|
||||
#[Groups(['bom_entry:read', 'bom_entry:write', 'import', 'simple', 'extended', 'full'])]
|
||||
protected string $designator = '';
|
||||
|
||||
/**
|
||||
* @var string|null An optional name describing this BOM entry (useful for non-part entries)
|
||||
*/
|
||||
|
|
@ -192,6 +197,16 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getDesignator(): string
|
||||
{
|
||||
return $this->designator;
|
||||
}
|
||||
|
||||
public function setDesignator(string $designator): void
|
||||
{
|
||||
$this->designator = $designator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ use App\Entity\Attachments\UserAttachment;
|
|||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Contracts\LogWithEventUndoInterface;
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\Parameters\AssemblyParameter;
|
||||
use App\Entity\Parameters\PartCustomStateParameter;
|
||||
use App\Entity\Parts\PartCustomState;
|
||||
use App\Entity\Parameters\AssemblyParameter;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Entity\Parameters\AttachmentTypeParameter;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
|
||||
|
|
@ -78,6 +80,9 @@ enum LogTargetType: int
|
|||
case ASSEMBLY_BOM_ENTRY = 24;
|
||||
case PART_CUSTOM_STATE = 25;
|
||||
|
||||
case ASSEMBLY = 23;
|
||||
case ASSEMBLY_BOM_ENTRY = 24;
|
||||
|
||||
/**
|
||||
* Returns the class name of the target type or null if the target type is NONE.
|
||||
* @return string|null
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Parts;
|
||||
|
||||
use App\Entity\Parts\PartTraits\AssemblyTrait;
|
||||
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
|
|
|
|||
|
|
@ -45,12 +45,17 @@ class AssemblyBOMEntryType extends AbstractType
|
|||
])
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'assembly.bom.name',
|
||||
'help' => 'assembly.bom.name.help',
|
||||
'required' => false,
|
||||
])
|
||||
->add('designator', TextType::class, [
|
||||
'label' => 'assembly.bom.designator',
|
||||
'help' => 'assembly.bom.designator.help',
|
||||
'required' => false
|
||||
])
|
||||
->add('mountnames', TextType::class, [
|
||||
'required' => false,
|
||||
'label' => 'assembly.bom.mountnames',
|
||||
'empty_data' => '',
|
||||
'attr' => [
|
||||
'class' => 'tagsinput',
|
||||
'data-controller' => 'elements--tagsinput',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
namespace App\Helpers\Assemblies;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Parts\Part;
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
|
|
@ -61,6 +62,7 @@ class AssemblyPartAggregator
|
|||
*/
|
||||
private function processAssembly(Assembly $assembly, float $multiplier, array &$aggregatedParts): void
|
||||
{
|
||||
/** @var AssemblyBOMEntry $bomEntry */
|
||||
foreach ($assembly->getBomEntries() as $bomEntry) {
|
||||
// If the BOM entry refers to a part, add its quantity
|
||||
if ($bomEntry->getPart() instanceof Part) {
|
||||
|
|
@ -70,6 +72,8 @@ class AssemblyPartAggregator
|
|||
$aggregatedParts[$part->getId()] = [
|
||||
'part' => $part,
|
||||
'assembly' => $assembly,
|
||||
'name' => $bomEntry->getName(),
|
||||
'designator' => $bomEntry->getDesignator(),
|
||||
'quantity' => $bomEntry->getQuantity(),
|
||||
'multiplier' => $multiplier,
|
||||
];
|
||||
|
|
@ -81,6 +85,8 @@ class AssemblyPartAggregator
|
|||
$aggregatedParts[] = [
|
||||
'part' => null,
|
||||
'assembly' => $assembly,
|
||||
'name' => $bomEntry->getName(),
|
||||
'designator' => $bomEntry->getDesignator(),
|
||||
'quantity' => $bomEntry->getQuantity(),
|
||||
'multiplier' => $multiplier,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ class AssemblyBuildHelper
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the assembly BOM entries for which parts are missing in the stock for the given number of builds
|
||||
* Returns the referenced assembly BOM entries for which parts are missing in the stock for the given number of builds
|
||||
* @param Assembly $assembly The assembly for which the BOM entries should be checked
|
||||
* @param int $number_of_builds How often should the assembly be build?
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\AssemblySystem\AssemblyBOMEntry;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentContainingDBElement;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ use Symfony\Component\Validator\ConstraintViolation;
|
|||
*/
|
||||
class BOMImporter
|
||||
{
|
||||
|
||||
private const IMPORT_TYPE_JSON = 'json';
|
||||
private const IMPORT_TYPE_CSV = 'csv';
|
||||
private const IMPORT_TYPE_KICAD_PCB = 'kicad_pcbnew';
|
||||
|
|
@ -64,29 +63,29 @@ class BOMImporter
|
|||
5 => 'Supplier and ref',
|
||||
];
|
||||
|
||||
private readonly PartRepository $partRepository;
|
||||
|
||||
private readonly ManufacturerRepository $manufacturerRepository;
|
||||
|
||||
private readonly CategoryRepository $categoryRepository;
|
||||
|
||||
private readonly DBElementRepository $projectBomEntryRepository;
|
||||
|
||||
private readonly DBElementRepository $assemblyBomEntryRepository;
|
||||
|
||||
private string $jsonRoot = '';
|
||||
|
||||
private PartRepository $partRepository;
|
||||
|
||||
private ManufacturerRepository $manufacturerRepository;
|
||||
|
||||
private CategoryRepository $categoryRepository;
|
||||
|
||||
private DBElementRepository $projectBomEntryRepository;
|
||||
|
||||
private DBElementRepository $assemblyBomEntryRepository;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly BOMValidationService $validationService
|
||||
private readonly BOMValidationService $validationService,
|
||||
private readonly TranslatorInterface $translator
|
||||
) {
|
||||
$this->partRepository = $entityManager->getRepository(Part::class);
|
||||
$this->manufacturerRepository = $entityManager->getRepository(Manufacturer::class);
|
||||
$this->categoryRepository = $entityManager->getRepository(Category::class);
|
||||
$this->projectBomEntryRepository = $entityManager->getRepository(ProjectBOMEntry::class);
|
||||
$this->assemblyBomEntryRepository = $entityManager->getRepository(AssemblyBOMEntry::class);
|
||||
$this->partRepository = $this->entityManager->getRepository(Part::class);
|
||||
$this->manufacturerRepository = $this->entityManager->getRepository(Manufacturer::class);
|
||||
$this->categoryRepository = $this->entityManager->getRepository(Category::class);
|
||||
$this->projectBomEntryRepository = $this->entityManager->getRepository(ProjectBOMEntry::class);
|
||||
$this->assemblyBomEntryRepository = $this->entityManager->getRepository(AssemblyBOMEntry::class);
|
||||
}
|
||||
|
||||
protected function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||
|
|
@ -167,22 +166,6 @@ class BOMImporter
|
|||
return $this->stringToBOMEntries($importObject, $file->getContent(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BOM data before importing
|
||||
* @return array Validation result with errors, warnings, and info
|
||||
*/
|
||||
public function validateBOMData(string $data, array $options): array
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
$resolver = $this->configureOptions($resolver);
|
||||
$options = $resolver->resolve($options);
|
||||
|
||||
return match ($options['type']) {
|
||||
'kicad_pcbnew' => $this->validateKiCADPCB($data),
|
||||
'kicad_schematic' => $this->validateKiCADSchematicData($data, $options),
|
||||
default => throw new InvalidArgumentException('Invalid import type!'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the conversion of an uploaded file into an ImporterResult for a given project or assembly.
|
||||
|
|
@ -238,6 +221,23 @@ class BOMImporter
|
|||
return $this->stringToImporterResult($importObject, $file->getContent(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BOM data before importing
|
||||
* @return array Validation result with errors, warnings, and info
|
||||
*/
|
||||
public function validateBOMData(string $data, array $options): array
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
$resolver = $this->configureOptions($resolver);
|
||||
$options = $resolver->resolve($options);
|
||||
|
||||
return match ($options['type']) {
|
||||
'kicad_pcbnew' => $this->validateKiCADPCB($data),
|
||||
'kicad_schematic' => $this->validateKiCADSchematicData($data, $options),
|
||||
default => throw new InvalidArgumentException('Invalid import type!'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import string data into an array of BOM entries, which are not yet assigned to a project.
|
||||
*
|
||||
|
|
@ -255,7 +255,7 @@ class BOMImporter
|
|||
|
||||
return match ($options['type']) {
|
||||
self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject)->getBomEntries(),
|
||||
self::IMPORT_TYPE_KICAD_SCHEMATIC => $this->parseKiCADPCB($data, $importObject)->getBomEntries(),
|
||||
self::IMPORT_TYPE_KICAD_SCHEMATIC => $this->parseKiCADSchematic($data, $options),
|
||||
default => throw new InvalidArgumentException($this->translator->trans('validator.bom_importer.invalid_import_type', [], 'validators')),
|
||||
};
|
||||
}
|
||||
|
|
@ -296,9 +296,8 @@ class BOMImporter
|
|||
* validates the required fields, and creates BOM entries for each record in the data.
|
||||
* The BOM entries are added to the provided Project or Assembly, depending on the context.
|
||||
*
|
||||
* @param string $data The semicolon- or comma-delimited CSV data to be parsed.
|
||||
* @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
|
||||
* @param string $data The semicolon- or comma-delimited CSV data to be parsed
|
||||
*
|
||||
* @return ImporterResult The result of the import process, containing the created BOM entries.
|
||||
*
|
||||
* @throws UnexpectedValueException If required fields are missing in the provided data.
|
||||
|
|
@ -858,7 +857,7 @@ class BOMImporter
|
|||
if (isset($entry['name'])) {
|
||||
$givenName = trim($entry['name']) === '' ? null : trim ($entry['name']);
|
||||
|
||||
if ($givenName !== null && $part !== null && $part->getName() !== $givenName) {
|
||||
if ($givenName !== null && $bomEntry->getPart() !== null && $bomEntry->getPart()->getName() !== $givenName) {
|
||||
//Apply different names for parts list entry
|
||||
$bomEntry->setName(trim($entry['name']) === '' ? null : trim ($entry['name']));
|
||||
}
|
||||
|
|
@ -867,7 +866,11 @@ class BOMImporter
|
|||
}
|
||||
|
||||
if (isset($entry['designator'])) {
|
||||
$bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator']));
|
||||
if ($bomEntry instanceof ProjectBOMEntry) {
|
||||
$bomEntry->setMountnames(trim($entry['designator']) === '' ? '' : trim($entry['designator']));
|
||||
} elseif ($bomEntry instanceof AssemblyBOMEntry) {
|
||||
$bomEntry->setDesignator(trim($entry['designator']) === '' ? '' : trim($entry['designator']));
|
||||
}
|
||||
}
|
||||
|
||||
$bomEntry->setPart($part);
|
||||
|
|
@ -957,6 +960,7 @@ class BOMImporter
|
|||
return $out;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a JSON-based constraint violation.
|
||||
*
|
||||
|
|
@ -964,25 +968,13 @@ class BOMImporter
|
|||
* The violation includes a message, property path, invalid value, and other contextual information.
|
||||
* Translations for the violation message can be applied through the translator service.
|
||||
*
|
||||
* @param string $message The translation key for the validation message.
|
||||
* @param string $propertyPath The property path where the violation occurred.
|
||||
* @param mixed|null $invalidValue The value that caused the violation (optional).
|
||||
* @param array $parameters Additional parameters for message placeholders (default is an empty array).
|
||||
* @param string $message The translation key for the validation message.
|
||||
* @param string $propertyPath The property path where the violation occurred.
|
||||
* @param mixed|null $invalidValue The value that caused the violation (optional).
|
||||
* @param array $parameters Additional parameters for message placeholders (default is an empty array).
|
||||
*
|
||||
* @return ConstraintViolation The created constraint violation object.
|
||||
*/
|
||||
private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation
|
||||
{
|
||||
return new ConstraintViolation(
|
||||
message: $this->translator->trans($message, $parameters, 'validators'),
|
||||
messageTemplate: $message,
|
||||
parameters: $parameters,
|
||||
root: $this->jsonRoot,
|
||||
propertyPath: $propertyPath,
|
||||
invalidValue: $invalidValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse KiCad schematic BOM with flexible field mapping
|
||||
*/
|
||||
|
|
@ -1460,4 +1452,30 @@ class BOMImporter
|
|||
|
||||
return array_values($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a JSON-based constraint violation.
|
||||
*
|
||||
* This method creates a `ConstraintViolation` object that represents a validation error.
|
||||
* The violation includes a message, property path, invalid value, and other contextual information.
|
||||
* Translations for the violation message can be applied through the translator service.
|
||||
*
|
||||
* @param string $message The translation key for the validation message.
|
||||
* @param string $propertyPath The property path where the violation occurred.
|
||||
* @param mixed|null $invalidValue The value that caused the violation (optional).
|
||||
* @param array $parameters Additional parameters for message placeholders (default is an empty array).
|
||||
*
|
||||
* @return ConstraintViolation The created constraint violation object.
|
||||
*/
|
||||
private function buildJsonViolation(string $message, string $propertyPath, mixed $invalidValue = null, array $parameters = []): ConstraintViolation
|
||||
{
|
||||
return new ConstraintViolation(
|
||||
message: $this->translator->trans($message, $parameters, 'validators'),
|
||||
messageTemplate: $message,
|
||||
parameters: $parameters,
|
||||
root: $this->jsonRoot,
|
||||
propertyPath: $propertyPath,
|
||||
invalidValue: $invalidValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ use App\Entity\Parts\StorageLocation;
|
|||
use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||
use App\Helpers\Assemblies\AssemblyPartAggregator;
|
||||
use App\Helpers\FilenameSanatizer;
|
||||
use App\Serializer\APIPlatform\SkippableItemNormalizer;
|
||||
|
|
@ -379,49 +380,62 @@ class EntityExporter
|
|||
],
|
||||
Project::class => [
|
||||
'header' => [
|
||||
'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName', 'BomQuantity',
|
||||
'BomPartId', 'BomPartIpn', 'BomPartMpnr', 'BomPartName', 'BomDesignator', 'BomPartDescription',
|
||||
'BomMountNames'
|
||||
'Id', 'ParentId', 'Type', 'ProjectNameHierarchical', 'ProjectName', 'ProjectFullName',
|
||||
|
||||
//BOM relevant attributes
|
||||
'Quantity', 'PartId', 'PartName', 'Ipn', 'Manufacturer', 'Mpn', 'Name', 'Designator',
|
||||
'Description', 'MountNames'
|
||||
],
|
||||
'processEntity' => fn($entity, $depth) => [
|
||||
'ProjectId' => $entity->getId(),
|
||||
'ParentProjectId' => $entity->getParent()?->getId() ?? '',
|
||||
'Id' => $entity->getId(),
|
||||
'ParentId' => $entity->getParent()?->getId() ?? '',
|
||||
'Type' => 'project',
|
||||
'ProjectNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(),
|
||||
'ProjectName' => $entity->getName(),
|
||||
'ProjectFullName' => $this->getFullName($entity),
|
||||
'BomQuantity' => '-',
|
||||
'BomPartId' => '-',
|
||||
'BomPartIpn' => '-',
|
||||
'BomPartMpnr' => '-',
|
||||
'BomPartName' => '-',
|
||||
'BomDesignator' => '-',
|
||||
'BomPartDescription' => '-',
|
||||
'BomMountNames' => '-',
|
||||
|
||||
//BOM relevant attributes
|
||||
'Quantity' => '-',
|
||||
'PartId' => '-',
|
||||
'PartName' => '-',
|
||||
'Ipn' => '-',
|
||||
'Manufacturer' => '-',
|
||||
'Mpn' => '-',
|
||||
'Name' => '-',
|
||||
'Designator' => '-',
|
||||
'Description' => '-',
|
||||
'MountNames' => '-',
|
||||
],
|
||||
'processBomEntries' => fn($entity, $depth) => array_map(fn(AssemblyBOMEntry $bomEntry) => [
|
||||
'processBomEntries' => fn($entity, $depth) => array_map(fn(ProjectBOMEntry $bomEntry) => [
|
||||
'Id' => $entity->getId(),
|
||||
'ParentId' => '',
|
||||
'Type' => 'project_bom_entry',
|
||||
'ProjectNameHierarchical' => str_repeat('--', $depth) . '> ' . $entity->getName(),
|
||||
'ProjectName' => $entity->getName(),
|
||||
'ProjectFullName' => $this->getFullName($entity),
|
||||
'BomQuantity' => $bomEntry->getQuantity() ?? '',
|
||||
'BomPartId' => $bomEntry->getPart()?->getId() ?? '',
|
||||
'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '',
|
||||
'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '',
|
||||
'BomPartName' => $bomEntry->getPart()?->getName() ?? '',
|
||||
'BomDesignator' => $bomEntry->getName() ?? '',
|
||||
'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '',
|
||||
'BomMountNames' => $bomEntry->getMountNames(),
|
||||
|
||||
//BOM relevant attributes
|
||||
'Quantity' => $bomEntry->getQuantity() ?? '',
|
||||
'PartId' => $bomEntry->getPart()?->getId() ?? '',
|
||||
'PartName' => $bomEntry->getPart()?->getName() ?? '',
|
||||
'Ipn' => $bomEntry->getPart()?->getIpn() ?? '',
|
||||
'Manufacturer' => $bomEntry->getPart()?->getManufacturer()?->getName() ?? '',
|
||||
'Mpn' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '',
|
||||
'Name' => $bomEntry->getPart()?->getName() ?? '',
|
||||
'Designator' => $bomEntry->getMountnames() ?? '',
|
||||
'Description' => $bomEntry->getPart()?->getDescription() ?? '',
|
||||
'MountNames' => $bomEntry->getMountNames(),
|
||||
], $entity->getBomEntries()->toArray()),
|
||||
],
|
||||
Assembly::class => [
|
||||
'header' => [
|
||||
'Id', 'ParentId', 'Type', 'AssemblyIpn', 'AssemblyNameHierarchical', 'AssemblyName',
|
||||
'AssemblyFullName', 'BomQuantity', 'BomMultiplier', 'BomPartId', 'BomPartIpn', 'BomPartMpnr',
|
||||
'BomPartName', 'BomDesignator', 'BomPartDescription', 'BomMountNames', 'BomReferencedAssemblyId',
|
||||
'BomReferencedAssemblyIpn', 'BomReferencedAssemblyFullName'
|
||||
'AssemblyFullName',
|
||||
|
||||
//BOM relevant attributes
|
||||
'Quantity', 'PartId', 'PartName', 'Ipn', 'Manufacturer', 'Mpn', 'Name', 'Designator',
|
||||
'Description', 'MountNames', 'ReferencedAssemblyId', 'ReferencedAssemblyIpn',
|
||||
'ReferencedAssemblyFullName'
|
||||
],
|
||||
'processEntity' => fn($entity, $depth) => [
|
||||
'Id' => $entity->getId(),
|
||||
|
|
@ -431,18 +445,21 @@ class EntityExporter
|
|||
'AssemblyNameHierarchical' => str_repeat('--', $depth) . ' ' . $entity->getName(),
|
||||
'AssemblyName' => $entity->getName(),
|
||||
'AssemblyFullName' => $this->getFullName($entity),
|
||||
'BomQuantity' => '-',
|
||||
'BomMultiplier' => '-',
|
||||
'BomPartId' => '-',
|
||||
'BomPartIpn' => '-',
|
||||
'BomPartMpnr' => '-',
|
||||
'BomPartName' => '-',
|
||||
'BomDesignator' => '-',
|
||||
'BomPartDescription' => '-',
|
||||
'BomMountNames' => '-',
|
||||
'BomReferencedAssemblyId' => '-',
|
||||
'BomReferencedAssemblyIpn' => '-',
|
||||
'BomReferencedAssemblyFullName' => '-',
|
||||
|
||||
//BOM relevant attributes
|
||||
'Quantity' => '-',
|
||||
'PartId' => '-',
|
||||
'PartName' => '-',
|
||||
'Ipn' => '-',
|
||||
'Manufacturer' => '-',
|
||||
'Mpn' => '-',
|
||||
'Name' => '-',
|
||||
'Designator' => '-',
|
||||
'Description' => '-',
|
||||
'MountNames' => '-',
|
||||
'ReferencedAssemblyId' => '-',
|
||||
'ReferencedAssemblyIpn' => '-',
|
||||
'ReferencedAssemblyFullName' => '-',
|
||||
],
|
||||
'processBomEntries' => fn($entity, $depth) => $this->processBomEntriesWithAggregatedParts($entity, $depth),
|
||||
],
|
||||
|
|
@ -555,6 +572,7 @@ class EntityExporter
|
|||
{
|
||||
$rows = [];
|
||||
|
||||
/** @var AssemblyBOMEntry $bomEntry */
|
||||
foreach ($assembly->getBomEntries() as $bomEntry) {
|
||||
// Add the BOM entry itself
|
||||
$rows[] = [
|
||||
|
|
@ -565,18 +583,21 @@ class EntityExporter
|
|||
'AssemblyNameHierarchical' => str_repeat('--', $depth) . '> ' . $assembly->getName(),
|
||||
'AssemblyName' => $assembly->getName(),
|
||||
'AssemblyFullName' => $this->getFullName($assembly),
|
||||
'BomQuantity' => $bomEntry->getQuantity() ?? '',
|
||||
'BomMultiplier' => '',
|
||||
'BomPartId' => $bomEntry->getPart()?->getId() ?? '-',
|
||||
'BomPartIpn' => $bomEntry->getPart()?->getIpn() ?? '-',
|
||||
'BomPartMpnr' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-',
|
||||
'BomPartName' => $bomEntry->getPart()?->getName() ?? '-',
|
||||
'BomDesignator' => $bomEntry->getName() ?? '-',
|
||||
'BomPartDescription' => $bomEntry->getPart()?->getDescription() ?? '-',
|
||||
'BomMountNames' => $bomEntry->getMountNames(),
|
||||
'BomReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '-',
|
||||
'BomReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '-',
|
||||
'BomReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null),
|
||||
|
||||
//BOM relevant attributes
|
||||
'Quantity' => $bomEntry->getQuantity() ?? '',
|
||||
'PartId' => $bomEntry->getPart()?->getId() ?? '-',
|
||||
'PartName' => $bomEntry->getPart()?->getName() ?? '-',
|
||||
'Ipn' => $bomEntry->getPart()?->getIpn() ?? '-',
|
||||
'Manufacturer' => $bomEntry->getPart()?->getManufacturer()?->getName() ?? '-',
|
||||
'Mpn' => $bomEntry->getPart()?->getManufacturerProductNumber() ?? '-',
|
||||
'Name' => $bomEntry->getName() ?? '-',
|
||||
'Designator' => $bomEntry->getDesignator(),
|
||||
'MountNames' => $bomEntry->getMountNames(),
|
||||
'Description' => $bomEntry->getPart()?->getDescription() ?? '-',
|
||||
'ReferencedAssemblyId' => $bomEntry->getReferencedAssembly()?->getId() ?? '-',
|
||||
'ReferencedAssemblyIpn' => $bomEntry->getReferencedAssembly()?->getIpn() ?? '-',
|
||||
'ReferencedAssemblyFullName' => $this->getFullName($bomEntry->getReferencedAssembly() ?? null),
|
||||
];
|
||||
|
||||
// If a referenced assembly exists, add aggregated parts
|
||||
|
|
@ -597,18 +618,21 @@ class EntityExporter
|
|||
'AssemblyNameHierarchical' => '',
|
||||
'AssemblyName' => $partAssembly ? $partAssembly->getName() : '',
|
||||
'AssemblyFullName' => $this->getFullName($partAssembly),
|
||||
'BomQuantity' => $partData['quantity'],
|
||||
'BomMultiplier' => $partData['multiplier'],
|
||||
'BomPartId' => $partData['part']?->getId(),
|
||||
'BomPartIpn' => $partData['part']?->getIpn(),
|
||||
'BomPartMpnr' => $partData['part']?->getManufacturerProductNumber(),
|
||||
'BomPartName' => $partData['part']?->getName(),
|
||||
'BomDesignator' => $partData['part']?->getName(),
|
||||
'BomPartDescription' => $partData['part']?->getDescription(),
|
||||
'BomMountNames' => '-',
|
||||
'BomReferencedAssemblyId' => '-',
|
||||
'BomReferencedAssemblyIpn' => '-',
|
||||
'BomReferencedAssemblyFullName' => '-',
|
||||
|
||||
//BOM relevant attributes
|
||||
'Quantity' => $partData['quantity'],
|
||||
'PartId' => $partData['part']?->getId(),
|
||||
'PartName' => $partData['part']?->getName(),
|
||||
'Ipn' => $partData['part']?->getIpn(),
|
||||
'Manufacturer' => $partData['part']?->getManufacturer()?->getName(),
|
||||
'Mpn' => $partData['part']?->getManufacturerProductNumber(),
|
||||
'Name' => $partData['name'] ?? '',
|
||||
'Designator' => $partData['designator'],
|
||||
'Description' => $partData['part']?->getDescription(),
|
||||
'MountNames' => '-',
|
||||
'ReferencedAssemblyId' => '-',
|
||||
'ReferencedAssemblyIpn' => '-',
|
||||
'ReferencedAssemblyFullName' => '-',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,15 +51,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||
*/
|
||||
class ToolsTreeBuilder
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
protected UrlGeneratorInterface $urlGenerator,
|
||||
protected TagAwareCacheInterface $cache,
|
||||
protected UserCacheKeyGenerator $keyGenerator,
|
||||
protected Security $security,
|
||||
protected ?array $dataSourceSynonyms = [],
|
||||
) {
|
||||
$this->dataSourceSynonyms = $dataSourceSynonyms ?? [];
|
||||
public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,43 +167,43 @@ class ToolsTreeBuilder
|
|||
}
|
||||
if ($this->security->isGranted('read', new Category())) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()),
|
||||
$this->translator->trans('tree.tools.edit.categories'),
|
||||
$this->urlGenerator->generate('category_new')
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-tags');
|
||||
}
|
||||
if ($this->security->isGranted('read', new Project())) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()),
|
||||
$this->translator->trans('tree.tools.edit.projects'),
|
||||
$this->urlGenerator->generate('project_new')
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-archive');
|
||||
}
|
||||
if ($this->security->isGranted('read', new Assembly())) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->getTranslatedDataSourceOrSynonym('assembly', 'tree.tools.edit.assemblies', $this->translator->getLocale()),
|
||||
$this->translator->trans('tree.tools.edit.assemblies'),
|
||||
$this->urlGenerator->generate('assembly_new')
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-list');
|
||||
}
|
||||
if ($this->security->isGranted('read', new Supplier())) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()),
|
||||
$this->translator->trans('tree.tools.edit.suppliers'),
|
||||
$this->urlGenerator->generate('supplier_new')
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-truck');
|
||||
}
|
||||
if ($this->security->isGranted('read', new Manufacturer())) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()),
|
||||
$this->translator->trans('tree.tools.edit.manufacturer'),
|
||||
$this->urlGenerator->generate('manufacturer_new')
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-industry');
|
||||
}
|
||||
if ($this->security->isGranted('read', new StorageLocation())) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()),
|
||||
$this->translator->trans('tree.tools.edit.storelocation'),
|
||||
$this->urlGenerator->generate('store_location_new')
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-cube');
|
||||
}
|
||||
if ($this->security->isGranted('read', new Footprint())) {
|
||||
$nodes[] = (new TreeViewNode(
|
||||
$this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()),
|
||||
$this->translator->trans('tree.tools.edit.footprint'),
|
||||
$this->urlGenerator->generate('footprint_new')
|
||||
))->setIcon('fa-fw fa-treeview fa-solid fa-microchip');
|
||||
}
|
||||
|
|
@ -324,22 +317,4 @@ class ToolsTreeBuilder
|
|||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string
|
||||
{
|
||||
$currentTranslation = $this->translator->trans($translationKey);
|
||||
|
||||
// Call alternatives from DataSourcesynonyms (if available)
|
||||
if (!empty($this->dataSourceSynonyms[$dataSource][$locale])) {
|
||||
$alternativeTranslation = $this->dataSourceSynonyms[$dataSource][$locale];
|
||||
|
||||
// Use alternative translation when it deviates from the standard translation
|
||||
if ($alternativeTranslation !== $currentTranslation) {
|
||||
return $alternativeTranslation;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise return the standard translation
|
||||
return $currentTranslation;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,11 +68,9 @@ class TreeViewGenerator
|
|||
protected TranslatorInterface $translator,
|
||||
private readonly UrlGeneratorInterface $router,
|
||||
private readonly SidebarSettings $sidebarSettings,
|
||||
protected ?array $dataSourceSynonyms = [],
|
||||
) {
|
||||
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
|
||||
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
|
||||
$this->dataSourceSynonyms = $dataSourceSynonyms ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -228,16 +226,14 @@ class TreeViewGenerator
|
|||
|
||||
protected function entityClassToRootNodeString(string $class): string
|
||||
{
|
||||
$locale = $this->translator->getLocale();
|
||||
|
||||
return match ($class) {
|
||||
Category::class => $this->getTranslatedOrSynonym('category', $locale),
|
||||
StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale),
|
||||
Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale),
|
||||
Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale),
|
||||
Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale),
|
||||
Project::class => $this->getTranslatedOrSynonym('project', $locale),
|
||||
Assembly::class => $this->getTranslatedOrSynonym('assembly', $locale),
|
||||
Category::class => $this->translator->trans('category.labelp'),
|
||||
StorageLocation::class => $this->translator->trans('storelocation.labelp'),
|
||||
Footprint::class => $this->translator->trans('footprint.labelp'),
|
||||
Manufacturer::class => $this->translator->trans('manufacturer.labelp'),
|
||||
Supplier::class => $this->translator->trans('supplier.labelp'),
|
||||
Project::class => $this->translator->trans('project.labelp'),
|
||||
Assembly::class => $this->translator->trans('assembly.labelp'),
|
||||
default => $this->translator->trans('tree.root_node.text'),
|
||||
};
|
||||
}
|
||||
|
|
@ -294,22 +290,4 @@ class TreeViewGenerator
|
|||
return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line
|
||||
});
|
||||
}
|
||||
|
||||
protected function getTranslatedOrSynonym(string $key, string $locale): string
|
||||
{
|
||||
$currentTranslation = $this->translator->trans($key . '.labelp');
|
||||
|
||||
// Call alternatives from DataSourcesynonyms (if available)
|
||||
if (!empty($this->dataSourceSynonyms[$key][$locale])) {
|
||||
$alternativeTranslation = $this->dataSourceSynonyms[$key][$locale];
|
||||
|
||||
// Use alternative translation when it deviates from the standard translation
|
||||
if ($alternativeTranslation !== $currentTranslation) {
|
||||
return $alternativeTranslation;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise return the standard translation
|
||||
return $currentTranslation;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||
|
||||
enum AssemblyBomTableColumns : string implements TranslatableInterface
|
||||
{
|
||||
|
||||
case NAME = "name";
|
||||
case ID = "id";
|
||||
case QUANTITY = "quantity";
|
||||
case IPN = "ipn";
|
||||
case DESCRIPTION = "description";
|
||||
case CATEGORY = "category";
|
||||
case MANUFACTURER = "manufacturer";
|
||||
case DESIGNATOR = "designator";
|
||||
case MOUNTNAMES = "mountnames";
|
||||
case STORAGE_LOCATION = "storage_location";
|
||||
case AMOUNT = "amount";
|
||||
case ADDED_DATE = "addedDate";
|
||||
case LAST_MODIFIED = "lastModified";
|
||||
case EDIT = "edit";
|
||||
|
||||
public function trans(TranslatorInterface $translator, ?string $locale = null): string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ class TableSettings
|
|||
#[Assert\Unique()]
|
||||
#[Assert\All([new Assert\Type(AssemblyBomTableColumns::class)])]
|
||||
|
||||
public array $assembliesBomDefaultColumns = [AssemblyBomTableColumns::QUANTITY, AssemblyTableColumns::ID, AssemblyTableColumns::IPN,
|
||||
AssemblyTableColumns::NAME, AssemblyTableColumns::DESCRIPTION];
|
||||
public array $assembliesBomDefaultColumns = [AssemblyBomTableColumns::QUANTITY, AssemblyBomTableColumns::ID,
|
||||
AssemblyBomTableColumns::IPN, AssemblyBomTableColumns::NAME, AssemblyBomTableColumns::DESCRIPTION];
|
||||
|
||||
#[SettingsParameter(label: new TM("settings.behavior.table.preview_image_min_width"),
|
||||
formOptions: ['attr' => ['min' => 1, 'max' => 100]],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue