mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-07-05 08:51:34 +00:00
Add batch EDA field editing from parts table
Users can now select multiple parts in any parts table and batch-edit their EDA/KiCad fields (symbol, footprint, reference prefix, value, visibility, exclude from BOM/board/sim). Each field has an "Apply" checkbox so users control exactly which fields are changed.
This commit is contained in:
parent
078f04fe67
commit
f314578790
6 changed files with 385 additions and 0 deletions
95
src/Controller/BatchEdaController.php
Normal file
95
src/Controller/BatchEdaController.php
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Form\Part\EDA\BatchEdaType;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class BatchEdaController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/tools/batch_eda_edit', name: 'batch_eda_edit')]
|
||||||
|
public function batchEdaEdit(Request $request): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('@parts.edit');
|
||||||
|
|
||||||
|
$ids = $request->query->getString('ids', '');
|
||||||
|
$redirectUrl = $request->query->getString('_redirect', '');
|
||||||
|
|
||||||
|
//Parse part IDs and load parts
|
||||||
|
$idArray = array_filter(array_map('intval', explode(',', $ids)));
|
||||||
|
$parts = $this->entityManager->getRepository(Part::class)->findBy(['id' => $idArray]);
|
||||||
|
|
||||||
|
if ($parts === []) {
|
||||||
|
$this->addFlash('error', 'batch_eda.no_parts_selected');
|
||||||
|
|
||||||
|
return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(BatchEdaType::class);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$updated = 0;
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->denyAccessUnlessGranted('edit', $part);
|
||||||
|
$edaInfo = $part->getEdaInfo();
|
||||||
|
|
||||||
|
if ($form->get('apply_reference_prefix')->getData()) {
|
||||||
|
$edaInfo->setReferencePrefix($form->get('reference_prefix')->getData() ?: null);
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
if ($form->get('apply_value')->getData()) {
|
||||||
|
$edaInfo->setValue($form->get('value')->getData() ?: null);
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
if ($form->get('apply_kicad_symbol')->getData()) {
|
||||||
|
$edaInfo->setKicadSymbol($form->get('kicad_symbol')->getData() ?: null);
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
if ($form->get('apply_kicad_footprint')->getData()) {
|
||||||
|
$edaInfo->setKicadFootprint($form->get('kicad_footprint')->getData() ?: null);
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
if ($form->get('apply_visibility')->getData()) {
|
||||||
|
$edaInfo->setVisibility($form->get('visibility')->getData());
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
if ($form->get('apply_exclude_from_bom')->getData()) {
|
||||||
|
$edaInfo->setExcludeFromBom($form->get('exclude_from_bom')->getData());
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
if ($form->get('apply_exclude_from_board')->getData()) {
|
||||||
|
$edaInfo->setExcludeFromBoard($form->get('exclude_from_board')->getData());
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
if ($form->get('apply_exclude_from_sim')->getData()) {
|
||||||
|
$edaInfo->setExcludeFromSim($form->get('exclude_from_sim')->getData());
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->addFlash('success', 'batch_eda.success');
|
||||||
|
|
||||||
|
return $redirectUrl !== '' ? $this->redirect($redirectUrl) : $this->redirectToRoute('parts_show_all');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('parts/batch_eda_edit.html.twig', [
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'parts' => $parts,
|
||||||
|
'redirect_url' => $redirectUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/Form/Part/EDA/BatchEdaType.php
Normal file
112
src/Form/Part/EDA/BatchEdaType.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Part\EDA;
|
||||||
|
|
||||||
|
use App\Form\Type\TriStateCheckboxType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
use function Symfony\Component\Translation\t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form type for batch editing EDA/KiCad fields on multiple parts at once.
|
||||||
|
* Each field has an "apply" checkbox — only checked fields are applied.
|
||||||
|
*/
|
||||||
|
class BatchEdaType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('reference_prefix', TextType::class, [
|
||||||
|
'label' => 'eda_info.reference_prefix',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => ['placeholder' => t('eda_info.reference_prefix.placeholder')],
|
||||||
|
])
|
||||||
|
->add('apply_reference_prefix', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('value', TextType::class, [
|
||||||
|
'label' => 'eda_info.value',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => ['placeholder' => t('eda_info.value.placeholder')],
|
||||||
|
])
|
||||||
|
->add('apply_value', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('kicad_symbol', KicadFieldAutocompleteType::class, [
|
||||||
|
'label' => 'eda_info.kicad_symbol',
|
||||||
|
'type' => KicadFieldAutocompleteType::TYPE_SYMBOL,
|
||||||
|
'required' => false,
|
||||||
|
'attr' => ['placeholder' => t('eda_info.kicad_symbol.placeholder')],
|
||||||
|
])
|
||||||
|
->add('apply_kicad_symbol', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('kicad_footprint', KicadFieldAutocompleteType::class, [
|
||||||
|
'label' => 'eda_info.kicad_footprint',
|
||||||
|
'type' => KicadFieldAutocompleteType::TYPE_FOOTPRINT,
|
||||||
|
'required' => false,
|
||||||
|
'attr' => ['placeholder' => t('eda_info.kicad_footprint.placeholder')],
|
||||||
|
])
|
||||||
|
->add('apply_kicad_footprint', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('visibility', TriStateCheckboxType::class, [
|
||||||
|
'label' => 'eda_info.visibility',
|
||||||
|
])
|
||||||
|
->add('apply_visibility', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('exclude_from_bom', TriStateCheckboxType::class, [
|
||||||
|
'label' => 'eda_info.exclude_from_bom',
|
||||||
|
])
|
||||||
|
->add('apply_exclude_from_bom', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('exclude_from_board', TriStateCheckboxType::class, [
|
||||||
|
'label' => 'eda_info.exclude_from_board',
|
||||||
|
])
|
||||||
|
->add('apply_exclude_from_board', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('exclude_from_sim', TriStateCheckboxType::class, [
|
||||||
|
'label' => 'eda_info.exclude_from_sim',
|
||||||
|
])
|
||||||
|
->add('apply_exclude_from_sim', CheckboxType::class, [
|
||||||
|
'label' => 'batch_eda.apply',
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
])
|
||||||
|
->add('submit', SubmitType::class, [
|
||||||
|
'label' => 'batch_eda.submit',
|
||||||
|
'attr' => ['class' => 'btn btn-primary'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -127,6 +127,15 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($action === 'batch_edit_eda') {
|
||||||
|
$ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts));
|
||||||
|
return new RedirectResponse(
|
||||||
|
$this->urlGenerator->generate('batch_eda_edit', [
|
||||||
|
'ids' => $ids,
|
||||||
|
'_redirect' => $redirect_url
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//Iterate over the parts and apply the action to it:
|
//Iterate over the parts and apply the action to it:
|
||||||
foreach ($selected_parts as $part) {
|
foreach ($selected_parts as $part) {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@
|
||||||
<option {% if not is_granted('@projects.read') %}disabled{% endif %} value="add_to_project" data-url="{{ path('select_project')}}">{% trans %}part_list.action.projects.add_to_project{% endtrans %}</option>
|
<option {% if not is_granted('@projects.read') %}disabled{% endif %} value="add_to_project" data-url="{{ path('select_project')}}">{% trans %}part_list.action.projects.add_to_project{% endtrans %}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup label="{% trans %}part_list.action.group.eda{% endtrans %}">
|
||||||
|
<option {% if not is_granted('@parts.edit') %}disabled{% endif %} value="batch_edit_eda" data-turbo="false">{% trans %}part_list.action.batch_edit_eda{% endtrans %}</option>
|
||||||
|
</optgroup>
|
||||||
<optgroup label="{% trans %}part_list.action.action.delete{% endtrans %}">
|
<optgroup label="{% trans %}part_list.action.action.delete{% endtrans %}">
|
||||||
<option {% if not is_granted('@parts.delete') %}disabled{% endif %} value="delete">{% trans %}part_list.action.action.delete{% endtrans %}</option>
|
<option {% if not is_granted('@parts.delete') %}disabled{% endif %} value="delete">{% trans %}part_list.action.action.delete{% endtrans %}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
|
||||||
88
templates/parts/batch_eda_edit.html.twig
Normal file
88
templates/parts/batch_eda_edit.html.twig
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
{% extends "main_card.html.twig" %}
|
||||||
|
|
||||||
|
{% block title %}{% trans %}batch_eda.title{% endtrans %}{% endblock %}
|
||||||
|
|
||||||
|
{% block card_title %}
|
||||||
|
<i class="fas fa-bolt"></i> {% trans %}batch_eda.title{% endtrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block card_content %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<p>{% trans with {'%count%': parts|length} %}batch_eda.description{% endtrans %}</p>
|
||||||
|
<details>
|
||||||
|
<summary>{% trans %}batch_eda.show_parts{% endtrans %}</summary>
|
||||||
|
<ul class="list-unstyled ms-3 mt-1">
|
||||||
|
{% for part in parts %}
|
||||||
|
<li><a href="{{ path('part_edit', {id: part.id}) }}">{{ part.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_start(form) }}
|
||||||
|
|
||||||
|
<p class="text-muted small">{% trans %}batch_eda.apply_hint{% endtrans %}</p>
|
||||||
|
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 30px;">{% trans %}batch_eda.apply{% endtrans %}</th>
|
||||||
|
<th>{% trans %}batch_eda.field{% endtrans %}</th>
|
||||||
|
<th>{% trans %}batch_eda.value{% endtrans %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_reference_prefix) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.reference_prefix) }}</td>
|
||||||
|
<td>{{ form_widget(form.reference_prefix, {'attr': {'class': 'form-control-sm'}}) }}{{ form_errors(form.reference_prefix) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_value) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.value) }}</td>
|
||||||
|
<td>{{ form_widget(form.value, {'attr': {'class': 'form-control-sm'}}) }}{{ form_errors(form.value) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_kicad_symbol) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.kicad_symbol) }}</td>
|
||||||
|
<td>{{ form_widget(form.kicad_symbol) }}{{ form_errors(form.kicad_symbol) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_kicad_footprint) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.kicad_footprint) }}</td>
|
||||||
|
<td>{{ form_widget(form.kicad_footprint) }}{{ form_errors(form.kicad_footprint) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_visibility) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.visibility) }}</td>
|
||||||
|
<td>{{ form_widget(form.visibility) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_exclude_from_bom) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.exclude_from_bom) }}</td>
|
||||||
|
<td>{{ form_widget(form.exclude_from_bom) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_exclude_from_board) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.exclude_from_board) }}</td>
|
||||||
|
<td>{{ form_widget(form.exclude_from_board) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center align-middle">{{ form_widget(form.apply_exclude_from_sim) }}</td>
|
||||||
|
<td class="align-middle">{{ form_label(form.exclude_from_sim) }}</td>
|
||||||
|
<td>{{ form_widget(form.exclude_from_sim) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
{% if redirect_url %}
|
||||||
|
<a href="{{ redirect_url }}" class="btn btn-secondary">{% trans %}batch_eda.cancel{% endtrans %}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ path('parts_show_all') }}" class="btn btn-secondary">{% trans %}batch_eda.cancel{% endtrans %}</a>
|
||||||
|
{% endif %}
|
||||||
|
{{ form_widget(form.submit) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -10917,6 +10917,84 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
||||||
<target>Bulk Info Provider Import</target>
|
<target>Bulk Info Provider Import</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="batchEdaGroup" name="part_list.action.group.eda">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part_list.action.group.eda</source>
|
||||||
|
<target>EDA / KiCad</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaAction" name="part_list.action.batch_edit_eda">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part_list.action.batch_edit_eda</source>
|
||||||
|
<target>Batch Edit EDA Fields</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaTitle" name="batch_eda.title">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.title</source>
|
||||||
|
<target>Batch Edit EDA Fields</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaDesc" name="batch_eda.description">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.description</source>
|
||||||
|
<target>Edit EDA/KiCad fields for %count% selected parts. Check the "Apply" box next to each field you want to change.</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaShowParts" name="batch_eda.show_parts">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.show_parts</source>
|
||||||
|
<target>Show selected parts</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaApplyHint" name="batch_eda.apply_hint">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.apply_hint</source>
|
||||||
|
<target>Only fields with the "Apply" checkbox checked will be changed. Unchecked fields are left unchanged.</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaApply" name="batch_eda.apply">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.apply</source>
|
||||||
|
<target>Apply</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaField" name="batch_eda.field">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.field</source>
|
||||||
|
<target>Field</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaValue" name="batch_eda.value">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.value</source>
|
||||||
|
<target>Value</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaSubmit" name="batch_eda.submit">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.submit</source>
|
||||||
|
<target>Apply to Selected Parts</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaCancel" name="batch_eda.cancel">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.cancel</source>
|
||||||
|
<target>Cancel</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaSuccess" name="batch_eda.success">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.success</source>
|
||||||
|
<target>EDA fields updated successfully.</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="batchEdaNoParts" name="batch_eda.no_parts_selected">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>batch_eda.no_parts_selected</source>
|
||||||
|
<target>No parts were selected for batch editing.</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
<unit id="yzpXFkB" name="info_providers.bulk_import.step1.spn_recommendation">
|
<unit id="yzpXFkB" name="info_providers.bulk_import.step1.spn_recommendation">
|
||||||
<segment state="translated">
|
<segment state="translated">
|
||||||
<source>info_providers.bulk_import.step1.spn_recommendation</source>
|
<source>info_providers.bulk_import.step1.spn_recommendation</source>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue