// Kanban: one lane per label of a status column; drag cards between lanes. import { html, useState } from '../vendor/preact-htm.mjs'; import { api } from './api.js'; import { navigate, boardPath, mutateBoard } from './store.js'; import { Icon, AvatarStack, textOn, fmtDate, fmtRange, Empty } from './ui.js'; import { labelOf, Cell } from './cells.js'; import * as A from './actions.js'; export function KanbanView({ board, view, items, hidden }) { const boardId = board.board.id; const statusCols = board.columns.filter((c) => c.type === 'status'); const col = statusCols.find((c) => c.id === view.settings?.column) || statusCols[0]; const [drag, setDrag] = useState(null); const [over, setOver] = useState(null); const [adding, setAdding] = useState(null); if (!col) return html`<${Empty} icon="kanban" title="Kanban needs a status column"> `; const lanes = [...(col.settings.labels || []), { id: null, name: 'Empty', color: '#c4c4c4' }]; const people = board.columns.find((c) => c.type === 'people'); const date = board.columns.find((c) => c.type === 'date' || c.type === 'timeline'); const extra = board.columns.filter((c) => !hidden.includes(c.id) && c !== col && c !== people && c !== date && ['text', 'number', 'dropdown', 'tags'].includes(c.type)).slice(0, 3); const setCol = (id) => { mutateBoard(boardId, () => { view.settings = { ...view.settings, column: id }; }); api.patch(`/api/views/${view.id}`, { settings: { ...view.settings, column: id } }); }; return html`
${statusCols.length > 1 ? html`
Lanes from
` : null}
${lanes.map((l) => { const cards = items.filter((it) => (it.vals[col.id] || null) === l.id); if (l.id === null && !cards.length) return null; return html`
{ e.preventDefault(); setOver(l.id); }} onDragLeave=${() => setOver(null)} onDrop=${(e) => { e.preventDefault(); setOver(null); if (drag && (drag.vals[col.id] || null) !== l.id) A.setValue(boardId, drag.id, col.id, l.id); setDrag(null); }}>
${l.name} ${cards.length}
${cards.map((it) => html`
setDrag(it)} onDragEnd=${() => setDrag(null)} onClick=${() => navigate(boardPath(boardId, { viewId: view.id, itemId: it.id }))}>
${it.name}
${extra.map((c) => it.vals[c.id] != null ? html`
${c.title}
e.stopPropagation()}><${Cell} board=${board} col=${c} item=${it} compact />
` : null)}
${date && it.vals[date.id] ? html`<${Icon} name="calendar" size=${12} /> ${date.type === 'date' ? fmtDate(it.vals[date.id]) : fmtRange(it.vals[date.id])}` : html``} ${people ? html`<${AvatarStack} ids=${it.vals[people.id] || []} size=${22} />` : null}
`)} ${adding === l.id ? html` { if (e.key === 'Enter' && e.target.value.trim()) { const name = e.target.value.trim(); e.target.value = ''; await A.createItem(boardId, { group_id: board.groups[0]?.id, name, vals: l.id ? { [col.id]: l.id } : {} }); } if (e.key === 'Escape') setAdding(undefined); }} onBlur=${() => setAdding(undefined)} />` : html``}
`; })}
`; }