Added simple info page for projects

This commit is contained in:
Jan Böhmer 2022-12-18 21:58:21 +01:00
parent 855b3070bb
commit d5b1c6be0a
12 changed files with 647 additions and 76 deletions

View file

@ -0,0 +1,61 @@
<?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/>.
*/
namespace App\Controller;
use App\DataTables\ProjectBomEntriesDataTable;
use App\Entity\ProjectSystem\Project;
use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/project")
*/
class ProjectController extends AbstractController
{
private DataTableFactory $dataTableFactory;
public function __construct(DataTableFactory $dataTableFactory)
{
$this->dataTableFactory = $dataTableFactory;
}
/**
* @Route("/{id}", name="project_info")
*/
public function info(Project $project, Request $request)
{
$this->denyAccessUnlessGranted('read', $project);
$table = $this->dataTableFactory->createFromType(ProjectBomEntriesDataTable::class, ['project' => $project])
->handleRequest($request);
if ($table->isCallback()) {
return $table->getResponse();
}
return $this->render('Projects/info.html.twig', [
'datatable' => $table,
'project' => $project,
]);
}
}

View file

@ -65,8 +65,8 @@ class EntityColumn extends AbstractColumn
});
$resolver->setDefault('render', function (Options $options) {
return function ($value, Part $context) use ($options) {
/** @var AbstractDBElement|null $entity */
return function ($value, $context) use ($options) {
/** @var AbstractNamedDBElement|null $entity */
$entity = $this->accessor->getValue($context, $options['property']);
if (null !== $entity) {
@ -74,7 +74,7 @@ class EntityColumn extends AbstractColumn
return sprintf(
'<a href="%s">%s</a>',
$this->urlGenerator->listPartsURL($entity),
$value
$entity->getName()
);
}

View file

@ -0,0 +1,91 @@
<?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/>.
*/
namespace App\DataTables\Helpers;
use App\Entity\Parts\Part;
use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\Attachments\PartPreviewGenerator;
use App\Services\EntityURLGenerator;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* A helper service which contains common code to render columns for part related tables
*/
class PartDataTableHelper
{
private PartPreviewGenerator $previewGenerator;
private AttachmentURLGenerator $attachmentURLGenerator;
private TranslatorInterface $translator;
private EntityURLGenerator $entityURLGenerator;
public function __construct(PartPreviewGenerator $previewGenerator, AttachmentURLGenerator $attachmentURLGenerator,
EntityURLGenerator $entityURLGenerator, TranslatorInterface $translator)
{
$this->previewGenerator = $previewGenerator;
$this->attachmentURLGenerator = $attachmentURLGenerator;
$this->translator = $translator;
$this->entityURLGenerator = $entityURLGenerator;
}
public function renderName(Part $context): string
{
$icon = '';
//Depending on the part status we show a different icon (the later conditions have higher priority)
if ($context->isFavorite()) {
$icon = sprintf('<i class="fa-solid fa-star fa-fw me-1" title="%s"></i>', $this->translator->trans('part.favorite.badge'));
}
if ($context->isNeedsReview()) {
$icon = sprintf('<i class="fa-solid fa-ambulance fa-fw me-1" title="%s"></i>', $this->translator->trans('part.needs_review.badge'));
}
return sprintf(
'<a href="%s">%s%s</a>',
$this->entityURLGenerator->infoURL($context),
$icon,
htmlentities($context->getName())
);
}
public function renderPicture(Part $context): string
{
$preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context);
if (null === $preview_attachment) {
return '';
}
$title = htmlspecialchars($preview_attachment->getName());
if ($preview_attachment->getFilename()) {
$title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')';
}
return sprintf(
'<img alt="%s" src="%s" data-thumbnail="%s" class="%s" data-title="%s" data-controller="elements--hoverpic">',
'Part image',
$this->attachmentURLGenerator->getThumbnailURL($preview_attachment),
$this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'),
'img-fluid hoverpic',
$title
);
}
}

View file

@ -34,6 +34,7 @@ use App\DataTables\Column\SIUnitNumberColumn;
use App\DataTables\Column\TagsColumn;
use App\DataTables\Filters\PartFilter;
use App\DataTables\Filters\PartSearchFilter;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
@ -63,26 +64,27 @@ final class PartsDataTable implements DataTableTypeInterface
private TranslatorInterface $translator;
private NodesListBuilder $treeBuilder;
private AmountFormatter $amountFormatter;
private PartPreviewGenerator $previewGenerator;
private AttachmentURLGenerator $attachmentURLGenerator;
private Security $security;
private PartDataTableHelper $partDataTableHelper;
/**
* @var EntityURLGenerator
*/
private $urlGenerator;
public function __construct(EntityURLGenerator $urlGenerator, TranslatorInterface $translator,
NodesListBuilder $treeBuilder, AmountFormatter $amountFormatter,
PartPreviewGenerator $previewGenerator, AttachmentURLGenerator $attachmentURLGenerator, Security $security)
NodesListBuilder $treeBuilder, AmountFormatter $amountFormatter,PartDataTableHelper $partDataTableHelper,
AttachmentURLGenerator $attachmentURLGenerator, Security $security)
{
$this->urlGenerator = $urlGenerator;
$this->translator = $translator;
$this->treeBuilder = $treeBuilder;
$this->amountFormatter = $amountFormatter;
$this->previewGenerator = $previewGenerator;
$this->attachmentURLGenerator = $attachmentURLGenerator;
$this->security = $security;
$this->partDataTableHelper = $partDataTableHelper;
}
public function configureOptions(OptionsResolver $optionsResolver): void
@ -122,46 +124,13 @@ final class PartsDataTable implements DataTableTypeInterface
'label' => '',
'className' => 'no-colvis',
'render' => function ($value, Part $context) {
$preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context);
if (null === $preview_attachment) {
return '';
}
$title = htmlspecialchars($preview_attachment->getName());
if ($preview_attachment->getFilename()) {
$title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')';
}
return sprintf(
'<img alt="%s" src="%s" data-thumbnail="%s" class="%s" data-title="%s" data-controller="elements--hoverpic">',
'Part image',
$this->attachmentURLGenerator->getThumbnailURL($preview_attachment),
$this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'),
'img-fluid hoverpic',
$title
);
return $this->partDataTableHelper->renderPicture($context);
},
])
->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'render' => function ($value, Part $context) {
$icon = '';
//Depending on the part status we show a different icon (the later conditions have higher priority)
if ($context->isFavorite()) {
$icon = sprintf('<i class="fa-solid fa-star fa-fw me-1" title="%s"></i>', $this->translator->trans('part.favorite.badge'));
}
if ($context->isNeedsReview()) {
$icon = sprintf('<i class="fa-solid fa-ambulance fa-fw me-1" title="%s"></i>', $this->translator->trans('part.needs_review.badge'));
}
return sprintf(
'<a href="%s">%s%s</a>',
$this->urlGenerator->infoURL($context),
$icon,
htmlentities($context->getName())
);
return $this->partDataTableHelper->renderName($context);
},
])
->add('id', TextColumn::class, [

View file

@ -0,0 +1,178 @@
<?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/>.
*/
namespace App\DataTables;
use App\DataTables\Column\EntityColumn;
use App\DataTables\Column\LocaleDateTimeColumn;
use App\DataTables\Column\MarkdownColumn;
use App\DataTables\Column\SelectColumn;
use App\DataTables\Helpers\PartDataTableHelper;
use App\Entity\Attachments\Attachment;
use App\Entity\Parts\Part;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter;
use Doctrine\ORM\QueryBuilder;
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ProjectBomEntriesDataTable implements DataTableTypeInterface
{
protected TranslatorInterface $translator;
protected PartDataTableHelper $partDataTableHelper;
protected EntityURLGenerator $entityURLGenerator;
protected AmountFormatter $amountFormatter;
public function __construct(TranslatorInterface $translator, PartDataTableHelper $partDataTableHelper,
EntityURLGenerator $entityURLGenerator, AmountFormatter $amountFormatter)
{
$this->translator = $translator;
$this->partDataTableHelper = $partDataTableHelper;
$this->entityURLGenerator = $entityURLGenerator;
$this->amountFormatter = $amountFormatter;
}
public function configure(DataTable $dataTable, array $options)
{
$dataTable
//->add('select', SelectColumn::class)
->add('picture', TextColumn::class, [
'label' => '',
'className' => 'no-colvis',
'render' => function ($value, ProjectBOMEntry $context) {
if($context->getPart() === null) {
return '';
}
return $this->partDataTableHelper->renderPicture($context->getPart());
},
])
->add('id', TextColumn::class, [
'label' => $this->translator->trans('part.table.id'),
'visible' => false,
])
->add('quantity', TextColumn::class, [
'label' => $this->translator->trans('project.bom.quantity'),
'className' => 'text-center',
'render' => function ($value, ProjectBOMEntry $context) {
//If we have a non-part entry, only show the rounded quantity
if ($context->getPart() === null) {
return round($context->getQuantity());
}
//Otherwise use the unit of the part to format the quantity
return $this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit());
},
])
->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'orderable' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if($context->getPart() === null) {
return $context->getName();
}
if($context->getPart() !== null) {
$tmp = $this->partDataTableHelper->renderName($context->getPart());
if(!empty($context->getName())) {
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
}
return $tmp;
}
},
])
->add('description', MarkdownColumn::class, [
'label' => $this->translator->trans('part.table.description'),
'data' => function (ProjectBOMEntry $context) {
if($context->getPart() !== null) {
return $context->getPart()->getDescription();
}
//For non-part BOM entries show the comment field
return $context->getComment();
},
])
->add('category', EntityColumn::class, [
'label' => $this->translator->trans('part.table.category'),
'property' => 'part.category',
])
->add('footprint', EntityColumn::class, [
'property' => 'part.footprint',
'label' => $this->translator->trans('part.table.footprint'),
])
->add('manufacturer', EntityColumn::class, [
'property' => 'part.manufacturer',
'label' => $this->translator->trans('part.table.manufacturer'),
])
->add('mountnames', TextColumn::class, [
])
->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->createAdapter(ORMAdapter::class, [
'entity' => Attachment::class,
'query' => function (QueryBuilder $builder) use ($options): void {
$this->getQuery($builder, $options);
},
'criteria' => [
function (QueryBuilder $builder) use ($options): void {
$this->buildCriteria($builder, $options);
},
new SearchCriteriaProvider(),
],
]);
}
private function getQuery(QueryBuilder $builder, array $options): void
{
$builder->select('bom_entry')
->addSelect('part')
->from(ProjectBOMEntry::class, 'bom_entry')
->leftJoin('bom_entry.part', 'part')
->where('bom_entry.project = :project')
->setParameter('project', $options['project']);
;
}
private function buildCriteria(QueryBuilder $builder, array $options): void
{
}
}

