-
- ${components.Highlight({hit: item, attribute: 'name'})}
-
+
+
+
+
-
- ${components.Highlight({hit: item, attribute: 'description'})}
- ${item.category ? html`
${components.Highlight({hit: item, attribute: 'category'})}
` : ""}
- ${item.footprint ? html`
${components.Highlight({hit: item, attribute: 'footprint'})}
` : ""}
+
+
+
+ ${components.Highlight({hit: item, attribute: 'name'})}
+
+
+
+ ${components.Highlight({hit: item, attribute: 'description'})}
+ ${item.category ? html`
${components.Highlight({hit: item, attribute: 'category'})}
` : ""}
+ ${item.footprint ? html`
${components.Highlight({hit: item, attribute: 'footprint'})}
` : ""}
+
-
-
- `;
+
+ `;
},
},
},
];
+
+ if (hasAssemblyDetailUrl) {
+ sources.push(
+ // Assemblies source (filtered from the same mixed endpoint results)
+ {
+ sourceId: 'assemblies',
+ getItems() {
+ return fetchMixedItems(query).then((items) =>
+ items.filter((item) => item.type === "assembly")
+ );
+ },
+ getItemUrl({ item }) {
+ return assembly_detail_uri_template.replace('__ID__', item.id);
+ },
+ templates: {
+ header({ html }) {
+ return html`
+ `;
+ },
+ item({ item, components, html }) {
+ const details_url = assembly_detail_uri_template.replace('__ID__', item.id);
+
+ return html`
+
+
+
+

+
+
+
+
+ ${components.Highlight({hit: item, attribute: 'name'})}
+
+
+
+ ${components.Highlight({hit: item, attribute: 'description'})}
+
+
+
+
+ `;
+ },
+ },
+ }
+ );
+ }
+
+ if (hasProjectDetailUrl) {
+ sources.push(
+ // Projects source (filtered from the same mixed endpoint results)
+ {
+ sourceId: 'projects',
+ getItems() {
+ return fetchMixedItems(query).then((items) =>
+ items.filter((item) => item.type === "project")
+ );
+ },
+ getItemUrl({ item }) {
+ return project_detail_uri_template.replace('__ID__', item.id);
+ },
+ templates: {
+ header({ html }) {
+ return html`
+ `;
+ },
+ item({ item, components, html }) {
+ const details_url = project_detail_uri_template.replace('__ID__', item.id);
+
+ return html`
+
+
+
+

+
+
+
+
+ ${components.Highlight({hit: item, attribute: 'name'})}
+
+
+
+ ${components.Highlight({hit: item, attribute: 'description'})}
+
+
+
+
+ `;
+ },
+ },
+ }
+ );
+ }
+
+ return sources;
},
});
@@ -192,6 +316,5 @@ export default class extends Controller {
this._autocomplete.setIsOpen(false);
});
}
-
}
}
diff --git a/src/Controller/AssemblyController.php b/src/Controller/AssemblyController.php
index be97045b..5c558d66 100644
--- a/src/Controller/AssemblyController.php
+++ b/src/Controller/AssemblyController.php
@@ -128,7 +128,8 @@ class AssemblyController extends AbstractController
], $additonal_template_vars));
}
- #[Route(path: '/{id}/info', name: 'assembly_info', requirements: ['id' => '\d+'])]
+ #[Route(path: '/{id}/info', name: 'assembly_info')]
+ #[Route(path: '/{id}', requirements: ['id' => '\d+'])]
public function info(Assembly $assembly, Request $request): Response
{
$this->denyAccessUnlessGranted('read', $assembly);
diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php
index e510506f..aa0b3f76 100644
--- a/src/Controller/ProjectController.php
+++ b/src/Controller/ProjectController.php
@@ -58,7 +58,8 @@ class ProjectController extends AbstractController
) {
}
- #[Route(path: '/{id}/info', name: 'project_info', requirements: ['id' => '\d+'])]
+ #[Route(path: '/{id}/info', name: 'project_info')]
+ #[Route(path: '/{id}', requirements: ['id' => '\d+'])]
public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper, TableSettings $tableSettings): Response
{
$this->denyAccessUnlessGranted('read', $project);
diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php
index efa17bb0..13a57dd7 100644
--- a/src/Controller/TypeaheadController.php
+++ b/src/Controller/TypeaheadController.php
@@ -24,6 +24,8 @@ namespace App\Controller;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Parameters\AbstractParameter;
+use App\Entity\ProjectSystem\Project;
+use App\Services\Attachments\ProjectPreviewGenerator;
use App\Settings\MiscSettings\IpnSuggestSettings;
use App\Services\Attachments\AssemblyPreviewGenerator;
use Symfony\Component\HttpFoundation\Response;
@@ -124,7 +126,10 @@ class TypeaheadController extends AbstractController
public function parts(
EntityManagerInterface $entityManager,
PartPreviewGenerator $previewGenerator,
+ ProjectPreviewGenerator $projectPreviewGenerator,
+ AssemblyPreviewGenerator $assemblyPreviewGenerator,
AttachmentURLGenerator $attachmentURLGenerator,
+ Request $request,
string $query = ""
): JsonResponse {
$this->denyAccessUnlessGranted('@parts.read');
@@ -133,18 +138,20 @@ class TypeaheadController extends AbstractController
$parts = $partRepository->autocompleteSearch($query, 100);
+ /** @var Part[]|Assembly[] $data */
$data = [];
foreach ($parts as $part) {
//Determine the picture to show:
$preview_attachment = $previewGenerator->getTablePreviewAttachment($part);
if($preview_attachment instanceof Attachment) {
- $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
+ $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment);
} else {
$preview_url = '';
}
/** @var Part $part */
$data[] = [
+ 'type' => 'part',
'id' => $part->getID(),
'name' => $part->getName(),
'category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : 'Unknown',
@@ -154,6 +161,64 @@ class TypeaheadController extends AbstractController
];
}
+ $multiDataSources = $request->query->getBoolean('multidatasources');
+
+ if ($multiDataSources) {
+ if ($this->isGranted('@projects.read')) {
+ $projectRepository = $entityManager->getRepository(Project::class);
+
+ $projects = $projectRepository->autocompleteSearch($query, 100);
+
+ foreach ($projects as $project) {
+ $preview_attachment = $projectPreviewGenerator->getTablePreviewAttachment($project);
+
+ if ($preview_attachment instanceof Attachment) {
+ $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment);
+ } else {
+ $preview_url = '';
+ }
+
+ /** @var Project $project */
+ $data[] = [
+ 'type' => 'project',
+ 'id' => $project->getID(),
+ 'name' => $project->getName(),
+ 'category' => '',
+ 'footprint' => '',
+ 'description' => mb_strimwidth($project->getDescription(), 0, 127, '...'),
+ 'image' => $preview_url,
+ ];
+ }
+ }
+
+ if ($this->isGranted('@assemblies.read')) {
+ $assemblyRepository = $entityManager->getRepository(Assembly::class);
+
+ $assemblies = $assemblyRepository->autocompleteSearch($query, 100);
+
+ foreach ($assemblies as $assembly) {
+ $preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly);
+
+ if ($preview_attachment instanceof Attachment) {
+ $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment);
+ } else {
+ $preview_url = '';
+ }
+
+ /** @var Assembly $assembly */
+ $data[] = [
+ 'type' => 'assembly',
+ 'id' => $assembly->getID(),
+ 'name' => $assembly->getName(),
+ 'category' => '',
+ 'footprint' => '',
+ 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
+ 'image' => $preview_url,
+ ];
+ }
+ }
+ }
+
return new JsonResponse($data);
}
diff --git a/src/DataTables/Filters/PartSearchFilter.php b/src/DataTables/Filters/PartSearchFilter.php
index 1515e6b5..6c94e324 100644
--- a/src/DataTables/Filters/PartSearchFilter.php
+++ b/src/DataTables/Filters/PartSearchFilter.php
@@ -122,6 +122,7 @@ class PartSearchFilter implements FilterInterface
}
if ($this->assembly) {
$fields_to_search[] = '_assembly.name';
+ $fields_to_search[] = '_assembly.ipn';
}
return $fields_to_search;
diff --git a/src/Repository/AssemblyRepository.php b/src/Repository/AssemblyRepository.php
index eef36690..d4c57cbb 100644
--- a/src/Repository/AssemblyRepository.php
+++ b/src/Repository/AssemblyRepository.php
@@ -57,7 +57,8 @@ class AssemblyRepository extends StructuralDBElementRepository
$qb = $this->createQueryBuilder('assembly');
$qb->select('assembly')
->where('ILIKE(assembly.name, :query) = TRUE')
- ->orWhere('ILIKE(assembly.description, :query) = TRUE');
+ ->orWhere('ILIKE(assembly.description, :query) = TRUE')
+ ->orWhere('ILIKE(assembly.ipn, :query) = TRUE');
$qb->setParameter('query', '%'.$query.'%');
diff --git a/src/Repository/Parts/DeviceRepository.php b/src/Repository/Parts/DeviceRepository.php
index 442c91e5..3fa93183 100644
--- a/src/Repository/Parts/DeviceRepository.php
+++ b/src/Repository/Parts/DeviceRepository.php
@@ -51,4 +51,18 @@ class DeviceRepository extends StructuralDBElementRepository
//Prevent user from deleting devices, to not accidentally remove filled devices from old versions
return 1;
}
+
+ public function autocompleteSearch(string $query, int $max_limits = 50): array
+ {
+ $qb = $this->createQueryBuilder('p');
+ $qb->select('p')
+ ->where('ILIKE(p.name, :query) = TRUE');
+
+ $qb->setParameter('query', '%'.$query.'%');
+
+ $qb->setMaxResults($max_limits);
+ $qb->orderBy('NATSORT(p.name)', 'ASC');
+
+ return $qb->getQuery()->getResult();
+ }
}
diff --git a/src/Services/Attachments/ProjectPreviewGenerator.php b/src/Services/Attachments/ProjectPreviewGenerator.php
new file mode 100644
index 00000000..9929dbd3
--- /dev/null
+++ b/src/Services/Attachments/ProjectPreviewGenerator.php
@@ -0,0 +1,93 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\Services\Attachments;
+
+use App\Entity\Attachments\Attachment;
+use App\Entity\ProjectSystem\Project;
+
+class ProjectPreviewGenerator
+{
+ public function __construct(protected AttachmentManager $attachmentHelper)
+ {
+ }
+
+ /**
+ * Returns a list of attachments that can be used for previewing the project ordered by priority.
+ *
+ * @param Project $project the project for which the attachments should be determined
+ *
+ * @return (Attachment|null)[]
+ *
+ * @psalm-return list
+ */
+ public function getPreviewAttachments(Project $project): array
+ {
+ $list = [];
+
+ //Master attachment has top priority
+ $attachment = $project->getMasterPictureAttachment();
+ if ($this->isAttachmentValidPicture($attachment)) {
+ $list[] = $attachment;
+ }
+
+ //Then comes the other images of the project
+ foreach ($project->getAttachments() as $attachment) {
+ //Dont show the master attachment twice
+ if ($this->isAttachmentValidPicture($attachment) && $attachment !== $project->getMasterPictureAttachment()) {
+ $list[] = $attachment;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Determines what attachment should be used for previewing a project (especially in project table).
+ * The returned attachment is guaranteed to be existing and be a picture.
+ *
+ * @param Project $project The project for which the attachment should be determined
+ */
+ public function getTablePreviewAttachment(Project $project): ?Attachment
+ {
+ $attachment = $project->getMasterPictureAttachment();
+ if ($this->isAttachmentValidPicture($attachment)) {
+ return $attachment;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if a attachment is exising and a valid picture.
+ *
+ * @param Attachment|null $attachment the attachment that should be checked
+ *
+ * @return bool true if the attachment is valid
+ */
+ protected function isAttachmentValidPicture(?Attachment $attachment): bool
+ {
+ return $attachment instanceof Attachment
+ && $attachment->isPicture()
+ && $this->attachmentHelper->isFileExisting($attachment);
+ }
+}
diff --git a/templates/components/search.macro.html.twig b/templates/components/search.macro.html.twig
index 17002d13..361b20e4 100644
--- a/templates/components/search.macro.html.twig
+++ b/templates/components/search.macro.html.twig
@@ -100,7 +100,9 @@
data-navbar-mode="{% if is_navbar %}true{% else %}false{% endif %}"
data-placeholder-image="{{ asset('img/part_placeholder.svg') }}"
data-autocomplete="{{ path('typeahead_parts', {'query': '__QUERY__'}) }}"
- data-detail-url="{{ path('part_info', {'id': '__ID__'}) }}">
+ data-detail-url="{{ path('part_info', {'id': '__ID__'}) }}"
+ data-project-detail-url="{{ path('project_info', {'id': '__ID__'}) }}"
+ data-assembly-detail-url="{{ path('assembly_info', {'id': '__ID__'}) }}">
diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf
index 26bcc03d..a413fe07 100644
--- a/translations/messages.cs.xlf
+++ b/translations/messages.cs.xlf
@@ -1839,6 +1839,18 @@ Související prvky budou přesunuty nahoru.
-
\ No newline at end of file
+
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index c0045b42..07290f58 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -1797,6 +1797,18 @@ Sub elements will be moved upwards.
-
\ No newline at end of file
+
diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf
index 33368720..7c0095e5 100644
--- a/translations/messages.pl.xlf
+++ b/translations/messages.pl.xlf
@@ -1836,6 +1836,18 @@ Po usunięciu pod elementy zostaną przeniesione na górę.