diff --git a/web/modules/custom/figli_compta_ledger/css/admin-chrome.css b/web/modules/custom/figli_compta_ledger/css/admin-chrome.css index cb5a2fc..6ad8ee8 100644 --- a/web/modules/custom/figli_compta_ledger/css/admin-chrome.css +++ b/web/modules/custom/figli_compta_ledger/css/admin-chrome.css @@ -36,6 +36,13 @@ html.gin--dark-mode .figli-page-nav a.is-active { 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 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 diff --git a/web/modules/custom/figli_compta_ledger/src/Form/LinkEntreeForm.php b/web/modules/custom/figli_compta_ledger/src/Form/LinkEntreeForm.php index 4d73ff0..6070245 100644 --- a/web/modules/custom/figli_compta_ledger/src/Form/LinkEntreeForm.php +++ b/web/modules/custom/figli_compta_ledger/src/Form/LinkEntreeForm.php @@ -15,9 +15,15 @@ use Drupal\node\NodeInterface; * 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 * multi-value (cardinality unlimited) since one payment sometimes covers - * several client invoices at once; #tags renders that as a single - * comma-separated autocomplete field instead of a Drupal "add another - * item" widget. + * several client invoices at once; one autocomplete field per linked + * entrée plus an "Ajouter une référence" button, mirroring the classic + * 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 { @@ -34,20 +40,50 @@ class LinkEntreeForm extends FormBase { public function buildForm(array $form, FormStateInterface $form_state, ?NodeInterface $node = NULL) { $form_state->set('node', $node); - $form['field_entree_liee'] = [ - '#type' => 'entity_autocomplete', - '#title' => $this->t('Entrées clients liées'), - '#target_type' => 'node', - '#tags' => TRUE, - '#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' => $node->get('field_entree_liee')->referencedEntities(), - '#description' => $this->t('Laisser vide pour retirer tous les liens. Plusieurs entrées possibles (paiement en plusieurs fois) : séparez-les par une virgule.'), + // Number of entrée-liée fields to render -- seeded from the node's + // current values on first build, then only grown by addItem() across + // AJAX rebuilds. At least one field even when nothing is linked yet. + if ($form_state->get('items_count') === NULL) { + $existing_count = $node->get('field_entree_liee')->count(); + $form_state->set('items_count', max($existing_count, 1)); + } + $existing = $node->get('field_entree_liee')->referencedEntities(); + + $form['#tree'] = TRUE; + $form['items'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'figli-entree-liee-items'], + ]; + 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' => '

' . $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.') . '

', ]; $form['actions'] = ['#type' => 'actions']; @@ -62,17 +98,34 @@ class LinkEntreeForm extends FormBase { 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} */ public function submitForm(array &$form, FormStateInterface $form_state) { /** @var \Drupal\node\NodeInterface $node */ $node = $form_state->get('node'); - // #tags => TRUE normalizes the submitted value to the field API's own - // multi-value shape (an array of ['target_id' => ..., ...] items, one - // per comma-separated entry), so this is just field-API assignment, - // no manual tag-string parsing needed. - $values = $form_state->getValue('field_entree_liee') ?: []; + $values = []; + foreach ($form_state->getValue('items') as $item) { + if (!empty($item['target'])) { + $values[] = ['target_id' => $item['target']]; + } + } $node->set('field_entree_liee', $values); $node->save(); }