Repository: Pycord-Development/pycord

Issue:
Components get moved randomly in a full Action Row on message/view update
### Summary

The 3 Link buttons get moved into the Select ActionRow which causes an error

### Reproduction Steps

1. Use the following code below and start the view
2.  select the 2nd select item multiple times
3. An error will randomly? occur

### Minimal Reproducible Code

```python
class TestView(discord.ui.View):

    def __init__(self):
        super().__init__()
        help_select = discord.ui.Select(options=[
            discord.SelectOption(label='Home', value='home', emoji='🏠', default=True),
            discord.SelectOption(label='Name', value='stats', emoji='📊'),
        ])
        help_select.callback = self.select_page
        self.add_item(help_select)

        self.add_item(discord.ui.Button(style=discord.ButtonStyle.url, label='Example 1', url=f'https://youtube.com'))
        self.add_item(discord.ui.Button(style=discord.ButtonStyle.url, label='Example 2', url='https://discord.com'))
        self.add_item(discord.ui.Button(style=discord.ButtonStyle.url, label='Example 3', url='https://google.com'))

    async def select_page(self, interaction: discord.Interaction):
        select: discord.ui.Select = discord.utils.get(self.children, custom_id=interaction.data['custom_id'])
        if not select.values:
            return
        await interaction.message.edit(view=self)
```


### Expected Results

The components just normally update.

`to_components()` looks like:
[{'type': 1, 'components': [{'type': 3, 'custom_id': 'f0a5a63f62e6b58e179eadba3014652b', 'min_values': 1, 'max_values': 1, 'options': [{'label': 'Home', 'value': 'home', 'default': True, 'emoji': {'name': '🏠'}}, {'label': 'Name', 'value': 'stats', 'default': False, 'emoji': {'name': '📊'}}]}]}, {'type': 1, 'components': [{'type': 2, 'style': 5, 'label': 'Example 1', 'disabled': False, 'url': 'https://youtube.com'}, {'type': 2, 'style': 5, 'label': 'Example 2', 'disabled': False, 'url': 'https://discord.com'}, {'type': 2, 'style': 5, 'label': 'Example 3', 'disabled': False, 'url': 'https://google.com'}]}]

### Actual Results

An error occurrs:
```py
Ignoring exception in view <TestView timeout=180.0 children=4> for item <Select placeholder=None min_values=1 max_values=1 options=[<SelectOption label='Home' value='home' description=None emoji=<PartialEmoji animated=False name='🏠' id=None> default=True>, <SelectOption label='Name' value='stats' description=None emoji=<PartialEmoji animated=False name='📊' id=None> default=False>]>:
Traceback (most recent call last):
  File "C:\Users\Lucas Hardt\Development\EasySystem\EasyFnStatsV3\venv\lib\site-packages\discord\ui\view.py", line 358, in _scheduled_task
    await item.callback(interaction)
  File "C:\Users\Lucas Hardt\Development\EasySystem\EasyFnStatsV3\utils\views.py", line 2516, in select_page
    await interaction.message.edit(view=self)
  File "C:\Users\Lucas Hardt\Development\EasySystem\EasyFnStatsV3\venv\lib\site-packages\discord\message.py", line 1249, in edit
    data = await self._state.http.edit_message(self.channel.id, self.id, **payload)
  File "C:\Users\Lucas Hardt\Development\EasySystem\EasyFnStatsV3\venv\lib\site-packages\discord\http.py", line 337, in request
    raise HTTPException(response, data)
discord.errors.HTTPException: 400 Bad Request (error code: 50035): Invalid Form Body
In components.0.components.1: The specified component exceeds the maximum width
In components.0.components.2: The specified component exceeds the maximum width
In components.0.components.3: The specified component exceeds the maximum width
```

