119 lines
4.0 KiB
TypeScript
119 lines
4.0 KiB
TypeScript
interface Group {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
interface GroupMember {
|
|
id: string;
|
|
group_id: string;
|
|
member_type: 'user' | 'group';
|
|
member_id: string;
|
|
}
|
|
|
|
class GroupEditor extends HTMLElement {
|
|
connectedCallback(): void {
|
|
this.render();
|
|
}
|
|
|
|
async render(): Promise<void> {
|
|
const res = await fetch('/api/groups');
|
|
const groups = await res.json() as Group[];
|
|
|
|
this.innerHTML = `
|
|
<div class="group-editor">
|
|
<h2>Groups</h2>
|
|
<form class="create-form">
|
|
<input name="name" placeholder="New group name" required>
|
|
<button type="submit">Create</button>
|
|
</form>
|
|
<ul class="group-list">
|
|
${groups.map(g => `
|
|
<li data-id="${g.id}">
|
|
<div class="group-header">
|
|
<strong>${g.name}</strong>
|
|
<button class="btn-members" data-id="${g.id}">Members</button>
|
|
<button class="btn-delete" data-id="${g.id}">Delete</button>
|
|
</div>
|
|
<div class="members-panel" data-group="${g.id}" style="display:none"></div>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
|
|
this.querySelector('.create-form')!.addEventListener('submit', async (e: Event) => {
|
|
e.preventDefault();
|
|
const form = e.target as HTMLFormElement;
|
|
const name = (form.elements.namedItem('name') as HTMLInputElement).value.trim();
|
|
const res = await fetch('/api/groups', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name })
|
|
});
|
|
if (res.ok) this.render();
|
|
});
|
|
|
|
this.querySelectorAll('.btn-delete').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
if (!confirm('Delete group?')) return;
|
|
await fetch(`/api/groups/${(btn as HTMLElement).dataset.id}`, { method: 'DELETE' });
|
|
this.render();
|
|
});
|
|
});
|
|
|
|
this.querySelectorAll('.btn-members').forEach(btn => {
|
|
btn.addEventListener('click', () => this.toggleMembers((btn as HTMLElement).dataset.id!));
|
|
});
|
|
}
|
|
|
|
async toggleMembers(groupId: string): Promise<void> {
|
|
const panel = this.querySelector(`.members-panel[data-group="${groupId}"]`) as HTMLElement;
|
|
if (panel.style.display !== 'none') { panel.style.display = 'none'; return; }
|
|
panel.style.display = 'block';
|
|
await this.loadMembers(groupId, panel);
|
|
}
|
|
|
|
async loadMembers(groupId: string, panel: HTMLElement): Promise<void> {
|
|
const res = await fetch(`/api/groups/${groupId}/members`);
|
|
const members = await res.json() as GroupMember[];
|
|
|
|
panel.innerHTML = `
|
|
<ul class="member-list">
|
|
${members.map(m => `
|
|
<li>${m.member_type}: ${m.member_id}
|
|
<button class="btn-remove-member" data-gid="${groupId}" data-mid="${m.id}">Remove</button>
|
|
</li>
|
|
`).join('') || '<li class="empty">No members</li>'}
|
|
</ul>
|
|
<form class="add-member-form">
|
|
<select name="member_type">
|
|
<option value="user">User ID</option>
|
|
<option value="group">Group ID</option>
|
|
</select>
|
|
<input name="member_id" type="number" placeholder="ID" required>
|
|
<button type="submit">Add</button>
|
|
</form>
|
|
`;
|
|
|
|
panel.querySelectorAll('.btn-remove-member').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const el = btn as HTMLElement;
|
|
await fetch(`/api/groups/${el.dataset.gid}/members/${el.dataset.mid}`, { method: 'DELETE' });
|
|
await this.loadMembers(groupId, panel);
|
|
});
|
|
});
|
|
|
|
(panel.querySelector('.add-member-form') as HTMLFormElement).addEventListener('submit', async (e: Event) => {
|
|
e.preventDefault();
|
|
const data = Object.fromEntries(new FormData(e.target as HTMLFormElement));
|
|
await fetch(`/api/groups/${groupId}/members`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
await this.loadMembers(groupId, panel);
|
|
});
|
|
}
|
|
}
|
|
customElements.define('group-editor', GroupEditor);
|