View file

@ -52,7 +52,7 @@ class Project extends AbstractStructuralDBElement
protected $parent;
/**
* @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="device")
* @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="project")
*/
protected $bom_entries;

View file

@ -68,7 +68,7 @@ class ProjectBOMEntry extends AbstractDBElement
* @ORM\ManyToOne(targetEntity="Project", inversedBy="parts")
* @ORM\JoinColumn(name="id_device", referencedColumnName="id")
*/
protected ?Project $device = null;
protected ?Project $project = null;
/**
* @var Part|null The part associated with this
@ -76,4 +76,116 @@ class ProjectBOMEntry extends AbstractDBElement
* @ORM\JoinColumn(name="id_part", referencedColumnName="id", nullable=true)
*/
protected ?Part $part = null;
/**
* @return float
*/
public function getQuantity(): float
{
return $this->quantity;
}
/**
* @param float $quantity
* @return ProjectBOMEntry
*/
public function setQuantity(float $quantity): ProjectBOMEntry
{
$this->quantity = $quantity;
return $this;
}
/**
* @return string
*/
public function getMountnames(): string
{
return $this->mountnames;
}
/**
* @param string $mountnames
* @return ProjectBOMEntry
*/
public function setMountnames(string $mountnames): ProjectBOMEntry
{
$this->mountnames = $mountnames;
return $this;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
* @return ProjectBOMEntry
*/
public function setName(string $name): ProjectBOMEntry
{
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getComment(): string
{
return $this->comment;
}
/**
* @param string $comment
* @return ProjectBOMEntry
*/
public function setComment(string $comment): ProjectBOMEntry
{
$this->comment = $comment;
return $this;
}
/**
* @return Project|null
*/
public function getProject(): ?Project
{
return $this->project;
}
/**
* @param Project|null $project
* @return ProjectBOMEntry
*/
public function setProject(?Project $project): ProjectBOMEntry
{
$this->project = $project;
return $this;
}
/**
* @return Part|null
*/
public function getPart(): ?Part
{
return $this->part;
}
/**
* @param Part|null $part
* @return ProjectBOMEntry
*/
public function setPart(?Part $part): ProjectBOMEntry
{
$this->part = $part;
return $this;
}
}

View file

@ -322,6 +322,8 @@ class EntityURLGenerator
public function listPartsURL(AbstractDBElement $entity): string
{
$map = [
Project::class => 'project_info',
Category::class => 'part_list_category',
Footprint::class => 'part_list_footprint',
Manufacturer::class => 'part_list_manufacturer',

View file

@ -110,7 +110,7 @@ class TreeViewGenerator
}
if ($mode === 'devices') {
$href_type = '';
$href_type = 'list_parts';
}
$generic = $this->getGenericTree($class, $parent);

View file

@ -0,0 +1,133 @@
{% import "helper.twig" as helper %}
{% import "LabelSystem/dropdown_macro.html.twig" as dropdown %}
{{ helper.breadcrumb_entity_link(project) }}
<div class="accordion mb-4" id="listAccordion">
<div class="accordion-item">
<div class="accordion-header">
<button class="accordion-button collapsed py-2" data-bs-toggle="collapse" data-bs-target="#entityInfo" aria-expanded="true">
{% if project.masterPictureAttachment is not null and attachment_manager.isFileExisting(project.masterPictureAttachment) %}
<img class="hoverpic ms-0 me-1 d-inline" {{ stimulus_controller('elements/hoverpic') }} data-thumbnail="{{ entity_url(project.masterPictureAttachment, 'file_view') }}" src="{{ attachment_thumbnail(project.masterPictureAttachment, 'thumbnail_sm') }}">
{% else %}
{{ helper.entity_icon(project, "me-1") }}
{% endif %}
{% trans %}device.label{% endtrans %}:&nbsp;<b>{{ project.name }}</b>
</button>
</div>
<div id="entityInfo" class="accordion-collapse collapse show" data-bs-parent="#listAccordion">
<div class="accordion-body">
{% if project.description is not empty %}
{{ project.description|format_markdown }}
{% endif %}
<div class="row">
<div class="col-sm-2">
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<a class="nav-link active" id="v-pills-home-tab" data-bs-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">
<i class="fas fa-info-circle fa-fw"></i>
{% trans %}entity.info.common.tab{% endtrans %}
</a>
<a class="nav-link" id="v-pills-statistics-tab" data-bs-toggle="pill" href="#v-pills-statistics" role="tab" aria-controls="v-pills-profile" aria-selected="false">
<i class="fas fa-chart-pie fa-fw"></i>
{% trans %}entity.info.statistics.tab{% endtrans %}
</a>
{% if project.attachments is not empty %}
<a class="nav-link" id="v-pills-attachments-tab" data-bs-toggle="pill" href="#v-pills-attachments" role="tab" aria-controls="v-pills-attachments" aria-selected="false">
<i class="fas fa-paperclip fa-fw"></i>
{% trans %}entity.info.attachments.tab{% endtrans %}
</a>
{% endif %}
{% if project.parameters is not empty %}
<a class="nav-link" id="v-pills-parameters-tab" data-bs-toggle="pill" href="#v-pills-parameters" role="tab" aria-controls="v-pills-parameters" aria-selected="false">
<i class="fas fa-atlas fa-fw"></i>
{% trans %}entity.info.parameters.tab{% endtrans %}
</a>
{% endif %}
{% if project.comment is not empty %}
<a class="nav-link" id="v-pills-comment-tab" data-bs-toggle="pill" href="#v-pills-comment" role="tab">
<i class="fas fa-comment-alt fa-fw"></i>
{% trans %}comment.label{% endtrans %}
</a>
{% endif %}
</div>
</div>
<div class="col-sm-10">
<div class="tab-content" id="v-pills-tabContent">
<div class="tab-pane fade show active" id="v-pills-home" role="tabpanel" aria-labelledby="v-pills-home-tab">
<div class="row">
<div class="col-sm-9 form-horizontal">
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.name{% endtrans %}:</label>
<span class="col-sm form-control-static">{{ project.name }}</span>
</div>
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.parent{% endtrans %}:</label>
<span class="col-sm form-control-static">
{% if project.parent %}
{{ project.parent.fullPath }}
{% else %}
-
{% endif %}
</span>
</div>
</div>
<div class="col-sm-3">
{% block quick_links %}{% endblock %}
<a class="btn btn-secondary w-100 mb-2" href="{{ entity_url(project, 'edit') }}">
<i class="fas fa-edit"></i> {% trans %}entity.edit.btn{% endtrans %}
</a>
<div class="">
<span class="text-muted" title="{% trans %}lastModified{% endtrans %}">
<i class="fas fa-history fa-fw"></i> {{ project.lastModified | format_datetime("short") }}
</span>
<br>
<span class="text-muted mt-1" title="{% trans %}createdAt{% endtrans %}">
<i class="fas fa-calendar-plus fa-fw"></i> {{ project.addedDate | format_datetime("short") }}
</span>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="v-pills-statistics" role="tabpanel" aria-labelledby="v-pills-statistics-tab">
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.children_count{% endtrans %}:</label>
<span class="col-sm form-control-static">{{ project.children | length }}</span>
</div>
<div class="form-group">
<label class="col-sm-4">{% trans %}entity.info.parts_count{% endtrans %}:</label>
<span class="col-sm form-control-static">{{ project.bomEntries | length }}</span>
</div>
</div>
</div>
{% if project.attachments is not empty %}
<div class="tab-pane fade" id="v-pills-attachments" role="tabpanel" aria-labelledby="v-pills-attachments-tab">
{% include "Parts/info/_attachments_info.html.twig" with {"part": project} %}
</div>
{% endif %}
{% if project.parameters is not empty %}
<div class="tab-pane fade" id="v-pills-parameters" role="tabpanel" aria-labelledby="v-pills-parameters-tab">
{% for name, parameters in project.groupedParameters %}
{% if name is not empty %}<h5 class="mt-1">{{ name }}</h5>{% endif %}
{{ helper.parameters_table(project) }}
{% endfor %}
</div>
{% endif %}
{% if project.comment is not empty %}
<div class="tab-pane fade" id="v-pills-comment" role="tabpanel" aria-labelledby="home-tab">
<div class="container-fluid mt-2 latex" data-controller="common--latex">
{{ project.comment|format_markdown }}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,19 @@
{% extends "base.html.twig" %}
{% import "components/datatables.macro.html.twig" as datatables %}
{% block title %}
{% trans %}parts_list.category.title{% endtrans %} {{ project.name }}
{% endblock %}
{% block content %}
{% include "Projects/_info_card.html.twig" %}
{{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }}
{# {% include "Parts/lists/_action_bar.html.twig" with {'url_options': {'category': entity.iD}} %}
{% include "Parts/lists/_parts_list.html.twig" %} #}
{% endblock %}

View file

@ -8693,7 +8693,7 @@ Element 3</target>
</segment>
</unit>
<unit id="RAX6xpX" name="tfa.provider.webauthn_two_factor_provider">
<segment state="translated">
<segment>
<source>tfa.provider.webauthn_two_factor_provider</source>
<target>Security key</target>
</segment>
@ -9768,184 +9768,190 @@ Element 3</target>
</segment>
</unit>
<unit id="dU7EyhM" name="entity.info.parts_count_recursive">
<segment state="translated">
<segment>
<source>entity.info.parts_count_recursive</source>
<target>Number of parts with this element or its subelements</target>
</segment>
</unit>
<unit id="_hKlKv." name="tools.server_infos.title">
<segment state="translated">
<segment>
<source>tools.server_infos.title</source>
<target>Server Infos</target>
</segment>
</unit>
<unit id="NvclBUL" name="permission.preset.read_only">
<segment state="translated">
<segment>
<source>permission.preset.read_only</source>
<target>Read-Only</target>
</segment>
</unit>
<unit id="HD3j3BW" name="permission.preset.read_only.desc">
<segment state="translated">
<segment>
<source>permission.preset.read_only.desc</source>
<target>Only allow read operations on data</target>
</segment>
</unit>
<unit id="Ge20aJg" name="permission.preset.all_inherit">
<segment state="translated">
<segment>
<source>permission.preset.all_inherit</source>
<target>Inherit all</target>
</segment>
</unit>
<unit id="DJpsLcr" name="permission.preset.all_inherit.desc">
<segment state="translated">
<segment>
<source>permission.preset.all_inherit.desc</source>
<target>Set all permissions to Inherit</target>
</segment>
</unit>
<unit id="lzjvvzm" name="permission.preset.all_forbid">
<segment state="translated">
<segment>
<source>permission.preset.all_forbid</source>
<target>Forbid all</target>
</segment>
</unit>
<unit id="QqQDTyH" name="permission.preset.all_forbid.desc">
<segment state="translated">
<segment>
<source>permission.preset.all_forbid.desc</source>
<target>Set all permissions to Forbid</target>
</segment>
</unit>
<unit id="DV2fh6l" name="permission.preset.all_allow">
<segment state="translated">
<segment>
<source>permission.preset.all_allow</source>
<target>Allow all</target>
</segment>
</unit>
<unit id="_m.Pbza" name="permission.preset.all_allow.desc">
<segment state="translated">
<segment>
<source>permission.preset.all_allow.desc</source>
<target>Set all permissions to allow</target>
</segment>
</unit>
<unit id="VIDdo5K" name="perm.server_infos">
<segment state="translated">
<segment>
<source>perm.server_infos</source>
<target>Server infos</target>
</segment>
</unit>
<unit id="d6SOlzR" name="permission.preset.editor">
<segment state="translated">
<segment>
<source>permission.preset.editor</source>
<target>Editor</target>
</segment>
</unit>
<unit id="8KYl_wh" name="permission.preset.editor.desc">
<segment state="translated">
<segment>
<source>permission.preset.editor.desc</source>
<target>Allow to change parts and data structures</target>
</segment>
</unit>
<unit id="dYudjp." name="permission.preset.admin">
<segment state="translated">
<segment>
<source>permission.preset.admin</source>
<target>Admin</target>
</segment>
</unit>
<unit id="0o2M0uj" name="permission.preset.admin.desc">
<segment state="translated">
<segment>
<source>permission.preset.admin.desc</source>
<target>Allow administrative actions</target>
</segment>
</unit>
<unit id="SnAIVQf" name="permission.preset.button">
<segment state="translated">
<segment>
<source>permission.preset.button</source>
<target>Apply preset</target>
</segment>
</unit>
<unit id="6q4uHDx" name="perm.attachments.show_private">
<segment state="translated">
<segment>
<source>perm.attachments.show_private</source>
<target>Show private attachments</target>
</segment>
</unit>
<unit id="NL9t5hy" name="perm.attachments.list_attachments">
<segment state="translated">
<segment>
<source>perm.attachments.list_attachments</source>
<target>Show list of all attachments</target>
</segment>
</unit>
<unit id="PYh9dNP" name="user.edit.permission_success">
<segment state="translated">
<segment>
<source>user.edit.permission_success</source>
<target>Permission preset applied successfully. Check if the new permissions fit your needs.</target>
</segment>
</unit>
<unit id="cP8VNKS" name="perm.group.data">
<segment state="translated">
<segment>
<source>perm.group.data</source>
<target>Data</target>
</segment>
</unit>
<unit id="AAoGo_X" name="part_list.action.action.group.needs_review">
<segment state="translated">
<segment>
<source>part_list.action.action.group.needs_review</source>
<target>Needs Review</target>
</segment>
</unit>
<unit id="hcvOTrH" name="part_list.action.action.set_needs_review">
<segment state="translated">
<segment>
<source>part_list.action.action.set_needs_review</source>
<target>Set Needs Review Status</target>
</segment>
</unit>
<unit id="E1AQubV" name="part_list.action.action.unset_needs_review">
<segment state="translated">
<segment>
<source>part_list.action.action.unset_needs_review</source>
<target>Unset Needs Review Status</target>
</segment>
</unit>
<unit id="DNEEkTy" name="part.edit.ipn">
<segment state="translated">
<segment>
<source>part.edit.ipn</source>
<target>Internal Part Number (IPN)</target>
</segment>
</unit>
<unit id="bT6yxOA" name="part.ipn.not_defined">
<segment state="translated">
<segment>
<source>part.ipn.not_defined</source>
<target>Not defined</target>
</segment>
</unit>
<unit id="SHo2Ejq" name="part.table.ipn">
<segment state="translated">
<segment>
<source>part.table.ipn</source>
<target>IPN</target>
</segment>
</unit>
<unit id="1HcqCmo" name="currency.edit.update_rate">
<segment state="translated">
<segment>
<source>currency.edit.update_rate</source>
<target>Retrieve exchange rate</target>
</segment>
</unit>
<unit id="jSf6Wmz" name="currency.edit.exchange_rate_update.unsupported_currency">
<segment state="translated">
<segment>
<source>currency.edit.exchange_rate_update.unsupported_currency</source>
<target>The currency is unsupported by the exchange rate provider. Check your exchange rate provider configuration.</target>
</segment>
</unit>
<unit id="D481NZD" name="currency.edit.exchange_rate_update.generic_error">
<segment state="translated">
<segment>
<source>currency.edit.exchange_rate_update.generic_error</source>
<target>Unable to retrieve the exchange rate. Check your exchange rate provider configuration.</target>
</segment>
</unit>
<unit id="E_M7mZ5" name="currency.edit.exchange_rate_updated.success">
<segment state="translated">
<segment>
<source>currency.edit.exchange_rate_updated.success</source>
<target>Retrieved the exchange rate successfully.</target>
</segment>
</unit>
<unit id="HbPND5j" name="project.bom.quantity">
<segment>
<source>project.bom.quantity</source>
<target>BOM Qty.</target>
</segment>
</unit>
</file>
</xliff>