diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index bdaf1ebc..d376b148 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -32,10 +32,15 @@ namespace App\Controller; +use App\DataTables\AttachmentDataTable; +use App\DataTables\PartsDataTable; use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\PartAttachment; use App\Services\AttachmentHelper; +use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Routing\Annotation\Route; @@ -104,4 +109,26 @@ class AttachmentFileController extends AbstractController return $response; } + /** + * @Route("/attachment/list", name="attachment_list") + * @param DataTableFactory $dataTable + * @param Request $request + * @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function attachmentsTable(DataTableFactory $dataTable, Request $request) + { + $this->denyAccessUnlessGranted('read', new PartAttachment()); + + $table = $dataTable->createFromType(AttachmentDataTable::class) + ->handleRequest($request); + + if ($table->isCallback()) { + return $table->getResponse(); + } + + return $this->render('attachment_list.html.twig', [ + 'datatable' => $table + ]); + } + } \ No newline at end of file diff --git a/src/DataTables/AttachmentDataTable.php b/src/DataTables/AttachmentDataTable.php new file mode 100644 index 00000000..d7424fee --- /dev/null +++ b/src/DataTables/AttachmentDataTable.php @@ -0,0 +1,222 @@ +translator = $translator; + $this->entityURLGenerator = $entityURLGenerator; + $this->attachmentHelper = $attachmentHelper; + $this->elementTypeNameGenerator = $elementTypeNameGenerator; + } + + protected function getQuery(QueryBuilder $builder) + { + $builder->distinct()->select('attachment') + ->addSelect('attachment_type') + //->addSelect('element') + ->from(Attachment::class, 'attachment') + ->leftJoin('attachment.attachment_type', 'attachment_type'); + //->leftJoin('attachment.element', 'element'); + } + + /** + * @param DataTable $dataTable + * @param array $options + */ + public function configure(DataTable $dataTable, array $options) + { + $dataTable->add('picture', TextColumn::class, [ + 'label' => '', + 'render' => function ($value, Attachment $context) { + if ($context->isPicture() + && !$context->isExternal() + && $this->attachmentHelper->isFileExisting($context)) { + return sprintf( + '%s', + 'Part image', + $this->entityURLGenerator->viewURL($context), + 'img-fluid hoverpic' + ); + } + + return ''; + } + ]); + + $dataTable->add('name', TextColumn::class, [ + 'label' => $this->translator->trans('attachment.edit.name'), + 'render' => function ($value, Attachment $context) { + //Link to external source + if ($context->isExternal()) { + return sprintf( + '%s', + htmlspecialchars($context->getURL()), + htmlspecialchars($value) + ); + } + + if ($this->attachmentHelper->isFileExisting($context)) { + return sprintf( + '%s', + $this->entityURLGenerator->viewURL($context), + htmlspecialchars($value) + ); + } + + return $value; + } + ]); + + $dataTable->add('attachment_type', TextColumn::class, [ + 'field' => 'attachment_type.name', + 'render' => function ($value, Attachment $context) { + return sprintf( + '%s', + $this->entityURLGenerator->editURL($context->getAttachmentType()), + htmlspecialchars($value) + ); + } + ]); + + $dataTable->add('element', TextColumn::class, [ + 'label' => $this->translator->trans('attachment.table.element'), + //'propertyPath' => 'element.name', + 'render' => function ($value, Attachment $context) { + return sprintf( + '%s', + $this->entityURLGenerator->infoURL($context->getElement()), + $this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true) + ); + } + ]); + + $dataTable->add('filename', TextColumn::class, [ + 'propertyPath' => 'filename' + ]); + + $dataTable->add('filesize', TextColumn::class, [ + 'render' => function ($value, Attachment $context) { + if ($this->attachmentHelper->isFileExisting($context)) { + return $this->attachmentHelper->getHumanFileSize($context); + } + if ($context->isExternal()) { + return '' . $this->translator->trans('attachment.external') . ''; + } + + return sprintf( + ' + %s + ', + $this->translator->trans('attachment.file_not_found') + ); + } + ]); + + $dataTable + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.addedDate'), + 'visible' => false + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.lastModified'), + 'visible' => false + ]); + + $dataTable->add('show_in_table', BoolColumn::class, [ + 'label' => $this->translator->trans('attachment.edit.show_in_table'), + 'trueValue' => $this->translator->trans('true'), + 'falseValue' => $this->translator->trans('false'), + 'nullValue' => '', + 'visible' => false + ]); + + $dataTable->add('isPicture', BoolColumn::class, [ + 'label' => $this->translator->trans('attachment.edit.isPicture'), + 'trueValue' => $this->translator->trans('true'), + 'falseValue' => $this->translator->trans('false'), + 'nullValue' => '', + 'visible' => false, + 'propertyPath' => 'picture' + ]); + + $dataTable->add('is3DModel', BoolColumn::class, [ + 'label' => $this->translator->trans('attachment.edit.is3DModel'), + 'trueValue' => $this->translator->trans('true'), + 'falseValue' => $this->translator->trans('false'), + 'nullValue' => '', + 'visible' => false, + 'propertyPath' => '3dmodel' + ]); + + $dataTable->add('isBuiltin', BoolColumn::class, [ + 'label' => $this->translator->trans('attachment.edit.isBuiltin'), + 'trueValue' => $this->translator->trans('true'), + 'falseValue' => $this->translator->trans('false'), + 'nullValue' => '', + 'visible' => false, + 'propertyPath' => 'builtin' + ]); + + $dataTable->createAdapter(ORMAdapter::class, [ + 'entity' => Attachment::class, + 'query' => function (QueryBuilder $builder) { + $this->getQuery($builder); + }, + ]); + } +} \ No newline at end of file diff --git a/src/Exceptions/EntityNotSupported.php b/src/Exceptions/EntityNotSupportedException.php similarity index 94% rename from src/Exceptions/EntityNotSupported.php rename to src/Exceptions/EntityNotSupportedException.php index 91963a8d..8253bc65 100644 --- a/src/Exceptions/EntityNotSupported.php +++ b/src/Exceptions/EntityNotSupportedException.php @@ -29,6 +29,6 @@ namespace App\Exceptions; -class EntityNotSupported extends \Exception +class EntityNotSupportedException extends \InvalidArgumentException { } diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php new file mode 100644 index 00000000..f6bf8094 --- /dev/null +++ b/src/Services/ElementTypeNameGenerator.php @@ -0,0 +1,132 @@ +translator = $translator; + + //Child classes has to become before parent classes + $this->mapping = [ + Attachment::class => $this->translator->trans('attachment.label'), + Category::class => $this->translator->trans('category.label'), + AttachmentType::class => $this->translator->trans('attachment_type.label'), + Device::class => $this->translator->trans('device.label'), + Footprint::class => $this->translator->trans('footprint.label'), + Manufacturer::class => $this->translator->trans('manufacturer.label'), + MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), + Part::class => $this->translator->trans('part.label'), + PartLot::class => $this->translator->trans('part_lot.label'), + Storelocation::class => $this->translator->trans('storelocation.label'), + Supplier::class => $this->translator->trans('supplier.label'), + Currency::class => $this->translator->trans('currency.label'), + Orderdetail::class => $this->translator->trans('orderdetail.label'), + Pricedetail::class => $this->translator->trans('pricedetail.label'), + Group::class => $this->translator->trans('group.label'), + User::class => $this->translator->trans('user.label'), + ]; + } + + /** + * Gets an localized label for the type of the entity. + * A part element becomes "Part" ("Bauteil" in german) and a category object becomes "Category". + * Useful when the type should be shown to user. + * Throws an exception if the class is not supported. + * @param DBElement $entity The element for which the label should be generated + * @return string The locatlized label for the entity type. + * @throws EntityNotSupportedException When the passed entity is not supported. + */ + public function getLocalizedTypeLabel(DBElement $entity) : string + { + //Check if we have an direct array entry for our entity class, then we can use it + if (isset($this->mapping[get_class($entity)])) { + return $this->mapping[get_class($entity)]; + } + + //Otherwise iterate over array and check for inheritance (needed when the proxy element from doctrine are passed) + foreach ($this->mapping as $class => $translation) { + if ($entity instanceof $class) { + return $translation; + } + } + + //When nothing was found throw an exception + throw new EntityNotSupportedException( + sprintf('No localized label for the element with type %s was found!', get_class($entity)) + ); + } + + /** + * Returns a string like in the format ElementType: ElementName. + * For example this could be something like: "Part: BC547". + * It uses getLocalizedLabel to determine the type. + * @param NamedDBElement $entity The entity for which the string should be generated. + * @param bool $use_html If set to true, a html string is returned, where the type is set italic + * @return string The localized string + */ + public function getTypeNameCombination(NamedDBElement $entity, bool $use_html = false) : string + { + $type = $this->getLocalizedTypeLabel($entity); + if ($use_html) { + return '' . $type . ': ' . $entity->getName(); + } + return $type . ": " . htmlspecialchars($entity->getName()); + } +} \ No newline at end of file diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index ef568b87..c032cc2b 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -44,7 +44,7 @@ use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; -use App\Exceptions\EntityNotSupported; +use App\Exceptions\EntityNotSupportedException; use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -72,7 +72,7 @@ class EntityURLGenerator * @param array $map The map that should be used for determing the controller * @param $entity mixed The entity for which the controller name should be determined. * @return string The name of the controller fitting the entity class - * @throws EntityNotSupported + * @throws EntityNotSupportedException */ protected function mapToController(array $map, $entity): string { @@ -87,7 +87,7 @@ class EntityURLGenerator } } - throw new EntityNotSupported(sprintf( + throw new EntityNotSupportedException(sprintf( 'The given entity is not supported yet! Passed class type: %s', get_class($entity) )); @@ -104,7 +104,7 @@ class EntityURLGenerator * @param $entity mixed The element for which the page should be generated. * @param string $type The page type. Currently supported: 'info', 'edit', 'create', 'clone', 'list'/'list_parts' * @return string The link to the desired page. - * @throws EntityNotSupported Thrown if the entity is not supported for the given type. + * @throws EntityNotSupportedException Thrown if the entity is not supported for the given type. * @throws \InvalidArgumentException Thrown if the givent type is not existing. */ public function getURL($entity, string $type) @@ -142,7 +142,7 @@ class EntityURLGenerator } //Otherwise throw an error - throw new EntityNotSupported('The given entity is not supported yet!'); + throw new EntityNotSupportedException('The given entity is not supported yet!'); } public function downloadURL($entity): string @@ -155,7 +155,7 @@ class EntityURLGenerator } //Otherwise throw an error - throw new EntityNotSupported(sprintf( + throw new EntityNotSupportedException(sprintf( 'The given entity is not supported yet! Passed class type: %s', get_class($entity) )); @@ -166,12 +166,25 @@ class EntityURLGenerator * * @param $entity mixed The entity for which the info should be generated. * @return string The URL to the info page - * @throws EntityNotSupported If the method is not supported for the given Entity + * @throws EntityNotSupportedException If the method is not supported for the given Entity */ public function infoURL(DBElement $entity): string { $map = [ - Part::class => 'part_info' + Part::class => 'part_info', + + //As long we does not have own things for it use edit page + AttachmentType::class => 'attachment_type_edit', + Category::class => 'category_edit', + Device::class => 'device_edit', + Supplier::class => 'supplier_edit', + Manufacturer::class => 'manufacturer_edit', + Storelocation::class => 'store_location_edit', + Footprint::class => 'footprint_edit', + User::class => 'user_edit', + Currency::class => 'currency_edit', + MeasurementUnit::class => 'measurement_unit_edit', + Group::class => 'group_edit' ]; return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); @@ -182,7 +195,7 @@ class EntityURLGenerator * * @param $entity mixed The entity for which the edit link should be generated. * @return string The URL to the edit page. - * @throws EntityNotSupported If the method is not supported for the given Entity + * @throws EntityNotSupportedException If the method is not supported for the given Entity */ public function editURL($entity): string { @@ -209,7 +222,7 @@ class EntityURLGenerator * * @param $entity mixed The entity for which the link should be generated. * @return string The URL to the page. - * @throws EntityNotSupported If the method is not supported for the given Entity + * @throws EntityNotSupportedException If the method is not supported for the given Entity */ public function createURL($entity): string { @@ -237,7 +250,7 @@ class EntityURLGenerator * * @param $entity mixed The entity for which the link should be generated. * @return string The URL to the page. - * @throws EntityNotSupported If the method is not supported for the given Entity + * @throws EntityNotSupportedException If the method is not supported for the given Entity */ public function cloneURL(DBElement $entity): string { @@ -253,7 +266,7 @@ class EntityURLGenerator * * @param $entity mixed The entity for which the link should be generated. * @return string The URL to the page. - * @throws EntityNotSupported If the method is not supported for the given Entity + * @throws EntityNotSupportedException If the method is not supported for the given Entity */ public function listPartsURL(DBElement $entity): string { diff --git a/src/Services/ToolsTreeBuilder.php b/src/Services/ToolsTreeBuilder.php index 3d06aa63..9e9b9e76 100644 --- a/src/Services/ToolsTreeBuilder.php +++ b/src/Services/ToolsTreeBuilder.php @@ -30,6 +30,7 @@ namespace App\Services; use App\Entity\Attachments\AttachmentType; +use App\Entity\Attachments\PartAttachment; use App\Entity\Devices\Device; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -50,6 +51,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; /** * This Service generates the tree structure for the tools. + * Whenever you change something here, you has to clear the cache, because the results are cached for performance reasons. * @package App\Services */ class ToolsTreeBuilder @@ -78,7 +80,7 @@ class ToolsTreeBuilder /** * Generates the tree for the tools menu. * The result is cached. - * @return TreeViewNode The array containing all Nodes for the tools menu. + * @return TreeViewNode[] The array containing all Nodes for the tools menu. */ public function getTree() : array { @@ -175,10 +177,18 @@ class ToolsTreeBuilder protected function getShowNodes() : array { $show_nodes = array(); - $show_nodes[] = new TreeViewNode($this->translator->trans('tree.tools.show.all_parts'), + $show_nodes[] = new TreeViewNode( + $this->translator->trans('tree.tools.show.all_parts'), $this->urlGenerator->generate('parts_show_all') ); + if ($this->security->isGranted('read', new PartAttachment())) { + $show_nodes[] = new TreeViewNode( + $this->translator->trans('tree.tools.show.all_attachments'), + $this->urlGenerator->generate('attachment_list') + ); + } + return $show_nodes; } diff --git a/src/Services/TreeBuilder.php b/src/Services/TreeBuilder.php index eb7f5f21..c45261af 100644 --- a/src/Services/TreeBuilder.php +++ b/src/Services/TreeBuilder.php @@ -80,7 +80,7 @@ class TreeBuilder * @param DBElement|null $selectedElement When a element is given here, its tree node will be marked as selected in * the resulting tree. When $selectedElement is not existing in the tree, then nothing happens. * @return TreeViewNode The Node for the given Element. - * @throws \App\Exceptions\EntityNotSupported + * @throws \App\Exceptions\EntityNotSupportedException */ public function elementToTreeNode(NamedDBElement $element, ?string $href_type = 'list_parts', DBElement $selectedElement = null) : TreeViewNode { @@ -123,7 +123,7 @@ class TreeBuilder * @param DBElement|null $selectedElement When a element is given here, its tree node will be marked as selected in * the resulting tree. When $selectedElement is not existing in the tree, then nothing happens. * @return TreeViewNode[] Returns an array, containing all nodes. It is empty if the given class has no elements. - * @throws \App\Exceptions\EntityNotSupported + * @throws \App\Exceptions\EntityNotSupportedException */ public function typeToTree(string $class_name, ?string $href_type = 'list_parts', DBElement $selectedElement = null) : array { diff --git a/templates/attachment_list.html.twig b/templates/attachment_list.html.twig new file mode 100644 index 00000000..a198d6ee --- /dev/null +++ b/templates/attachment_list.html.twig @@ -0,0 +1,16 @@ +{% extends "base.html.twig" %} + +{% block title %}{% trans %}attachment.list.title{% endtrans %}{% endblock %} + +{% block content %} +
+
+
+
+

{% trans %}part_list.loading.caption{% endtrans %}

+
{% trans %}part_list.loading.message{% endtrans %}
+
+
+
+
+{% endblock %} \ No newline at end of file