Bis fuenf Eintraege Knoepfe statt Auswahlmenue
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Du hattest recht. Bei vier Anliegen ist ein Auswahlmenue der schlechtere Weg:
zwei Klicks statt einem, und die Moeglichkeiten sieht man erst nach dem
Aufklappen. Ein Knopf ist beides — sichtbar und sofort.

Umgekehrt gilt es nicht immer. Ab sechs wird die Knopfreihe zur Wand und
Discord bricht sie um; dann ist das Menue das ruhigere Bild. Also nicht
"Knoepfe statt Menue", sondern der Uebergang: bis fuenf Knoepfe, darueber
Menue. Fuenf, weil genau so viele in eine Reihe passen.

Betroffen waren nur zwei Stellen — Ticket-Anliegen und Wunsch-Bereiche. Alles
andere im Bot waren ohnehin schon Knoepfe.

Eine Sache verliert man dabei: die graue Beschreibungszeile, die ein Menue
unter jedem Eintrag anzeigt. Die steht jetzt im Text des Aufruf-Posts, und da
ist sie ehrlich gesagt besser aufgehoben — man liest sie, bevor man klickt,
statt danach.

Geprueft gegen discord.js selbst, weil Ueberlaengen und ungueltige Emojis sonst
erst beim Posten auffallen: null Eintraege (der eine alte Knopf), eins bis
fuenf (Knoepfe mit Emoji und der ID in der Kennung), sechs (zurueck zum Menue),
Murks-Emoji (faellt weg, Knopf bleibt), 200 Zeichen langer Name (auf 80
gekuerzt), und die Beschreibungszeile in allen drei Faellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 09:14:59 +02:00
co-authored by Claude Opus 5
parent b3758f1f76
commit b6b767acb7
5 changed files with 68 additions and 13 deletions
+12 -7
View File
@@ -200,12 +200,12 @@ export async function startBot() {
return;
}
// Ideen einreichen: Knopf oder Auswahl im Aufruf-Post -> Eingabefenster
if ((interaction.isButton() && interaction.customId === 'wunsch_neu')
// Ideen einreichen: Knopf (wunsch_neu / wunsch_pick:3) oder Auswahlmenue
if ((interaction.isButton() && /^wunsch_(neu|pick:\d+)$/.test(interaction.customId))
|| (interaction.isStringSelectMenu() && interaction.customId === 'wunsch_pick')) {
try {
if (interaction.isButton()) await handleWunschButton(interaction);
else await handleWunschPick(interaction);
if (interaction.isStringSelectMenu()) await handleWunschPick(interaction);
else await handleWunschButton(interaction);
} catch (error) {
console.error('[voting] Eingabefenster fehlgeschlagen:', error);
}
@@ -263,12 +263,17 @@ export async function startBot() {
return;
}
// Ticket: erstellen (Knopf ohne Anliegen, Menue mit), uebernehmen, schliessen
const ticketErstellen = (interaction.isButton() && interaction.customId === 'ticket_create')
// Ticket erstellen. Das Anliegen kommt je nach Zahl der Anliegen aus
// einem Knopf (ticket_pick:3) oder aus dem Auswahlmenue.
const ticketErstellen = (interaction.isButton()
&& /^ticket_(create|pick:\d+)$/.test(interaction.customId))
|| (interaction.isStringSelectMenu() && interaction.customId === 'ticket_pick');
if (ticketErstellen) {
try {
const gewaehlt = interaction.isStringSelectMenu() ? Number(interaction.values[0]) : null;
const roh = interaction.isStringSelectMenu()
? interaction.values[0]
: interaction.customId.split(':')[1];
const gewaehlt = Number(roh);
await createTicket(interaction, Number.isFinite(gewaehlt) ? gewaehlt : null);
} catch (error) {
console.error('[ticket] Erstellung fehlgeschlagen:', error);
+4 -2
View File
@@ -2,7 +2,7 @@
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
import { ticketChannelId, brandColor, brandFooter } from '../../runtime-settings.js';
import { renderTemplate } from '../../templates.js';
import { ticketPanelComponents } from '../tickets.js';
import { ticketPanelComponents, anliegenListe } from '../tickets.js';
import { listTicketCategories } from '../../db.js';
export const data = new SlashCommandBuilder()
@@ -22,7 +22,9 @@ export async function execute(interaction) {
const embed = new EmbedBuilder()
.setColor(brandColor())
.setTitle(renderTemplate('ticket.panel_title'))
.setDescription(renderTemplate('ticket.panel_text'))
// Bei Knoepfen ist fuer die Kurzbeschreibungen kein Platz — dann
// stehen sie hier im Text, wo man sie ohnehin vorher liest
.setDescription(renderTemplate('ticket.panel_text') + anliegenListe())
.setFooter({ text: brandFooter('SUPPORT') });
// Ohne Anliegen ein Knopf, mit Anliegen ein Auswahlmenue
+34 -1
View File
@@ -39,7 +39,14 @@ export function alsEmoji(text) {
return /\p{Extended_Pictographic}/u.test(zeichen) ? { name: zeichen } : null;
}
/** Knopf oder Auswahlmenü fürs Panel — je nachdem, ob es Anliegen gibt */
/**
* Ab wann ein Auswahlmenü statt Knöpfen. Fünf passen in eine Reihe; bis dahin
* ist ein Knopf besser als ein Menü — ein Klick statt zwei, und man sieht die
* Auswahl, ohne erst aufzuklappen. Darüber wird die Knopfreihe zur Wand.
*/
export const MAX_KNOEPFE = 5;
/** Knöpfe oder Auswahlmenü fürs Panel — je nachdem, wie viele Anliegen es gibt */
export function ticketPanelComponents() {
const kategorien = listTicketCategories();
if (kategorien.length === 0) {
@@ -52,6 +59,18 @@ export function ticketPanelComponents() {
)];
}
if (kategorien.length <= MAX_KNOEPFE) {
return [new ActionRowBuilder().addComponents(kategorien.map((k) => {
const knopf = new ButtonBuilder()
.setCustomId(`ticket_pick:${k.id}`)
.setStyle(ButtonStyle.Secondary)
.setLabel(k.name.slice(0, 80));
const emoji = alsEmoji(k.emoji);
if (emoji) knopf.setEmoji(emoji);
return knopf;
}))];
}
const menu = new StringSelectMenuBuilder()
.setCustomId('ticket_pick')
.setPlaceholder('Worum geht es?')
@@ -66,6 +85,20 @@ export function ticketPanelComponents() {
return [new ActionRowBuilder().addComponents(menu)];
}
/**
* Die Kurzbeschreibungen fürs Panel-Embed. Im Menü stehen sie als graue Zeile;
* bei Knöpfen ist dafür kein Platz — dann gehören sie in den Text, wo man sie
* ohnehin liest, bevor man klickt.
*/
export function anliegenListe() {
const kategorien = listTicketCategories();
if (kategorien.length === 0 || kategorien.length > MAX_KNOEPFE) return '';
const zeilen = kategorien
.filter((k) => k.beschreibung)
.map((k) => `${k.emoji || '•'} **${k.name}** — ${k.beschreibung}`);
return zeilen.length > 0 ? `\n\n${zeilen.join('\n')}` : '';
}
/** Knopfleiste im Ticket-Thread — der mittlere Knopf hängt am Bearbeiter */
export function ticketThreadComponents(claimedBy) {
const uebernehmen = claimedBy
+17 -2
View File
@@ -21,7 +21,7 @@ import { bilderSichern, bilderLoeschen, bildOrdner } from './bilder.js';
import { votingChannelId, publicUrl, brandColor, brandFooter } from '../runtime-settings.js';
import { renderTemplate } from '../templates.js';
import { tuning } from '../tuning.js';
import { alsEmoji } from './tickets.js';
import { alsEmoji, MAX_KNOEPFE } from './tickets.js';
// Bilder aus den Threads liegen neben der SQLite, wie bei Devlogs und Galerie
export const wunschBilderDir = bildOrdner('wish_images');
@@ -42,7 +42,7 @@ export const STATUS_EMOJI = {
offen: '💡', geplant: '📌', in_arbeit: '🔨', umgesetzt: '✅', abgelehnt: '🚫',
};
/** Knopf oder Auswahlmenü für den Aufruf-Post — je nachdem, ob es Bereiche gibt */
/** Knöpfe oder Auswahlmenü für den Aufruf-Post — je nachdem, wie viele Bereiche */
export function wunschPanelComponents() {
const bereiche = listWishCategories();
if (bereiche.length === 0) {
@@ -54,6 +54,21 @@ export function wunschPanelComponents() {
.setEmoji('💡')
)];
}
// Bis fünf Bereiche Knöpfe: ein Klick statt zwei, und die Auswahl steht da,
// ohne dass man erst aufklappt. Siehe MAX_KNOEPFE in tickets.js.
if (bereiche.length <= MAX_KNOEPFE) {
return [new ActionRowBuilder().addComponents(bereiche.map((b) => {
const knopf = new ButtonBuilder()
.setCustomId(`wunsch_pick:${b.id}`)
.setStyle(ButtonStyle.Secondary)
.setLabel(b.name.slice(0, 80));
const emoji = alsEmoji(b.emoji);
if (emoji) knopf.setEmoji(emoji);
return knopf;
}))];
}
const menu = new StringSelectMenuBuilder()
.setCustomId('wunsch_pick')
.setPlaceholder('Worum geht es?')