import { html, nothing } from 'lit'; import { LightElement } from '../../lib/base.js'; import { t } from '../../lib/i18n.js'; import { ProjectFilesPanel } from './project-files.js'; /// A project's detail page: header + description, then two tabs — **Files** (a /// live explorer over the project folder, ``) and /// **Sharing** (member picker with read/write, mirroring the shared-folders UI). export class ProjectBoardSection extends LightElement { static properties = { _project: { state: true }, _users: { state: true }, _add: { state: true }, _error: { state: true }, _tab: { state: true }, }; constructor() { super(); this._project = null; this._users = []; this._add = { user_id: '', can_write: false }; this._error = null; this._projectId = null; this._tab = 'files'; } 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, tab) { this._projectId = projectId; this._project = null; this._error = null; this._tab = tab === 'sharing' ? 'sharing' : 'files'; 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; } } // Switch the visible tab without reloading (host back/forward sync). setTab(tab) { this._tab = tab === 'sharing' ? 'sharing' : 'files'; } _selectTab(tab) { if (tab === this._tab) return; this._tab = tab; this.dispatchEvent(new CustomEvent('project-tab-change', { detail: { tab }, bubbles: true, composed: true, })); } _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, session_id } = await res.json(); window.dispatchEvent(new CustomEvent('project-chat-open', { detail: { source, session_id, 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` ${this._userLabel(m.user_id)} ${isOwner ? html`${t('projects.share.owner')}` : nothing} ${isOwner ? html` ${t('projects.share.access.readwrite')} ` : manage ? html` m.can_write && this._setAccess(m.user_id, false)}> ${t('projects.share.access.read')} !m.can_write && this._setAccess(m.user_id, true)}> ${t('projects.share.access.write')} this._removeMember(m.user_id)}> ` : html` ${m.can_write ? t('projects.share.access.readwrite') : t('projects.share.access.readonly')} `} `; } _renderSharePanel() { const candidates = this._candidates(); const manage = this._canManage(); return html` ${t('projects.share.title')} ${(this._project.members ?? []).map(m => this._renderMember(m))} ${manage ? html` ${candidates.length > 0 ? html` this._add = { ...this._add, user_id: e.target.value }}> ${t('projects.share.choose_user')} ${candidates.map(u => html` ${u.display_name || u.username}`)} this._add = { ...this._add, can_write: e.target.value === 'write' }}> ${t('projects.share.access.readonly')} ${t('projects.share.access.readwrite')} this._addMember()}> ${t('projects.share.add')} ` : html`${t('projects.share.all_added')}`} ` : nothing} `; } _renderTabs() { const tab = (id, icon, label) => html` this._selectTab(id)}> ${label} `; return html` ${tab('files', 'bi-folder2-open', t('projects.tabs.files'))} ${tab('sharing', 'bi-people', t('projects.tabs.sharing'))} `; } render() { if (!this._project) { return html` `; } return html` this._back()}> ${this._project.name} ${this._project.is_owner ? html`${t('projects.badge.owned')}` : html`${t('projects.badge.shared_by', { name: this._project.owner_name })}`} this._openChat()}> ${t('project_board.open_chat')} ${this._error ? html` ${this._error} ` : nothing} ${this._project.description ? html` ${this._project.description} ` : nothing} ${this._renderTabs()} ${this._tab === 'sharing' ? this._renderSharePanel() : nothing} `; } } customElements.define('project-files-panel', ProjectFilesPanel);
${this._project.description}