mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-08-16 16:20:15 +00:00
Add support for label components and select in modals
This commit is contained in:
parent
9fb74fd7a1
commit
3fb627d078
@ -70,6 +70,7 @@ if TYPE_CHECKING:
|
||||
ThumbnailComponent as ThumbnailComponentPayload,
|
||||
ContainerComponent as ContainerComponentPayload,
|
||||
UnfurledMediaItem as UnfurledMediaItemPayload,
|
||||
LabelComponent as LabelComponentPayload,
|
||||
)
|
||||
|
||||
from .emoji import Emoji
|
||||
@ -109,6 +110,7 @@ __all__ = (
|
||||
'Container',
|
||||
'TextDisplay',
|
||||
'SeparatorComponent',
|
||||
'LabelComponent',
|
||||
)
|
||||
|
||||
|
||||
@ -348,6 +350,10 @@ class SelectMenu(Component):
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
required: :class:`bool`
|
||||
Whether the select is required. Only applicable within modals.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
|
||||
@ -361,6 +367,7 @@ class SelectMenu(Component):
|
||||
'disabled',
|
||||
'channel_types',
|
||||
'default_values',
|
||||
'required',
|
||||
'id',
|
||||
)
|
||||
|
||||
@ -372,6 +379,7 @@ class SelectMenu(Component):
|
||||
self.placeholder: Optional[str] = data.get('placeholder')
|
||||
self.min_values: int = data.get('min_values', 1)
|
||||
self.max_values: int = data.get('max_values', 1)
|
||||
self.required: bool = data.get('required', False)
|
||||
self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])]
|
||||
self.disabled: bool = data.get('disabled', False)
|
||||
self.channel_types: List[ChannelType] = [try_enum(ChannelType, t) for t in data.get('channel_types', [])]
|
||||
@ -544,7 +552,7 @@ class TextInput(Component):
|
||||
------------
|
||||
custom_id: Optional[:class:`str`]
|
||||
The ID of the text input that gets received during an interaction.
|
||||
label: :class:`str`
|
||||
label: Optional[:class:`str`]
|
||||
The label to display above the text input.
|
||||
style: :class:`TextStyle`
|
||||
The style of the text input.
|
||||
@ -580,7 +588,7 @@ class TextInput(Component):
|
||||
|
||||
def __init__(self, data: TextInputPayload, /) -> None:
|
||||
self.style: TextStyle = try_enum(TextStyle, data['style'])
|
||||
self.label: str = data['label']
|
||||
self.label: Optional[str] = data.get('label')
|
||||
self.custom_id: str = data['custom_id']
|
||||
self.placeholder: Optional[str] = data.get('placeholder')
|
||||
self.value: Optional[str] = data.get('value')
|
||||
@ -1309,6 +1317,62 @@ class Container(Component):
|
||||
return payload
|
||||
|
||||
|
||||
class LabelComponent(Component):
|
||||
"""Represents a label component from the Discord Bot UI Kit.
|
||||
|
||||
This inherits from :class:`Component`.
|
||||
|
||||
.. note::
|
||||
|
||||
The user constructible and usable type for creating a label is
|
||||
:class:`discord.ui.Label` not this one.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Attributes
|
||||
----------
|
||||
label: :class:`str`
|
||||
The label text to display.
|
||||
description: Optional[:class:`str`]
|
||||
The description text to display below the label, if any.
|
||||
component: :class:`Component`
|
||||
The component that this label is associated with.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of this component.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'label',
|
||||
'description',
|
||||
'commponent',
|
||||
'id',
|
||||
)
|
||||
|
||||
__repr_info__ = ('label', 'description', 'commponent', 'id,')
|
||||
|
||||
def __init__(self, data: LabelComponentPayload, state: Optional[ConnectionState]) -> None:
|
||||
self.component: Component = _component_factory(data['component'], state) # type: ignore
|
||||
self.label: str = data['label']
|
||||
self.id: Optional[int] = data.get('id')
|
||||
self.description: Optional[str] = data.get('description')
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.label]:
|
||||
return ComponentType.label
|
||||
|
||||
def to_dict(self) -> LabelComponentPayload:
|
||||
payload: LabelComponentPayload = {
|
||||
'type': self.type.value,
|
||||
'label': self.label,
|
||||
'component': self.component.to_dict(), # type: ignore
|
||||
}
|
||||
if self.description:
|
||||
payload['description'] = self.description
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
return payload
|
||||
|
||||
|
||||
def _component_factory(data: ComponentPayload, state: Optional[ConnectionState] = None) -> Optional[Component]:
|
||||
if data['type'] == 1:
|
||||
return ActionRow(data)
|
||||
@ -1332,3 +1396,5 @@ def _component_factory(data: ComponentPayload, state: Optional[ConnectionState]
|
||||
return SeparatorComponent(data)
|
||||
elif data['type'] == 17:
|
||||
return Container(data, state)
|
||||
elif data['type'] == 18:
|
||||
return LabelComponent(data, state)
|
||||
|
@ -677,6 +677,7 @@ class ComponentType(Enum):
|
||||
file = 13
|
||||
separator = 14
|
||||
container = 17
|
||||
label = 18
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
|
@ -30,7 +30,7 @@ from typing_extensions import NotRequired
|
||||
from .emoji import PartialEmoji
|
||||
from .channel import ChannelType
|
||||
|
||||
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17]
|
||||
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 18]
|
||||
ButtonStyle = Literal[1, 2, 3, 4, 5, 6]
|
||||
TextStyle = Literal[1, 2]
|
||||
DefaultValueType = Literal['user', 'role', 'channel']
|
||||
@ -110,7 +110,7 @@ class TextInput(ComponentBase):
|
||||
type: Literal[4]
|
||||
custom_id: str
|
||||
style: TextStyle
|
||||
label: str
|
||||
label: Optional[str]
|
||||
placeholder: NotRequired[str]
|
||||
value: NotRequired[str]
|
||||
required: NotRequired[bool]
|
||||
@ -120,6 +120,7 @@ class TextInput(ComponentBase):
|
||||
|
||||
class SelectMenu(SelectComponent):
|
||||
type: Literal[3, 5, 6, 7, 8]
|
||||
required: NotRequired[bool] # Only for StringSelect within modals
|
||||
options: NotRequired[List[SelectOption]]
|
||||
channel_types: NotRequired[List[ChannelType]]
|
||||
default_values: NotRequired[List[SelectDefaultValues]]
|
||||
@ -187,6 +188,13 @@ class ContainerComponent(ComponentBase):
|
||||
components: List[ContainerChildComponent]
|
||||
|
||||
|
||||
class LabelComponent(ComponentBase):
|
||||
type: Literal[18]
|
||||
label: str
|
||||
description: NotRequired[str]
|
||||
component: Union[StringSelectComponent, TextInput]
|
||||
|
||||
|
||||
ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput]
|
||||
ContainerChildComponent = Union[
|
||||
ActionRow,
|
||||
@ -199,4 +207,4 @@ ContainerChildComponent = Union[
|
||||
SeparatorComponent,
|
||||
ThumbnailComponent,
|
||||
]
|
||||
Component = Union[ActionRowChildComponent, ContainerChildComponent]
|
||||
Component = Union[ActionRowChildComponent, LabelComponent, ContainerChildComponent]
|
||||
|
@ -209,7 +209,13 @@ class ModalSubmitTextInputInteractionData(TypedDict):
|
||||
value: str
|
||||
|
||||
|
||||
ModalSubmitComponentItemInteractionData = ModalSubmitTextInputInteractionData
|
||||
class ModalSubmitStringSelectInteractionData(TypedDict):
|
||||
type: Literal[3]
|
||||
custom_id: str
|
||||
values: List[str]
|
||||
|
||||
|
||||
ModalSubmitComponentItemInteractionData = Union[ModalSubmitTextInputInteractionData, ModalSubmitStringSelectInteractionData]
|
||||
|
||||
|
||||
class ModalSubmitActionRowInteractionData(TypedDict):
|
||||
@ -217,7 +223,14 @@ class ModalSubmitActionRowInteractionData(TypedDict):
|
||||
components: List[ModalSubmitComponentItemInteractionData]
|
||||
|
||||
|
||||
ModalSubmitComponentInteractionData = Union[ModalSubmitActionRowInteractionData, ModalSubmitComponentItemInteractionData]
|
||||
class ModalSubmitLabelInteractionData(TypedDict):
|
||||
type: Literal[18]
|
||||
component: ModalSubmitComponentItemInteractionData
|
||||
|
||||
|
||||
ModalSubmitComponentInteractionData = Union[
|
||||
ModalSubmitLabelInteractionData, ModalSubmitActionRowInteractionData, ModalSubmitComponentItemInteractionData
|
||||
]
|
||||
|
||||
|
||||
class ModalSubmitInteractionData(TypedDict):
|
||||
|
@ -24,3 +24,4 @@ from .separator import *
|
||||
from .text_display import *
|
||||
from .thumbnail import *
|
||||
from .action_row import *
|
||||
from .label import *
|
||||
|
140
discord/ui/label.py
Normal file
140
discord/ui/label.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Generator, Literal, Optional, Tuple, TypeVar
|
||||
|
||||
from ..components import LabelComponent
|
||||
from ..enums import ComponentType
|
||||
from ..utils import MISSING
|
||||
from .item import Item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from ..types.components import LabelComponent as LabelComponentPayload
|
||||
from .view import View
|
||||
|
||||
|
||||
# fmt: off
|
||||
__all__ = (
|
||||
'Label',
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
V = TypeVar('V', bound='View', covariant=True)
|
||||
|
||||
|
||||
class Label(Item[V]):
|
||||
"""Represents a UI label within a modal.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
------------
|
||||
text: :class:`str`
|
||||
The text to display above the input field.
|
||||
Can only be up to 45 characters.
|
||||
description: Optional[:class:`str`]
|
||||
The description text to display right below the label text.
|
||||
Can only be up to 100 characters.
|
||||
component: Union[:class:`discord.ui.TextInput`, :class:`discord.ui.Select`]
|
||||
The component to display below the label.
|
||||
id: Optional[:class:`int`]
|
||||
The ID of the component. This must be unique across the view.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
text: :class:`str`
|
||||
The text to display above the input field.
|
||||
Can only be up to 45 characters.
|
||||
description: Optional[:class:`str`]
|
||||
The description text to display right below the label text.
|
||||
Can only be up to 100 characters.
|
||||
component: :class:`Item`
|
||||
The component to display below the label. Currently only
|
||||
supports :class:`TextInput` and :class:`Select`.
|
||||
"""
|
||||
|
||||
__item_repr_attributes__: Tuple[str, ...] = (
|
||||
'text',
|
||||
'description',
|
||||
'component',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
text: str,
|
||||
component: Item[V],
|
||||
description: Optional[str] = None,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.component: Item[V] = component
|
||||
self.text: str = text
|
||||
self.description: Optional[str] = description
|
||||
self.id = id
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 5
|
||||
|
||||
def _has_children(self) -> bool:
|
||||
return True
|
||||
|
||||
def walk_children(self) -> Generator[Item[V], None, None]:
|
||||
yield self.component
|
||||
|
||||
def to_component_dict(self) -> LabelComponentPayload:
|
||||
payload: LabelComponentPayload = {
|
||||
'type': ComponentType.label.value,
|
||||
'label': self.text,
|
||||
'component': self.component.to_component_dict(), # type: ignore
|
||||
}
|
||||
if self.description:
|
||||
payload['description'] = self.description
|
||||
if self.id is not None:
|
||||
payload['id'] = self.id
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
def from_component(cls, component: LabelComponent) -> Self:
|
||||
from .view import _component_to_item
|
||||
|
||||
self = cls(
|
||||
text=component.label,
|
||||
component=MISSING,
|
||||
description=component.description,
|
||||
)
|
||||
self.component = _component_to_item(component.component, self)
|
||||
return self
|
||||
|
||||
@property
|
||||
def type(self) -> Literal[ComponentType.label]:
|
||||
return ComponentType.label
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
return False
|
@ -34,6 +34,7 @@ from ..utils import MISSING, find
|
||||
from .._types import ClientT
|
||||
from .item import Item
|
||||
from .view import View
|
||||
from .label import Label
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
@ -170,8 +171,10 @@ class Modal(View):
|
||||
for component in components:
|
||||
if component['type'] == 1:
|
||||
self._refresh(interaction, component['components'])
|
||||
elif component['type'] == 18:
|
||||
self._refresh(interaction, [component['component']])
|
||||
else:
|
||||
item = find(lambda i: i.custom_id == component['custom_id'], self._children) # type: ignore
|
||||
item = find(lambda i: getattr(i, 'custom_id', None) == component['custom_id'], self.walk_children()) # type: ignore
|
||||
if item is None:
|
||||
_log.debug("Modal interaction referencing unknown item custom_id %s. Discarding", component['custom_id'])
|
||||
continue
|
||||
@ -194,6 +197,28 @@ class Modal(View):
|
||||
# In the future, maybe this will require checking if we set an error response.
|
||||
self.stop()
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
def key(item: Item) -> int:
|
||||
return item._rendered_row or 0
|
||||
|
||||
children = sorted(self._children, key=key)
|
||||
components: List[Dict[str, Any]] = []
|
||||
for child in children:
|
||||
if isinstance(child, Label):
|
||||
components.append(child.to_component_dict()) # type: ignore
|
||||
else:
|
||||
# Every implicit child wrapped in an ActionRow in a modal
|
||||
# has a single child of width 5
|
||||
# It's also deprecated to use ActionRow in modals
|
||||
components.append(
|
||||
{
|
||||
'type': 1,
|
||||
'components': [child.to_component_dict()],
|
||||
}
|
||||
)
|
||||
|
||||
return components
|
||||
|
||||
def _dispatch_submit(
|
||||
self, interaction: Interaction, components: List[ModalSubmitComponentInteractionDataPayload]
|
||||
) -> None:
|
||||
|
@ -239,6 +239,7 @@ class BaseSelect(Item[V]):
|
||||
min_values: Optional[int] = None,
|
||||
max_values: Optional[int] = None,
|
||||
disabled: bool = False,
|
||||
required: bool = False,
|
||||
options: List[SelectOption] = MISSING,
|
||||
channel_types: List[ChannelType] = MISSING,
|
||||
default_values: Sequence[SelectDefaultValue] = MISSING,
|
||||
@ -257,6 +258,7 @@ class BaseSelect(Item[V]):
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
disabled=disabled,
|
||||
required=required,
|
||||
channel_types=[] if channel_types is MISSING else channel_types,
|
||||
options=[] if options is MISSING else options,
|
||||
default_values=[] if default_values is MISSING else default_values,
|
||||
@ -332,6 +334,18 @@ class BaseSelect(Item[V]):
|
||||
def disabled(self, value: bool) -> None:
|
||||
self._underlying.disabled = bool(value)
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
""":class:`bool`: Whether the select is required or not. Only supported in modals.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return self._underlying.required
|
||||
|
||||
@required.setter
|
||||
def required(self, value: bool) -> None:
|
||||
self._underlying.required = bool(value)
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return 5
|
||||
@ -399,6 +413,10 @@ class Select(BaseSelect[V]):
|
||||
Can only contain up to 25 items.
|
||||
disabled: :class:`bool`
|
||||
Whether the select is disabled or not.
|
||||
required: :class:`bool`
|
||||
Whether the select is required. Only applicable within modals.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
row: Optional[:class:`int`]
|
||||
The relative row this select menu belongs to. A Discord component can only have 5
|
||||
rows. By default, items are arranged automatically into those 5 rows. If you'd
|
||||
@ -426,6 +444,7 @@ class Select(BaseSelect[V]):
|
||||
max_values: int = 1,
|
||||
options: List[SelectOption] = MISSING,
|
||||
disabled: bool = False,
|
||||
required: bool = True,
|
||||
row: Optional[int] = None,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
@ -436,6 +455,7 @@ class Select(BaseSelect[V]):
|
||||
min_values=min_values,
|
||||
max_values=max_values,
|
||||
disabled=disabled,
|
||||
required=required,
|
||||
options=options,
|
||||
row=row,
|
||||
id=id,
|
||||
|
@ -29,7 +29,7 @@ from typing import TYPE_CHECKING, Literal, Optional, Tuple, TypeVar
|
||||
|
||||
from ..components import TextInput as TextInputComponent
|
||||
from ..enums import ComponentType, TextStyle
|
||||
from ..utils import MISSING
|
||||
from ..utils import MISSING, deprecated
|
||||
from .item import Item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -63,9 +63,15 @@ class TextInput(Item[V]):
|
||||
|
||||
Parameters
|
||||
------------
|
||||
label: :class:`str`
|
||||
label: Optional[:class:`str`]
|
||||
The label to display above the text input.
|
||||
Can only be up to 45 characters.
|
||||
|
||||
.. deprecated:: 2.6
|
||||
This parameter is deprecated, use :class:`discord.ui.Label` instead.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
This parameter is now optional and defaults to ``None``.
|
||||
custom_id: :class:`str`
|
||||
The ID of the text input that gets received during an interaction.
|
||||
If not given then one is generated for you.
|
||||
@ -108,7 +114,7 @@ class TextInput(Item[V]):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
label: str,
|
||||
label: Optional[str] = None,
|
||||
style: TextStyle = TextStyle.short,
|
||||
custom_id: str = MISSING,
|
||||
placeholder: Optional[str] = None,
|
||||
@ -166,12 +172,14 @@ class TextInput(Item[V]):
|
||||
return self._value or ''
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
@deprecated('discord.ui.Label')
|
||||
def label(self) -> Optional[str]:
|
||||
""":class:`str`: The label of the text input."""
|
||||
return self._underlying.label
|
||||
|
||||
@label.setter
|
||||
def label(self, value: str) -> None:
|
||||
@deprecated('discord.ui.Label')
|
||||
def label(self, value: Optional[str]) -> None:
|
||||
self._underlying.label = value
|
||||
|
||||
@property
|
||||
|
@ -65,6 +65,7 @@ from ..components import (
|
||||
SeparatorComponent,
|
||||
ThumbnailComponent,
|
||||
Container as ContainerComponent,
|
||||
LabelComponent,
|
||||
)
|
||||
from ..utils import get as _utils_get, find as _utils_find
|
||||
|
||||
@ -147,6 +148,10 @@ def _component_to_item(component: Component, parent: Optional[Item] = None) -> I
|
||||
from .container import Container
|
||||
|
||||
item = Container.from_component(component)
|
||||
elif isinstance(component, LabelComponent):
|
||||
from .label import Label
|
||||
|
||||
item = Label.from_component(component)
|
||||
else:
|
||||
item = Item.from_component(component)
|
||||
|
||||
|
@ -113,6 +113,15 @@ TextInput
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
LabelComponent
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: LabelComponent
|
||||
|
||||
.. autoclass:: LabelComponent()
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
SectionComponent
|
||||
~~~~~~~~~~~~~~~~
|
||||
@ -425,7 +434,7 @@ Enumerations
|
||||
.. attribute:: media_gallery
|
||||
|
||||
Represents a media gallery component.
|
||||
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: file
|
||||
@ -446,6 +455,12 @@ Enumerations
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. attribute:: label
|
||||
|
||||
Represents a label container component, usually in a modal.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
.. class:: ButtonStyle
|
||||
|
||||
Represents the style of the button component.
|
||||
@ -742,6 +757,15 @@ File
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
Label
|
||||
~~~~~~
|
||||
|
||||
.. attributetable:: discord.ui.Label
|
||||
|
||||
.. autoclass:: discord.ui.Label
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
MediaGallery
|
||||
~~~~~~~~~~~~
|
||||
|
Loading…
x
Reference in New Issue
Block a user