Files
Void-Homelab/public/views/space.js

109 lines
4.7 KiB
JavaScript

// Space view — rich project cards (status, research, edit/delete) + open tasks,
// then a full pages & references table.
import { api } from '../api.js';
import { el, mount } from '../dom.js';
import { exportMenu } from '../components/export_menu.js';
import { projectCard } from '../components/project_card.js';
import { openProjectModal } from '../components/project_modal.js';
function taskItem(t) {
return el('li', {},
el('span', { class: 'status' + (t.status === 'blocked' ? ' bad' : '') }, t.status), ' ', t.title);
}
function pageLink(p) {
return el('a', { href: '#/page/' + p.id }, p.title || '(untitled)');
}
// Static 3-level descent (root → child → grandchild); pages nested deeper than
// 3 levels are intentionally not shown. Our spaces never nest beyond that.
function renderPageTree(pages, refs) {
const byParent = new Map();
for (const p of pages) {
const k = p.parent_id || '__root__';
if (!byParent.has(k)) byParent.set(k, []);
byParent.get(k).push(p);
}
const roots = byParent.get('__root__') || [];
const blocks = [];
for (const r of roots) {
const kids = byParent.get(r.id) || [];
blocks.push(el('div', { class: 'doc-section' },
el('h4', { style: { margin: '12px 0 4px' } }, pageLink(r)),
kids.length
? el('ul', { class: 'plain', style: { margin: '0 0 0 14px' } },
kids.map(k => {
const gk = byParent.get(k.id) || [];
return el('li', {}, pageLink(k),
gk.length ? el('ul', { class: 'plain', style: { margin: '0 0 0 14px' } },
gk.map(g => el('li', {}, pageLink(g)))) : null);
}))
: null));
}
if (refs.length) blocks.push(el('div', { class: 'doc-section' },
el('h4', { style: { margin: '12px 0 4px' } }, 'References'),
el('ul', { class: 'plain', style: { margin: '0 0 0 14px' } },
refs.map(rf => el('li', {}, el('a', { href: '#/ref/' + rf.id }, rf.title || rf.source_url))))));
return blocks;
}
export async function render(main, ctx) {
const id = ctx.params.id;
mount(main, el('h1', { class: 'view-h1' }, 'Space'), el('p', { class: 'view-sub muted' }, 'Loading …'));
localStorage.setItem('last_space_id', id);
let space;
try { space = await api.get('/api/spaces/' + id); }
catch (e) { mount(main, el('h1', { class: 'view-h1' }, 'Space not found'), el('p', { class: 'view-sub muted' }, e.message)); return; }
let projects = [];
const [tasks, pages, refs] = await Promise.all([
api.get(`/api/spaces/${id}/tasks?status=todo`).catch(() => []),
api.get(`/api/spaces/${id}/pages`).catch(() => []),
api.get(`/api/refs?space_id=${id}&limit=200`).catch(() => [])
]);
try { projects = await api.get(`/api/spaces/${id}/projects`); } catch { /* */ }
// ---- Projects section (rich cards) ----
const projWrap = el('div', { class: 'proj-list' });
function renderProjects() {
projWrap.replaceChildren();
projHead.textContent = `Projects${projects.length ? ` (${projects.length})` : ''}`;
if (!projects.length) { projWrap.appendChild(el('div', { class: 'muted', style: { padding: '4px 2px' } }, 'No projects yet.')); return; }
for (const p of projects) projWrap.appendChild(projectCard(p, { reload, rerender: renderProjects }));
}
async function reload() { try { projects = await api.get(`/api/spaces/${id}/projects`); } catch { /* */ } renderProjects(); }
const projHead = el('h3', {}, 'Projects');
renderProjects();
mount(main,
el('div', { class: 'doc-head' },
el('h1', { class: 'view-h1', style: { margin: '0' } }, space.name),
exportMenu({
filenameBase: 'space-' + (space.slug || space.name),
getContent: async () => {
const full = await Promise.all(pages.map(p => api.get('/api/pages/' + p.id).catch(() => null)));
const md = full.filter(Boolean).map(p => `# ${p.title}\n\n${p.body_md || ''}`).join('\n\n---\n\n');
return { title: space.name, md };
}
})
),
el('p', { class: 'view-sub' }, space.description || el('span', { class: 'muted' }, 'No description.')),
el('div', { class: 'card' },
el('div', { class: 'card-head' }, projHead,
el('button', { class: 'primary', onclick: () => openProjectModal(id, null, reload) }, '+ New')),
projWrap),
el('div', { class: 'card' },
el('h3', {}, `Open tasks${tasks.length ? ` (${tasks.length})` : ''}`),
tasks.length ? el('ul', { class: 'plain' }, tasks.map(taskItem)) : el('p', { class: 'muted' }, 'Clear board.')),
el('div', { class: 'card' },
el('h3', {}, `Pages & references${(pages.length + refs.length) ? ` (${pages.length + refs.length})` : ''}`),
(pages.length + refs.length) > 0
? el('div', {}, renderPageTree(pages, refs))
: el('p', { class: 'muted' }, 'Nothing here yet.'))
);
}