import { html, nothing } from 'lit'; import { LightElement } from '../../lib/base.js'; import { t } from '../../lib/i18n.js'; /// A project's detail page: header + description, a sharing panel (member picker with /// read/write, mirroring the shared-folders UI), Open chat, and a Files section (the /// future primary surface — a file explorer over the project folder). No ticket board. export class ProjectBoardSection extends LightElement { static properties = { _project: { state: true }, _users: { state: true }, _add: { state: true }, _error: { state: true }, }; constructor() { super(); this._project = null; this._users = []; this._add = { user_id: '', can_write: false }; this._error = null; this._projectId = null; } connectedCallback() { super.connectedCallback(); this.__onLocaleChanged = () => this.requestUpdate(); window.addEventListener('locale-changed', this.__onLocaleChanged); } disconnectedCallback() { window.removeEventListener('locale-changed', this.__onLocaleChanged); super.disconnectedCallback(); } async load(projectId) { this._projectId = projectId; this._project = null; this._error = null; try { const [projRes, usersRes] = await Promise.all([ fetch(`/api/projects/${projectId}`), fetch('/api/users'), ]); if (!projRes.ok) throw new Error(`HTTP ${projRes.status}`); this._project = await projRes.json(); if (usersRes.ok) this._users = await usersRes.json(); } catch (e) { this._error = e.message; } } async _reload() { try { const res = await fetch(`/api/projects/${this._projectId}`); if (res.ok) this._project = await res.json(); } catch { /* transient */ } } _canManage() { return !!this._project && (this._project.is_owner || this._project.can_write); } _userLabel(id) { const u = this._users.find(u => u.id === id); return u ? (u.display_name || u.username) : id; } _candidates() { const taken = new Set((this._project?.members ?? []).map(m => m.user_id)); return this._users.filter(u => u.active !== false && !taken.has(u.id)); } // ── Membership actions ───────────────────────────────────────────────────────── async _addMember() { if (!this._add.user_id) return; try { const res = await fetch(`/api/projects/${this._projectId}/members`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: this._add.user_id, can_write: this._add.can_write }), }); if (!res.ok) throw new Error(await res.text()); this._project = { ...this._project, members: await res.json() }; this._add = { user_id: '', can_write: false }; } catch (e) { this._error = e.message; } } async _setAccess(userId, canWrite) { try { const res = await fetch(`/api/projects/${this._projectId}/members`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId, can_write: canWrite }), }); if (!res.ok) throw new Error(await res.text()); this._project = { ...this._project, members: await res.json() }; } catch (e) { this._error = e.message; } } async _removeMember(userId) { try { const res = await fetch(`/api/projects/${this._projectId}/members/${encodeURIComponent(userId)}`, { method: 'DELETE' }); if (!res.ok) throw new Error(await res.text()); this._project = { ...this._project, members: await res.json() }; } catch (e) { this._error = e.message; } } _back() { this.dispatchEvent(new CustomEvent('project-back', { bubbles: true, composed: true })); } async _openChat() { try { const res = await fetch(`/api/projects/${this._projectId}/session`, { method: 'POST' }); if (!res.ok) throw new Error(await res.text()); const { source } = await res.json(); window.dispatchEvent(new CustomEvent('project-chat-open', { detail: { source, label: this._project?.name ?? `Project ${this._projectId}` }, })); } catch (e) { this._error = e.message; } } // ── Rendering ───────────────────────────────────────────────────────────────── _renderMember(m) { const isOwner = m.user_id === this._project.owner_user_id; const manage = this._canManage(); return html`
${t('projects.files.placeholder')}
${this._project.description}
` : nothing} ${this._renderFilesPanel()} ${this._renderSharePanel()}