Files
christianandClaude Opus 5.5 fcf3805626
Deploy Waters Boards to NAS / deploy (push) Canceled after 0s
Waters Boards: self-hosted work management to replace monday.com
Boards with groups, items, subitems and 18 column types; table, kanban,
calendar, timeline, chart and files views; updates with @mentions,
replies and attachments; inbox, My work, search, automations; Google
sign-in restricted to company domains; live updates over SSE; a
monday.com API importer; phone layout and installable PWA; Docker +
Gitea Actions deploy to the NAS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:34:47 -04:00

53 lines
4.2 KiB
JavaScript

// 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">
<button class="btn primary" onClick=${() => A.addColumn(boardId, 'status')}>Add a status column</button><//>`;
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`<div class="kanban-wrap">
${statusCols.length > 1 ? html`<div class="view-options"><span class="muted small">Lanes from</span>
<select class="input sm" value=${col.id} onChange=${(e) => setCol(e.target.value)}>${statusCols.map((c) => html`<option value=${c.id}>${c.title}</option>`)}</select></div>` : null}
<div class="kanban">
${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`<div key=${l.id || 'none'} class="lane ${over === l.id ? 'over' : ''}"
onDragOver=${(e) => { 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); }}>
<div class="lane-head" style=${{ background: l.color, color: textOn(l.color) }}>${l.name} <span>${cards.length}</span></div>
<div class="lane-body">
${cards.map((it) => html`<div key=${it.id} class="kcard" draggable="true" onDragStart=${() => setDrag(it)} onDragEnd=${() => setDrag(null)}
onClick=${() => navigate(boardPath(boardId, { viewId: view.id, itemId: it.id }))}>
<div class="kcard-name">${it.name}</div>
${extra.map((c) => it.vals[c.id] != null ? html`<div key=${c.id} class="kcard-field"><span class="muted small">${c.title}</span><div onClick=${(e) => e.stopPropagation()}><${Cell} board=${board} col=${c} item=${it} compact /></div></div>` : null)}
<div class="kcard-foot">
${date && it.vals[date.id] ? html`<span class="muted small"><${Icon} name="calendar" size=${12} /> ${date.type === 'date' ? fmtDate(it.vals[date.id]) : fmtRange(it.vals[date.id])}</span>` : html`<span />`}
${people ? html`<${AvatarStack} ids=${it.vals[people.id] || []} size=${22} />` : null}
</div>
</div>`)}
${adding === l.id ? html`<input class="input sm" autofocus placeholder="Name"
onKeyDown=${async (e) => { 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`<button class="lane-add" onClick=${() => setAdding(l.id)}><${Icon} name="plus" /> Add ${board.board.item_label.toLowerCase()}</button>`}
</div>
</div>`;
})}
</div></div>`;
}