- Context menu tag toggle reads fresh body from state.cards — was overwriting with stale snapshot, dropping existing tags (e.g. #done) - Board empty-state centered with flex (vertical + horizontal) - Plugin settings: batch toggles via Apply button, single reload instead of one reload per toggle - New bucket creation activates it directly on the board Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
4.4 KiB
JavaScript
114 lines
4.4 KiB
JavaScript
// ── Plugin Settings UI ──
|
|
// Toggles collect pending changes client-side and commit together via
|
|
// the Apply button. Only reloads once all changes are committed.
|
|
|
|
import { $, esc, api } from './util.js';
|
|
|
|
let pluginsCache = null;
|
|
let pending = new Map(); // name -> desired enabled state
|
|
|
|
async function fetchPlugins() {
|
|
const res = await fetch('api/plugins');
|
|
if (!res.ok) throw new Error('failed to load plugins');
|
|
pluginsCache = await res.json();
|
|
return pluginsCache;
|
|
}
|
|
|
|
function render(list) {
|
|
const container = $('plugins-list');
|
|
if (!container) return;
|
|
if (!list.length) {
|
|
container.innerHTML = '<p class="text-xs text-t-dim italic">No plugins installed. Drop a plugin folder into <code>public/plugins/</code> to add one.</p>';
|
|
return;
|
|
}
|
|
container.innerHTML = list.map(p => {
|
|
const effectiveEnabled = pending.has(p.name) ? pending.get(p.name) : p.enabled;
|
|
const isPending = pending.has(p.name);
|
|
return `
|
|
<div class="flex items-start gap-3 px-3 py-2 border ${isPending ? 'border-t-accent' : 'border-t-border'} bg-t-bg rounded-sm" data-plugin="${esc(p.name)}">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-pixel text-[11px] text-t-bright">${esc(p.name)}</span>
|
|
<span class="font-pixel text-[9px] text-t-dim">v${esc(p.version || '?')}</span>
|
|
${p.hasBackend ? '<span class="font-pixel text-[8px] px-1 py-0.5 border border-t-border text-t-muted uppercase">php</span>' : ''}
|
|
${isPending ? `<span class="font-pixel text-[8px] px-1 py-0.5 text-t-accent border border-t-accent uppercase">${effectiveEnabled ? 'will enable' : 'will disable'}</span>` : ''}
|
|
</div>
|
|
${p.description ? `<div class="text-[10px] text-t-muted leading-snug mt-1">${esc(p.description)}</div>` : ''}
|
|
</div>
|
|
<label class="flex items-center cursor-pointer shrink-0 mt-0.5">
|
|
<input type="checkbox" class="plugin-toggle accent-t-accent w-4 h-4" ${effectiveEnabled ? 'checked' : ''} data-name="${esc(p.name)}" data-original="${p.enabled ? '1' : '0'}">
|
|
</label>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
container.querySelectorAll('.plugin-toggle').forEach(input => {
|
|
input.addEventListener('change', onToggle);
|
|
});
|
|
|
|
updateApplyBar();
|
|
}
|
|
|
|
function onToggle(e) {
|
|
const input = e.currentTarget;
|
|
const name = input.dataset.name;
|
|
const original = input.dataset.original === '1';
|
|
const desired = input.checked;
|
|
if (desired === original) {
|
|
pending.delete(name);
|
|
} else {
|
|
pending.set(name, desired);
|
|
}
|
|
render(pluginsCache); // re-render to update badge and border
|
|
}
|
|
|
|
function updateApplyBar() {
|
|
const bar = $('plugins-apply-bar');
|
|
if (!bar) return;
|
|
if (pending.size === 0) {
|
|
bar.classList.add('hidden');
|
|
return;
|
|
}
|
|
bar.classList.remove('hidden');
|
|
const count = pending.size;
|
|
const label = $('plugins-apply-count');
|
|
if (label) label.textContent = `${count} pending change${count !== 1 ? 's' : ''}`;
|
|
}
|
|
|
|
async function applyChanges() {
|
|
if (pending.size === 0) return;
|
|
const btn = $('plugins-apply-btn');
|
|
if (btn) { btn.disabled = true; btn.textContent = 'applying...'; }
|
|
try {
|
|
for (const [name, enabled] of pending) {
|
|
const res = await api('api/plugins/toggle', { method: 'POST', body: { name, enabled } });
|
|
if (res.error) throw new Error(`${name}: ${res.error}`);
|
|
}
|
|
pending.clear();
|
|
// Reload so the new plugin set takes effect
|
|
location.reload();
|
|
} catch (err) {
|
|
if (btn) { btn.disabled = false; btn.textContent = 'Apply'; }
|
|
alert('Failed to apply: ' + err.message);
|
|
}
|
|
}
|
|
|
|
function discardChanges() {
|
|
pending.clear();
|
|
render(pluginsCache);
|
|
}
|
|
|
|
export async function openPluginSettings() {
|
|
const container = $('plugins-list');
|
|
if (!container) return;
|
|
container.innerHTML = '<p class="text-xs text-t-dim italic">Loading...</p>';
|
|
try {
|
|
const list = await fetchPlugins();
|
|
render(list);
|
|
} catch (err) {
|
|
container.innerHTML = `<p class="text-xs text-t-danger">Failed to load plugins: ${esc(err.message)}</p>`;
|
|
}
|
|
}
|
|
|
|
$('plugins-apply-btn')?.addEventListener('click', applyChanges);
|
|
$('plugins-discard-btn')?.addEventListener('click', discardChanges);
|