diff --git a/assets/css/app/images.css b/assets/css/app/images.css
index 0212a85b..132cab99 100644
--- a/assets/css/app/images.css
+++ b/assets/css/app/images.css
@@ -61,3 +61,8 @@
.object-fit-cover {
object-fit: cover;
}
+
+.assembly-table-image {
+ max-height: 40px;
+ object-fit: contain;
+}
diff --git a/config/parameters.yaml b/config/parameters.yaml
index 345b8feb..e86072fe 100644
--- a/config/parameters.yaml
+++ b/config/parameters.yaml
@@ -48,14 +48,8 @@ parameters:
######################################################################################################################
# Table settings
######################################################################################################################
-<<<<<<< HEAD
partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order
-=======
- partdb.table.default_page_size: '%env(int:TABLE_DEFAULT_PAGE_SIZE)%' # The default number of entries shown per page in tables
- partdb.table.parts.default_columns: '%env(trim:string:TABLE_PARTS_DEFAULT_COLUMNS)%' # The default columns in part tables and their order
- partdb.table.assemblies.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_DEFAULT_COLUMNS)%' # The default columns in assembly tables and their order
partdb.table.assemblies_bom.default_columns: '%env(trim:string:TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS)%' # The default columns in assembly bom tables and their order
->>>>>>> 2779c55a (Baugruppen Stückliste um referenzierte Baugruppe erweitern)
######################################################################################################################
# Miscellaneous
diff --git a/docs/configuration.md b/docs/configuration.md
index 498308b0..242164bf 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -137,8 +137,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
time).
Also specify the default order of the columns. This is a comma separated list of column names. Available columns
are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`.
-* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first
- time).
+* `TABLE_ASSEMBLIES_DEFAULT_COLUMNS`: The columns in assemblies tables, which are visible by default (when loading table for first time).
Also specify the default order of the columns. This is a comma separated list of column names. Available columns
are: `name`, `id`, `ipn`, `description`, `referencedAssemblies`, `edit`, `addedDate`, `lastModified`.
* `TABLE_ASSEMBLIES_BOM_DEFAULT_COLUMNS`: The columns in assemblies bom tables, which are visible by default (when loading table for first time).
diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php
index 9106f677..a1ba7fa6 100644
--- a/src/Controller/AssemblyController.php
+++ b/src/Controller/AssemblyController.php
@@ -23,15 +23,22 @@ declare(strict_types=1);
namespace App\Controller;
use App\DataTables\AssemblyBomEntriesDataTable;
+use App\DataTables\AssemblyDataTable;
+use App\DataTables\ErrorDataTable;
+use App\DataTables\Filters\AssemblyFilter;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Entity\Parts\Part;
+use App\Exceptions\InvalidRegexException;
use App\Form\AssemblySystem\AssemblyAddPartsType;
use App\Form\AssemblySystem\AssemblyBuildType;
+use App\Form\Filters\AssemblyFilterType;
use App\Helpers\Assemblies\AssemblyBuildRequest;
use App\Services\ImportExportSystem\BOMImporter;
use App\Services\AssemblySystem\AssemblyBuildHelper;
+use App\Services\Trees\NodesListBuilder;
use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\DBAL\Exception\DriverException;
use Doctrine\ORM\EntityManagerInterface;
use League\Csv\SyntaxError;
use Omines\DataTablesBundle\DataTableFactory;
@@ -54,9 +61,76 @@ class AssemblyController extends AbstractController
public function __construct(
private readonly DataTableFactory $dataTableFactory,
private readonly TranslatorInterface $translator,
+ private readonly NodesListBuilder $nodesListBuilder
) {
}
+ #[Route(path: '/list', name: 'assemblies_list')]
+ public function showAll(Request $request): Response
+ {
+ return $this->showListWithFilter($request,'assemblies/lists/all_list.html.twig');
+ }
+
+ /**
+ * Common implementation for the part list pages.
+ * @param Request $request The request to parse
+ * @param string $template The template that should be rendered
+ * @param callable|null $filter_changer A function that is called with the filter object as parameter. This function can be used to customize the filter
+ * @param callable|null $form_changer A function that is called with the form object as parameter. This function can be used to customize the form
+ * @param array $additonal_template_vars Any additional template variables that should be passed to the template
+ * @param array $additional_table_vars Any additional variables that should be passed to the table creation
+ */
+ protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response
+ {
+ $this->denyAccessUnlessGranted('@assemblies.read');
+
+ $formRequest = clone $request;
+ $formRequest->setMethod('GET');
+ $filter = new AssemblyFilter($this->nodesListBuilder);
+ if($filter_changer !== null){
+ $filter_changer($filter);
+ }
+
+ $filterForm = $this->createForm(AssemblyFilterType::class, $filter, ['method' => 'GET']);
+ if($form_changer !== null) {
+ $form_changer($filterForm);
+ }
+
+ $filterForm->handleRequest($formRequest);
+
+ $table = $this->dataTableFactory->createFromType(
+ AssemblyDataTable::class,
+ array_merge(['filter' => $filter], $additional_table_vars),
+ ['lengthMenu' => AssemblyDataTable::LENGTH_MENU]
+ )
+ ->handleRequest($request);
+
+ if ($table->isCallback()) {
+ try {
+ try {
+ return $table->getResponse();
+ } catch (DriverException $driverException) {
+ if ($driverException->getCode() === 1139) {
+ //Convert the driver exception to InvalidRegexException so it has the same handler as for SQLite
+ throw InvalidRegexException::fromDriverException($driverException);
+ } else {
+ throw $driverException;
+ }
+ }
+ } catch (InvalidRegexException $exception) {
+ $errors = $this->translator->trans('assembly.table.invalid_regex').': '.$exception->getReason();
+ $request->request->set('order', []);
+
+ return ErrorDataTable::errorTable($this->dataTableFactory, $request, $errors);
+ }
+ }
+
+ return $this->render($template, array_merge([
+ 'datatable' => $table,
+ 'filterForm' => $filterForm->createView(),
+ ], $additonal_template_vars));
+ }
+
#[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])]
public function info(Assembly $assembly, Request $request, AssemblyBuildHelper $buildHelper): Response
{
diff --git a/src/DataTables/AssemblyDataTable.php b/src/DataTables/AssemblyDataTable.php
new file mode 100644
index 00000000..f3854ebc
--- /dev/null
+++ b/src/DataTables/AssemblyDataTable.php
@@ -0,0 +1,249 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\DataTables;
+
+use App\DataTables\Adapters\TwoStepORMAdapter;
+use App\DataTables\Column\IconLinkColumn;
+use App\DataTables\Column\LocaleDateTimeColumn;
+use App\DataTables\Column\MarkdownColumn;
+use App\DataTables\Column\SelectColumn;
+use App\DataTables\Filters\AssemblyFilter;
+use App\DataTables\Filters\AssemblySearchFilter;
+use App\DataTables\Helpers\AssemblyDataTableHelper;
+use App\DataTables\Helpers\ColumnSortHelper;
+use App\Doctrine\Helpers\FieldHelper;
+use App\Entity\AssemblySystem\Assembly;
+use App\Services\EntityURLGenerator;
+use Doctrine\ORM\AbstractQuery;
+use Doctrine\ORM\QueryBuilder;
+use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
+use Omines\DataTablesBundle\Column\TextColumn;
+use Omines\DataTablesBundle\DataTable;
+use Omines\DataTablesBundle\DataTableTypeInterface;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+final class AssemblyDataTable implements DataTableTypeInterface
+{
+ const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]];
+
+ public function __construct(
+ private readonly EntityURLGenerator $urlGenerator,
+ private readonly TranslatorInterface $translator,
+ private readonly AssemblyDataTableHelper $assemblyDataTableHelper,
+ private readonly Security $security,
+ private readonly string $visible_columns,
+ private readonly ColumnSortHelper $csh,
+ ) {
+ }
+
+ public function configureOptions(OptionsResolver $optionsResolver): void
+ {
+ $optionsResolver->setDefaults([
+ 'filter' => null,
+ 'search' => null
+ ]);
+
+ $optionsResolver->setAllowedTypes('filter', [AssemblyFilter::class, 'null']);
+ $optionsResolver->setAllowedTypes('search', [AssemblySearchFilter::class, 'null']);
+ }
+
+ public function configure(DataTable $dataTable, array $options): void
+ {
+ $resolver = new OptionsResolver();
+ $this->configureOptions($resolver);
+ $options = $resolver->resolve($options);
+
+ $this->csh
+ ->add('select', SelectColumn::class, visibility_configurable: false)
+ ->add('picture', TextColumn::class, [
+ 'label' => '',
+ 'className' => 'no-colvis',
+ 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderPicture($context),
+ ], visibility_configurable: false)
+ ->add('name', TextColumn::class, [
+ 'label' => $this->translator->trans('assembly.table.name'),
+ 'render' => fn($value, Assembly $context) => $this->assemblyDataTableHelper->renderName($context),
+ 'orderField' => 'NATSORT(assembly.name)'
+ ])
+ ->add('id', TextColumn::class, [
+ 'label' => $this->translator->trans('assembly.table.id'),
+ ])
+ ->add('ipn', TextColumn::class, [
+ 'label' => $this->translator->trans('assembly.table.ipn'),
+ 'orderField' => 'NATSORT(assembly.ipn)'
+ ])
+ ->add('description', MarkdownColumn::class, [
+ 'label' => $this->translator->trans('assembly.table.description'),
+ ])
+ ->add('addedDate', LocaleDateTimeColumn::class, [
+ 'label' => $this->translator->trans('assembly.table.addedDate'),
+ ])
+ ->add('lastModified', LocaleDateTimeColumn::class, [
+ 'label' => $this->translator->trans('assembly.table.lastModified'),
+ ]);
+
+ //Add a assembly column to list where the assembly is used as referenced assembly as bom-entry, when the user has the permission to see the assemblies
+ if ($this->security->isGranted('read', Assembly::class)) {
+ $this->csh->add('referencedAssemblies', TextColumn::class, [
+ 'label' => $this->translator->trans('assembly.referencedAssembly.labelp'),
+ 'render' => function ($value, Assembly $context): string {
+ $assemblies = $context->getReferencedAssemblies();
+
+ $max = 5;
+ $tmp = "";
+
+ for ($i = 0; $i < min($max, count($assemblies)); $i++) {
+ $url = $this->urlGenerator->infoURL($assemblies[$i]);
+ $tmp .= sprintf('%s', $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('assembly.table.edit'),
+ 'href' => fn($value, Assembly $context) => $this->urlGenerator->editURL($context),
+ 'disabled' => fn($value, Assembly $context) => !$this->security->isGranted('edit', $context),
+ 'title' => $this->translator->trans('assembly.table.edit.title'),
+ ]);
+
+ //Apply the user configured order and visibility and add the columns to the table
+ $this->csh->applyVisibilityAndConfigureColumns($dataTable, $this->visible_columns, "TABLE_ASSEMBLIES_DEFAULT_COLUMNS");
+
+ $dataTable->addOrderBy('name')
+ ->createAdapter(TwoStepORMAdapter::class, [
+ 'filter_query' => $this->getFilterQuery(...),
+ 'detail_query' => $this->getDetailQuery(...),
+ 'entity' => Assembly::class,
+ 'hydrate' => AbstractQuery::HYDRATE_OBJECT,
+ //Use the simple total query, as we just want to get the total number of assemblies without any conditions
+ //For this the normal query would be pretty slow
+ 'simple_total_query' => true,
+ 'criteria' => [
+ function (QueryBuilder $builder) use ($options): void {
+ $this->buildCriteria($builder, $options);
+ },
+ new SearchCriteriaProvider(),
+ ],
+ 'query_modifier' => $this->addJoins(...),
+ ]);
+ }
+
+
+ private function getFilterQuery(QueryBuilder $builder): void
+ {
+ /* In the filter query we only select the IDs. The fetching of the full entities is done in the detail query.
+ * We only need to join the entities here, so we can filter by them.
+ * The filter conditions are added to this QB in the buildCriteria method.
+ *
+ * The amountSum field and the joins are dynamically added by the addJoins method, if the fields are used in the query.
+ * This improves the performance, as we do not need to join all tables, if we do not need them.
+ */
+ $builder
+ ->select('assembly.id')
+ ->from(Assembly::class, 'assembly')
+
+ //The other group by fields, are dynamically added by the addJoins method
+ ->addGroupBy('assembly');
+ }
+
+ private function getDetailQuery(QueryBuilder $builder, array $filter_results): void
+ {
+ $ids = array_map(static fn($row) => $row['id'], $filter_results);
+
+ /*
+ * In this query we take the IDs which were filtered, paginated and sorted in the filter query, and fetch the
+ * full entities.
+ * We can do complex fetch joins, as we do not need to filter or sort here (which would kill the performance).
+ * The only condition should be for the IDs.
+ * It is important that elements are ordered the same way, as the IDs are passed, or ordering will be wrong.
+ *
+ * We do not require the subqueries like amountSum here, as it is not used to render the table (and only for sorting)
+ */
+ $builder
+ ->select('assembly')
+ ->addSelect('master_picture_attachment')
+ ->addSelect('attachments')
+ ->from(Assembly::class, 'assembly')
+ ->leftJoin('assembly.master_picture_attachment', 'master_picture_attachment')
+ ->leftJoin('assembly.attachments', 'attachments')
+ ->where('assembly.id IN (:ids)')
+ ->setParameter('ids', $ids)
+ ->addGroupBy('assembly')
+ ->addGroupBy('master_picture_attachment')
+ ->addGroupBy('attachments');
+
+ //Get the results in the same order as the IDs were passed
+ FieldHelper::addOrderByFieldParam($builder, 'assembly.id', 'ids');
+ }
+
+ /**
+ * This function is called right before the filter query is executed.
+ * We use it to dynamically add joins to the query, if the fields are used in the query.
+ * @param QueryBuilder $builder
+ * @return QueryBuilder
+ */
+ private function addJoins(QueryBuilder $builder): QueryBuilder
+ {
+ //Check if the query contains certain conditions, for which we need to add additional joins
+ //The join fields get prefixed with an underscore, so we can check if they are used in the query easy without confusing them for a assembly subfield
+ $dql = $builder->getDQL();
+
+ if (str_contains($dql, '_master_picture_attachment')) {
+ $builder->leftJoin('assembly.master_picture_attachment', '_master_picture_attachment');
+ $builder->addGroupBy('_master_picture_attachment');
+ }
+ if (str_contains($dql, '_attachments')) {
+ $builder->leftJoin('assembly.attachments', '_attachments');
+ }
+
+ return $builder;
+ }
+
+ private function buildCriteria(QueryBuilder $builder, array $options): void
+ {
+ //Apply the search criterias first
+ if ($options['search'] instanceof AssemblySearchFilter) {
+ $search = $options['search'];
+ $search->apply($builder);
+ }
+
+ //We do the most stuff here in the filter class
+ if ($options['filter'] instanceof AssemblyFilter) {
+ $filter = $options['filter'];
+ $filter->apply($builder);
+ }
+ }
+}
diff --git a/src/DataTables/Filters/AssemblyFilter.php b/src/DataTables/Filters/AssemblyFilter.php
new file mode 100644
index 00000000..d8d07a1e
--- /dev/null
+++ b/src/DataTables/Filters/AssemblyFilter.php
@@ -0,0 +1,68 @@
+.
+ */
+namespace App\DataTables\Filters;
+
+use App\DataTables\Filters\Constraints\DateTimeConstraint;
+use App\DataTables\Filters\Constraints\EntityConstraint;
+use App\DataTables\Filters\Constraints\IntConstraint;
+use App\DataTables\Filters\Constraints\TextConstraint;
+use App\Entity\Attachments\AttachmentType;
+use App\Services\Trees\NodesListBuilder;
+use Doctrine\ORM\QueryBuilder;
+
+class AssemblyFilter implements FilterInterface
+{
+
+ use CompoundFilterTrait;
+
+ public readonly IntConstraint $dbId;
+ public readonly TextConstraint $ipn;
+ public readonly TextConstraint $name;
+ public readonly TextConstraint $description;
+ public readonly TextConstraint $comment;
+ public readonly DateTimeConstraint $lastModified;
+ public readonly DateTimeConstraint $addedDate;
+
+ public readonly IntConstraint $attachmentsCount;
+ public readonly EntityConstraint $attachmentType;
+ public readonly TextConstraint $attachmentName;
+
+ public function __construct(NodesListBuilder $nodesListBuilder)
+ {
+ $this->name = new TextConstraint('assembly.name');
+ $this->description = new TextConstraint('assembly.description');
+ $this->comment = new TextConstraint('assembly.comment');
+ $this->dbId = new IntConstraint('assembly.id');
+ $this->ipn = new TextConstraint('assembly.ipn');
+ $this->addedDate = new DateTimeConstraint('assembly.addedDate');
+ $this->lastModified = new DateTimeConstraint('assembly.lastModified');
+ $this->attachmentsCount = new IntConstraint('COUNT(_attachments)');
+ $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, '_attachments.attachment_type');
+ $this->attachmentName = new TextConstraint('_attachments.name');
+ }
+
+ public function apply(QueryBuilder $queryBuilder): void
+ {
+ $this->applyAllChildFilters($queryBuilder);
+ }
+}
diff --git a/src/DataTables/Filters/AssemblySearchFilter.php b/src/DataTables/Filters/AssemblySearchFilter.php
new file mode 100644
index 00000000..1627cc61
--- /dev/null
+++ b/src/DataTables/Filters/AssemblySearchFilter.php
@@ -0,0 +1,183 @@
+.
+ */
+namespace App\DataTables\Filters;
+use Doctrine\ORM\QueryBuilder;
+
+class AssemblySearchFilter implements FilterInterface
+{
+
+ /** @var boolean Whether to use regex for searching */
+ protected bool $regex = false;
+
+ /** @var bool Use name field for searching */
+ protected bool $name = true;
+
+ /** @var bool Use description for searching */
+ protected bool $description = true;
+
+ /** @var bool Use comment field for searching */
+ protected bool $comment = true;
+
+ /** @var bool Use ordernr for searching */
+ protected bool $ordernr = true;
+
+ /** @var bool Use Internal part number for searching */
+ protected bool $ipn = true;
+
+ public function __construct(
+ /** @var string The string to query for */
+ protected string $keyword
+ )
+ {
+ }
+
+ protected function getFieldsToSearch(): array
+ {
+ $fields_to_search = [];
+
+ if($this->name) {
+ $fields_to_search[] = 'assembly.name';
+ }
+ if($this->description) {
+ $fields_to_search[] = 'assembly.description';
+ }
+ if ($this->comment) {
+ $fields_to_search[] = 'assembly.comment';
+ }
+ if ($this->ipn) {
+ $fields_to_search[] = 'assembly.ipn';
+ }
+
+ return $fields_to_search;
+ }
+
+ public function apply(QueryBuilder $queryBuilder): void
+ {
+ $fields_to_search = $this->getFieldsToSearch();
+
+ //If we have nothing to search for, do nothing
+ if ($fields_to_search === [] || $this->keyword === '') {
+ return;
+ }
+
+ //Convert the fields to search to a list of expressions
+ $expressions = array_map(function (string $field): string {
+ if ($this->regex) {
+ return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
+ }
+
+ return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
+ }, $fields_to_search);
+
+ //Add Or concatenation of the expressions to our query
+ $queryBuilder->andWhere(
+ $queryBuilder->expr()->orX(...$expressions)
+ );
+
+ //For regex, we pass the query as is, for like we add % to the start and end as wildcards
+ if ($this->regex) {
+ $queryBuilder->setParameter('search_query', $this->keyword);
+ } else {
+ $queryBuilder->setParameter('search_query', '%' . $this->keyword . '%');
+ }
+ }
+
+ public function getKeyword(): string
+ {
+ return $this->keyword;
+ }
+
+ public function setKeyword(string $keyword): AssemblySearchFilter
+ {
+ $this->keyword = $keyword;
+ return $this;
+ }
+
+ public function isRegex(): bool
+ {
+ return $this->regex;
+ }
+
+ public function setRegex(bool $regex): AssemblySearchFilter
+ {
+ $this->regex = $regex;
+ return $this;
+ }
+
+ public function isName(): bool
+ {
+ return $this->name;
+ }
+
+ public function setName(bool $name): AssemblySearchFilter
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function isCategory(): bool
+ {
+ return $this->category;
+ }
+
+ public function setCategory(bool $category): AssemblySearchFilter
+ {
+ $this->category = $category;
+ return $this;
+ }
+
+ public function isDescription(): bool
+ {
+ return $this->description;
+ }
+
+ public function setDescription(bool $description): AssemblySearchFilter
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function isIPN(): bool
+ {
+ return $this->ipn;
+ }
+
+ public function setIPN(bool $ipn): AssemblySearchFilter
+ {
+ $this->ipn = $ipn;
+ return $this;
+ }
+
+ public function isComment(): bool
+ {
+ return $this->comment;
+ }
+
+ public function setComment(bool $comment): AssemblySearchFilter
+ {
+ $this->comment = $comment;
+ return $this;
+ }
+
+
+}
diff --git a/src/DataTables/Helpers/AssemblyDataTableHelper.php b/src/DataTables/Helpers/AssemblyDataTableHelper.php
new file mode 100644
index 00000000..dda563ea
--- /dev/null
+++ b/src/DataTables/Helpers/AssemblyDataTableHelper.php
@@ -0,0 +1,77 @@
+.
+ */
+
+namespace App\DataTables\Helpers;
+
+use App\Entity\AssemblySystem\Assembly;
+use App\Entity\Attachments\Attachment;
+use App\Services\Attachments\AssemblyPreviewGenerator;
+use App\Services\Attachments\AttachmentURLGenerator;
+use App\Services\EntityURLGenerator;
+
+/**
+ * A helper service which contains common code to render columns for assembly related tables
+ */
+class AssemblyDataTableHelper
+{
+ public function __construct(
+ private readonly EntityURLGenerator $entityURLGenerator,
+ private readonly AssemblyPreviewGenerator $previewGenerator,
+ private readonly AttachmentURLGenerator $attachmentURLGenerator
+ ) {
+ }
+
+ public function renderName(Assembly $context): string
+ {
+ $icon = '';
+
+ return sprintf(
+ '%s%s',
+ $this->entityURLGenerator->infoURL($context),
+ $icon,
+ htmlspecialchars($context->getName())
+ );
+ }
+
+ public function renderPicture(Assembly $context): string
+ {
+ $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context);
+ if (!$preview_attachment instanceof Attachment) {
+ return '';
+ }
+
+ $title = htmlspecialchars($preview_attachment->getName());
+ if ($preview_attachment->getFilename()) {
+ $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')';
+ }
+
+ return sprintf(
+ '',
+ 'Assembly image',
+ $this->attachmentURLGenerator->getThumbnailURL($preview_attachment),
+ $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'),
+ 'hoverpic assembly-table-image',
+ $title
+ );
+ }
+}
diff --git a/src/DataTables/Helpers/ProjectDataTableHelper.php b/src/DataTables/Helpers/ProjectDataTableHelper.php
index 0118d5d5..baa0e24e 100644
--- a/src/DataTables/Helpers/ProjectDataTableHelper.php
+++ b/src/DataTables/Helpers/ProjectDataTableHelper.php
@@ -27,7 +27,7 @@ use App\Entity\ProjectSystem\Project;
use App\Services\EntityURLGenerator;
/**
- * A helper service which contains common code to render columns for assembly related tables
+ * A helper service which contains common code to render columns for project related tables
*/
class ProjectDataTableHelper
{
diff --git a/src/Form/Filters/AssemblyFilterType.php b/src/Form/Filters/AssemblyFilterType.php
new file mode 100644
index 00000000..acfbb1a8
--- /dev/null
+++ b/src/Form/Filters/AssemblyFilterType.php
@@ -0,0 +1,114 @@
+.
+ */
+namespace App\Form\Filters;
+
+use App\DataTables\Filters\AssemblyFilter;
+use App\Entity\Attachments\AttachmentType;
+use App\Form\Filters\Constraints\DateTimeConstraintType;
+use App\Form\Filters\Constraints\NumberConstraintType;
+use App\Form\Filters\Constraints\StructuralEntityConstraintType;
+use App\Form\Filters\Constraints\TextConstraintType;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\ResetType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class AssemblyFilterType extends AbstractType
+{
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'compound' => true,
+ 'data_class' => AssemblyFilter::class,
+ 'csrf_protection' => false,
+ ]);
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ /*
+ * Common tab
+ */
+
+ $builder->add('name', TextConstraintType::class, [
+ 'label' => 'assembly.filter.name',
+ ]);
+
+ $builder->add('description', TextConstraintType::class, [
+ 'label' => 'assembly.filter.description',
+ ]);
+
+ $builder->add('comment', TextConstraintType::class, [
+ 'label' => 'assembly.filter.comment'
+ ]);
+
+ /*
+ * Advanced tab
+ */
+
+ $builder->add('dbId', NumberConstraintType::class, [
+ 'label' => 'assembly.filter.dbId',
+ 'min' => 1,
+ 'step' => 1,
+ ]);
+
+ $builder->add('ipn', TextConstraintType::class, [
+ 'label' => 'assembly.filter.ipn',
+ ]);
+
+ $builder->add('lastModified', DateTimeConstraintType::class, [
+ 'label' => 'lastModified'
+ ]);
+
+ $builder->add('addedDate', DateTimeConstraintType::class, [
+ 'label' => 'createdAt'
+ ]);
+
+ /**
+ * Attachments count
+ */
+ $builder->add('attachmentsCount', NumberConstraintType::class, [
+ 'label' => 'assembly.filter.attachments_count',
+ 'step' => 1,
+ 'min' => 0,
+ ]);
+
+ $builder->add('attachmentType', StructuralEntityConstraintType::class, [
+ 'label' => 'attachment.attachment_type',
+ 'entity_class' => AttachmentType::class
+ ]);
+
+ $builder->add('attachmentName', TextConstraintType::class, [
+ 'label' => 'assembly.filter.attachmentName',
+ ]);
+
+ $builder->add('submit', SubmitType::class, [
+ 'label' => 'filter.submit',
+ ]);
+
+ $builder->add('discard', ResetType::class, [
+ 'label' => 'filter.discard',
+ ]);
+ }
+}
diff --git a/templates/assemblies/lists/_action_bar.html.twig b/templates/assemblies/lists/_action_bar.html.twig
new file mode 100644
index 00000000..37289812
--- /dev/null
+++ b/templates/assemblies/lists/_action_bar.html.twig
@@ -0,0 +1,6 @@
+