From f988ec3934ecc99598f264c2c27ebb7455237f8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Harri=20H=C3=A4iv=C3=A4l=C3=A4?= <haivala@cc.hut.fi>
Date: Thu, 10 Apr 2025 10:30:53 +0300
Subject: [PATCH 1/4] WIP: sorting for live component

---
 .../src/Form/Type/LiveCollectionType.php      | 60 +++++++++++++++++++
 src/LiveComponent/src/LiveCollectionTrait.php | 40 +++++++++++++
 .../templates/form_theme.html.twig            |  6 ++
 3 files changed, 106 insertions(+)

diff --git a/src/LiveComponent/src/Form/Type/LiveCollectionType.php b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
index 2fc3aea95f3..c1a54742fdf 100644
--- a/src/LiveComponent/src/Form/Type/LiveCollectionType.php
+++ b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
@@ -35,6 +35,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
             $prototype = $builder->create('delete', $options['button_delete_type'], $options['button_delete_options']);
             $builder->setAttribute('button_delete_prototype', $prototype->getForm());
         }
+        
+        if ($options['allow_sort']) {
+            $moveUpPrototype = $builder->create('move_up', $options['button_move_up_type'], $options['button_move_up_options']);
+            $builder->setAttribute('button_move_up_prototype', $moveUpPrototype->getForm());
+            
+            $moveDownPrototype = $builder->create('move_down', $options['button_move_down_type'], $options['button_move_down_options']);
+            $builder->setAttribute('button_move_down_prototype', $moveDownPrototype->getForm());
+        }
     }
 
     public function buildView(FormView $view, FormInterface $form, array $options): void
@@ -92,6 +100,53 @@ public function finishView(FormView $view, FormInterface $form, array $options):
                 array_splice($entryView->vars['button_delete']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete');
             }
         }
+        
+        // Add move up and move down buttons
+        if ($form->getConfig()->hasAttribute('button_move_up_prototype')) {
+            $prototype = $form->getConfig()->getAttribute('button_move_up_prototype');
+
+            $prototypes = [];
+            foreach ($form as $k => $entry) {
+                $prototypes[$k] = clone $prototype;
+                $prototypes[$k]->setParent($entry);
+            }
+
+            foreach ($view as $k => $entryView) {
+                $entryView->vars['button_move_up'] = $prototypes[$k]->createView($entryView);
+
+                $attr = $entryView->vars['button_move_up']->vars['attr'];
+                $attr['data-action'] ??= 'live#action';
+                $attr['data-live-action-param'] ??= 'moveCollectionItemUp';
+                $attr['data-live-name-param'] ??= $view->vars['full_name'];
+                $attr['data-live-index-param'] ??= $k;
+                $entryView->vars['button_move_up']->vars['attr'] = $attr;
+
+                array_splice($entryView->vars['button_move_up']->vars['block_prefixes'], 1, 0, 'live_collection_button_move_up');
+            }
+        }
+        
+        if ($form->getConfig()->hasAttribute('button_move_down_prototype')) {
+            $prototype = $form->getConfig()->getAttribute('button_move_down_prototype');
+
+            $prototypes = [];
+            foreach ($form as $k => $entry) {
+                $prototypes[$k] = clone $prototype;
+                $prototypes[$k]->setParent($entry);
+            }
+
+            foreach ($view as $k => $entryView) {
+                $entryView->vars['button_move_down'] = $prototypes[$k]->createView($entryView);
+
+                $attr = $entryView->vars['button_move_down']->vars['attr'];
+                $attr['data-action'] ??= 'live#action';
+                $attr['data-live-action-param'] ??= 'moveCollectionItemDown';
+                $attr['data-live-name-param'] ??= $view->vars['full_name'];
+                $attr['data-live-index-param'] ??= $k;
+                $entryView->vars['button_move_down']->vars['attr'] = $attr;
+
+                array_splice($entryView->vars['button_move_down']->vars['block_prefixes'], 1, 0, 'live_collection_button_move_down');
+            }
+        }
     }
 
     public function configureOptions(OptionsResolver $resolver): void
@@ -105,8 +160,13 @@ public function configureOptions(OptionsResolver $resolver): void
             'button_add_options' => [],
             'button_delete_type' => ButtonType::class,
             'button_delete_options' => [],
+            'button_move_up_type' => ButtonType::class,
+            'button_move_up_options' => [],
+            'button_move_down_type' => ButtonType::class,
+            'button_move_down_options' => [],
             'allow_add' => true,
             'allow_delete' => true,
