pycascadeui 3.3.3__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pycascadeui-3.3.3/pycascadeui.egg-info → pycascadeui-3.4.0}/PKG-INFO +3 -2
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/README.md +2 -1
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/__init__.py +18 -3
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/inputs.py +56 -7
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/patterns/__init__.py +11 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/patterns/v2.py +1023 -7
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/types.py +9 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/wrappers.py +19 -6
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/devtools.py +28 -5
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/backends/memory.py +12 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/backends/postgres.py +63 -15
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/backends/sqlite.py +57 -17
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/manager.py +251 -58
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/protocols.py +18 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/actions.py +9 -6
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/middleware/logging.py +12 -6
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/middleware/persistence.py +105 -12
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/middleware/undo.py +36 -13
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/store.py +1 -1
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/utils/__init__.py +2 -1
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/utils/logging.py +66 -11
- pycascadeui-3.4.0/cascadeui/utils/strings.py +72 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/utils/tasks.py +29 -30
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/validation.py +19 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/_interaction.py +32 -3
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/_navigation.py +230 -65
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/_placement.py +45 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/base.py +335 -23
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/layout.py +120 -39
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/leaderboard.py +76 -33
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/paginated.py +3 -2
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/roles.py +31 -23
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/tabs.py +5 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/persistent.py +71 -4
- {pycascadeui-3.3.3 → pycascadeui-3.4.0/pycascadeui.egg-info}/PKG-INFO +3 -2
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/pycascadeui.egg-info/SOURCES.txt +2 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/pyproject.toml +1 -1
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_auto_defer.py +35 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_backends.py +83 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_batching.py +12 -8
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_devtools.py +70 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_ephemeral_refresh.py +74 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_inputs.py +99 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_instance_limit.py +21 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_layout_view.py +602 -2
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_leaderboard_patterns.py +136 -10
- pycascadeui-3.4.0/tests/test_logging_middleware.py +160 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_message_cleanup.py +45 -4
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_navigation.py +452 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_navigation_artifact_restore.py +21 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_persistence.py +825 -4
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_persistence_middleware.py +77 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_persistent_views.py +14 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_placement.py +100 -1
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_roles_patterns.py +14 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_tab_patterns.py +45 -0
- pycascadeui-3.4.0/tests/test_task_manager.py +204 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_undo.py +72 -0
- pycascadeui-3.4.0/tests/test_v2_helpers.py +1814 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_validation.py +72 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_view_init.py +91 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_wrappers.py +42 -0
- pycascadeui-3.3.3/cascadeui/utils/strings.py +0 -20
- pycascadeui-3.3.3/tests/test_v2_helpers.py +0 -672
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/LICENSE +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/__main__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/base.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/buttons.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/patterns/v1.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/selects.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/components/v1_composition.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/exceptions.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/backends/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/config.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/migrations.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/schema.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/persistence/schema_postgres.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/py.typed +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/setup.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/computed.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/middleware/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/reducers.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/singleton.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/slots.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/state/types.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/theming/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/theming/context.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/theming/core.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/theming/themes.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/tracing.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/utils/coercion.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/utils/decorators.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/utils/errors.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/utils/fetch.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/__init__.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/form.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/menu.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/types.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/patterns/wizard.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/cascadeui/views/view.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/pycascadeui.egg-info/dependency_links.txt +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/pycascadeui.egg-info/requires.txt +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/pycascadeui.egg-info/top_level.txt +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/setup.cfg +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_backends_postgres.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_backends_raw_sql.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_button_grid.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_components.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_computed.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_dynamic_persistent_button.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_emoji_grid.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_fetch.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_five_pillars.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_form_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_hooks.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_input_coercion.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_layout_pagination.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_layout_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_layout_persistent.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_menu_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_middleware.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_nav_version.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_owner_only.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_paginated_patterns.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_pagination.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_pillar_isolation.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_rebuild_callback.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_reducers.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_scoping.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_selects.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_state_slots.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_state_store.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_theming.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_update_coalescing.py +0 -0
- {pycascadeui-3.3.3 → pycascadeui-3.4.0}/tests/test_wizard_patterns.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycascadeui
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Redux-inspired UI framework for discord.py
|
|
5
5
|
Author-email: HollowTheSilver <hollow@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -651,7 +651,8 @@ for row in rows:
|
|
|
651
651
|
### Components
|
|
652
652
|
- Stateful buttons, selects, and modals with state integration
|
|
653
653
|
- Select callbacks can opt into a `values` second parameter
|
|
654
|
-
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
654
|
+
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `choice_row()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
655
|
+
- V2 stateful composites: `PaginatedRegion` (per-section pager), `Collapsible` (inline disclosure)
|
|
655
656
|
- Grid helpers: `emoji_grid()` and `button_grid()`
|
|
656
657
|
- Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
|
|
657
658
|
- Declarative `FormSchema` and `WizardSchema` base classes
|
|
@@ -608,7 +608,8 @@ for row in rows:
|
|
|
608
608
|
### Components
|
|
609
609
|
- Stateful buttons, selects, and modals with state integration
|
|
610
610
|
- Select callbacks can opt into a `values` second parameter
|
|
611
|
-
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
611
|
+
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `choice_row()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
612
|
+
- V2 stateful composites: `PaginatedRegion` (per-section pager), `Collapsible` (inline disclosure)
|
|
612
613
|
- Grid helpers: `emoji_grid()` and `button_grid()`
|
|
613
614
|
- Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
|
|
614
615
|
- Declarative `FormSchema` and `WizardSchema` base classes
|
|
@@ -22,8 +22,11 @@ from .components.buttons import (
|
|
|
22
22
|
)
|
|
23
23
|
from .components.inputs import Checkbox, CheckboxGroup, FileUpload, Modal, RadioGroup, TextInput
|
|
24
24
|
from .components.patterns import (
|
|
25
|
+
Choice,
|
|
26
|
+
Collapsible,
|
|
25
27
|
ConfirmationButtons,
|
|
26
28
|
EmojiGrid,
|
|
29
|
+
PaginatedRegion,
|
|
27
30
|
PaginationControls,
|
|
28
31
|
ProgressBar,
|
|
29
32
|
ToggleGroup,
|
|
@@ -32,6 +35,7 @@ from .components.patterns import (
|
|
|
32
35
|
button_grid,
|
|
33
36
|
button_row,
|
|
34
37
|
card,
|
|
38
|
+
choice_row,
|
|
35
39
|
confirm_section,
|
|
36
40
|
cycle_button,
|
|
37
41
|
divider,
|
|
@@ -55,7 +59,7 @@ from .components.selects import (
|
|
|
55
59
|
RoleSelect,
|
|
56
60
|
UserSelect,
|
|
57
61
|
)
|
|
58
|
-
from .components.types import EmojiInput, MediaInput
|
|
62
|
+
from .components.types import MAX_SELECT_OPTIONS, EmojiInput, MediaInput
|
|
59
63
|
from .components.v1_composition import CompositeComponent, get_component, register_component
|
|
60
64
|
from .components.wrappers import with_confirmation, with_cooldown, with_loading_state
|
|
61
65
|
from .devtools import DevToolsCog, InspectorView
|
|
@@ -100,11 +104,12 @@ from .utils.decorators import cascade_component, cascade_reducer
|
|
|
100
104
|
from .utils.errors import safe_execute, with_error_boundary, with_retry
|
|
101
105
|
from .utils.fetch import fetch_as_file
|
|
102
106
|
from .utils.logging import setup_logging
|
|
103
|
-
from .utils.strings import slugify
|
|
107
|
+
from .utils.strings import is_emoji, slugify
|
|
104
108
|
from .utils.tasks import get_task_manager
|
|
105
109
|
from .validation import (
|
|
106
110
|
ValidationResult,
|
|
107
111
|
choices,
|
|
112
|
+
emoji,
|
|
108
113
|
max_length,
|
|
109
114
|
max_value,
|
|
110
115
|
min_length,
|
|
@@ -147,7 +152,7 @@ if "SQLiteBackend" in _persistence_all:
|
|
|
147
152
|
# // ========================================( Script )======================================== // #
|
|
148
153
|
|
|
149
154
|
|
|
150
|
-
__version__ = "3.
|
|
155
|
+
__version__ = "3.4.0"
|
|
151
156
|
|
|
152
157
|
# Export public API
|
|
153
158
|
__all__ = [
|
|
@@ -226,6 +231,7 @@ __all__ = [
|
|
|
226
231
|
# Type aliases
|
|
227
232
|
"EmojiInput",
|
|
228
233
|
"MediaInput",
|
|
234
|
+
"MAX_SELECT_OPTIONS",
|
|
229
235
|
# V2 Cards & Sections
|
|
230
236
|
"card",
|
|
231
237
|
"action_section",
|
|
@@ -237,6 +243,9 @@ __all__ = [
|
|
|
237
243
|
"button_row",
|
|
238
244
|
"cycle_button",
|
|
239
245
|
"toggle_button",
|
|
246
|
+
# V2 Choices
|
|
247
|
+
"Choice",
|
|
248
|
+
"choice_row",
|
|
240
249
|
# V2 Content
|
|
241
250
|
"key_value",
|
|
242
251
|
"alert",
|
|
@@ -247,6 +256,10 @@ __all__ = [
|
|
|
247
256
|
"gap",
|
|
248
257
|
# V2 Navigation
|
|
249
258
|
"tab_nav",
|
|
259
|
+
# V2 Pagination
|
|
260
|
+
"PaginatedRegion",
|
|
261
|
+
# V2 Collapsible
|
|
262
|
+
"Collapsible",
|
|
250
263
|
# V2 Media
|
|
251
264
|
"gallery",
|
|
252
265
|
"file_attachment",
|
|
@@ -279,6 +292,7 @@ __all__ = [
|
|
|
279
292
|
"ValidationResult",
|
|
280
293
|
"validate_field",
|
|
281
294
|
"validate_fields",
|
|
295
|
+
"emoji",
|
|
282
296
|
"min_length",
|
|
283
297
|
"max_length",
|
|
284
298
|
"regex",
|
|
@@ -307,6 +321,7 @@ __all__ = [
|
|
|
307
321
|
"cascade_reducer",
|
|
308
322
|
"cascade_component",
|
|
309
323
|
"slugify",
|
|
324
|
+
"is_emoji",
|
|
310
325
|
"fetch_as_file",
|
|
311
326
|
# DevTools
|
|
312
327
|
"InspectorView",
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# // ========================================( Modules )======================================== // #
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
import logging
|
|
4
5
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
5
6
|
|
|
6
7
|
import discord
|
|
7
8
|
from discord import CheckboxGroupOption, Interaction, RadioGroupOption, TextStyle
|
|
8
9
|
|
|
10
|
+
from ..utils.strings import slugify
|
|
9
11
|
from ..validation import validate_fields
|
|
10
12
|
from .base import StatefulComponent
|
|
11
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
12
16
|
# // ========================================( Classes )======================================== // #
|
|
13
17
|
|
|
14
18
|
|
|
@@ -72,11 +76,12 @@ class TextInput(StatefulComponent):
|
|
|
72
76
|
def _slug(cls, label: str) -> str:
|
|
73
77
|
"""Derive a stable ``custom_id`` slug from a field label.
|
|
74
78
|
|
|
75
|
-
Single source of truth for the slug rule.
|
|
76
|
-
:
|
|
77
|
-
|
|
79
|
+
Single source of truth for the modal-input slug rule. Prefixes
|
|
80
|
+
``input_`` and delegates to :func:`slugify`, so modal inputs share
|
|
81
|
+
the one safe lowercase-alphanumeric derivation the rest of the
|
|
82
|
+
library uses.
|
|
78
83
|
"""
|
|
79
|
-
return f"input_{label
|
|
84
|
+
return f"input_{slugify(label)}"
|
|
80
85
|
|
|
81
86
|
def create_discord_component(self):
|
|
82
87
|
"""Build a ``ui.Label`` wrapping the inner ``ui.TextInput``."""
|
|
@@ -418,6 +423,7 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
418
423
|
if isinstance(input_item, _WRAPPED_INPUT_TYPES):
|
|
419
424
|
label_wrapper = input_item.create_discord_component()
|
|
420
425
|
inner = label_wrapper.component
|
|
426
|
+
self._reject_duplicate_input(input_item.custom_id)
|
|
421
427
|
self.add_item(label_wrapper)
|
|
422
428
|
self.inputs[input_item.custom_id] = input_item
|
|
423
429
|
self._wrapped_pairs.append((input_item, inner))
|
|
@@ -428,10 +434,28 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
428
434
|
# modal-compatible component the user constructed directly.
|
|
429
435
|
self.add_item(input_item)
|
|
430
436
|
inner = _unwrap_label(input_item)
|
|
437
|
+
self._reject_duplicate_input(inner.custom_id)
|
|
431
438
|
self.inputs[inner.custom_id] = input_item
|
|
432
439
|
|
|
433
440
|
self.user_callback = callback
|
|
434
441
|
|
|
442
|
+
def _reject_duplicate_input(self, custom_id: str) -> None:
|
|
443
|
+
"""Raise before a duplicate input custom_id silently overwrites.
|
|
444
|
+
|
|
445
|
+
Modal input ids derive from the label (``input_{label}``), so two
|
|
446
|
+
inputs with the same label collide on one key. Without this guard
|
|
447
|
+
the second silently overwrites the first in ``self.inputs`` and
|
|
448
|
+
its submitted value is lost at ``on_submit``.
|
|
449
|
+
"""
|
|
450
|
+
if custom_id in self.inputs:
|
|
451
|
+
raise ValueError(
|
|
452
|
+
f"Duplicate modal input custom_id: {custom_id!r}. Modal input ids derive "
|
|
453
|
+
f"from the label ('input_{{label}}'), so two inputs with the same label "
|
|
454
|
+
f"collide on one key: one would silently overwrite the other and its "
|
|
455
|
+
f"submitted value would be lost.\n"
|
|
456
|
+
f" Fix: Give the colliding inputs distinct labels."
|
|
457
|
+
)
|
|
458
|
+
|
|
435
459
|
async def on_submit(self, interaction: Interaction):
|
|
436
460
|
"""Handle modal submission with optional validation."""
|
|
437
461
|
# Collect values from the underlying discord.py components,
|
|
@@ -469,8 +493,11 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
469
493
|
if errors:
|
|
470
494
|
lines = []
|
|
471
495
|
for field_id, field_errors in errors.items():
|
|
496
|
+
# Field label ("Emoji"), not the derived custom_id slug ("input_emoji").
|
|
497
|
+
field = self.inputs.get(field_id)
|
|
498
|
+
name = getattr(field, "label", field_id)
|
|
472
499
|
for err in field_errors:
|
|
473
|
-
lines.append(f"**{
|
|
500
|
+
lines.append(f"**{name}**: {err.message}")
|
|
474
501
|
await interaction.response.send_message("\n".join(lines), ephemeral=True)
|
|
475
502
|
return
|
|
476
503
|
|
|
@@ -492,7 +519,29 @@ class Modal(discord.ui.Modal, StatefulComponent):
|
|
|
492
519
|
if self.user_callback:
|
|
493
520
|
await self.user_callback(interaction, values)
|
|
494
521
|
# Safety net: defer if the callback forgot to respond
|
|
495
|
-
|
|
496
|
-
await interaction.response.defer()
|
|
522
|
+
await self._safe_post_submit_defer(interaction)
|
|
497
523
|
else:
|
|
524
|
+
await self._safe_post_submit_defer(interaction)
|
|
525
|
+
|
|
526
|
+
async def _safe_post_submit_defer(self, interaction: Interaction) -> None:
|
|
527
|
+
"""Acknowledge the modal submission if the callback left it unanswered.
|
|
528
|
+
|
|
529
|
+
Mirrors the post-callback defer in the view's ``_scheduled_task``:
|
|
530
|
+
the submission's state dispatch has already landed, so a dead or
|
|
531
|
+
already-acked interaction on the trailing ack must not turn a
|
|
532
|
+
successful submit into an unhandled error routed to ``on_error``.
|
|
533
|
+
"""
|
|
534
|
+
if interaction.response.is_done():
|
|
535
|
+
return
|
|
536
|
+
try:
|
|
498
537
|
await interaction.response.defer()
|
|
538
|
+
except discord.NotFound:
|
|
539
|
+
# 10062: the interaction token is already gone -- nothing to ack.
|
|
540
|
+
logger.debug("Modal post-submit defer hit a dead interaction (10062)")
|
|
541
|
+
except discord.HTTPException as e:
|
|
542
|
+
# 40060 (already acknowledged) is a benign cancellation race; any
|
|
543
|
+
# other status is a genuine ack failure the user saw as a toast.
|
|
544
|
+
if e.code == 40060:
|
|
545
|
+
logger.debug("Modal post-submit defer raced an existing ack (40060)")
|
|
546
|
+
else:
|
|
547
|
+
logger.warning(f"Modal post-submit defer failed: status={e.status} code={e.code}")
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
from .v1 import ConfirmationButtons, PaginationControls, ProgressBar, ToggleGroup
|
|
5
5
|
from .v2 import (
|
|
6
|
+
Choice,
|
|
7
|
+
Collapsible,
|
|
6
8
|
EmojiGrid,
|
|
9
|
+
PaginatedRegion,
|
|
7
10
|
action_section,
|
|
8
11
|
alert,
|
|
9
12
|
button_grid,
|
|
10
13
|
button_row,
|
|
11
14
|
card,
|
|
15
|
+
choice_row,
|
|
12
16
|
confirm_section,
|
|
13
17
|
cycle_button,
|
|
14
18
|
divider,
|
|
@@ -46,6 +50,9 @@ __all__ = [
|
|
|
46
50
|
"button_row",
|
|
47
51
|
"cycle_button",
|
|
48
52
|
"toggle_button",
|
|
53
|
+
# V2 choices
|
|
54
|
+
"Choice",
|
|
55
|
+
"choice_row",
|
|
49
56
|
# V2 content
|
|
50
57
|
"key_value",
|
|
51
58
|
"alert",
|
|
@@ -56,6 +63,10 @@ __all__ = [
|
|
|
56
63
|
"gap",
|
|
57
64
|
# V2 navigation
|
|
58
65
|
"tab_nav",
|
|
66
|
+
# V2 pagination
|
|
67
|
+
"PaginatedRegion",
|
|
68
|
+
# V2 collapsible
|
|
69
|
+
"Collapsible",
|
|
59
70
|
# V2 media
|
|
60
71
|
"gallery",
|
|
61
72
|
"file_attachment",
|