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

98 lines
3.9 KiB
JavaScript

// Space view — Projects + Open tasks side by side at the top, then a full
// pages & references table below (all pages; refs up to the API max).
import { api } from '../api.js';
import { el, mount } from '../dom.js';
import { exportMenu } from '../components/export_menu.js';
function projItem(p) {
return el('li', {},
el('a', { href: '#/project/' + p.id }, p.name), ' ',
el('span', { class: 'status' + (p.status === 'done' ? ' ok' : p.status === 'paused' ? ' warn' : '') }, p.status));
}
function taskItem(t) {
return el('li', {},
el('span', { class: 'status' + (t.status === 'blocked' ? ' bad' : '') }, t.status), ' ', t.title);
}
function tableRow(href, title, type) {
return el('tr', {},
el('td', { style: { padding: '5px 8px', borderTop: '1px solid var(--border)' } }, el('a', { href }, title || '(untitled)')),
el('td', { class: 'muted', style: { padding: '5px 8px', borderTop: '1px solid var(--border)', width: '90px' } }, type));
}
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;
}
const [projects, tasks, pages, refs] = await Promise.all([
api.get(`/api/spaces/${id}/projects`).catch(() => []),
api.get(`/api/spaces/${id}/tasks?status=todo`).catch(() => []),
api.get(`/api/spaces/${id}/pages`).catch(() => []), // returns ALL pages
api.get(`/api/refs?space_id=${id}&limit=200`).catch(() => []) // 200 = API max
]);
const rows = [
...pages.map(p => tableRow('#/page/' + p.id, p.title, 'page')),
...refs.map(r => tableRow('#/ref/' + r.id, r.title || r.source_url, r.kind))
];
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.')),
// Top: Projects + Open tasks, side by side.
el('div', { class: 'row' },
el('div', { class: 'card' },
el('h3', {}, `Projects${projects.length ? ` (${projects.length})` : ''}`),
projects.length
? el('ul', { class: 'plain' }, projects.map(projItem))
: el('p', { class: 'muted' }, 'None yet.')
),
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.')
)
),
// Below: the full pages & references table.
el('div', { class: 'card' },
el('h3', {}, `Pages & references${rows.length ? ` (${pages.length + refs.length})` : ''}`),
rows.length
? el('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: '13px' } },
el('thead', {},
el('tr', {},
el('th', { class: 'muted', style: { textAlign: 'left', padding: '5px 8px', fontWeight: '500' } }, 'Title'),
el('th', { class: 'muted', style: { textAlign: 'left', padding: '5px 8px', width: '90px', fontWeight: '500' } }, 'Type'))),
el('tbody', {}, rows))
: el('p', { class: 'muted' }, 'Nothing here yet.')
)
);
}