+            'allow_sort' => true,
             'by_reference' => false,
         ]);
     }
diff --git a/src/LiveComponent/src/LiveCollectionTrait.php b/src/LiveComponent/src/LiveCollectionTrait.php
index 70f11f23b8b..6bc0e167a7f 100644
--- a/src/LiveComponent/src/LiveCollectionTrait.php
+++ b/src/LiveComponent/src/LiveCollectionTrait.php
@@ -46,6 +46,46 @@ public function removeCollectionItem(PropertyAccessorInterface $propertyAccessor
         $propertyAccessor->setValue($this->formValues, $propertyPath, $data);
     }
 
+    #[LiveAction]
+    public function moveCollectionItemUp(PropertyAccessorInterface $propertyAccessor, #[LiveArg] string $name, #[LiveArg] int $index): void
+    {
+        if ($index <= 0) {
+            return;
+        }
+        
+        $propertyPath = $this->fieldNameToPropertyPath($name, $this->formName);
+        $data = $propertyAccessor->getValue($this->formValues, $propertyPath);
+        
+        if (!\is_array($data) || !isset($data[$index]) || !isset($data[$index - 1])) {
+            return;
+        }
+        
+        // Swap the current item with the one above it
+        $temp = $data[$index - 1];
+        $data[$index - 1] = $data[$index];
+        $data[$index] = $temp;
+        
+        $propertyAccessor->setValue($this->formValues, $propertyPath, $data);
+    }
+
+    #[LiveAction]
+    public function moveCollectionItemDown(PropertyAccessorInterface $propertyAccessor, #[LiveArg] string $name, #[LiveArg] int $index): void
+    {
+        $propertyPath = $this->fieldNameToPropertyPath($name, $this->formName);
+        $data = $propertyAccessor->getValue($this->formValues, $propertyPath);
+        
+        if (!\is_array($data) || !isset($data[$index]) || !isset($data[$index + 1])) {
+            return;
+        }
+        
+        // Swap the current item with the one below it
+        $temp = $data[$index + 1];
+        $data[$index + 1] = $data[$index];
+        $data[$index] = $temp;
+        
+        $propertyAccessor->setValue($this->formValues, $propertyPath, $data);
+    }
+
     private function fieldNameToPropertyPath(string $collectionFieldName, string $rootFormName): string
     {
         $propertyPath = $collectionFieldName;
diff --git a/src/LiveComponent/templates/form_theme.html.twig b/src/LiveComponent/templates/form_theme.html.twig
index 1042510f2f9..4349883469a 100644
--- a/src/LiveComponent/templates/form_theme.html.twig
+++ b/src/LiveComponent/templates/form_theme.html.twig
@@ -10,4 +10,10 @@
     {%- if button_delete is defined and not button_delete.rendered -%}
         {{ form_row(button_delete) }}
     {%- endif -%}
+    {%- if button_move_up is defined and not button_move_up.rendered -%}
+        {{ form_row(button_move_up) }}
+    {%- endif -%}
+    {%- if button_move_down is defined and not button_move_down.rendered -%}
+        {{ form_row(button_move_down) }}
+    {%- endif -%}
 {%- endblock live_collection_entry_row -%}

From 7e5557dbcf40729c3eed365b13bd32a9862688cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Harri=20H=C3=A4iv=C3=A4l=C3=A4?= <haivala@cc.hut.fi>
Date: Thu, 10 Apr 2025 10:39:16 +0300
Subject: [PATCH 2/4] Coding standard

---
 .../src/Form/Type/LiveCollectionType.php           |  8 ++++----
 src/LiveComponent/src/LiveCollectionTrait.php      | 14 +++++++-------
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/LiveComponent/src/Form/Type/LiveCollectionType.php b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
index c1a54742fdf..7c34d7e6e38 100644
--- a/src/LiveComponent/src/Form/Type/LiveCollectionType.php
+++ b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
@@ -35,11 +35,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
             $prototype = $builder->create('delete', $options['button_delete_type'], $options['button_delete_options']);
             $builder->setAttribute('button_delete_prototype', $prototype->getForm());
         }
-        
+
         if ($options['allow_sort']) {
             $moveUpPrototype = $builder->create('move_up', $options['button_move_up_type'], $options['button_move_up_options']);
             $builder->setAttribute('button_move_up_prototype', $moveUpPrototype->getForm());
-            
+
             $moveDownPrototype = $builder->create('move_down', $options['button_move_down_type'], $options['button_move_down_options']);
             $builder->setAttribute('button_move_down_prototype', $moveDownPrototype->getForm());
         }
@@ -100,7 +100,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
                 array_splice($entryView->vars['button_delete']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete');
             }
         }
-        
+
         // Add move up and move down buttons
         if ($form->getConfig()->hasAttribute('button_move_up_prototype')) {
             $prototype = $form->getConfig()->getAttribute('button_move_up_prototype');
@@ -124,7 +124,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
                 array_splice($entryView->vars['button_move_up']->vars['block_prefixes'], 1, 0, 'live_collection_button_move_up');
             }
         }
