mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-03 22:09:35 +00:00
Unterstützung für Projekt- und Baugruppensuche zum QuickSearch-Suggest hinzufügen
This commit is contained in:
parent
4911b5bf24
commit
74513b748d
23 changed files with 519 additions and 50 deletions
|
|
@ -54,10 +54,19 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
// The endpoint for searching parts
|
// The endpoint for searching parts or assemblies
|
||||||
const base_url = this.element.dataset.autocomplete;
|
const base_url = this.element.dataset.autocomplete;
|
||||||
// The URL template for the part detail pages
|
// The URL template for the part detail pages
|
||||||
const part_detail_uri_template = this.element.dataset.detailUrl;
|
const part_detail_uri_template = this.element.dataset.detailUrl;
|
||||||
|
// The URL template for the assembly detail pages
|
||||||
|
const assembly_detail_uri_template = this.element.dataset.assemblyDetailUrl;
|
||||||
|
// The URL template for the project detail pages
|
||||||
|
const project_detail_uri_template = this.element.dataset.projectDetailUrl;
|
||||||
|
|
||||||
|
const hasAssemblyDetailUrl =
|
||||||
|
typeof assembly_detail_uri_template === "string" && assembly_detail_uri_template.length > 0;
|
||||||
|
const hasProjectDetailUrl =
|
||||||
|
typeof project_detail_uri_template === "string" && project_detail_uri_template.length > 0;
|
||||||
|
|
||||||
//The URL of the placeholder picture
|
//The URL of the placeholder picture
|
||||||
const placeholder_image = this.element.dataset.placeholderImage;
|
const placeholder_image = this.element.dataset.placeholderImage;
|
||||||
|
|
@ -72,6 +81,43 @@ export default class extends Controller {
|
||||||
limit: 5,
|
limit: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cache the last query to avoid fetching the same endpoint twice (parts source + assemblies source)
|
||||||
|
let lastQuery = null;
|
||||||
|
let lastFetchPromise = null;
|
||||||
|
|
||||||
|
const fetchMixedItems = (query) => {
|
||||||
|
if (query === lastQuery && lastFetchPromise) {
|
||||||
|
return lastFetchPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastQuery = query;
|
||||||
|
|
||||||
|
const urlString = base_url.replace('__QUERY__', encodeURIComponent(query));
|
||||||
|
const url = new URL(urlString, window.location.href);
|
||||||
|
if (hasAssemblyDetailUrl || hasProjectDetailUrl) {
|
||||||
|
url.searchParams.set('multidatasources', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
lastFetchPromise = fetch(url.toString())
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((items) => {
|
||||||
|
//Iterate over all fields besides the id and highlight them (if present)
|
||||||
|
const fields = ["name", "description", "category", "footprint"];
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
for (const field of fields) {
|
||||||
|
if (item[field] !== undefined && item[field] !== null) {
|
||||||
|
item[field] = that._highlight(item[field], query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
return lastFetchPromise;
|
||||||
|
};
|
||||||
|
|
||||||
this._autocomplete = autocomplete({
|
this._autocomplete = autocomplete({
|
||||||
container: this.element,
|
container: this.element,
|
||||||
//Place the panel in the navbar, if the element is in navbar mode
|
//Place the panel in the navbar, if the element is in navbar mode
|
||||||
|
|
@ -102,7 +148,7 @@ export default class extends Controller {
|
||||||
},
|
},
|
||||||
|
|
||||||
// If the form is submitted, forward the term to the form
|
// If the form is submitted, forward the term to the form
|
||||||
onSubmit({state, event, ...setters}) {
|
onSubmit({ state, event, ...setters }) {
|
||||||
//Put the current text into each target input field
|
//Put the current text into each target input field
|
||||||
const input = that.inputTarget;
|
const input = that.inputTarget;
|
||||||
|
|
||||||
|
|
@ -119,31 +165,15 @@ export default class extends Controller {
|
||||||
input.form.requestSubmit();
|
input.form.requestSubmit();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
getSources({ query }) {
|
getSources({ query }) {
|
||||||
return [
|
const sources = [
|
||||||
// The parts source
|
// Parts source (filtered from mixed endpoint results)
|
||||||
{
|
{
|
||||||
sourceId: 'parts',
|
sourceId: 'parts',
|
||||||
getItems() {
|
getItems() {
|
||||||
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
return fetchMixedItems(query).then((items) =>
|
||||||
|
items.filter((item) => item.type !== "assembly")
|
||||||
const data = fetch(url)
|
);
|
||||||
.then((response) => response.json())
|
|
||||||
;
|
|
||||||
|
|
||||||
//Iterate over all fields besides the id and highlight them
|
|
||||||
const fields = ["name", "description", "category", "footprint"];
|
|
||||||
|
|
||||||
data.then((items) => {
|
|
||||||
items.forEach((item) => {
|
|
||||||
for (const field of fields) {
|
|
||||||
item[field] = that._highlight(item[field], query);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
},
|
||||||
getItemUrl({ item }) {
|
getItemUrl({ item }) {
|
||||||
return part_detail_uri_template.replace('__ID__', item.id);
|
return part_detail_uri_template.replace('__ID__', item.id);
|
||||||
|
|
@ -151,36 +181,130 @@ export default class extends Controller {
|
||||||
templates: {
|
templates: {
|
||||||
header({ html }) {
|
header({ html }) {
|
||||||
return html`<span class="aa-SourceHeaderTitle">${trans("part.labelp")}</span>
|
return html`<span class="aa-SourceHeaderTitle">${trans("part.labelp")}</span>
|
||||||
<div class="aa-SourceHeaderLine" />`;
|
<div class="aa-SourceHeaderLine" />`;
|
||||||
},
|
},
|
||||||
item({item, components, html}) {
|
item({ item, components, html }) {
|
||||||
const details_url = part_detail_uri_template.replace('__ID__', item.id);
|
const details_url = part_detail_uri_template.replace('__ID__', item.id);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<a class="aa-ItemLink" href="${details_url}">
|
<a class="aa-ItemLink" href="${details_url}">
|
||||||
<div class="aa-ItemContent">
|
<div class="aa-ItemContent">
|
||||||
<div class="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
|
<div class="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
|
||||||
<img src="${item.image !== "" ? item.image : placeholder_image}" alt="${item.name}" width="30" height="30"/>
|
<img src="${item.image !== "" ? item.image : placeholder_image}" alt="${item.name}" width="30" height="30"/>
|
||||||
</div>
|
|
||||||
<div class="aa-ItemContentBody">
|
|
||||||
<div class="aa-ItemContentTitle">
|
|
||||||
<b>
|
|
||||||
${components.Highlight({hit: item, attribute: 'name'})}
|
|
||||||
</b>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="aa-ItemContentDescription">
|
<div class="aa-ItemContentBody">
|
||||||
${components.Highlight({hit: item, attribute: 'description'})}
|
<div class="aa-ItemContentTitle">
|
||||||
${item.category ? html`<p class="m-0"><span class="fa-solid fa-tags fa-fw"></span>${components.Highlight({hit: item, attribute: 'category'})}</p>` : ""}
|
<b>
|
||||||
${item.footprint ? html`<p class="m-0"><span class="fa-solid fa-microchip fa-fw"></span>${components.Highlight({hit: item, attribute: 'footprint'})}</p>` : ""}
|
${components.Highlight({hit: item, attribute: 'name'})}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
<div class="aa-ItemContentDescription">
|
||||||
|
${components.Highlight({hit: item, attribute: 'description'})}
|
||||||
|
${item.category ? html`<p class="m-0"><span class="fa-solid fa-tags fa-fw"></span>${components.Highlight({hit: item, attribute: 'category'})}</p>` : ""}
|
||||||
|
${item.footprint ? html`<p class="m-0"><span class="fa-solid fa-microchip fa-fw"></span>${components.Highlight({hit: item, attribute: 'footprint'})}</p>` : ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
`;
|
||||||
`;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_ASSEMBLIES)}</span>
|
||||||
|
<div class="aa-SourceHeaderLine" />`;
|
||||||
|
},
|
||||||
|
item({ item, components, html }) {
|
||||||
|
const details_url = assembly_detail_uri_template.replace('__ID__', item.id);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<a class="aa-ItemLink" href="${details_url}">
|
||||||
|
<div class="aa-ItemContent">
|
||||||
|
<div class="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
|
||||||
|
<img src="${item.image !== "" ? item.image : placeholder_image}" alt="${item.name}" width="30" height="30"/>
|
||||||
|
</div>
|
||||||
|
<div class="aa-ItemContentBody">
|
||||||
|
<div class="aa-ItemContentTitle">
|
||||||
|
<b>
|
||||||
|
${components.Highlight({hit: item, attribute: 'name'})}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
<div class="aa-ItemContentDescription">
|
||||||
|
${components.Highlight({hit: item, attribute: 'description'})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_PROJECTS)}</span>
|
||||||
|
<div class="aa-SourceHeaderLine" />`;
|
||||||
|
},
|
||||||
|
item({ item, components, html }) {
|
||||||
|
const details_url = project_detail_uri_template.replace('__ID__', item.id);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<a class="aa-ItemLink" href="${details_url}">
|
||||||
|
<div class="aa-ItemContent">
|
||||||
|
<div class="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
|
||||||
|
<img src="${item.image !== "" ? item.image : placeholder_image}" alt="${item.name}" width="30" height="30"/>
|
||||||
|
</div>
|
||||||
|
<div class="aa-ItemContentBody">
|
||||||
|
<div class="aa-ItemContentTitle">
|
||||||
|
<b>
|
||||||
|
${components.Highlight({hit: item, attribute: 'name'})}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
<div class="aa-ItemContentDescription">
|
||||||
|
${components.Highlight({hit: item, attribute: 'description'})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -192,6 +316,5 @@ export default class extends Controller {
|
||||||
this._autocomplete.setIsOpen(false);
|
this._autocomplete.setIsOpen(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,8 @@ class AssemblyController extends AbstractController
|
||||||
], $additonal_template_vars));
|
], $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
|
public function info(Assembly $assembly, Request $request): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('read', $assembly);
|
$this->denyAccessUnlessGranted('read', $assembly);
|
||||||
|
|
|
||||||
|
|
@ -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
|
public function info(Project $project, Request $request, ProjectBuildHelper $buildHelper, TableSettings $tableSettings): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('read', $project);
|
$this->denyAccessUnlessGranted('read', $project);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\AssemblySystem\Assembly;
|
use App\Entity\AssemblySystem\Assembly;
|
||||||
use App\Entity\Parameters\AbstractParameter;
|
use App\Entity\Parameters\AbstractParameter;
|
||||||
|
use App\Entity\ProjectSystem\Project;
|
||||||
|
use App\Services\Attachments\ProjectPreviewGenerator;
|
||||||
use App\Settings\MiscSettings\IpnSuggestSettings;
|
use App\Settings\MiscSettings\IpnSuggestSettings;
|
||||||
use App\Services\Attachments\AssemblyPreviewGenerator;
|
use App\Services\Attachments\AssemblyPreviewGenerator;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
@ -124,7 +126,10 @@ class TypeaheadController extends AbstractController
|
||||||
public function parts(
|
public function parts(
|
||||||
EntityManagerInterface $entityManager,
|
EntityManagerInterface $entityManager,
|
||||||
PartPreviewGenerator $previewGenerator,
|
PartPreviewGenerator $previewGenerator,
|
||||||
|
ProjectPreviewGenerator $projectPreviewGenerator,
|
||||||
|
AssemblyPreviewGenerator $assemblyPreviewGenerator,
|
||||||
AttachmentURLGenerator $attachmentURLGenerator,
|
AttachmentURLGenerator $attachmentURLGenerator,
|
||||||
|
Request $request,
|
||||||
string $query = ""
|
string $query = ""
|
||||||
): JsonResponse {
|
): JsonResponse {
|
||||||
$this->denyAccessUnlessGranted('@parts.read');
|
$this->denyAccessUnlessGranted('@parts.read');
|
||||||
|
|
@ -133,18 +138,20 @@ class TypeaheadController extends AbstractController
|
||||||
|
|
||||||
$parts = $partRepository->autocompleteSearch($query, 100);
|
$parts = $partRepository->autocompleteSearch($query, 100);
|
||||||
|
|
||||||
|
/** @var Part[]|Assembly[] $data */
|
||||||
$data = [];
|
$data = [];
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
//Determine the picture to show:
|
//Determine the picture to show:
|
||||||
$preview_attachment = $previewGenerator->getTablePreviewAttachment($part);
|
$preview_attachment = $previewGenerator->getTablePreviewAttachment($part);
|
||||||
if($preview_attachment instanceof Attachment) {
|
if($preview_attachment instanceof Attachment) {
|
||||||
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
|
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment);
|
||||||
} else {
|
} else {
|
||||||
$preview_url = '';
|
$preview_url = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var Part $part */
|
/** @var Part $part */
|
||||||
$data[] = [
|
$data[] = [
|
||||||
|
'type' => 'part',
|
||||||
'id' => $part->getID(),
|
'id' => $part->getID(),
|
||||||
'name' => $part->getName(),
|
'name' => $part->getName(),
|
||||||
'category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : 'Unknown',
|
'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);
|
return new JsonResponse($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ class PartSearchFilter implements FilterInterface
|
||||||
}
|
}
|
||||||
if ($this->assembly) {
|
if ($this->assembly) {
|
||||||
$fields_to_search[] = '_assembly.name';
|
$fields_to_search[] = '_assembly.name';
|
||||||
|
$fields_to_search[] = '_assembly.ipn';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $fields_to_search;
|
return $fields_to_search;
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ class AssemblyRepository extends StructuralDBElementRepository
|
||||||
$qb = $this->createQueryBuilder('assembly');
|
$qb = $this->createQueryBuilder('assembly');
|
||||||
$qb->select('assembly')
|
$qb->select('assembly')
|
||||||
->where('ILIKE(assembly.name, :query) = TRUE')
|
->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.'%');
|
$qb->setParameter('query', '%'.$query.'%');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,18 @@ class DeviceRepository extends StructuralDBElementRepository
|
||||||
//Prevent user from deleting devices, to not accidentally remove filled devices from old versions
|
//Prevent user from deleting devices, to not accidentally remove filled devices from old versions
|
||||||
return 1;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
93
src/Services/Attachments/ProjectPreviewGenerator.php
Normal file
93
src/Services/Attachments/ProjectPreviewGenerator.php
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2022 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Attachment|null>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -100,7 +100,9 @@
|
||||||
data-navbar-mode="{% if is_navbar %}true{% else %}false{% endif %}"
|
data-navbar-mode="{% if is_navbar %}true{% else %}false{% endif %}"
|
||||||
data-placeholder-image="{{ asset('img/part_placeholder.svg') }}"
|
data-placeholder-image="{{ asset('img/part_placeholder.svg') }}"
|
||||||
data-autocomplete="{{ path('typeahead_parts', {'query': '__QUERY__'}) }}"
|
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__'}) }}">
|
||||||
|
|
||||||
<input type="hidden" name="keyword" required {{ stimulus_target('elements/part_search', 'input') }}>
|
<input type="hidden" name="keyword" required {{ stimulus_target('elements/part_search', 'input') }}>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1839,6 +1839,18 @@ Související prvky budou přesunuty nahoru.</target>
|
||||||
<target>Díly</target>
|
<target>Díly</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projekty</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Sestavy</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1797,6 +1797,18 @@ Underelementer vil blive flyttet opad.</target>
|
||||||
<target>Komponenter</target>
|
<target>Komponenter</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projekter</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Samlinger</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1796,6 +1796,18 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
|
||||||
<target>Bauteile</target>
|
<target>Bauteile</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Baugruppen</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projekte</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1871,5 +1871,17 @@
|
||||||
<target>Επεξεργασία</target>
|
<target>Επεξεργασία</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Συναρμολογήσεις</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Έργα</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|
|
||||||
|
|
@ -1797,6 +1797,18 @@ Sub elements will be moved upwards.</target>
|
||||||
<target>Parts</target>
|
<target>Parts</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Assemblies</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projects</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1839,6 +1839,18 @@ Subelementos serán desplazados hacia arriba.</target>
|
||||||
<target>Componentes</target>
|
<target>Componentes</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Proyectos</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Ensamblajes</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1822,6 +1822,18 @@ Show/Hide sidebar</target>
|
||||||
<target>Composants</target>
|
<target>Composants</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projets</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Assemblages</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="QRdK9zd" name="statistics.data_structures">
|
<unit id="QRdK9zd" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1750,6 +1750,18 @@
|
||||||
<target>Alkatrészek</target>
|
<target>Alkatrészek</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projektek</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Összeállítások</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1839,6 +1839,18 @@ I sub elementi saranno spostati verso l'alto.</target>
|
||||||
<target>Componenti</target>
|
<target>Componenti</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Progetti</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Assiemi</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1822,6 +1822,18 @@
|
||||||
<target>部品</target>
|
<target>部品</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>プロジェクト</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>アセンブリ</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="QRdK9zd" name="statistics.data_structures">
|
<unit id="QRdK9zd" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1548,5 +1548,17 @@
|
||||||
<target>_MENU_</target>
|
<target>_MENU_</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Assemblages</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projecten</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|
|
||||||
|
|
@ -1836,6 +1836,18 @@ Po usunięciu pod elementy zostaną przeniesione na górę.</target>
|
||||||
<target>Komponenty</target>
|
<target>Komponenty</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Projekty</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Zespoły</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1840,6 +1840,18 @@
|
||||||
<target>Компоненты</target>
|
<target>Компоненты</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>Проекты</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>Сборки</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="a2idAxu" name="statistics.data_structures">
|
<unit id="a2idAxu" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
|
|
@ -1839,6 +1839,18 @@
|
||||||
<target>部件</target>
|
<target>部件</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="statistics.projects" name="statistics.projects">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.projects</source>
|
||||||
|
<target>项目</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bd7zT1d" name="statistics.assemblies">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>statistics.assemblies</source>
|
||||||
|
<target>组件</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="QRdK9zd" name="statistics.data_structures">
|
<unit id="QRdK9zd" name="statistics.data_structures">
|
||||||
<notes>
|
<notes>
|
||||||
<note priority="1">new</note>
|
<note priority="1">new</note>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue