Replace the comma-separated tags field with one field per linked entrée
field_entree_liee is multi-value, but the single #tags entity_autocomplete rendered it as one comma-separated text box -- functionally correct but easy to mistake for "only one entrée can be linked". Rebuilt as one autocomplete field per link plus an "Ajouter une référence" button (the classic Drupal multi-value pattern, matching the Répartition paragraphs widget already used elsewhere in this form). Clearing a field's text drops that link on save, rather than a "remove" button -- a remove button would need to shift every later delta, which fights Drupal's own value-restoration-on-AJAX-rebuild (verified live: that restoration only works cleanly for pure appends at a stable delta, which is all addItem() does). Verified end-to-end via raw AJAX POSTs against a real 3-entrée versement: initial load pre-fills 3 separate fields, "Ajouter" appends a 4th empty one without disturbing the first 3, and submitting with one field cleared and a new one filled saves exactly the expected set.
This commit is contained in:
@@ -36,6 +36,13 @@ html.gin--dark-mode .figli-page-nav a.is-active {
|
|||||||
color: #e8e9ea;
|
color: #e8e9ea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "Lier à une entrée client" modal (LinkEntreeForm): one autocomplete
|
||||||
|
field per linked entrée instead of a single comma-separated field --
|
||||||
|
tighten the default spacing between them a little. */
|
||||||
|
#figli-entree-liee-items .form-item {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Drupal status messages: fixed top-right, out of document flow, so they
|
/* Drupal status messages: fixed top-right, out of document flow, so they
|
||||||
never shift the page layout the way the default in-flow placement does
|
never shift the page layout the way the default in-flow placement does
|
||||||
-- and shrunk to a fraction of Gin's default size, which is sized for a
|
-- and shrunk to a fraction of Gin's default size, which is sized for a
|
||||||
|
|||||||
@@ -15,9 +15,15 @@ use Drupal\node\NodeInterface;
|
|||||||
* need to touch this one field to link a versement/achat/hébergement to
|
* need to touch this one field to link a versement/achat/hébergement to
|
||||||
* the entrée client(s) it pays out against. field_entree_liee is
|
* the entrée client(s) it pays out against. field_entree_liee is
|
||||||
* multi-value (cardinality unlimited) since one payment sometimes covers
|
* multi-value (cardinality unlimited) since one payment sometimes covers
|
||||||
* several client invoices at once; #tags renders that as a single
|
* several client invoices at once; one autocomplete field per linked
|
||||||
* comma-separated autocomplete field instead of a Drupal "add another
|
* entrée plus an "Ajouter une référence" button, mirroring the classic
|
||||||
* item" widget.
|
* Drupal multi-value widget (e.g. the ligne_comptable form's own
|
||||||
|
* Répartition paragraphs) rather than a single comma-separated field.
|
||||||
|
* Clearing a field's text (rather than a dedicated "remove" button)
|
||||||
|
* drops that link -- see submitForm() -- since a "remove" button would
|
||||||
|
* need to shift every later delta, which fights Drupal's own
|
||||||
|
* value-restoration-on-AJAX-rebuild (that only works cleanly for pure
|
||||||
|
* appends at a stable delta, which is all addItem() below ever does).
|
||||||
*/
|
*/
|
||||||
class LinkEntreeForm extends FormBase {
|
class LinkEntreeForm extends FormBase {
|
||||||
|
|
||||||
@@ -34,20 +40,50 @@ class LinkEntreeForm extends FormBase {
|
|||||||
public function buildForm(array $form, FormStateInterface $form_state, ?NodeInterface $node = NULL) {
|
public function buildForm(array $form, FormStateInterface $form_state, ?NodeInterface $node = NULL) {
|
||||||
$form_state->set('node', $node);
|
$form_state->set('node', $node);
|
||||||
|
|
||||||
$form['field_entree_liee'] = [
|
// Number of entrée-liée fields to render -- seeded from the node's
|
||||||
'#type' => 'entity_autocomplete',
|
// current values on first build, then only grown by addItem() across
|
||||||
'#title' => $this->t('Entrées clients liées'),
|
// AJAX rebuilds. At least one field even when nothing is linked yet.
|
||||||
'#target_type' => 'node',
|
if ($form_state->get('items_count') === NULL) {
|
||||||
'#tags' => TRUE,
|
$existing_count = $node->get('field_entree_liee')->count();
|
||||||
'#selection_handler' => 'figli_compta_ledger:entree_client',
|
$form_state->set('items_count', max($existing_count, 1));
|
||||||
'#selection_settings' => [
|
}
|
||||||
'target_bundles' => ['ligne_comptable' => 'ligne_comptable'],
|
$existing = $node->get('field_entree_liee')->referencedEntities();
|
||||||
// Narrows the autocomplete to the same client as this sortie --
|
|
||||||
// read by EntreeClientSelection::buildEntityQuery().
|
$form['#tree'] = TRUE;
|
||||||
'entity' => $node,
|
$form['items'] = [
|
||||||
],
|
'#type' => 'container',
|
||||||
'#default_value' => $node->get('field_entree_liee')->referencedEntities(),
|
'#attributes' => ['id' => 'figli-entree-liee-items'],
|
||||||
'#description' => $this->t('Laisser vide pour retirer tous les liens. Plusieurs entrées possibles (paiement en plusieurs fois) : séparez-les par une virgule.'),
|
];
|
||||||
|
for ($delta = 0; $delta < $form_state->get('items_count'); $delta++) {
|
||||||
|
$form['items'][$delta] = [
|
||||||
|
'target' => [
|
||||||
|
'#type' => 'entity_autocomplete',
|
||||||
|
'#title' => $this->t('Entrée cliente liée'),
|
||||||
|
'#title_display' => 'invisible',
|
||||||
|
'#target_type' => 'node',
|
||||||
|
'#selection_handler' => 'figli_compta_ledger:entree_client',
|
||||||
|
'#selection_settings' => [
|
||||||
|
'target_bundles' => ['ligne_comptable' => 'ligne_comptable'],
|
||||||
|
// Narrows the autocomplete to the same client as this sortie
|
||||||
|
// -- read by EntreeClientSelection::buildEntityQuery().
|
||||||
|
'entity' => $node,
|
||||||
|
],
|
||||||
|
'#default_value' => $existing[$delta] ?? NULL,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$form['add'] = [
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#value' => $this->t('+ Ajouter une référence'),
|
||||||
|
'#submit' => ['::addItem'],
|
||||||
|
'#ajax' => ['callback' => '::ajaxRefreshItems', 'wrapper' => 'figli-entree-liee-items'],
|
||||||
|
'#limit_validation_errors' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$form['description'] = [
|
||||||
|
'#weight' => -10,
|
||||||
|
'#markup' => '<p class="description">' . $this->t('Plusieurs entrées possibles (paiement en plusieurs fois) : une référence par champ, ajoutez-en avec le bouton ci-dessus. Vider un champ retire ce lien.') . '</p>',
|
||||||
];
|
];
|
||||||
|
|
||||||
$form['actions'] = ['#type' => 'actions'];
|
$form['actions'] = ['#type' => 'actions'];
|
||||||
@@ -62,17 +98,34 @@ class LinkEntreeForm extends FormBase {
|
|||||||
return $form;
|
return $form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #submit for "+ Ajouter une référence".
|
||||||
|
*/
|
||||||
|
public function addItem(array &$form, FormStateInterface $form_state) {
|
||||||
|
$form_state->set('items_count', $form_state->get('items_count') + 1);
|
||||||
|
$form_state->setRebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #ajax callback for "Ajouter" -- replaces just the fields container,
|
||||||
|
* leaving the rest of the (still-open) modal alone.
|
||||||
|
*/
|
||||||
|
public function ajaxRefreshItems(array &$form, FormStateInterface $form_state) {
|
||||||
|
return $form['items'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||||
/** @var \Drupal\node\NodeInterface $node */
|
/** @var \Drupal\node\NodeInterface $node */
|
||||||
$node = $form_state->get('node');
|
$node = $form_state->get('node');
|
||||||
// #tags => TRUE normalizes the submitted value to the field API's own
|
$values = [];
|
||||||
// multi-value shape (an array of ['target_id' => ..., ...] items, one
|
foreach ($form_state->getValue('items') as $item) {
|
||||||
// per comma-separated entry), so this is just field-API assignment,
|
if (!empty($item['target'])) {
|
||||||
// no manual tag-string parsing needed.
|
$values[] = ['target_id' => $item['target']];
|
||||||
$values = $form_state->getValue('field_entree_liee') ?: [];
|
}
|
||||||
|
}
|
||||||
$node->set('field_entree_liee', $values);
|
$node->set('field_entree_liee', $values);
|
||||||
$node->save();
|
$node->save();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user