-        
+
         if ($form->getConfig()->hasAttribute('button_move_down_prototype')) {
             $prototype = $form->getConfig()->getAttribute('button_move_down_prototype');
 
diff --git a/src/LiveComponent/src/LiveCollectionTrait.php b/src/LiveComponent/src/LiveCollectionTrait.php
index 6bc0e167a7f..354e0b0ec98 100644
--- a/src/LiveComponent/src/LiveCollectionTrait.php
+++ b/src/LiveComponent/src/LiveCollectionTrait.php
@@ -52,19 +52,19 @@ public function moveCollectionItemUp(PropertyAccessorInterface $propertyAccessor
         if ($index <= 0) {
             return;
         }
-        
+
         $propertyPath = $this->fieldNameToPropertyPath($name, $this->formName);
         $data = $propertyAccessor->getValue($this->formValues, $propertyPath);
-        
+
         if (!\is_array($data) || !isset($data[$index]) || !isset($data[$index - 1])) {
             return;
         }
-        
+
         // Swap the current item with the one above it
         $temp = $data[$index - 1];
         $data[$index - 1] = $data[$index];
         $data[$index] = $temp;
-        
+
         $propertyAccessor->setValue($this->formValues, $propertyPath, $data);
     }
 
@@ -73,16 +73,16 @@ public function moveCollectionItemDown(PropertyAccessorInterface $propertyAccess
     {
         $propertyPath = $this->fieldNameToPropertyPath($name, $this->formName);
         $data = $propertyAccessor->getValue($this->formValues, $propertyPath);
-        
+
         if (!\is_array($data) || !isset($data[$index]) || !isset($data[$index + 1])) {
             return;
         }
-        
+
         // Swap the current item with the one below it
         $temp = $data[$index + 1];
         $data[$index + 1] = $data[$index];
         $data[$index] = $temp;
-        
+
         $propertyAccessor->setValue($this->formValues, $propertyPath, $data);
     }
 

From 0b5bdfce4787636498aa9ab5f64ef109917adf89 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Harri=20H=C3=A4iv=C3=A4l=C3=A4?= <haivala@cc.hut.fi>
Date: Fri, 25 Apr 2025 00:51:20 +0300
Subject: [PATCH 3/4] reorder buttons, add sorting property

---
 .../src/Form/Type/LiveCollectionType.php      | 11 ++---
 src/LiveComponent/src/LiveCollectionTrait.php | 40 +++++++++++++++----
 .../templates/form_theme.html.twig            |  8 ++--
 3 files changed, 42 insertions(+), 17 deletions(-)

diff --git a/src/LiveComponent/src/Form/Type/LiveCollectionType.php b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
index 7c34d7e6e38..8cc5d219e6c 100644
--- a/src/LiveComponent/src/Form/Type/LiveCollectionType.php
+++ b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
@@ -35,11 +35,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
             $prototype = $builder->create('delete', $options['button_delete_type'], $options['button_delete_options']);
             $builder->setAttribute('button_delete_prototype', $prototype->getForm());
         }
-
+        
         if ($options['allow_sort']) {
             $moveUpPrototype = $builder->create('move_up', $options['button_move_up_type'], $options['button_move_up_options']);
             $builder->setAttribute('button_move_up_prototype', $moveUpPrototype->getForm());
-
+            
             $moveDownPrototype = $builder->create('move_down', $options['button_move_down_type'], $options['button_move_down_options']);
             $builder->setAttribute('button_move_down_prototype', $moveDownPrototype->getForm());
         }
@@ -100,7 +100,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
                 array_splice($entryView->vars['button_delete']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete');
             }
         }
-
+        
         // Add move up and move down buttons
         if ($form->getConfig()->hasAttribute('button_move_up_prototype')) {
             $prototype = $form->getConfig()->getAttribute('button_move_up_prototype');
@@ -124,7 +124,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
                 array_splice($entryView->vars['button_move_up']->vars['block_prefixes'], 1, 0, 'live_collection_button_move_up');
             }
         }
