import { html, nothing } from 'lit'; import { LightElement } from '../lib/base.js'; export class ApprovalGroupsPage extends LightElement { static properties = { _open: { state: true }, _groups: { state: true }, _rules: { state: true }, _error: { state: true }, _groupEditId: { state: true }, _groupForm: { state: true }, _groupSaving: { state: true }, _duplicateOf: { state: true }, // group being duplicated, or null _dupForm: { state: true }, // { id, name } _dupSaving: { state: true }, }; constructor() { super(); this._open = false; this._groups = []; this._rules = []; this._error = null; this._groupEditId = null; this._groupForm = { id: '', name: '', description: '' }; this._groupSaving = false; this._duplicateOf = null; this._dupForm = { id: '', name: '' }; this._dupSaving = false; } connectedCallback() { super.connectedCallback(); window.addEventListener('llm-page-change', async (e) => { this._open = e.detail.page === 'approval'; this.style.display = this._open ? 'flex' : 'none'; if (!this._open) return; await this._load(); const match = window.location.hash.match(/^#approval\/(.+)$/); if (match) { const group = this._groups.find(g => g.id === decodeURIComponent(match[1])); if (group) { this._navigateTo(group); return; } } }); window.addEventListener('approval-navigate', (e) => { if (e.detail.group !== null) return; // Returning from rules view — show groups again this._open = true; this.style.display = 'flex'; this._load(); }); window.addEventListener('hashchange', () => { if (!this._open) return; const match = window.location.hash.match(/^#approval\/(.+)$/); if (!match) return; const group = this._groups.find(g => g.id === decodeURIComponent(match[1])); if (group) this._navigateTo(group); }); } async _load() { this._error = null; try { const [gRes, rRes] = await Promise.all([ fetch('/api/tool-permission-groups'), fetch('/api/approval/rules'), ]); if (!gRes.ok) throw new Error(`Groups: HTTP ${gRes.status}`); if (!rRes.ok) throw new Error(`Rules: HTTP ${rRes.status}`); const groups = await gRes.json(); this._groups = groups.sort((a, b) => { if (a.id === 'default') return -1; if (b.id === 'default') return 1; return a.name.localeCompare(b.name); }); this._rules = await rRes.json(); } catch (e) { this._error = e.message; } } _rulesForGroup(groupId) { return this._rules.filter(r => (r.group_id ?? 'default') === groupId); } // ── Navigation ──────────────────────────────────────────────────────────────── _navigateTo(group) { this._open = false; this.style.display = 'none'; window.location.hash = `approval/${group.id}`; window.dispatchEvent(new CustomEvent('approval-navigate', { detail: { group } })); } // ── Group management ────────────────────────────────────────────────────────── _startNewGroup() { this._groupEditId = 'new'; this._groupForm = { id: '', name: '', description: '' }; this._duplicateOf = null; } _startEditGroup(group) { this._groupEditId = group.id; this._groupForm = { id: group.id, name: group.name, description: group.description ?? '' }; this._duplicateOf = null; } _cancelGroupEdit() { this._groupEditId = null; } _patchGroup(field, value) { this._groupForm = { ...this._groupForm, [field]: value }; } async _saveGroup() { const isNew = this._groupEditId === 'new'; if (!this._groupForm.name.trim()) { this._error = 'Group name is required.'; return; } if (isNew && !this._groupForm.id.trim()) { this._error = 'Group ID is required.'; return; } this._groupSaving = true; this._error = null; try { const body = isNew ? { id: this._groupForm.id.trim(), name: this._groupForm.name.trim(), description: this._groupForm.description.trim() || null } : { name: this._groupForm.name.trim(), description: this._groupForm.description.trim() || null }; const url = isNew ? '/api/tool-permission-groups' : `/api/tool-permission-groups/${this._groupEditId}`; const res = await fetch(url, { method: isNew ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await res.text()); this._groupEditId = null; await this._load(); } catch (e) { this._error = e.message; } finally { this._groupSaving = false; } } async _deleteGroup(group) { const count = this._rulesForGroup(group.id).length; const msg = count > 0 ? `Delete group "${group.name}" and its ${count} rule${count === 1 ? '' : 's'}?` : `Delete group "${group.name}"?`; if (!confirm(msg)) return; try { const res = await fetch(`/api/tool-permission-groups/${group.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(await res.text()); await this._load(); } catch (e) { this._error = e.message; } } // ── Duplicate group ─────────────────────────────────────────────────────────── _startDuplicate(group) { this._duplicateOf = group; this._dupForm = { id: `${group.id}_copy`, name: `Copy of ${group.name}`, }; this._groupEditId = null; // close any open create/rename form } _cancelDuplicate() { this._duplicateOf = null; } async _saveDuplicate() { if (!this._dupForm.name.trim()) { this._error = 'Name is required.'; return; } if (!this._dupForm.id.trim()) { this._error = 'ID is required.'; return; } this._dupSaving = true; this._error = null; try { const res = await fetch(`/api/tool-permission-groups/${this._duplicateOf.id}/duplicate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: this._dupForm.id.trim(), name: this._dupForm.name.trim() }), }); if (!res.ok) throw new Error(await res.text()); this._duplicateOf = null; await this._load(); } catch (e) { this._error = e.message; } finally { this._dupSaving = false; } } // ── Group form ──────────────────────────────────────────────────────────────── _renderGroupForm() { const isNew = this._groupEditId === 'new'; const f = this._groupForm; return html`
No groups yet.