Playtester werden jetzt beworben, nicht angeklickt
Der Knopf unter dem Playtester-Aufruf hat jeden aufgenommen, der ihn gedrueckt hat — ohne Frage, ohne Pruefung. Fuer ein Programm, dessen Teilnehmer Zugang und Alpha-Keys bekommen, ist das die falsche Tuer. Statt einen zweiten Pruef-Ablauf danebenzustellen, laeuft die Aufnahme jetzt ueber die Bewerbungs-Formulare, die es laengst gibt: Modal ausfuellen, Review-Embed im Staff-Kanal, ✅ oder ❌, Rolle und Antwort-DM bei Zusage. Das ist dieselbe Maschinerie, nur ein anderer Aufhaenger — und damit auch nur eine Stelle, an der spaeter etwas kaputtgehen kann. Ein Formular laesst sich als Playtester-Formular markieren ("Annahme traegt als Playtester ein"). Wer dort angenommen wird, landet zusaetzlich in der Playtester-Liste, damit die Schluessel-Verteilung ihn kennt. Nur eines kann es sein — sonst wuesste /playtester-setup nicht, welches es posten soll. /playtester-setup postet jetzt den Knopf dieses Formulars. Fehlt das Formular, fehlen Fragen oder fehlt der Review-Kanal, sagt der Befehl das, statt einen Aufruf zu posten, der ins Leere fuehrt. Der alte Knopf in bereits geposteten Nachrichten nimmt niemanden mehr auf: er verweist auf den Aufruf. Austreten bleibt Selbstbedienung — dafuer braucht es keine Freigabe. Im Panel steht im Playtester-Bereich, ueber welches Formular die Aufnahme laeuft, ob es schon gepostet ist, und ein Weg dorthin. Ohne markiertes Formular steht dort, dass gerade niemand hereinkommt — das ist sonst der Grund, warum sich tagelang niemand bewirbt. Geprueft: Routen, SQL gegen das Schema, und im Browser beide Zustaende des Hinweises sowie der neue Schalter — beim Wechsel zwischen zwei Formularen folgt er dem richtigen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,10 +76,10 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen.
|
||||
| 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung; am Gewinner-Post 🔁 Neu auslosen + 👥 Teilnehmerliste |
|
||||
| 🎂 **Geburtstage** | `/geburtstag` zum Eintragen; morgens Gratulation im Kanal + Tages-Rolle |
|
||||
| 📅 **Events** | Neue Discord-Events werden angekündigt + öffentliche Events-Seite |
|
||||
| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; eigener Panel-Bereich mit Liste, Rolle und dem Schlüssel-Vorrat — Playtester lassen sich dort auch wieder entfernen (die Rolle geht mit) |
|
||||
| 🧪 **Playtester-Programm** | Aufnahme läuft über ein **Bewerbungs-Formular**: ausfüllen, das Team nimmt an oder lehnt ab. Wer angenommen wird, bekommt Rolle und Listeneintrag. Eigener Panel-Bereich mit Liste, Rolle und Schlüssel-Vorrat — Playtester lassen sich dort auch wieder entfernen (die Rolle geht mit) |
|
||||
| 🎟️ **Alpha-Keys** | Schlüssel-Vorrat im Panel: nachlegen, gezielt an eine Person schicken, an alle Wartenden verteilen, zurückziehen oder löschen. Zustellung per Direktnachricht — kommt sie nicht an, bleibt der Schlüssel frei |
|
||||
| 🎭 **Rollen-Menüs** | Menüs im Webinterface bauen (Emoji, Label, Rolle, optional exklusiv), Bot postet Button-Embeds; Posts jederzeit editierbar |
|
||||
| 📋 **Bewerbungen** | Formulare (bis 5 Fragen) als Discord-Modal, Review mit ✅/❌ im Staff-Kanal, Rolle + DM bei Annahme — z. B. FiveM-Whitelist |
|
||||
| 📋 **Bewerbungen** | Formulare (bis 5 Fragen) als Discord-Modal, Review mit ✅/❌ im Staff-Kanal, Rolle + DM bei Annahme — z. B. FiveM-Whitelist. Ein Formular lässt sich als **Playtester-Formular** markieren; Angenommene landen dann auch im Playtester-Programm |
|
||||
| 🪪 **Verknüpfte Rollen** | Discords *Linked Roles*: Level, Playtester-Status und Dabei-seit werden als geprüfte Kennzahlen an Discord gemeldet — Rollen vergibt dann Discord selbst |
|
||||
|
||||
### Moderation & Support
|
||||
|
||||
@@ -322,7 +322,7 @@ export default function Settings({ me }) {
|
||||
// Bewerbungs-Formulare
|
||||
const emptyForm = {
|
||||
id: null, title: '', description: '', review_channel_id: '',
|
||||
approve_role_id: '', post_channel_id: '', questions: [''],
|
||||
approve_role_id: '', post_channel_id: '', questions: [''], playtester: false,
|
||||
};
|
||||
const [appForms, setAppForms] = useState([]);
|
||||
const [formDraft, setFormDraft] = useState(emptyForm);
|
||||
@@ -2195,10 +2195,29 @@ export default function Settings({ me }) {
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Playtester</h2>
|
||||
<p className="section-intro">
|
||||
Wer sich über den Knopf aus <strong>/playtester-setup</strong> meldet, landet
|
||||
hier — mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt sie nicht
|
||||
an, bleibt der Schlüssel im Vorrat.
|
||||
Beworben wird sich über ein Formular, angenommen vom Team — wer angenommen
|
||||
wird, landet hier mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt
|
||||
sie nicht an, bleibt der Schlüssel im Vorrat.
|
||||
</p>
|
||||
{(() => {
|
||||
// Ohne markiertes Formular gibt es keinen Weg herein — das
|
||||
// ist der haeufigste Grund, warum sich niemand bewirbt
|
||||
const ptForm = appForms.find((f) => f.playtester);
|
||||
return ptForm ? (
|
||||
<p className="section-intro">
|
||||
Aufnahme läuft über <strong>{ptForm.title}</strong>
|
||||
{ptForm.message_id ? '' : ' — das Formular ist noch nicht gepostet'}.
|
||||
{' '}<button className="btn btn-mini" onClick={() => setTab('bewerbungen')}>Formular öffnen</button>
|
||||
</p>
|
||||
) : (
|
||||
<p className="notice">
|
||||
Noch kein Playtester-Formular. Ohne eines kommt niemand herein —
|
||||
unter <strong>Bewerbungen</strong> ein Formular anlegen und dort
|
||||
„Annahme trägt als Playtester ein" ankreuzen.
|
||||
{' '}<button className="btn btn-mini" onClick={() => setTab('bewerbungen')}>Zu den Bewerbungen</button>
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
<div className="field" data-setting="playtester_role_id">
|
||||
<label>Playtester-Rolle <span className="field-hint">Bewerbungs-Post via /playtester-setup; leer = aus</span></label>
|
||||
<PickSelect
|
||||
@@ -2367,12 +2386,16 @@ export default function Settings({ me }) {
|
||||
{appForms.map((f) => (
|
||||
<div className="key-row" key={f.id}>
|
||||
<span className="key-name">{f.title}</span>
|
||||
<span className="key-scopes">{f.questions.length} Fragen{f.message_id ? ' · gepostet' : ' · Entwurf'}</span>
|
||||
<span className="key-scopes">
|
||||
{f.questions.length} Fragen{f.message_id ? ' · gepostet' : ' · Entwurf'}
|
||||
{f.playtester ? ' · Playtester' : ''}
|
||||
</span>
|
||||
<button className="btn btn-mini" onClick={() => setFormDraft({
|
||||
id: f.id, title: f.title, description: f.description,
|
||||
review_channel_id: f.review_channel_id ?? '', approve_role_id: f.approve_role_id ?? '',
|
||||
post_channel_id: f.post_channel_id ?? '',
|
||||
questions: f.questions.length ? f.questions : [''],
|
||||
playtester: Boolean(f.playtester),
|
||||
})}>Bearbeiten</button>
|
||||
<button className="card-delete" title="Formular + Post löschen" onClick={() => removeForm(f.id)}>✕</button>
|
||||
</div>
|
||||
@@ -2411,6 +2434,21 @@ export default function Settings({ me }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(formDraft.playtester)}
|
||||
onChange={(e) => setFormDraft({ ...formDraft, playtester: e.target.checked })}
|
||||
/>
|
||||
<span>Annahme trägt als Playtester ein</span>
|
||||
<span className="field-hint">
|
||||
Angenommene landen zusätzlich in der Playtester-Liste und bekommen von dort
|
||||
ihren Schlüssel. <strong>/playtester-setup</strong> postet dann diesen Knopf —
|
||||
so kann sich niemand mehr selbst aufnehmen. Es kann nur ein Formular sein.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Fragen <span className="field-hint">max 5 (Discord-Modal-Limit)</span></label>
|
||||
{formDraft.questions.map((q, i) => (
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ModalBuilder, TextInputBuilder, TextInputStyle,
|
||||
} from 'discord.js';
|
||||
import {
|
||||
getAppForm, setAppFormMessage,
|
||||
getAppForm, setAppFormMessage, addPlaytester,
|
||||
createApplication, getApplication, openApplicationOf, setApplicationStatus,
|
||||
} from '../db.js';
|
||||
import { brandColor, brandColor2, brandFooter } from '../runtime-settings.js';
|
||||
@@ -172,6 +172,11 @@ export async function handleReviewButton(interaction) {
|
||||
console.error(`[bewerbung] Rolle fehlgeschlagen: ${e.message}`)
|
||||
);
|
||||
}
|
||||
// Playtester-Formular: Annahme trägt zusätzlich in die Liste ein, damit die
|
||||
// Schlüssel-Verteilung denjenigen kennt
|
||||
if (approved && form?.playtester) {
|
||||
addPlaytester(application.user_id, application.username ?? application.user_id);
|
||||
}
|
||||
// DM an den Bewerber
|
||||
const user = await interaction.client.users.fetch(application.user_id).catch(() => null);
|
||||
await user?.send(renderTemplate(approved ? 'application.approved' : 'application.denied', {
|
||||
|
||||
+16
-10
@@ -22,7 +22,7 @@ import {
|
||||
import { handleRoleMenuButton } from './role-menus.js';
|
||||
import { handleGiveawayAdminButton } from './giveaways.js';
|
||||
import {
|
||||
addPlaytester, removePlaytester, isWish, bumpWish,
|
||||
removePlaytester, playtesterForm, isWish, bumpWish,
|
||||
getGiveaway, toggleGiveawayEntry, giveawayEntries,
|
||||
} from '../db.js';
|
||||
import * as ping from './commands/ping.js';
|
||||
@@ -309,22 +309,28 @@ export async function startBot() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🧪-Button: Playtester-Rolle + Liste togglen
|
||||
// 🧪-Button aus älteren Posts. Aufgenommen wird hier niemand mehr —
|
||||
// das läuft über das Playtester-Formular mit Prüfung durch das Team.
|
||||
// Austreten bleibt Selbstbedienung: dafür braucht es keine Freigabe.
|
||||
if (interaction.isButton() && interaction.customId === 'playtester_toggle') {
|
||||
const roleId = playtesterRoleId();
|
||||
try {
|
||||
if (!roleId || !interaction.inGuild()) throw new Error('Keine Playtester-Rolle konfiguriert');
|
||||
const member = interaction.member;
|
||||
const hasRole = member.roles.cache.has(roleId);
|
||||
if (hasRole) {
|
||||
await member.roles.remove(roleId);
|
||||
removePlaytester(interaction.user.id);
|
||||
} else {
|
||||
await member.roles.add(roleId);
|
||||
addPlaytester(interaction.user.id, interaction.user.username);
|
||||
if (!member.roles.cache.has(roleId)) {
|
||||
const form = playtesterForm();
|
||||
await interaction.reply({
|
||||
content: form
|
||||
? '❕ Dieser Knopf ist veraltet. Bewirb dich über den Playtester-Aufruf — dort füllst du ein kurzes Formular aus, das Team meldet sich.'
|
||||
: '❕ Bewerbungen laufen gerade nicht über diesen Knopf. Melde dich beim Team.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await member.roles.remove(roleId);
|
||||
removePlaytester(interaction.user.id);
|
||||
await interaction.reply({
|
||||
content: renderTemplate(hasRole ? 'playtester.left' : 'playtester.joined', {
|
||||
content: renderTemplate('playtester.left', {
|
||||
user: member.displayName ?? interaction.user.username,
|
||||
mention: `<@${interaction.user.id}>`,
|
||||
}),
|
||||
|
||||
@@ -1,20 +1,52 @@
|
||||
// /playtester-setup — postet das Bewerbungs-Embed mit Button in den aktuellen Kanal (Admin)
|
||||
// /playtester-setup — postet den Playtester-Aufruf in den aktuellen Kanal (Admin)
|
||||
//
|
||||
// Ist ein Bewerbungs-Formular als Playtester-Formular markiert, führt der Knopf
|
||||
// dorthin: ausfüllen, das Team entscheidet. Ohne Formular gäbe es nur den alten
|
||||
// Selbstbedienungs-Knopf — und der macht jeden zum Playtester, der draufdrückt.
|
||||
// Deshalb sagt der Befehl dann, was zu tun ist, statt ihn zu posten.
|
||||
import {
|
||||
SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
|
||||
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
|
||||
} from 'discord.js';
|
||||
import { playtesterRoleId, brandColor, brandFooter } from '../../runtime-settings.js';
|
||||
import { renderTemplate } from '../../templates.js';
|
||||
import { playtesterForm } from '../../db.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('playtester-setup')
|
||||
.setDescription('Postet das Playtester-Bewerbungs-Embed in diesen Kanal')
|
||||
.setDescription('Postet den Playtester-Aufruf in diesen Kanal')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator);
|
||||
|
||||
export async function execute(interaction) {
|
||||
if (!playtesterRoleId()) {
|
||||
await interaction.reply({
|
||||
content: '❌ Erst auf der Config-Seite eine Playtester-Rolle wählen.',
|
||||
content: '❌ Erst in der Config eine Playtester-Rolle wählen.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const form = playtesterForm();
|
||||
if (!form) {
|
||||
await interaction.reply({
|
||||
content: '❌ Kein Playtester-Formular gesetzt.\n'
|
||||
+ 'Config → **Bewerbungen**: ein Formular anlegen und dort '
|
||||
+ '„Annahme trägt als Playtester ein" ankreuzen. Dann kann sich '
|
||||
+ 'niemand mehr per Knopfdruck selbst aufnehmen.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (JSON.parse(form.questions || '[]').length === 0) {
|
||||
await interaction.reply({
|
||||
content: `❌ Das Formular „${form.title}" hat noch keine Fragen.`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!form.review_channel_id) {
|
||||
await interaction.reply({
|
||||
content: `❌ Für „${form.title}" fehlt der Review-Kanal — sonst sieht niemand die Bewerbungen.`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
@@ -26,14 +58,19 @@ export async function execute(interaction) {
|
||||
.setDescription(renderTemplate('playtester.panel_text'))
|
||||
.setFooter({ text: brandFooter('PLAYTEST') });
|
||||
|
||||
// Derselbe Knopf wie beim Bewerbungs-Formular — das Formular, die Prüfung
|
||||
// und die Antwort-DMs sind dieselbe Maschinerie, nur ein anderer Aufhänger.
|
||||
const row = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('playtester_toggle')
|
||||
.setCustomId(`apply:${form.id}`)
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setLabel('Als Playtester bewerben')
|
||||
.setEmoji('🧪')
|
||||
);
|
||||
|
||||
await interaction.channel.send({ embeds: [embed], components: [row] });
|
||||
await interaction.reply({ content: '✅ Bewerbungs-Post erstellt.', flags: MessageFlags.Ephemeral });
|
||||
await interaction.reply({
|
||||
content: `✅ Aufruf gepostet — Bewerbungen laufen über „${form.title}".`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -340,24 +340,38 @@ db.exec(`
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
// Migration: Annahme kann zusaetzlich ins Playtester-Programm eintragen
|
||||
{
|
||||
const cols = db.prepare('PRAGMA table_info(app_forms)').all().map((c) => c.name);
|
||||
if (!cols.includes('playtester')) {
|
||||
db.exec('ALTER TABLE app_forms ADD COLUMN playtester INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
const insertAppForm = db.prepare(`
|
||||
INSERT INTO app_forms (title, description, review_channel_id, approve_role_id, post_channel_id, questions)
|
||||
VALUES (@title, @description, @review_channel_id, @approve_role_id, @post_channel_id, @questions)
|
||||
INSERT INTO app_forms (title, description, review_channel_id, approve_role_id, post_channel_id, questions, playtester)
|
||||
VALUES (@title, @description, @review_channel_id, @approve_role_id, @post_channel_id, @questions, @playtester)
|
||||
`);
|
||||
const updateAppFormStmt = db.prepare(`
|
||||
UPDATE app_forms SET title = @title, description = @description, review_channel_id = @review_channel_id,
|
||||
approve_role_id = @approve_role_id, post_channel_id = @post_channel_id, questions = @questions
|
||||
approve_role_id = @approve_role_id, post_channel_id = @post_channel_id, questions = @questions,
|
||||
playtester = @playtester
|
||||
WHERE id = @id
|
||||
`);
|
||||
const setAppFormMessageStmt = db.prepare('UPDATE app_forms SET message_id = ? WHERE id = ?');
|
||||
const getAppFormStmt = db.prepare('SELECT * FROM app_forms WHERE id = ?');
|
||||
const listAppFormsStmt = db.prepare('SELECT * FROM app_forms ORDER BY id');
|
||||
const deleteAppFormStmt = db.prepare('DELETE FROM app_forms WHERE id = ?');
|
||||
// Nur eines kann das Playtester-Formular sein — sonst weiss /playtester-setup
|
||||
// nicht, welches es posten soll
|
||||
const clearPlaytesterFormStmt = db.prepare('UPDATE app_forms SET playtester = 0 WHERE id != ?');
|
||||
const playtesterFormStmt = db.prepare('SELECT * FROM app_forms WHERE playtester = 1 LIMIT 1');
|
||||
export const createAppForm = (f) => insertAppForm.run(f).lastInsertRowid;
|
||||
export const updateAppForm = (f) => updateAppFormStmt.run(f).changes > 0;
|
||||
export const setAppFormMessage = (id, messageId) => setAppFormMessageStmt.run(messageId, id);
|
||||
export const getAppForm = (id) => getAppFormStmt.get(id) ?? null;
|
||||
export const listAppForms = () => listAppFormsStmt.all();
|
||||
export const playtesterForm = () => playtesterFormStmt.get() ?? null;
|
||||
export const clearOtherPlaytesterForms = (id) => clearPlaytesterFormStmt.run(id);
|
||||
export const deleteAppForm = (id) => deleteAppFormStmt.run(id).changes > 0;
|
||||
|
||||
const insertApplication = db.prepare(`
|
||||
|
||||
+2
-2
@@ -213,8 +213,8 @@ export const TEMPLATES = [
|
||||
vars: [],
|
||||
default:
|
||||
'Du willst **EcoGame** vor allen anderen spielen und mit deinem Feedback die Entwicklung mitgestalten?\n\n' +
|
||||
'Ein Klick auf den Button setzt dich auf die Playtester-Liste — sobald die Alpha startet, ' +
|
||||
'bekommst du deinen Zugang direkt per DM. Abmelden geht jederzeit mit einem zweiten Klick.',
|
||||
'Klick auf den Button und füll das kurze Formular aus. Wir schauen es uns an und ' +
|
||||
'melden uns per DM — bei einer Zusage bekommst du Rolle und Zugang.',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+11
-3
@@ -9,7 +9,7 @@ import {
|
||||
topLevels, saveTag, listTags, deleteTag,
|
||||
createScheduledPost, listScheduledPosts, deleteScheduledPost,
|
||||
addAlphaKeys, freeAlphaKeyCount, assignedAlphaKeys, alphaKeyOf, reserveAlphaKey, unreserveAlphaKey,
|
||||
createAppForm, updateAppForm, getAppForm, listAppForms, deleteAppForm,
|
||||
createAppForm, updateAppForm, getAppForm, listAppForms, deleteAppForm, clearOtherPlaytesterForms,
|
||||
saveTrigger, listTriggers, deleteTrigger, activityRange,
|
||||
createGameserver, updateGameserver, listGameservers, getGameserver, deleteGameserver,
|
||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||
@@ -1446,9 +1446,15 @@ ${rssItems}
|
||||
approve_role_id: roleId || null,
|
||||
post_channel_id: String(body.post_channel_id ?? '') || null,
|
||||
questions: JSON.stringify(questions),
|
||||
playtester: body.playtester ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** Nur ein Formular kann das Playtester-Formular sein */
|
||||
function playtesterExklusiv(form, id) {
|
||||
if (form.playtester) clearOtherPlaytesterForms(id);
|
||||
}
|
||||
|
||||
app.get('/api/appforms', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'bewerbungen')) return;
|
||||
return { forms: listAppForms().map(formToJson) };
|
||||
@@ -1459,7 +1465,8 @@ ${rssItems}
|
||||
const form = parseFormBody(request, reply);
|
||||
if (!form) return;
|
||||
const id = createAppForm(form);
|
||||
return { ok: true, form: formToJson(getAppForm(id)) };
|
||||
playtesterExklusiv(form, id);
|
||||
return { ok: true, form: formToJson(getAppForm(id)), forms: listAppForms().map(formToJson) };
|
||||
});
|
||||
|
||||
app.put('/api/appforms/:id', async (request, reply) => {
|
||||
@@ -1469,12 +1476,13 @@ ${rssItems}
|
||||
const form = parseFormBody(request, reply);
|
||||
if (!form) return;
|
||||
updateAppForm({ ...form, id });
|
||||
playtesterExklusiv(form, id);
|
||||
if (getAppForm(id).message_id) {
|
||||
await publishAppForm(client, id).catch((e) =>
|
||||
request.log.warn(`Formular ${id}: Discord-Update fehlgeschlagen: ${e.message}`)
|
||||
);
|
||||
}
|
||||
return { ok: true, form: formToJson(getAppForm(id)) };
|
||||
return { ok: true, form: formToJson(getAppForm(id)), forms: listAppForms().map(formToJson) };
|
||||
});
|
||||
|
||||
app.post('/api/appforms/:id/publish', async (request, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user