-
+        
         if ($form->getConfig()->hasAttribute('button_move_down_prototype')) {
             $prototype = $form->getConfig()->getAttribute('button_move_down_prototype');
 
@@ -166,7 +166,8 @@ public function configureOptions(OptionsResolver $resolver): void
             'button_move_down_options' => [],
             'allow_add' => true,
             'allow_delete' => true,
-            'allow_sort' => true,
+            'allow_sort' => false,
+            'order_property_path' => null,
             'by_reference' => false,
         ]);
     }
diff --git a/src/LiveComponent/src/LiveCollectionTrait.php b/src/LiveComponent/src/LiveCollectionTrait.php
index 354e0b0ec98..631d5f27e36 100644
--- a/src/LiveComponent/src/LiveCollectionTrait.php
+++ b/src/LiveComponent/src/LiveCollectionTrait.php
@@ -60,10 +60,22 @@ public function moveCollectionItemUp(PropertyAccessorInterface $propertyAccessor
             return;
         }
 
-        // Swap the current item with the one above it
-        $temp = $data[$index - 1];
-        $data[$index - 1] = $data[$index];
-        $data[$index] = $temp;
+        $formConfig = $this->getForm()->get($name)->getConfig();
+        $orderPropertyPath = $formConfig->getOption('order_property_path');
+
+        if ($orderPropertyPath !== null) {
+            // Swap positions using the specified order property
+            $prevItemOrder = $propertyAccessor->getValue($data[$index - 1], $orderPropertyPath);
+            $currentItemOrder = $propertyAccessor->getValue($data[$index], $orderPropertyPath);
+            
+            $propertyAccessor->setValue($data[$index], $orderPropertyPath, $prevItemOrder);
+            $propertyAccessor->setValue($data[$index - 1], $orderPropertyPath, $currentItemOrder);
+        } else {
+            // Legacy behavior - simple array swap (with warning about limitations)
+            $temp = $data[$index - 1];
+            $data[$index - 1] = $data[$index];
+            $data[$index] = $temp;
+        }
 
         $propertyAccessor->setValue($this->formValues, $propertyPath, $data);
     }
@@ -78,10 +90,22 @@ public function moveCollectionItemDown(PropertyAccessorInterface $propertyAccess
             return;
         }
 
-        // Swap the current item with the one below it
-        $temp = $data[$index + 1];
-        $data[$index + 1] = $data[$index];
-        $data[$index] = $temp;
+        $formConfig = $this->getForm()->get($name)->getConfig();
+        $orderPropertyPath = $formConfig->getOption('order_property_path');
+
+        if ($orderPropertyPath !== null) {
+            // Swap positions using the specified order property
+            $nextItemOrder = $propertyAccessor->getValue($data[$index + 1], $orderPropertyPath);
+            $currentItemOrder = $propertyAccessor->getValue($data[$index], $orderPropertyPath);
+            
+            $propertyAccessor->setValue($data[$index], $orderPropertyPath, $nextItemOrder);
+            $propertyAccessor->setValue($data[$index + 1], $orderPropertyPath, $currentItemOrder);
+        } else {
+            // Legacy behavior - simple array swap (with warning about limitations)
+            $temp = $data[$index + 1];
+            $data[$index + 1] = $data[$index];
+            $data[$index] = $temp;
+        }
 
         $propertyAccessor->setValue($this->formValues, $propertyPath, $data);
     }
diff --git a/src/LiveComponent/templates/form_theme.html.twig b/src/LiveComponent/templates/form_theme.html.twig
index 4349883469a..cc6222a094e 100644
--- a/src/LiveComponent/templates/form_theme.html.twig
+++ b/src/LiveComponent/templates/form_theme.html.twig
@@ -6,14 +6,14 @@
 {%- endblock live_collection_widget -%}
 
 {%- block live_collection_entry_row -%}
-    {{ block('form_row') }}
-    {%- if button_delete is defined and not button_delete.rendered -%}
-        {{ form_row(button_delete) }}
-    {%- endif -%}
     {%- if button_move_up is defined and not button_move_up.rendered -%}
         {{ form_row(button_move_up) }}
     {%- endif -%}
     {%- if button_move_down is defined and not button_move_down.rendered -%}
         {{ form_row(button_move_down) }}
     {%- endif -%}
