projects: file explorer, ws improvements, i18n, and fs routing
Nightly Build / build (push) Successful in 6m47s

Add project-files component with tree navigation. Extend UserFs with
shared-folder resolution. Wire API routes for file browsing. Improve
WS session lifecycle and project-board layout. Add i18n keys for
projects and inbox across all locales.
This commit is contained in:
2026-07-22 18:35:13 +01:00
parent 8407a949fc
commit f34f800e5c
16 changed files with 733 additions and 40 deletions
+32 -5
View File
@@ -8,6 +8,7 @@ export class ProjectsPage extends LightElement {
_open: { state: true },
_view: { state: true },
_projectId: { state: true },
_tab: { state: true },
};
constructor() {
@@ -15,6 +16,7 @@ export class ProjectsPage extends LightElement {
this._open = false;
this._view = 'list';
this._projectId = null;
this._tab = 'files';
}
connectedCallback() {
@@ -24,9 +26,25 @@ export class ProjectsPage extends LightElement {
this._open = open;
this.style.display = open ? 'flex' : 'none';
if (open) {
const { view, id } = this._parseHash();
const { view, id, tab } = this._parseHash();
this._view = view;
this._projectId = id;
this._tab = tab;
this._loadCurrent();
}
});
window.addEventListener('hashchange', () => {
// Back/forward (or manual edit) between board tabs: same project → just
// switch the tab, no reload; anything else → re-sync from the hash.
if (!this._open || !location.hash.startsWith('#projects')) return;
const { view, id, tab } = this._parseHash();
if (view === 'board' && this._view === 'board' && id === this._projectId) {
this._tab = tab;
this.querySelector('project-board-section')?.setTab(tab);
} else {
this._view = view;
this._projectId = id;
this._tab = tab;
this._loadCurrent();
}
});
@@ -40,9 +58,10 @@ export class ProjectsPage extends LightElement {
_parseHash() {
const parts = location.hash.slice(1).split('/');
if (parts[0] === 'projects' && parts[1] && /^\d+$/.test(parts[1])) {
return { view: 'board', id: parseInt(parts[1], 10) };
const tab = parts[2] === 'sharing' ? 'sharing' : 'files';
return { view: 'board', id: parseInt(parts[1], 10), tab };
}
return { view: 'list', id: null };
return { view: 'list', id: null, tab: 'files' };
}
_loadCurrent() {
@@ -50,7 +69,7 @@ export class ProjectsPage extends LightElement {
if (this._view === 'list') {
this.querySelector('project-list-section')?.load();
} else {
this.querySelector('project-board-section')?.load(this._projectId);
this.querySelector('project-board-section')?.load(this._projectId, this._tab);
}
});
}
@@ -58,9 +77,10 @@ export class ProjectsPage extends LightElement {
_navigateToBoard(id) {
this._view = 'board';
this._projectId = id;
this._tab = 'files';
history.pushState({ page: 'projects', id }, '', `#projects/${id}`);
this.updateComplete.then(() => {
this.querySelector('project-board-section')?.load(id);
this.querySelector('project-board-section')?.load(id, 'files');
});
}
@@ -73,6 +93,12 @@ export class ProjectsPage extends LightElement {
});
}
_onTabChange(tab) {
this._tab = tab;
const hash = tab === 'sharing' ? `#projects/${this._projectId}/sharing` : `#projects/${this._projectId}`;
history.pushState({ page: 'projects', id: this._projectId }, '', hash);
}
render() {
if (!this._open) return nothing;
return html`
@@ -83,6 +109,7 @@ export class ProjectsPage extends LightElement {
` : html`
<project-board-section
@project-back=${() => this._navigateToList()}
@project-tab-change=${e => this._onTabChange(e.detail.tab)}
></project-board-section>
`}
`;