159 lines
5.6 KiB
TypeScript
159 lines
5.6 KiB
TypeScript
import { showConfirmDialog } from '../confirm-dialog/confirm-dialog.js';
|
|
|
|
interface CurrentUser {
|
|
userId: string;
|
|
email: string;
|
|
}
|
|
|
|
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 {
|
|
private currentUser: CurrentUser | null = null;
|
|
|
|
async connectedCallback(): Promise<void> {
|
|
const res = await fetch( '/api/auth/me' );
|
|
if ( res.ok ) this.currentUser = await res.json() as CurrentUser;
|
|
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 }" data-owner="${ p.owner_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 () => {
|
|
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
|
|
} );
|
|
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[];
|
|
|
|
const ownerId = panel.dataset.owner ?? '';
|
|
const isOwner = this.currentUser?.userId === ownerId;
|
|
const ownerLabel = isOwner
|
|
? `${ this.currentUser!.email } <span class="member-role">owner</span>`
|
|
: `${ ownerId } <span class="member-role">owner</span>`;
|
|
|
|
const memberRows = members.map( m => `
|
|
<li>
|
|
<span class="member-email">${ m.member_id }</span>
|
|
<span class="member-role">${ m.role }</span>
|
|
${ isOwner ? `<button class="btn-remove-member" data-pid="${ projectId }" data-mid="${ m.id }">Remove</button>` : '' }
|
|
</li>
|
|
` ).join( '' );
|
|
|
|
panel.innerHTML = `
|
|
<ul class="member-list">
|
|
<li class="member-owner">${ ownerLabel }</li>
|
|
${ memberRows || '' }
|
|
${ members.length === 0 ? '<li class="empty">No additional members</li>' : '' }
|
|
</ul>
|
|
${ isOwner ? `
|
|
<form class="add-member-form">
|
|
<input name="email" type="email" placeholder="Email address" 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 );
|
|
} );
|
|
} );
|
|
|
|
const addForm = panel.querySelector( '.add-member-form' ) as HTMLFormElement | null;
|
|
addForm?.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 );
|