@azurioh/discord-kernel 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/authz/authorizer.d.ts +45 -0
- package/dist/authz/authorizer.d.ts.map +1 -0
- package/dist/authz/authorizer.js +31 -0
- package/dist/authz/authorizer.js.map +1 -0
- package/dist/authz/require-level-guard.d.ts +23 -0
- package/dist/authz/require-level-guard.d.ts.map +1 -0
- package/dist/authz/require-level-guard.js +37 -0
- package/dist/authz/require-level-guard.js.map +1 -0
- package/dist/clock.d.ts +13 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +13 -0
- package/dist/clock.js.map +1 -0
- package/dist/concurrency/keyed-queue.d.ts +18 -0
- package/dist/concurrency/keyed-queue.d.ts.map +1 -0
- package/dist/concurrency/keyed-queue.js +37 -0
- package/dist/concurrency/keyed-queue.js.map +1 -0
- package/dist/concurrency/run-with-concurrency.d.ts +15 -0
- package/dist/concurrency/run-with-concurrency.d.ts.map +1 -0
- package/dist/concurrency/run-with-concurrency.js +40 -0
- package/dist/concurrency/run-with-concurrency.js.map +1 -0
- package/dist/config/env.d.ts +17 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +49 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/errors.d.ts +14 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +25 -0
- package/dist/config/errors.js.map +1 -0
- package/dist/datetime.d.ts +34 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +188 -0
- package/dist/datetime.js.map +1 -0
- package/dist/discord/api-errors.d.ts +20 -0
- package/dist/discord/api-errors.d.ts.map +1 -0
- package/dist/discord/api-errors.js +90 -0
- package/dist/discord/api-errors.js.map +1 -0
- package/dist/discord/channel-exporter.d.ts +42 -0
- package/dist/discord/channel-exporter.d.ts.map +1 -0
- package/dist/discord/channel-exporter.js +3 -0
- package/dist/discord/channel-exporter.js.map +1 -0
- package/dist/discord/command/autocomplete-dispatcher.d.ts +26 -0
- package/dist/discord/command/autocomplete-dispatcher.d.ts.map +1 -0
- package/dist/discord/command/autocomplete-dispatcher.js +41 -0
- package/dist/discord/command/autocomplete-dispatcher.js.map +1 -0
- package/dist/discord/command/autocomplete.d.ts +18 -0
- package/dist/discord/command/autocomplete.d.ts.map +1 -0
- package/dist/discord/command/autocomplete.js +65 -0
- package/dist/discord/command/autocomplete.js.map +1 -0
- package/dist/discord/command/context.d.ts +33 -0
- package/dist/discord/command/context.d.ts.map +1 -0
- package/dist/discord/command/context.js +48 -0
- package/dist/discord/command/context.js.map +1 -0
- package/dist/discord/command/cooldown-guard.d.ts +25 -0
- package/dist/discord/command/cooldown-guard.d.ts.map +1 -0
- package/dist/discord/command/cooldown-guard.js +60 -0
- package/dist/discord/command/cooldown-guard.js.map +1 -0
- package/dist/discord/command/create-command.d.ts +100 -0
- package/dist/discord/command/create-command.d.ts.map +1 -0
- package/dist/discord/command/create-command.js +245 -0
- package/dist/discord/command/create-command.js.map +1 -0
- package/dist/discord/command/create-context-menu-command.d.ts +51 -0
- package/dist/discord/command/create-context-menu-command.d.ts.map +1 -0
- package/dist/discord/command/create-context-menu-command.js +128 -0
- package/dist/discord/command/create-context-menu-command.js.map +1 -0
- package/dist/discord/command/errors.d.ts +19 -0
- package/dist/discord/command/errors.d.ts.map +1 -0
- package/dist/discord/command/errors.js +27 -0
- package/dist/discord/command/errors.js.map +1 -0
- package/dist/discord/command/guard.d.ts +30 -0
- package/dist/discord/command/guard.d.ts.map +1 -0
- package/dist/discord/command/guard.js +34 -0
- package/dist/discord/command/guard.js.map +1 -0
- package/dist/discord/command/localization.d.ts +9 -0
- package/dist/discord/command/localization.d.ts.map +1 -0
- package/dist/discord/command/localization.js +13 -0
- package/dist/discord/command/localization.js.map +1 -0
- package/dist/discord/command/options.d.ts +113 -0
- package/dist/discord/command/options.d.ts.map +1 -0
- package/dist/discord/command/options.js +509 -0
- package/dist/discord/command/options.js.map +1 -0
- package/dist/discord/command/permission-guard.d.ts +21 -0
- package/dist/discord/command/permission-guard.d.ts.map +1 -0
- package/dist/discord/command/permission-guard.js +69 -0
- package/dist/discord/command/permission-guard.js.map +1 -0
- package/dist/discord/command/router.d.ts +48 -0
- package/dist/discord/command/router.d.ts.map +1 -0
- package/dist/discord/command/router.js +118 -0
- package/dist/discord/command/router.js.map +1 -0
- package/dist/discord/command/types.d.ts +51 -0
- package/dist/discord/command/types.d.ts.map +1 -0
- package/dist/discord/command/types.js +3 -0
- package/dist/discord/command/types.js.map +1 -0
- package/dist/discord/components/component-router.d.ts +116 -0
- package/dist/discord/components/component-router.d.ts.map +1 -0
- package/dist/discord/components/component-router.js +123 -0
- package/dist/discord/components/component-router.js.map +1 -0
- package/dist/discord/components/interactive-message/index.d.ts +3 -0
- package/dist/discord/components/interactive-message/index.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/index.js +11 -0
- package/dist/discord/components/interactive-message/index.js.map +1 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.d.ts +195 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.js +151 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.js.map +1 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.d.ts +30 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.js +44 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.js.map +1 -0
- package/dist/discord/components/paginator/index.d.ts +5 -0
- package/dist/discord/components/paginator/index.d.ts.map +1 -0
- package/dist/discord/components/paginator/index.js +15 -0
- package/dist/discord/components/paginator/index.js.map +1 -0
- package/dist/discord/components/paginator/mount-paginator.d.ts +25 -0
- package/dist/discord/components/paginator/mount-paginator.d.ts.map +1 -0
- package/dist/discord/components/paginator/mount-paginator.js +41 -0
- package/dist/discord/components/paginator/mount-paginator.js.map +1 -0
- package/dist/discord/components/paginator/page-state.d.ts +21 -0
- package/dist/discord/components/paginator/page-state.d.ts.map +1 -0
- package/dist/discord/components/paginator/page-state.js +44 -0
- package/dist/discord/components/paginator/page-state.js.map +1 -0
- package/dist/discord/components/paginator/paginator-buttons.d.ts +17 -0
- package/dist/discord/components/paginator/paginator-buttons.d.ts.map +1 -0
- package/dist/discord/components/paginator/paginator-buttons.js +45 -0
- package/dist/discord/components/paginator/paginator-buttons.js.map +1 -0
- package/dist/discord/components/paginator/paginator-view.d.ts +26 -0
- package/dist/discord/components/paginator/paginator-view.d.ts.map +1 -0
- package/dist/discord/components/paginator/paginator-view.js +28 -0
- package/dist/discord/components/paginator/paginator-view.js.map +1 -0
- package/dist/discord/components/persistent-paginator.d.ts +47 -0
- package/dist/discord/components/persistent-paginator.d.ts.map +1 -0
- package/dist/discord/components/persistent-paginator.js +137 -0
- package/dist/discord/components/persistent-paginator.js.map +1 -0
- package/dist/discord/components/settings-editor/index.d.ts +7 -0
- package/dist/discord/components/settings-editor/index.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/index.js +22 -0
- package/dist/discord/components/settings-editor/index.js.map +1 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.d.ts +140 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.js +344 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.js.map +1 -0
- package/dist/discord/components/settings-editor/navigation.d.ts +49 -0
- package/dist/discord/components/settings-editor/navigation.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/navigation.js +39 -0
- package/dist/discord/components/settings-editor/navigation.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts +56 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.js +284 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts +69 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.js +353 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.d.ts +216 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.js +66 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor.view.d.ts +303 -0
- package/dist/discord/components/settings-editor/settings-editor.view.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor.view.js +316 -0
- package/dist/discord/components/settings-editor/settings-editor.view.js.map +1 -0
- package/dist/discord/events/create-event.d.ts +5 -0
- package/dist/discord/events/create-event.d.ts.map +1 -0
- package/dist/discord/events/create-event.js +8 -0
- package/dist/discord/events/create-event.js.map +1 -0
- package/dist/discord/events/event-router.d.ts +16 -0
- package/dist/discord/events/event-router.d.ts.map +1 -0
- package/dist/discord/events/event-router.js +51 -0
- package/dist/discord/events/event-router.js.map +1 -0
- package/dist/discord/events/types.d.ts +16 -0
- package/dist/discord/events/types.d.ts.map +1 -0
- package/dist/discord/events/types.js +3 -0
- package/dist/discord/events/types.js.map +1 -0
- package/dist/discord/i18n.d.ts +58 -0
- package/dist/discord/i18n.d.ts.map +1 -0
- package/dist/discord/i18n.js +173 -0
- package/dist/discord/i18n.js.map +1 -0
- package/dist/discord/interaction/button.d.ts +39 -0
- package/dist/discord/interaction/button.d.ts.map +1 -0
- package/dist/discord/interaction/button.js +36 -0
- package/dist/discord/interaction/button.js.map +1 -0
- package/dist/discord/interaction/interaction-router.d.ts +21 -0
- package/dist/discord/interaction/interaction-router.d.ts.map +1 -0
- package/dist/discord/interaction/interaction-router.js +24 -0
- package/dist/discord/interaction/interaction-router.js.map +1 -0
- package/dist/discord/interaction/modal.d.ts +171 -0
- package/dist/discord/interaction/modal.d.ts.map +1 -0
- package/dist/discord/interaction/modal.js +264 -0
- package/dist/discord/interaction/modal.js.map +1 -0
- package/dist/discord/interaction/select-menu.d.ts +104 -0
- package/dist/discord/interaction/select-menu.d.ts.map +1 -0
- package/dist/discord/interaction/select-menu.js +126 -0
- package/dist/discord/interaction/select-menu.js.map +1 -0
- package/dist/discord/permissions.d.ts +32 -0
- package/dist/discord/permissions.d.ts.map +1 -0
- package/dist/discord/permissions.js +40 -0
- package/dist/discord/permissions.js.map +1 -0
- package/dist/discord/placeholder-registry-errors.d.ts +10 -0
- package/dist/discord/placeholder-registry-errors.d.ts.map +1 -0
- package/dist/discord/placeholder-registry-errors.js +18 -0
- package/dist/discord/placeholder-registry-errors.js.map +1 -0
- package/dist/discord/placeholder-registry.d.ts +53 -0
- package/dist/discord/placeholder-registry.d.ts.map +1 -0
- package/dist/discord/placeholder-registry.js +52 -0
- package/dist/discord/placeholder-registry.js.map +1 -0
- package/dist/discord/presenter.d.ts +26 -0
- package/dist/discord/presenter.d.ts.map +1 -0
- package/dist/discord/presenter.js +3 -0
- package/dist/discord/presenter.js.map +1 -0
- package/dist/discord/ui/card.d.ts +114 -0
- package/dist/discord/ui/card.d.ts.map +1 -0
- package/dist/discord/ui/card.js +162 -0
- package/dist/discord/ui/card.js.map +1 -0
- package/dist/discord/ui/color-input-errors.d.ts +12 -0
- package/dist/discord/ui/color-input-errors.d.ts.map +1 -0
- package/dist/discord/ui/color-input-errors.js +21 -0
- package/dist/discord/ui/color-input-errors.js.map +1 -0
- package/dist/discord/ui/color-input.d.ts +37 -0
- package/dist/discord/ui/color-input.d.ts.map +1 -0
- package/dist/discord/ui/color-input.js +166 -0
- package/dist/discord/ui/color-input.js.map +1 -0
- package/dist/discord/ui/colors.d.ts +44 -0
- package/dist/discord/ui/colors.d.ts.map +1 -0
- package/dist/discord/ui/colors.js +45 -0
- package/dist/discord/ui/colors.js.map +1 -0
- package/dist/discord/ui/embed.d.ts +16 -0
- package/dist/discord/ui/embed.d.ts.map +1 -0
- package/dist/discord/ui/embed.js +53 -0
- package/dist/discord/ui/embed.js.map +1 -0
- package/dist/discord/ui/message-attachments.d.ts +40 -0
- package/dist/discord/ui/message-attachments.d.ts.map +1 -0
- package/dist/discord/ui/message-attachments.js +27 -0
- package/dist/discord/ui/message-attachments.js.map +1 -0
- package/dist/errors/business-error.d.ts +41 -0
- package/dist/errors/business-error.d.ts.map +1 -0
- package/dist/errors/business-error.js +39 -0
- package/dist/errors/business-error.js.map +1 -0
- package/dist/errors/describe-error.d.ts +29 -0
- package/dist/errors/describe-error.d.ts.map +1 -0
- package/dist/errors/describe-error.js +49 -0
- package/dist/errors/describe-error.js.map +1 -0
- package/dist/errors/incident-ref.d.ts +7 -0
- package/dist/errors/incident-ref.d.ts.map +1 -0
- package/dist/errors/incident-ref.js +13 -0
- package/dist/errors/incident-ref.js.map +1 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +15 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/i18n/business-message.d.ts +14 -0
- package/dist/i18n/business-message.d.ts.map +1 -0
- package/dist/i18n/business-message.js +19 -0
- package/dist/i18n/business-message.js.map +1 -0
- package/dist/i18n/catalog.d.ts +27 -0
- package/dist/i18n/catalog.d.ts.map +1 -0
- package/dist/i18n/catalog.js +25 -0
- package/dist/i18n/catalog.js.map +1 -0
- package/dist/i18n/errors.d.ts +10 -0
- package/dist/i18n/errors.d.ts.map +1 -0
- package/dist/i18n/errors.js +18 -0
- package/dist/i18n/errors.js.map +1 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +16 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/locale.d.ts +22 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +40 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/translator.d.ts +35 -0
- package/dist/i18n/translator.d.ts.map +1 -0
- package/dist/i18n/translator.js +40 -0
- package/dist/i18n/translator.js.map +1 -0
- package/dist/logger.d.ts +28 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +3 -0
- package/dist/logger.js.map +1 -0
- package/dist/module/module.d.ts +42 -0
- package/dist/module/module.d.ts.map +1 -0
- package/dist/module/module.js +3 -0
- package/dist/module/module.js.map +1 -0
- package/dist/persistence/database.d.ts +24 -0
- package/dist/persistence/database.d.ts.map +1 -0
- package/dist/persistence/database.js +3 -0
- package/dist/persistence/database.js.map +1 -0
- package/dist/resilience/retry.d.ts +36 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +78 -0
- package/dist/resilience/retry.js.map +1 -0
- package/dist/scheduler/errors.d.ts +25 -0
- package/dist/scheduler/errors.d.ts.map +1 -0
- package/dist/scheduler/errors.js +45 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +68 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.js +162 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/package.json +89 -0
- package/src/authz/authorizer.ts +76 -0
- package/src/authz/require-level-guard.ts +42 -0
- package/src/clock.ts +18 -0
- package/src/concurrency/keyed-queue.ts +50 -0
- package/src/concurrency/run-with-concurrency.ts +44 -0
- package/src/config/env.ts +58 -0
- package/src/config/errors.ts +21 -0
- package/src/datetime.ts +221 -0
- package/src/discord/api-errors.ts +91 -0
- package/src/discord/channel-exporter.ts +48 -0
- package/src/discord/command/autocomplete-dispatcher.ts +54 -0
- package/src/discord/command/autocomplete.ts +76 -0
- package/src/discord/command/context.ts +85 -0
- package/src/discord/command/cooldown-guard.ts +81 -0
- package/src/discord/command/create-command.ts +431 -0
- package/src/discord/command/create-context-menu-command.ts +227 -0
- package/src/discord/command/errors.ts +22 -0
- package/src/discord/command/guard.ts +54 -0
- package/src/discord/command/localization.ts +11 -0
- package/src/discord/command/options.ts +847 -0
- package/src/discord/command/permission-guard.ts +74 -0
- package/src/discord/command/router.ts +142 -0
- package/src/discord/command/types.ts +52 -0
- package/src/discord/components/component-router.ts +213 -0
- package/src/discord/components/interactive-message/index.ts +18 -0
- package/src/discord/components/interactive-message/interactive-message-collector.ts +374 -0
- package/src/discord/components/interactive-message/mount-interactive-message.ts +71 -0
- package/src/discord/components/paginator/index.ts +17 -0
- package/src/discord/components/paginator/mount-paginator.ts +69 -0
- package/src/discord/components/paginator/page-state.ts +45 -0
- package/src/discord/components/paginator/paginator-buttons.ts +60 -0
- package/src/discord/components/paginator/paginator-view.ts +61 -0
- package/src/discord/components/persistent-paginator.ts +197 -0
- package/src/discord/components/settings-editor/index.ts +41 -0
- package/src/discord/components/settings-editor/mount-settings-editor.ts +646 -0
- package/src/discord/components/settings-editor/navigation.ts +67 -0
- package/src/discord/components/settings-editor/settings-editor-card.view.ts +344 -0
- package/src/discord/components/settings-editor/settings-editor-field-modal.ts +538 -0
- package/src/discord/components/settings-editor/settings-editor-fields.ts +290 -0
- package/src/discord/components/settings-editor/settings-editor.view.ts +654 -0
- package/src/discord/events/create-event.ts +7 -0
- package/src/discord/events/event-router.ts +53 -0
- package/src/discord/events/types.ts +17 -0
- package/src/discord/i18n.ts +173 -0
- package/src/discord/interaction/button.ts +75 -0
- package/src/discord/interaction/interaction-router.ts +28 -0
- package/src/discord/interaction/modal.ts +505 -0
- package/src/discord/interaction/select-menu.ts +246 -0
- package/src/discord/permissions.ts +57 -0
- package/src/discord/placeholder-registry-errors.ts +11 -0
- package/src/discord/placeholder-registry.ts +77 -0
- package/src/discord/presenter.ts +26 -0
- package/src/discord/ui/card.ts +303 -0
- package/src/discord/ui/color-input-errors.ts +17 -0
- package/src/discord/ui/color-input.ts +179 -0
- package/src/discord/ui/colors.ts +58 -0
- package/src/discord/ui/embed.ts +64 -0
- package/src/discord/ui/message-attachments.ts +57 -0
- package/src/errors/business-error.ts +49 -0
- package/src/errors/describe-error.ts +73 -0
- package/src/errors/incident-ref.ts +10 -0
- package/src/errors/index.ts +12 -0
- package/src/i18n/business-message.ts +23 -0
- package/src/i18n/catalog.ts +41 -0
- package/src/i18n/errors.ts +11 -0
- package/src/i18n/index.ts +11 -0
- package/src/i18n/locale.ts +41 -0
- package/src/i18n/translator.ts +78 -0
- package/src/logger.ts +28 -0
- package/src/module/module.ts +42 -0
- package/src/persistence/database.ts +23 -0
- package/src/resilience/retry.ts +102 -0
- package/src/scheduler/errors.ts +40 -0
- package/src/scheduler/scheduler.ts +248 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Attachment,
|
|
3
|
+
ChannelType,
|
|
4
|
+
type MessageComponentInteraction,
|
|
5
|
+
type ModalBuilder,
|
|
6
|
+
type ModalMessageModalSubmitInteraction,
|
|
7
|
+
} from "discord.js";
|
|
8
|
+
import {
|
|
9
|
+
isPickerField,
|
|
10
|
+
type SettingsEditorFieldValue,
|
|
11
|
+
type SettingsEditorGroupField,
|
|
12
|
+
type SettingsEditorPickerField,
|
|
13
|
+
type SettingsEditorValueField,
|
|
14
|
+
} from "@/discord/components/settings-editor/settings-editor-fields";
|
|
15
|
+
import { SETTINGS_EDITOR_MESSAGES } from "@/discord/i18n";
|
|
16
|
+
import {
|
|
17
|
+
type ChannelFieldDef,
|
|
18
|
+
createModal,
|
|
19
|
+
type ModalFields,
|
|
20
|
+
type RoleFieldDef,
|
|
21
|
+
type SelectFieldDef,
|
|
22
|
+
type TextFieldDef,
|
|
23
|
+
type UserFieldDef,
|
|
24
|
+
} from "@/discord/interaction/modal";
|
|
25
|
+
import type { Locale, Translator } from "@/i18n";
|
|
26
|
+
|
|
27
|
+
const EDITOR_FIELD_MODAL_ID = "settings-editor:field-value";
|
|
28
|
+
const EDITOR_GROUP_MODAL_ID = "settings-editor:group-value";
|
|
29
|
+
|
|
30
|
+
/** Generous enough to write a whole embed description without racing a timeout. */
|
|
31
|
+
const MODAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `createModal(...).build()` with no state produces the same constant
|
|
35
|
+
* customId for every field modal (`EDITOR_FIELD_MODAL_ID`) and every group
|
|
36
|
+
* modal (`EDITOR_GROUP_MODAL_ID`) — one identifier shared by the whole
|
|
37
|
+
* editor. Combined with a `filter` that only checked the user, a stale
|
|
38
|
+
* `awaitModalSubmit` left behind by a modal the administrator opened and then
|
|
39
|
+
* dismissed (it keeps listening for the rest of {@link MODAL_TIMEOUT_MS})
|
|
40
|
+
* would resolve on the *next* modal that same user submitted, whatever field
|
|
41
|
+
* it belonged to — reported as opening "Add a game", abandoning it, then
|
|
42
|
+
* declaring a request field named "Date" ending up creating a game named
|
|
43
|
+
* "Date".
|
|
44
|
+
*
|
|
45
|
+
* The fix needs an identifier that is unique per *opening*, not just per
|
|
46
|
+
* field: the field/group key alone is not enough, because the same field can
|
|
47
|
+
* be reopened while an earlier, dismissed opening's wait is still alive. A
|
|
48
|
+
* process-local monotonic counter appended to the key is enough for that —
|
|
49
|
+
* it only has to distinguish opens within this process's own lifetime, never
|
|
50
|
+
* survive a restart, and carries no notion of time, so a `Clock` would be the
|
|
51
|
+
* wrong tool (and untestable without faking time for no reason).
|
|
52
|
+
*
|
|
53
|
+
* Making the customId dynamic like this is safe *here*: unlike a persistent
|
|
54
|
+
* component, this modal is never registered on the `ComponentRouter` — both
|
|
55
|
+
* `promptEditorFieldValue` and `promptEditorGroupValue` collect their
|
|
56
|
+
* submission directly with `awaitModalSubmit`, a one-shot collector that
|
|
57
|
+
* matches by whatever customId it is told to expect, not by the router's
|
|
58
|
+
* `customId`/`customId:*` rule. A dynamic id would break the router (it needs
|
|
59
|
+
* a stable prefix to keep matching after a restart); it does not break a
|
|
60
|
+
* collector.
|
|
61
|
+
*/
|
|
62
|
+
let modalOpenSequence = 0;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A customId suffix unique to one modal opening: `key` for readability when
|
|
66
|
+
* debugging, plus a counter so reopening the same field still yields a
|
|
67
|
+
* different id from the opening before it.
|
|
68
|
+
*/
|
|
69
|
+
function nextModalOpenState(key: string): string {
|
|
70
|
+
modalOpenSequence += 1;
|
|
71
|
+
return `${key}-${modalOpenSequence}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The customId a built modal actually carries, read back rather than reassembled. */
|
|
75
|
+
function customIdOf(modal: ModalBuilder): string {
|
|
76
|
+
return (modal.toJSON() as { custom_id: string }).custom_id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One image per override — the embed has exactly one slot for each. */
|
|
80
|
+
const MAX_UPLOADS = 1;
|
|
81
|
+
|
|
82
|
+
export interface SettingsEditorSubmission {
|
|
83
|
+
/** The modal's own submit interaction — the only one still usable to answer. */
|
|
84
|
+
readonly interaction: ModalMessageModalSubmitInteraction;
|
|
85
|
+
/** `null` when the field came back empty, which is how an override is cleared. */
|
|
86
|
+
readonly value: string | null;
|
|
87
|
+
/**
|
|
88
|
+
* The uploaded image, on the fields that offer one. Always `null` elsewhere,
|
|
89
|
+
* and `null` on those when nothing was uploaded.
|
|
90
|
+
*/
|
|
91
|
+
readonly upload: Attachment | null;
|
|
92
|
+
/**
|
|
93
|
+
* What a picker field reported. `null` when the selection was emptied — that
|
|
94
|
+
* is how such a setting is cleared. Always `null` on a value field.
|
|
95
|
+
*/
|
|
96
|
+
readonly ids: readonly string[] | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Ask for one field's new value, prefilled with the current one, and wait for
|
|
101
|
+
* the answer. Returns `null` on timeout or on any failure to show/collect it
|
|
102
|
+
* (the administrator dismissed the modal, say) — the caller treats that as
|
|
103
|
+
* "nothing happened", never as an error, and the screen stays as it was.
|
|
104
|
+
*
|
|
105
|
+
* `showModal` finally acknowledges `interaction`: nothing can `.update()` on it
|
|
106
|
+
* again, which is why the submission carries its own interaction to answer
|
|
107
|
+
* through.
|
|
108
|
+
*
|
|
109
|
+
* `MessageComponentInteraction` rather than `AnySelectMenuInteraction`: a menu
|
|
110
|
+
* entry opens the very same modal whether it is reached by picking it from a
|
|
111
|
+
* select (the embeds layout) or by clicking the button its own `Section`
|
|
112
|
+
* carries (the card layout), and both interaction kinds carry `showModal` and
|
|
113
|
+
* `awaitModalSubmit` from that shared base.
|
|
114
|
+
*/
|
|
115
|
+
export async function promptEditorFieldValue<F extends string>(
|
|
116
|
+
interaction: MessageComponentInteraction,
|
|
117
|
+
field: SettingsEditorValueField<F> | SettingsEditorPickerField<F>,
|
|
118
|
+
current: SettingsEditorFieldValue,
|
|
119
|
+
translator: Translator,
|
|
120
|
+
locale: Locale,
|
|
121
|
+
): Promise<SettingsEditorSubmission | null> {
|
|
122
|
+
const modal = isPickerField(field)
|
|
123
|
+
? createPickerModal(field, current, translator, locale)
|
|
124
|
+
: field.kind === "image"
|
|
125
|
+
? createImageModal(field, current, translator, locale)
|
|
126
|
+
: createTextModal(field, current, translator, locale);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const built = modal.build(nextModalOpenState(field.key));
|
|
130
|
+
const expectedCustomId = customIdOf(built);
|
|
131
|
+
await interaction.showModal(built);
|
|
132
|
+
const submitted = await interaction.awaitModalSubmit({
|
|
133
|
+
time: MODAL_TIMEOUT_MS,
|
|
134
|
+
filter: (submission) =>
|
|
135
|
+
submission.user.id === interaction.user.id && submission.customId === expectedCustomId,
|
|
136
|
+
});
|
|
137
|
+
// A modal opened from a message submits as a `ModalMessageModalSubmit`
|
|
138
|
+
// interaction — the only kind that can `.update()` the screen in place.
|
|
139
|
+
// Anything else did not come from it, so it is dropped rather than answered
|
|
140
|
+
// on a message it has no claim to.
|
|
141
|
+
if (!submitted.isFromMessage()) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return { interaction: submitted, ...modal.read(submitted) };
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** What one member field of a group came back with — the group's own per-member slice. */
|
|
151
|
+
export interface SettingsEditorGroupMemberSubmission {
|
|
152
|
+
readonly value: string | null;
|
|
153
|
+
readonly upload: Attachment | null;
|
|
154
|
+
readonly ids: readonly string[] | null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* What a group's modal came back with, one entry per member field it declared.
|
|
159
|
+
* A member absent from a submission cannot happen — every member the group
|
|
160
|
+
* declares is a component of the very modal that produced this — but the type
|
|
161
|
+
* stays a partial record rather than asserting it, since nothing here proves it
|
|
162
|
+
* to the compiler.
|
|
163
|
+
*/
|
|
164
|
+
export type SettingsEditorGroupSubmission<F extends string> = Readonly<
|
|
165
|
+
Partial<Record<F, SettingsEditorGroupMemberSubmission>>
|
|
166
|
+
>;
|
|
167
|
+
|
|
168
|
+
/** What opening a group's modal resolves to: the submitted values, and the one interaction left to answer through. */
|
|
169
|
+
export interface SettingsEditorGroupPrompt<F extends string> {
|
|
170
|
+
readonly interaction: ModalMessageModalSubmitInteraction;
|
|
171
|
+
readonly values: SettingsEditorGroupSubmission<F>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Ask for every setting a group declares at once, in a single modal — the
|
|
176
|
+
* group's counterpart of {@link promptEditorFieldValue}. Each member is laid
|
|
177
|
+
* out in the geometry its own `kind` imposes, exactly as it would if it were
|
|
178
|
+
* prompted on its own, and prefilled the same way: from `currentValue`, asked
|
|
179
|
+
* once per member.
|
|
180
|
+
*
|
|
181
|
+
* `legend` is extra text shown above the fields — a token list a message field
|
|
182
|
+
* reads against, say — already counted against the modal's own five-component
|
|
183
|
+
* budget by the caller that built it; `null` when the group has nothing to say
|
|
184
|
+
* there.
|
|
185
|
+
*/
|
|
186
|
+
export async function promptEditorGroupValue<F extends string>(
|
|
187
|
+
interaction: MessageComponentInteraction,
|
|
188
|
+
group: SettingsEditorGroupField<F>,
|
|
189
|
+
currentValue: (memberKey: F) => SettingsEditorFieldValue,
|
|
190
|
+
legend: string | null,
|
|
191
|
+
translator: Translator,
|
|
192
|
+
locale: Locale,
|
|
193
|
+
): Promise<SettingsEditorGroupPrompt<F> | null> {
|
|
194
|
+
const modal = createGroupModal(group, currentValue, legend, translator, locale);
|
|
195
|
+
try {
|
|
196
|
+
const built = modal.build(nextModalOpenState(group.key));
|
|
197
|
+
const expectedCustomId = customIdOf(built);
|
|
198
|
+
await interaction.showModal(built);
|
|
199
|
+
const submitted = await interaction.awaitModalSubmit({
|
|
200
|
+
time: MODAL_TIMEOUT_MS,
|
|
201
|
+
filter: (submission) =>
|
|
202
|
+
submission.user.id === interaction.user.id && submission.customId === expectedCustomId,
|
|
203
|
+
});
|
|
204
|
+
if (!submitted.isFromMessage()) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return { interaction: submitted, values: modal.read(submitted) };
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Reading a group's submission back — the group's own counterpart of {@link EditorFieldModal}. */
|
|
214
|
+
interface EditorGroupModal<F extends string> {
|
|
215
|
+
build(state?: string): ModalBuilder;
|
|
216
|
+
read(interaction: ModalMessageModalSubmitInteraction): SettingsEditorGroupSubmission<F>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A group's modal: one component per member field, each in the shape its own
|
|
221
|
+
* `kind` names, built through the very builders a single-field modal uses —
|
|
222
|
+
* `createModal`'s own generic `read` is what lets this stay one function
|
|
223
|
+
* whatever mix of text and picker members a group declares.
|
|
224
|
+
*
|
|
225
|
+
* A member of kind `image` is refused here rather than supported: an image
|
|
226
|
+
* field needs two components of its own (the upload, and the address beside
|
|
227
|
+
* it), which a group already spending one slot per member — and possibly one
|
|
228
|
+
* more on `legend` — has no general room for, and no group this repository
|
|
229
|
+
* declares needs one.
|
|
230
|
+
*/
|
|
231
|
+
function createGroupModal<F extends string>(
|
|
232
|
+
group: SettingsEditorGroupField<F>,
|
|
233
|
+
currentValue: (memberKey: F) => SettingsEditorFieldValue,
|
|
234
|
+
legend: string | null,
|
|
235
|
+
translator: Translator,
|
|
236
|
+
locale: Locale,
|
|
237
|
+
): EditorGroupModal<F> {
|
|
238
|
+
const label = translator.translate(locale, group.labelKey);
|
|
239
|
+
const fields: ModalFields = {};
|
|
240
|
+
for (const member of group.fields) {
|
|
241
|
+
const current = currentValue(member.key);
|
|
242
|
+
if (isPickerField(member)) {
|
|
243
|
+
fields[member.key] = pickerFieldDef(
|
|
244
|
+
member,
|
|
245
|
+
pickerShared(member, translator, locale),
|
|
246
|
+
current.picked,
|
|
247
|
+
translator,
|
|
248
|
+
locale,
|
|
249
|
+
);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (member.kind === "image") {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Settings editor group "${group.key}" declares member "${member.key}" of kind "image"; a group's modal has no room for the separate upload component an image field needs. Declare it as "text" instead.`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
fields[member.key] = valueFieldDefOf(member, current, translator, locale);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const modal = createModal({
|
|
261
|
+
id: EDITOR_GROUP_MODAL_ID,
|
|
262
|
+
title: modalTitle(label, translator, locale),
|
|
263
|
+
...(legend === null ? {} : { legend }),
|
|
264
|
+
fields,
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
build: (state) => modal.build(state),
|
|
268
|
+
read: (interaction) => {
|
|
269
|
+
const raw = modal.read(interaction) as unknown as Record<
|
|
270
|
+
string,
|
|
271
|
+
string | null | readonly string[]
|
|
272
|
+
>;
|
|
273
|
+
const values: Partial<Record<F, SettingsEditorGroupMemberSubmission>> = {};
|
|
274
|
+
for (const member of group.fields) {
|
|
275
|
+
values[member.key] = isPickerField(member)
|
|
276
|
+
? { value: null, upload: null, ids: raw[member.key] as readonly string[] | null }
|
|
277
|
+
: { value: raw[member.key] as string | null, upload: null, ids: null };
|
|
278
|
+
}
|
|
279
|
+
return values;
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Reading a submission back, whichever of the two modals produced it. Both
|
|
286
|
+
* answer the same two questions, so the caller never branches on which was
|
|
287
|
+
* shown.
|
|
288
|
+
*/
|
|
289
|
+
interface EditorFieldModal {
|
|
290
|
+
build(state?: string): ModalBuilder;
|
|
291
|
+
read(interaction: ModalMessageModalSubmitInteraction): {
|
|
292
|
+
value: string | null;
|
|
293
|
+
upload: Attachment | null;
|
|
294
|
+
ids: readonly string[] | null;
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function modalTitle(label: string, translator: Translator, locale: Locale): string {
|
|
299
|
+
return translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalTitle, { field: label });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The greyed-out example inside the box, for a field that declared one. */
|
|
303
|
+
function placeholderOf<F extends string>(
|
|
304
|
+
field: SettingsEditorValueField<F>,
|
|
305
|
+
translator: Translator,
|
|
306
|
+
locale: Locale,
|
|
307
|
+
): { placeholder?: string } {
|
|
308
|
+
if (field.placeholderKey === undefined) {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
placeholder: translator.translate(locale, field.placeholderKey, field.placeholderParams),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The one text-input component a `text`-kind field builds, whether it is
|
|
318
|
+
* prompted on its own (via {@link createTextModal}) or as a member of a group
|
|
319
|
+
* — the shape a group's modal needs from `createModal`'s own generic `read`
|
|
320
|
+
* is exactly this, unlabelled by which caller asked for it.
|
|
321
|
+
*/
|
|
322
|
+
function valueFieldDefOf<F extends string>(
|
|
323
|
+
field: SettingsEditorValueField<F>,
|
|
324
|
+
current: SettingsEditorFieldValue,
|
|
325
|
+
translator: Translator,
|
|
326
|
+
locale: Locale,
|
|
327
|
+
): TextFieldDef {
|
|
328
|
+
return {
|
|
329
|
+
kind: "text",
|
|
330
|
+
label: translator.translate(locale, field.labelKey),
|
|
331
|
+
description: translator.translate(
|
|
332
|
+
locale,
|
|
333
|
+
field.helperKey ?? SETTINGS_EDITOR_MESSAGES.modalHelper,
|
|
334
|
+
),
|
|
335
|
+
style: field.style,
|
|
336
|
+
required: false,
|
|
337
|
+
maxLength: field.maxLength,
|
|
338
|
+
...placeholderOf(field, translator, locale),
|
|
339
|
+
...(current.text === null ? {} : { value: current.text }),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Every field but the images: one optional text input, emptied to clear. */
|
|
344
|
+
function createTextModal<F extends string>(
|
|
345
|
+
field: SettingsEditorValueField<F>,
|
|
346
|
+
current: SettingsEditorFieldValue,
|
|
347
|
+
translator: Translator,
|
|
348
|
+
locale: Locale,
|
|
349
|
+
): EditorFieldModal {
|
|
350
|
+
const label = translator.translate(locale, field.labelKey);
|
|
351
|
+
const modal = createModal({
|
|
352
|
+
id: EDITOR_FIELD_MODAL_ID,
|
|
353
|
+
title: modalTitle(label, translator, locale),
|
|
354
|
+
fields: { value: valueFieldDefOf(field, current, translator, locale) },
|
|
355
|
+
});
|
|
356
|
+
return {
|
|
357
|
+
build: (state) => modal.build(state),
|
|
358
|
+
read: (interaction) => ({ value: modal.read(interaction).value, upload: null, ids: null }),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* An image field: a file upload and the address input side by side, both
|
|
364
|
+
* optional, two of the five components a modal may carry.
|
|
365
|
+
*
|
|
366
|
+
* Offering both is what keeps an administrator from having to host the file
|
|
367
|
+
* somewhere first, without taking away the address that a guild already
|
|
368
|
+
* pointing at its own CDN relies on.
|
|
369
|
+
*/
|
|
370
|
+
function createImageModal<F extends string>(
|
|
371
|
+
field: SettingsEditorValueField<F>,
|
|
372
|
+
current: SettingsEditorFieldValue,
|
|
373
|
+
translator: Translator,
|
|
374
|
+
locale: Locale,
|
|
375
|
+
): EditorFieldModal {
|
|
376
|
+
const label = translator.translate(locale, field.labelKey);
|
|
377
|
+
const modal = createModal({
|
|
378
|
+
id: EDITOR_FIELD_MODAL_ID,
|
|
379
|
+
title: modalTitle(label, translator, locale),
|
|
380
|
+
fields: {
|
|
381
|
+
upload: {
|
|
382
|
+
kind: "file",
|
|
383
|
+
label: translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalUploadLabel),
|
|
384
|
+
description: translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalUploadHelper, {
|
|
385
|
+
max: field.uploadMaxMib ?? 0,
|
|
386
|
+
}),
|
|
387
|
+
required: false,
|
|
388
|
+
maxValues: MAX_UPLOADS,
|
|
389
|
+
},
|
|
390
|
+
value: {
|
|
391
|
+
kind: "text",
|
|
392
|
+
label: translator.translate(locale, SETTINGS_EDITOR_MESSAGES.modalImageUrlLabel),
|
|
393
|
+
description: translator.translate(
|
|
394
|
+
locale,
|
|
395
|
+
current.uploaded
|
|
396
|
+
? SETTINGS_EDITOR_MESSAGES.modalImageHelperUploaded
|
|
397
|
+
: SETTINGS_EDITOR_MESSAGES.modalImageHelper,
|
|
398
|
+
),
|
|
399
|
+
style: field.style,
|
|
400
|
+
required: false,
|
|
401
|
+
maxLength: field.maxLength,
|
|
402
|
+
...(current.text === null ? {} : { value: current.text }),
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
build: (state) => modal.build(state),
|
|
408
|
+
read: (interaction) => {
|
|
409
|
+
const values = modal.read(interaction);
|
|
410
|
+
return { value: values.value, upload: values.upload?.[0] ?? null, ids: null };
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* A picker field's modal: one select of the shape its kind names — role, user,
|
|
417
|
+
* channel or a closed set of choices — prefilled with what it already holds.
|
|
418
|
+
*
|
|
419
|
+
* `category` narrows a channel select to {@link ChannelType.GuildCategory}: the
|
|
420
|
+
* very shorthand `createPickerRow` used to express, reproduced here rather than
|
|
421
|
+
* left behind now that the picker lives in a modal instead of the message.
|
|
422
|
+
*/
|
|
423
|
+
function createPickerModal<F extends string>(
|
|
424
|
+
field: SettingsEditorPickerField<F>,
|
|
425
|
+
current: SettingsEditorFieldValue,
|
|
426
|
+
translator: Translator,
|
|
427
|
+
locale: Locale,
|
|
428
|
+
): EditorFieldModal {
|
|
429
|
+
const label = translator.translate(locale, field.labelKey);
|
|
430
|
+
const modal = createModal({
|
|
431
|
+
id: EDITOR_FIELD_MODAL_ID,
|
|
432
|
+
title: modalTitle(label, translator, locale),
|
|
433
|
+
fields: {
|
|
434
|
+
value: pickerFieldDef(
|
|
435
|
+
field,
|
|
436
|
+
pickerShared(field, translator, locale),
|
|
437
|
+
current.picked,
|
|
438
|
+
translator,
|
|
439
|
+
locale,
|
|
440
|
+
),
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
build: (state) => modal.build(state),
|
|
445
|
+
read: (interaction) => {
|
|
446
|
+
const read = modal.read(interaction) as { value: readonly string[] | null };
|
|
447
|
+
return { value: null, upload: null, ids: read.value };
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* What every picker component shares, whatever its own kind narrows it to —
|
|
454
|
+
* used whether the field is prompted on its own or as a member of a group.
|
|
455
|
+
*/
|
|
456
|
+
function pickerShared<F extends string>(
|
|
457
|
+
field: SettingsEditorPickerField<F>,
|
|
458
|
+
translator: Translator,
|
|
459
|
+
locale: Locale,
|
|
460
|
+
): {
|
|
461
|
+
label: string;
|
|
462
|
+
required: boolean;
|
|
463
|
+
minValues?: number;
|
|
464
|
+
maxValues?: number;
|
|
465
|
+
placeholder?: string;
|
|
466
|
+
} {
|
|
467
|
+
const placeholder =
|
|
468
|
+
field.pickerPlaceholderKey === undefined
|
|
469
|
+
? undefined
|
|
470
|
+
: translator.translate(locale, field.pickerPlaceholderKey);
|
|
471
|
+
return {
|
|
472
|
+
label: translator.translate(locale, field.labelKey),
|
|
473
|
+
// Zero is what lets the administrator clear the setting by emptying the
|
|
474
|
+
// select — `createModal` then hands `null` back on an empty submission
|
|
475
|
+
// instead of refusing to open with nothing picked.
|
|
476
|
+
required: (field.minValues ?? 1) !== 0,
|
|
477
|
+
minValues: field.minValues,
|
|
478
|
+
maxValues: field.maxValues,
|
|
479
|
+
...(placeholder === undefined ? {} : { placeholder }),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The one component a picker modal carries, in the shape its field's kind names. */
|
|
484
|
+
function pickerFieldDef<F extends string>(
|
|
485
|
+
field: SettingsEditorPickerField<F>,
|
|
486
|
+
shared: {
|
|
487
|
+
label: string;
|
|
488
|
+
required: boolean;
|
|
489
|
+
minValues?: number;
|
|
490
|
+
maxValues?: number;
|
|
491
|
+
placeholder?: string;
|
|
492
|
+
},
|
|
493
|
+
held: readonly string[],
|
|
494
|
+
translator: Translator,
|
|
495
|
+
locale: Locale,
|
|
496
|
+
): RoleFieldDef | UserFieldDef | ChannelFieldDef | SelectFieldDef {
|
|
497
|
+
// `modal.ts`'s builders open a default whenever the key is present at all,
|
|
498
|
+
// empty array included — so an empty selection is left out entirely rather
|
|
499
|
+
// than handed through, the same guard `createRoleSelect` (the select-menu
|
|
500
|
+
// DSL the in-message picker used) already applied.
|
|
501
|
+
switch (field.kind) {
|
|
502
|
+
case "role":
|
|
503
|
+
return { kind: "role", ...shared, ...(held.length === 0 ? {} : { defaultRoleIds: held }) };
|
|
504
|
+
case "user":
|
|
505
|
+
return { kind: "user", ...shared, ...(held.length === 0 ? {} : { defaultUserIds: held }) };
|
|
506
|
+
case "category":
|
|
507
|
+
return {
|
|
508
|
+
kind: "channel",
|
|
509
|
+
...shared,
|
|
510
|
+
channelTypes: [ChannelType.GuildCategory],
|
|
511
|
+
...(held.length === 0 ? {} : { defaultChannelIds: held }),
|
|
512
|
+
};
|
|
513
|
+
case "channel":
|
|
514
|
+
return {
|
|
515
|
+
kind: "channel",
|
|
516
|
+
...shared,
|
|
517
|
+
channelTypes: field.channelTypes,
|
|
518
|
+
...(held.length === 0 ? {} : { defaultChannelIds: held }),
|
|
519
|
+
};
|
|
520
|
+
case "choice":
|
|
521
|
+
return {
|
|
522
|
+
kind: "select",
|
|
523
|
+
...shared,
|
|
524
|
+
options: field.choices.map((choice) => ({
|
|
525
|
+
label: translator.translate(locale, choice.labelKey),
|
|
526
|
+
value: choice.value,
|
|
527
|
+
...(choice.descriptionKey === undefined
|
|
528
|
+
? {}
|
|
529
|
+
: { description: translator.translate(locale, choice.descriptionKey) }),
|
|
530
|
+
// Marked rather than prefilled the way an entity select's default is:
|
|
531
|
+
// Discord has no "default value" concept for a string select, only a
|
|
532
|
+
// per-option flag, which is also how the reader still sees which of a
|
|
533
|
+
// closed set is in force.
|
|
534
|
+
default: held.includes(choice.value),
|
|
535
|
+
})),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|