+    {%- if button_delete is defined and not button_delete.rendered -%}
+        {{ form_row(button_delete) }}
+    {%- endif -%}
+    {{ block('form_row') }}
 {%- endblock live_collection_entry_row -%}

From e8f2c4a3dd29e3e439383d7c367f26a924022143 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Harri=20H=C3=A4iv=C3=A4l=C3=A4?= <haivala@cc.hut.fi>
Date: Fri, 25 Apr 2025 00:53:29 +0300
Subject: [PATCH 4/4] coding standard

---
 src/LiveComponent/src/Form/Type/LiveCollectionType.php | 8 ++++----
 src/LiveComponent/src/LiveCollectionTrait.php          | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/LiveComponent/src/Form/Type/LiveCollectionType.php b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
index 8cc5d219e6c..a7d214b51ad 100644
--- a/src/LiveComponent/src/Form/Type/LiveCollectionType.php
+++ b/src/LiveComponent/src/Form/Type/LiveCollectionType.php
@@ -35,11 +35,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
             $prototype = $builder->create('delete', $options['button_delete_type'], $options['button_delete_options']);
             $builder->setAttribute('button_delete_prototype', $prototype->getForm());
         }
-        
+
         if ($options['allow_sort']) {
             $moveUpPrototype = $builder->create('move_up', $options['button_move_up_type'], $options['button_move_up_options']);
             $builder->setAttribute('button_move_up_prototype', $moveUpPrototype->getForm());
-            
+
             $moveDownPrototype = $builder->create('move_down', $options['button_move_down_type'], $options['button_move_down_options']);
             $builder->setAttribute('button_move_down_prototype', $moveDownPrototype->getForm());
         }
@@ -100,7 +100,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
                 array_splice($entryView->vars['button_delete']->vars['block_prefixes'], 1, 0, 'live_collection_button_delete');
             }
         }
-        
+
         // Add move up and move down buttons
         if ($form->getConfig()->hasAttribute('button_move_up_prototype')) {
             $prototype = $form->getConfig()->getAttribute('button_move_up_prototype');
@@ -124,7 +124,7 @@ public function finishView(FormView $view, FormInterface $form, array $options):
                 array_splice($entryView->vars['button_move_up']->vars['block_prefixes'], 1, 0, 'live_collection_button_move_up');
             }
         }
-        
+
         if ($form->getConfig()->hasAttribute('button_move_down_prototype')) {
             $prototype = $form->getConfig()->getAttribute('button_move_down_prototype');
 
diff --git a/src/LiveComponent/src/LiveCollectionTrait.php b/src/LiveComponent/src/LiveCollectionTrait.php
index 631d5f27e36..ac2fa595a40 100644
--- a/src/LiveComponent/src/LiveCollectionTrait.php
+++ b/src/LiveComponent/src/LiveCollectionTrait.php
@@ -63,11 +63,11 @@ public function moveCollectionItemUp(PropertyAccessorInterface $propertyAccessor
         $formConfig = $this->getForm()->get($name)->getConfig();
         $orderPropertyPath = $formConfig->getOption('order_property_path');
 
-        if ($orderPropertyPath !== null) {
+        if (null !== $orderPropertyPath) {
             // Swap positions using the specified order property
             $prevItemOrder = $propertyAccessor->getValue($data[$index - 1], $orderPropertyPath);
             $currentItemOrder = $propertyAccessor->getValue($data[$index], $orderPropertyPath);
-            
+
             $propertyAccessor->setValue($data[$index], $orderPropertyPath, $prevItemOrder);
             $propertyAccessor->setValue($data[$index - 1], $orderPropertyPath, $currentItemOrder);
         } else {
@@ -93,11 +93,11 @@ public function moveCollectionItemDown(PropertyAccessorInterface $propertyAccess
         $formConfig = $this->getForm()->get($name)->getConfig();
         $orderPropertyPath = $formConfig->getOption('order_property_path');
 
-        if ($orderPropertyPath !== null) {
+        if (null !== $orderPropertyPath) {
             // Swap positions using the specified order property
             $nextItemOrder = $propertyAccessor->getValue($data[$index + 1], $orderPropertyPath);
             $currentItemOrder = $propertyAccessor->getValue($data[$index], $orderPropertyPath);
-            
+
             $propertyAccessor->setValue($data[$index], $orderPropertyPath, $nextItemOrder);
             $propertyAccessor->setValue($data[$index + 1], $orderPropertyPath, $currentItemOrder);
         } else {