diff --git a/frontend/src/components/ModulePage.jsx b/frontend/src/components/ModulePage.jsx index de9bc9a..f164e92 100644 --- a/frontend/src/components/ModulePage.jsx +++ b/frontend/src/components/ModulePage.jsx @@ -59,12 +59,12 @@ function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) { * form: object, setForm: (f: object) => void, * PickSelect: Function, channelItems: Array, roleItems: Array, * welcomePreviewUrl: string, flash: (msg: string) => void, - * goTab: (tab: string) => void, + * goTab: (tab: string) => void, reloadSettings: () => Promise, * }} props */ export default function ModulePage(props) { const { id, onBack, onToggle, form, setForm, PickSelect, channelItems, roleItems, - welcomePreviewUrl, flash, goTab } = props; + welcomePreviewUrl, flash, goTab, reloadSettings } = props; const [detail, setDetail] = useState(null); const [fehler, setFehler] = useState(false); @@ -104,7 +104,7 @@ export default function ModulePage(props) { if (Object.keys(geaendert).length === 0) return; try { const res = await apiPut('/api/tuning', geaendert); - setDetail((d) => ({ ...d, werte: res.values.filter((w) => w.module === d.id) })); + setDetail((d) => ({ ...d, werte: res.values.filter((w) => w.modules.includes(d.id)) })); setWerteEntwurf({}); flash('✓ Werte gespeichert'); } catch (error) { @@ -115,7 +115,9 @@ export default function ModulePage(props) { async function anlegen(setting) { try { const res = await apiPost('/api/setup/create', { setting }); - setForm(res.settings); + // Auffrischen statt nur setzen: der neue Kanal muss auch in der + // Auswahlliste stehen, sonst wirkt das Feld leer. + await reloadSettings(); laden(); flash(res.wiederverwendet ? `✓ „${res.name}" verknüpft` : `✓ „${res.name}" angelegt`); } catch (error) { diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 104c59e..d6265cc 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -542,8 +542,7 @@ export default function Settings({ me }) { try { const res = await apiPost('/api/setup/run', { kategorie: plan.kategorie }); setModules(res.modules); - setForm(res.settings); - setData((d) => (d ? { ...d, settings: res.settings } : d)); + await reloadSettings(); setSetupResult(res.ergebnisse); setPlan(null); const neu = res.ergebnisse.filter((r) => r.ok && !r.wiederverwendet).length; @@ -555,16 +554,25 @@ export default function Settings({ me }) { } } + /** + * Kanäle, Rollen und Einstellungen neu holen. Nötig nach dem Anlegen: der + * frische Kanal steht sonst zwar in der Einstellung, fehlt aber in der + * Auswahlliste — das Feld sähe leer aus, obwohl es gesetzt ist. + */ + async function reloadSettings() { + const frisch = await apiGet('/api/settings').catch(() => null); + if (!frisch) return; + setData(frisch); + setForm(frisch.settings); + } + /** Fehlenden Kanal / fehlende Rolle vom Bot anlegen lassen */ async function createMissing(entry) { setCreating(entry.setting); try { const res = await apiPost('/api/setup/create', { setting: entry.setting }); setModules(res.modules); - // Die neue Auswahl steht sofort im Formular — und zählt nicht als - // ungespeicherte Änderung, weil der Vergleichsstand mitwandert. - setForm(res.settings); - setData((d) => (d ? { ...d, settings: res.settings } : d)); + await reloadSettings(); flash(res.wiederverwendet ? `✓ Vorhandenen ${res.art === 'rolle' ? 'Rolle' : 'Kanal'} „${res.name}" verknüpft` : `✓ ${res.art === 'rolle' ? 'Rolle' : 'Kanal'} „${res.name}" angelegt`); @@ -2378,6 +2386,7 @@ export default function Settings({ me }) { welcomePreviewUrl={welcomePreviewUrl} flash={flash} goTab={setTab} + reloadSettings={reloadSettings} /> ) : (
diff --git a/src/tuning.js b/src/tuning.js index 8588d43..3089c7c 100644 --- a/src/tuning.js +++ b/src/tuning.js @@ -2,7 +2,9 @@ // Wartezeiten, Prüf-Intervalle, Obergrenzen. // // `module` verbindet einen Wert mit seiner Funktion — die Modul-Seite zeigt ihn -// dann dort, wo er hingehört, statt nur in der flachen Werte-Liste. +// dann dort, wo er hingehört, statt nur in der flachen Werte-Liste. Ein Wert +// darf zu mehreren gehören (die Thread-Archivdauer gilt für Tickets UND +// Modmail); dann steht dort eine Liste. // // Wie bei den Modulen und Vorlagen lebt der Standard im Code und die // Datenbank enthält nur Abweichungen. `min`/`max` sind keine Kosmetik: sie @@ -107,7 +109,7 @@ export const TUNING = [ unit: 'Commits', default: 10, min: 1, max: 20, }, { - id: 'thread_archive_days', module: 'tickets', group: 'grenzen', label: 'Threads archivieren nach', + id: 'thread_archive_days', module: ['tickets', 'modmail'], group: 'grenzen', label: 'Threads archivieren nach', hint: 'Für Ticket- und Modmail-Threads. Discord erlaubt 1, 3 oder 7 Tage.', unit: 'Tagen', default: 7, min: 1, max: 7, choices: [1, 3, 7], }, @@ -171,7 +173,10 @@ export function tuningStates() { const custom = getSetting(`tune_${t.id}`); return { id: t.id, label: t.label, hint: t.hint ?? null, - group: t.group, module: t.module ?? null, + group: t.group, + // Immer als Liste nach außen — der Aufrufer muss dann nicht + // zwischen einem Wert und mehreren unterscheiden + modules: t.module ? [t.module].flat() : [], unit: t.unit, min: t.min, max: t.max, choices: t.choices ?? null, default: t.default, diff --git a/src/web/api.js b/src/web/api.js index ee8cdbc..0bebec6 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -202,7 +202,7 @@ export function registerApiRoutes(app, client) { return { ...detail, texte: templateStates().filter((t) => t.module === detail.id), - werte: tuningStates().filter((w) => w.module === detail.id), + werte: tuningStates().filter((w) => w.modules.includes(detail.id)), }; });