`to_components()` looks like:
[{'type': 1, 'components': [{'type': 3, 'custom_id': 'f0a5a63f62e6b58e179eadba3014652b', 'min_values': 1, 'max_values': 1, 'options': [{'label': 'Home', 'value': 'home', 'default': True, 'emoji': {'name': '🏠'}}, {'label': 'Name', 'value': 'stats', 'default': False, 'emoji': {'name': '📊'}}]}, {'type': 2, 'style': 5, 'label': 'Example 1', 'disabled': False, 'url': 'https://youtube.com'}, {'type': 2, 'style': 5, 'label': 'Example 2', 'disabled': False, 'url': 'https://discord.com'}, {'type': 2, 'style': 5, 'label': 'Example 3', 'disabled': False, 'url': 'https://google.com'}]}]
occurs

### Intents

guilds, members, emojis, webhooks, voice_states, guild_messages, guild_reactions, dm_messages, dm_reactions

### System Information

- Python v3.9.5-final
- discord.py v2.0.0-alpha
    - discord.py pkg_resources: v2.0.0a3575+g45d498c1
- aiohttp v3.7.4.post0
- system info: Windows 10 10.0.22000


### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

This issue has been moved from https://github.com/Rapptz/discord.py/issues/7231


Discussion:
Not really a bug
Discord limits ui items to 5 per row- one select takes up 5 spaces so there is no place left for the buttons

You would need to add `row=1` where you are creating the buttons
Closing this as Div-100 has answered, you can re-open it if you still have problems.
> Not really a bug
> Discord limits ui items to 5 per row- one select takes up 5 spaces so there is no place left for the buttons
> 
> You would need to add `row=1` where you are creating the buttons

The library should automatically handle this. The first few edits it keeps the right order, but on some point (feels like it's random) it messes up the order which causes that more than 5 elements are in a row.
> > Not really a bug
> > Discord limits ui items to 5 per row- one select takes up 5 spaces so there is no place left for the buttons
> > You would need to add `row=1` where you are creating the buttons
> 
> The library should automatically handle this. The first few edits it keeps the right order, but on some point (feels like it's random) it messes up the order which causes that more than 5 elements are in a row.

The Library shouldn't- you should be the one to decide how your elements actually looks- y'know
> > > Not really a bug
> > > Discord limits ui items to 5 per row- one select takes up 5 spaces so there is no place left for the buttons
> > > You would need to add `row=1` where you are creating the buttons
> > 
> > 
> > The library should automatically handle this. The first few edits it keeps the right order, but on some point (feels like it's random) it messes up the order which causes that more than 5 elements are in a row.
> 
> The Library shouldn't- you should be the one to decide how your elements actually looks- y'know

No. The row parameter is optional. The documentation for row states "By default, items are arranged automatically into those 5 rows. If you’d like to control the relative positioning of the row then passing an index is advised."

Setting the row for the buttons also causes the same behavior. An issue occurs when using `to_components()` 
After further investigation I came to the following result:
- It only occurs when using URL buttons
- Somehow these buttons get re-created. This causes that the button got no `_rendered_row` or `view` attached.

`id(item)`:
```
[1412280926016, 1412282010112, 1412282010160, 1412282010688] # First edit
[1412280926016, 1412282010112, 1412282010160, 1412282010688] # 2nd edit
[1412280926016, 1412281030160, 1412281028768, 1412281031168] # 3rd edit (Error occurs)
```

After some edit, it creates a new Item Object. The next edit fail

The error might be caused by https://github.com/Dorukyum/pycord/blob/master/discord/ui/view.py#L400  throwing an error and creating the new item.

Resolving patch:
diff --git a/discord/ui/view.py b/discord/ui/view.py
--- a/discord/ui/view.py
+++ b/discord/ui/view.py
@@ -394,12 +394,15 @@ def refresh(self, components: List[Component]):
             if item.is_dispatchable()
         }
         # fmt: on
-        children: List[Item] = []
+        children: List[Item] = [item for item in self.children if not item.is_dispatchable()]
         for component in _walk_all_components(components):
             try:
                 older = old_state[(component.type.value, component.custom_id)]  # type: ignore
             except (KeyError, AttributeError):
-                children.append(_component_to_item(component))
+                item = _component_to_item(component)
+                if not item.is_dispatchable():
+                    continue
+                children.append(component)
             else:
                 older.refresh_component(component)
                 children.append(older)


Test patch: