mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Started to work on the possibilty to create new entities directly from the part edit page.
This fixes issue #203.
This commit is contained in:
parent
672d55624f
commit
1654010ea3
8 changed files with 142 additions and 84 deletions
|
@ -33,10 +33,13 @@ export default class extends Controller {
|
||||||
//Extract empty message from data attribute
|
//Extract empty message from data attribute
|
||||||
this._emptyMessage = this.element.getAttribute("data-empty-message") ?? "";
|
this._emptyMessage = this.element.getAttribute("data-empty-message") ?? "";
|
||||||
|
|
||||||
|
const allowAdd = this.element.getAttribute("data-allow-add") === "true";
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
selectOnTab: true,
|
selectOnTab: true,
|
||||||
maxOptions: null,
|
maxOptions: null,
|
||||||
|
create: allowAdd,
|
||||||
|
|
||||||
searchField: [
|
searchField: [
|
||||||
{field: "text", weight : 2},
|
{field: "text", weight : 2},
|
||||||
|
|
|
@ -75,6 +75,7 @@ class AttachmentFormType extends AbstractType
|
||||||
'label' => 'attachment.edit.attachment_type',
|
'label' => 'attachment.edit.attachment_type',
|
||||||
'class' => AttachmentType::class,
|
'class' => AttachmentType::class,
|
||||||
'disable_not_selectable' => true,
|
'disable_not_selectable' => true,
|
||||||
|
'allow_add' => $this->security->isGranted('@attachment_types.create'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->add('showInTable', CheckboxType::class, [
|
$builder->add('showInTable', CheckboxType::class, [
|
||||||
|
|
|
@ -63,6 +63,7 @@ class OrderdetailType extends AbstractType
|
||||||
'class' => Supplier::class,
|
'class' => Supplier::class,
|
||||||
'disable_not_selectable' => true,
|
'disable_not_selectable' => true,
|
||||||
'label' => 'orderdetails.edit.supplier',
|
'label' => 'orderdetails.edit.supplier',
|
||||||
|
'allow_add' => $this->security->isGranted('@suppliers.create'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->add('supplier_product_url', UrlType::class, [
|
$builder->add('supplier_product_url', UrlType::class, [
|
||||||
|
|
|
@ -104,15 +104,15 @@ class PartBaseType extends AbstractType
|
||||||
])
|
])
|
||||||
->add('category', StructuralEntityType::class, [
|
->add('category', StructuralEntityType::class, [
|
||||||
'class' => Category::class,
|
'class' => Category::class,
|
||||||
|
'allow_add' => $this->security->isGranted('@categories.create'),
|
||||||
'label' => 'part.edit.category',
|
'label' => 'part.edit.category',
|
||||||
'disable_not_selectable' => true,
|
'disable_not_selectable' => true,
|
||||||
'constraints' => [
|
|
||||||
],
|
|
||||||
])
|
])
|
||||||
->add('footprint', StructuralEntityType::class, [
|
->add('footprint', StructuralEntityType::class, [
|
||||||
'class' => Footprint::class,
|
'class' => Footprint::class,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'label' => 'part.edit.footprint',
|
'label' => 'part.edit.footprint',
|
||||||
|
'allow_add' => $this->security->isGranted('@footprints.create'),
|
||||||
'disable_not_selectable' => true,
|
'disable_not_selectable' => true,
|
||||||
])
|
])
|
||||||
->add('tags', TextType::class, [
|
->add('tags', TextType::class, [
|
||||||
|
@ -131,6 +131,7 @@ class PartBaseType extends AbstractType
|
||||||
'class' => Manufacturer::class,
|
'class' => Manufacturer::class,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'label' => 'part.edit.manufacturer.label',
|
'label' => 'part.edit.manufacturer.label',
|
||||||
|
'allow_add' => $this->security->isGranted('@manufacturers.create'),
|
||||||
'disable_not_selectable' => true,
|
'disable_not_selectable' => true,
|
||||||
])
|
])
|
||||||
->add('manufacturer_product_url', UrlType::class, [
|
->add('manufacturer_product_url', UrlType::class, [
|
||||||
|
|
|
@ -60,6 +60,7 @@ class PartLotType extends AbstractType
|
||||||
'label' => 'part_lot.edit.location',
|
'label' => 'part_lot.edit.location',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'disable_not_selectable' => true,
|
'disable_not_selectable' => true,
|
||||||
|
'allow_add' => $this->security->isGranted('@storelocations.create'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->add('amount', SIUnitType::class, [
|
$builder->add('amount', SIUnitType::class, [
|
||||||
|
|
104
src/Form/Type/Helper/StructuralEntityChoiceLoader.php
Normal file
104
src/Form/Type/Helper/StructuralEntityChoiceLoader.php
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<?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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form\Type\Helper;
|
||||||
|
|
||||||
|
use App\Entity\Base\AbstractDBElement;
|
||||||
|
use App\Entity\Base\AbstractStructuralDBElement;
|
||||||
|
use App\Services\Trees\NodesListBuilder;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader;
|
||||||
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
|
|
||||||
|
class StructuralEntityChoiceLoader extends AbstractChoiceLoader
|
||||||
|
{
|
||||||
|
private Options $options;
|
||||||
|
private NodesListBuilder $builder;
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
public function __construct(Options $options, NodesListBuilder $builder, EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->options = $options;
|
||||||
|
$this->builder = $builder;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadChoices(): iterable
|
||||||
|
{
|
||||||
|
return $this->builder->typeToNodesList($this->options['class'], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadChoicesForValues(array $values, callable $value = null)
|
||||||
|
{
|
||||||
|
$tmp = parent::loadChoicesForValues($values, $value);
|
||||||
|
|
||||||
|
if ($this->options['allow_add'] && empty($tmp)) {
|
||||||
|
if (count($values) > 1) {
|
||||||
|
throw new \InvalidArgumentException('Cannot add multiple entities at once.');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Dont create a new entity for the empty option
|
||||||
|
if ($values[0] === "" || $values[0] === null) {
|
||||||
|
return $tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$this->createNewEntityFromValue((string)$values[0])];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*public function loadValuesForChoices(array $choices, callable $value = null)
|
||||||
|
{
|
||||||
|
$tmp = parent::loadValuesForChoices($choices, $value);
|
||||||
|
|
||||||
|
if ($this->options['allow_add'] && count($tmp) === 1) {
|
||||||
|
if ($tmp[0] instanceof AbstractDBElement && $tmp[0]->getID() === null) {
|
||||||
|
return [$tmp[0]->getName()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(string)$choices[0]->getID()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tmp;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
public function createNewEntityFromValue(string $value): AbstractStructuralDBElement
|
||||||
|
{
|
||||||
|
if (!$this->options['allow_add']) {
|
||||||
|
throw new \RuntimeException('Cannot create new entity, because allow_add is not enabled!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($value) === '') {
|
||||||
|
throw new \InvalidArgumentException('Cannot create new entity, because the name is empty!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$class = $this->options['class'];
|
||||||
|
/** @var AbstractStructuralDBElement $entity */
|
||||||
|
$entity = new $class();
|
||||||
|
$entity->setName($value);
|
||||||
|
|
||||||
|
//Persist element to database
|
||||||
|
$this->entityManager->persist($entity);
|
||||||
|
|
||||||
|
return $entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ namespace App\Form\Type;
|
||||||
|
|
||||||
use App\Entity\Attachments\AttachmentType;
|
use App\Entity\Attachments\AttachmentType;
|
||||||
use App\Entity\Base\AbstractStructuralDBElement;
|
use App\Entity\Base\AbstractStructuralDBElement;
|
||||||
|
use App\Form\Type\Helper\StructuralEntityChoiceLoader;
|
||||||
use App\Services\Attachments\AttachmentURLGenerator;
|
use App\Services\Attachments\AttachmentURLGenerator;
|
||||||
use App\Services\Trees\NodesListBuilder;
|
use App\Services\Trees\NodesListBuilder;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
@ -37,6 +38,9 @@ use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\Form\FormView;
|
use Symfony\Component\Form\FormView;
|
||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
|
||||||
|
use Symfony\Component\Validator\Constraints\IsNull;
|
||||||
|
use Symfony\Component\Validator\Constraints\Valid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides a choice form type similar to EntityType, with the difference, that the tree structure
|
* This class provides a choice form type similar to EntityType, with the difference, that the tree structure
|
||||||
|
@ -63,9 +67,9 @@ class StructuralEntityType extends AbstractType
|
||||||
{
|
{
|
||||||
$builder->addModelTransformer(new CallbackTransformer(
|
$builder->addModelTransformer(new CallbackTransformer(
|
||||||
function ($value) use ($options) {
|
function ($value) use ($options) {
|
||||||
return $this->transform($value, $options);
|
return $this->modelTransform($value, $options);
|
||||||
}, function ($value) use ($options) {
|
}, function ($value) use ($options) {
|
||||||
return $this->reverseTransform($value, $options);
|
return $this->modelReverseTransform($value, $options);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,14 +77,13 @@ class StructuralEntityType extends AbstractType
|
||||||
{
|
{
|
||||||
$resolver->setRequired(['class']);
|
$resolver->setRequired(['class']);
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
|
'allow_add' => false,
|
||||||
'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext
|
'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext
|
||||||
'subentities_of' => null, //Only show entities with the given parent class
|
'subentities_of' => null, //Only show entities with the given parent class
|
||||||
'disable_not_selectable' => false, //Disable entries with not selectable property
|
'disable_not_selectable' => false, //Disable entries with not selectable property
|
||||||
'choice_value' => 'id', //Use the element id as option value and for comparing items
|
'choice_value' => 'id', //Use the element id as option value and for comparing items
|
||||||
'choice_loader' => function (Options $options) {
|
'choice_loader' => function (Options $options) {
|
||||||
return new CallbackChoiceLoader(function () use ($options) {
|
return new StructuralEntityChoiceLoader($options, $this->builder, $this->em);
|
||||||
return $this->getEntries($options);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
'choice_label' => function (Options $options) {
|
'choice_label' => function (Options $options) {
|
||||||
return function ($choice, $key, $value) use ($options) {
|
return function ($choice, $key, $value) use ($options) {
|
||||||
|
@ -95,6 +98,15 @@ class StructuralEntityType extends AbstractType
|
||||||
'choice_translation_domain' => false, //Don't translate the entity names
|
'choice_translation_domain' => false, //Don't translate the entity names
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
//Set the constraints for the case that allow add is enabled (we then have to check that the new element is valid)
|
||||||
|
$resolver->setNormalizer('constraints', function (Options $options, $value) {
|
||||||
|
if ($options['allow_add']) {
|
||||||
|
$value[] = new Valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
});
|
||||||
|
|
||||||
$resolver->setDefault('empty_message', null);
|
$resolver->setDefault('empty_message', null);
|
||||||
|
|
||||||
$resolver->setDefault('controller', 'elements--structural-entity-select');
|
$resolver->setDefault('controller', 'elements--structural-entity-select');
|
||||||
|
@ -102,6 +114,7 @@ class StructuralEntityType extends AbstractType
|
||||||
$resolver->setDefault('attr', static function (Options $options) {
|
$resolver->setDefault('attr', static function (Options $options) {
|
||||||
$tmp = [
|
$tmp = [
|
||||||
'data-controller' => $options['controller'],
|
'data-controller' => $options['controller'],
|
||||||
|
'data-allow-add' => $options['allow_add'] ? 'true' : 'false',
|
||||||
];
|
];
|
||||||
if ($options['empty_message']) {
|
if ($options['empty_message']) {
|
||||||
$tmp['data-empty-message'] = $options['empty_message'];
|
$tmp['data-empty-message'] = $options['empty_message'];
|
||||||
|
@ -111,91 +124,18 @@ class StructuralEntityType extends AbstractType
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the entries from database and return an array of them.
|
|
||||||
*/
|
|
||||||
public function getEntries(Options $options): array
|
|
||||||
{
|
|
||||||
return $this->builder->typeToNodesList($options['class'], null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getParent(): string
|
public function getParent(): string
|
||||||
{
|
{
|
||||||
return ChoiceType::class;
|
return ChoiceType::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function modelTransform($value, array $options)
|
||||||
* Transforms a value from the original representation to a transformed representation.
|
|
||||||
*
|
|
||||||
* This method is called when the form field is initialized with its default data, on
|
|
||||||
* two occasions for two types of transformers:
|
|
||||||
*
|
|
||||||
* 1. Model transformers which normalize the model data.
|
|
||||||
* This is mainly useful when the same form type (the same configuration)
|
|
||||||
* has to handle different kind of underlying data, e.g The DateType can
|
|
||||||
* deal with strings or \DateTime objects as input.
|
|
||||||
*
|
|
||||||
* 2. View transformers which adapt the normalized data to the view format.
|
|
||||||
* a/ When the form is simple, the value returned by convention is used
|
|
||||||
* directly in the view and thus can only be a string or an array. In
|
|
||||||
* this case the data class should be null.
|
|
||||||
*
|
|
||||||
* b/ When the form is compound the returned value should be an array or
|
|
||||||
* an object to be mapped to the children. Each property of the compound
|
|
||||||
* data will be used as model data by each child and will be transformed
|
|
||||||
* too. In this case data class should be the class of the object, or null
|
|
||||||
* when it is an array.
|
|
||||||
*
|
|
||||||
* All transformers are called in a configured order from model data to view value.
|
|
||||||
* At the end of this chain the view data will be validated against the data class
|
|
||||||
* setting.
|
|
||||||
*
|
|
||||||
* This method must be able to deal with empty values. Usually this will
|
|
||||||
* be NULL, but depending on your implementation other empty values are
|
|
||||||
* possible as well (such as empty strings). The reasoning behind this is
|
|
||||||
* that data transformers must be chainable. If the transform() method
|
|
||||||
* of the first data transformer outputs NULL, the second must be able to
|
|
||||||
* process that value.
|
|
||||||
*
|
|
||||||
* @param mixed $value The value in the original representation
|
|
||||||
*
|
|
||||||
* @return mixed The value in the transformed representation
|
|
||||||
*
|
|
||||||
* @throws TransformationFailedException when the transformation fails
|
|
||||||
*/
|
|
||||||
public function transform($value, array $options)
|
|
||||||
{
|
{
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function modelReverseTransform($value, array $options)
|
||||||
* Transforms a value from the transformed representation to its original
|
|
||||||
* representation.
|
|
||||||
*
|
|
||||||
* This method is called when {@link Form::submit()} is called to transform the requests tainted data
|
|
||||||
* into an acceptable format.
|
|
||||||
*
|
|
||||||
* The same transformers are called in the reverse order so the responsibility is to
|
|
||||||
* return one of the types that would be expected as input of transform().
|
|
||||||
*
|
|
||||||
* This method must be able to deal with empty values. Usually this will
|
|
||||||
* be an empty string, but depending on your implementation other empty
|
|
||||||
* values are possible as well (such as NULL). The reasoning behind
|
|
||||||
* this is that value transformers must be chainable. If the
|
|
||||||
* reverseTransform() method of the first value transformer outputs an
|
|
||||||
* empty string, the second value transformer must be able to process that
|
|
||||||
* value.
|
|
||||||
*
|
|
||||||
* By convention, reverseTransform() should return NULL if an empty string
|
|
||||||
* is passed.
|
|
||||||
*
|
|
||||||
* @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($value, array $options)
|
|
||||||
{
|
{
|
||||||
/* This step is important in combination with the caching!
|
/* This step is important in combination with the caching!
|
||||||
The elements deserialized from cache, are not known to Doctrinte ORM any more, so doctrine thinks,
|
The elements deserialized from cache, are not known to Doctrinte ORM any more, so doctrine thinks,
|
||||||
|
@ -209,9 +149,15 @@ class StructuralEntityType extends AbstractType
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//If the value is already in the db, retrieve it freshly
|
||||||
|
if ($value->getID()) {
|
||||||
return $this->em->find($options['class'], $value->getID());
|
return $this->em->find($options['class'], $value->getID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Otherwise just return the value
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
protected function generateChoiceAttr(AbstractStructuralDBElement $choice, $key, $value, $options): array
|
protected function generateChoiceAttr(AbstractStructuralDBElement $choice, $key, $value, $options): array
|
||||||
{
|
{
|
||||||
$tmp = [];
|
$tmp = [];
|
||||||
|
|
|
@ -59,7 +59,8 @@ class ValidPartLotValidator extends ConstraintValidator
|
||||||
if ($value->getStorageLocation()) {
|
if ($value->getStorageLocation()) {
|
||||||
$repo = $this->em->getRepository(Storelocation::class);
|
$repo = $this->em->getRepository(Storelocation::class);
|
||||||
//We can only determine associated parts, if the part have an ID
|
//We can only determine associated parts, if the part have an ID
|
||||||
if (null !== $value->getID()) {
|
//When the storage location is new (no ID), we can just assume there are no other parts
|
||||||
|
if (null !== $value->getID() && $value->getStorageLocation()->getID()) {
|
||||||
$parts = new ArrayCollection($repo->getParts($value->getStorageLocation()));
|
$parts = new ArrayCollection($repo->getParts($value->getStorageLocation()));
|
||||||
} else {
|
} else {
|
||||||
$parts = new ArrayCollection([]);
|
$parts = new ArrayCollection([]);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue