142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
|
|
import { showConfirmDialog } from '../confirm-dialog/confirm-dialog.js';
|
||
|
|
|
||
|
|
interface Project {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
owner_id: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ProjectMember {
|
||
|
|
id: string;
|
||
|
|
project_id: string;
|
||
|
|
member_type: 'user' | 'group';
|
||
|
|
member_id: string;
|
||
|
|
role: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
class ProjectEditor extends HTMLElement {
|
||
|
|
connectedCallback(): void {
|
||
|
|
this.render();
|
||
|
|
}
|
||
|
|
|
||
|
|
async render(): Promise<void> {
|
||
|
|
const res = await fetch('/api/projects');
|
||
|
|
const projects = await res.json() as Project[];
|
||
|
|
|
||
|
|
this.innerHTML = `
|
||
|
|
<div class="project-editor">
|
||
|
|
<h2>Projects</h2>
|
||
|
|
<form class="create-form">
|
||
|
|
<input name="name" placeholder="New project name" required>
|
||
|
|
<button type="submit">Create</button>
|
||
|
|
</form>
|
||
|
|
<ul class="project-list">
|
||
|
|
${projects.map(p => `
|
||
|
|
<li data-id="${p.id}">
|
||
|
|
<div class="project-header">
|
||
|
|
<strong>${p.name}</strong>
|
||
|
|
<a class="btn-edit" href="/editor.html?project=${p.id}&name=${encodeURIComponent(p.name)}">Edit</a>
|
||
|
|
<button class="btn-members" data-id="${p.id}">Members</button>
|
||
|
|
<button class="btn-delete" data-id="${p.id}" data-name="${p.name}">Delete</button>
|
||
|
|
</div>
|
||
|
|
<div class="members-panel" data-project="${p.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/projects', {
|
||
|
|
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 ( e ) => {
|
||
|
|
|
||
|
|
console.log( "Clicking:", e );
|
||
|
|
const el = btn as HTMLElement;
|
||
|
|
const ok = await showConfirmDialog({
|
||
|
|
icon: '🗑',
|
||
|
|
title: 'Delete Project',
|
||
|
|
message: `Delete "${el.dataset.name}"? This cannot be undone.`,
|
||
|
|
confirmLabel: 'Delete',
|
||
|
|
cancelLabel: 'Cancel',
|
||
|
|
danger: true
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log( "Result:", ok );
|
||
|
|
if (!ok) return;
|
||
|
|
await fetch(`/api/projects/${el.dataset.id}`, { method: 'DELETE' });
|
||
|
|
this.render();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
this.querySelectorAll('.btn-members').forEach(btn => {
|
||
|
|
btn.addEventListener('click', () => this.toggleMembers((btn as HTMLElement).dataset.id!));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async toggleMembers(projectId: string): Promise<void> {
|
||
|
|
const panel = this.querySelector(`.members-panel[data-project="${projectId}"]`) as HTMLElement;
|
||
|
|
if (panel.style.display !== 'none') { panel.style.display = 'none'; return; }
|
||
|
|
panel.style.display = 'block';
|
||
|
|
await this.loadMembers(projectId, panel);
|
||
|
|
}
|
||
|
|
|
||
|
|
async loadMembers(projectId: string, panel: HTMLElement): Promise<void> {
|
||
|
|
const res = await fetch(`/api/projects/${projectId}/members`);
|
||
|
|
const members = await res.json() as ProjectMember[];
|
||
|
|
|
||
|
|
panel.innerHTML = `
|
||
|
|
<ul class="member-list">
|
||
|
|
${members.map(m => `
|
||
|
|
<li>${m.member_type}: ${m.member_id} — ${m.role}
|
||
|
|
<button class="btn-remove-member" data-pid="${projectId}" 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>
|
||
|
|
<select name="role">
|
||
|
|
<option value="viewer">Viewer</option>
|
||
|
|
<option value="editor">Editor</option>
|
||
|
|
<option value="admin">Admin</option>
|
||
|
|
</select>
|
||
|
|
<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/projects/${el.dataset.pid}/members/${el.dataset.mid}`, { method: 'DELETE' });
|
||
|
|
await this.loadMembers(projectId, 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/projects/${projectId}/members`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(data)
|
||
|
|
});
|
||
|
|
await this.loadMembers(projectId, panel);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
customElements.define('project-editor', ProjectEditor);
|