// Board/item mutations: optimistic local update, then the server call. import { api } from './api.js'; import { state, mutateBoard, serverCall, loadBoard, loadTree, navigate, toast, boardPath } from './store.js'; import { confirmDialog, promptDialog } from './ui.js'; const B = (boardId) => state.boardData[boardId]; export function setValue(boardId, itemId, colId, value) { mutateBoard(boardId, (b) => { const it = b.items.find((i) => i.id === itemId); if (!it) return; if (value === null || value === '' || (Array.isArray(value) && !value.length)) delete it.vals[colId]; else it.vals[colId] = value; it.updated_at = Date.now(); }); return serverCall(boardId, api.patch(`/api/items/${itemId}`, { vals: { [colId]: value } })); } export function renameItem(boardId, itemId, name) { if (!name.trim()) return; mutateBoard(boardId, (b) => { const it = b.items.find((i) => i.id === itemId); if (it) it.name = name; }); return serverCall(boardId, api.patch(`/api/items/${itemId}`, { name })); } export async function createItem(boardId, { group_id, parent_id, name, vals = {}, top } = {}) { const b = B(boardId); const tmp = { id: 'tmp' + Math.random(), board_id: boardId, group_id: parent_id ? null : group_id, parent_id: parent_id || null, name, vals, position: top ? -1e9 : 1e9, created_at: Date.now(), updated_at: Date.now() }; if (b) { b.items.push(tmp); } mutateBoard(boardId, () => {}); try { const it = await api.post(`/api/boards/${boardId}/items`, { group_id, parent_id, name, vals, top }); mutateBoard(boardId, (bd) => { const i = bd.items.findIndex((x) => x.id === tmp.id); if (i >= 0) bd.items[i] = it; else bd.items.push(it); }); return it; } catch (e) { toast(e.message, 'error'); loadBoard(boardId); throw e; } } export async function deleteItems(boardId, ids, { ask = true } = {}) { if (ask && !(await confirmDialog({ title: ids.length > 1 ? `Delete ${ids.length} items?` : 'Delete this item?', body: 'Its subitems, updates and files are deleted too. This cannot be undone.' }))) return false; mutateBoard(boardId, (b) => { b.items = b.items.filter((i) => !ids.includes(i.id) && !ids.includes(i.parent_id)); }); if (state.route.itemId && ids.includes(state.route.itemId)) navigate(boardPath(boardId, { viewId: state.route.viewId })); await serverCall(boardId, ids.length === 1 ? api.del(`/api/items/${ids[0]}`) : api.post('/api/items/bulk', { ids, action: 'delete' })); toast(ids.length > 1 ? `${ids.length} items deleted` : 'Item deleted'); return true; } export async function archiveItems(boardId, ids) { mutateBoard(boardId, (b) => { b.items = b.items.filter((i) => !ids.includes(i.id) && !ids.includes(i.parent_id)); }); await serverCall(boardId, api.post('/api/items/bulk', { ids, action: 'archive' })); toast(ids.length > 1 ? `${ids.length} items archived` : 'Item archived'); } export async function duplicateItems(boardId, ids) { await serverCall(boardId, ids.length === 1 ? api.post(`/api/items/${ids[0]}/duplicate`) : api.post('/api/items/bulk', { ids, action: 'duplicate' })); loadBoard(boardId); } export async function moveItem(boardId, itemId, { group_id, position }) { mutateBoard(boardId, (b) => { const it = b.items.find((i) => i.id === itemId); if (it) { if (group_id) it.group_id = group_id; if (position != null) it.position = position; } }); return serverCall(boardId, api.patch(`/api/items/${itemId}`, { group_id, position })); } export async function moveItemsToGroup(boardId, ids, group_id) { mutateBoard(boardId, (b) => { for (const it of b.items) if (ids.includes(it.id)) it.group_id = group_id; }); await serverCall(boardId, api.post('/api/items/bulk', { ids, action: 'move', group_id })); } export async function moveItemToBoard(boardId, itemId, targetBoardId) { const target = state.boardData[targetBoardId] || await loadBoard(targetBoardId); const g = target.groups[0]; if (!g) return; mutateBoard(boardId, (b) => { b.items = b.items.filter((i) => i.id !== itemId && i.parent_id !== itemId); }); await serverCall(boardId, api.patch(`/api/items/${itemId}`, { group_id: g.id })); toast(`Moved to ${target.board.name}`); loadBoard(targetBoardId); } // Position between two neighbours (fractional indexing). export function between(a, b) { if (a == null && b == null) return 1; if (a == null) return b - 1; if (b == null) return a + 1; return (a + b) / 2; } // ─── Groups ────────────────────────────────────────────── export async function addGroup(boardId, { top = true } = {}) { const r = await serverCall(boardId, api.post(`/api/boards/${boardId}/groups`, { title: 'New Group', top })); await loadBoard(boardId); return r.id; } export function patchGroup(boardId, groupId, patch) { mutateBoard(boardId, (b) => { const g = b.groups.find((x) => x.id === groupId); if (g) Object.assign(g, patch); }); return serverCall(boardId, api.patch(`/api/groups/${groupId}`, patch)); } export async function deleteGroup(boardId, groupId) { const b = B(boardId); const g = b.groups.find((x) => x.id === groupId); const n = b.items.filter((i) => i.group_id === groupId).length; if (!(await confirmDialog({ title: `Delete "${g.title}"?`, body: n ? `This deletes the group and its ${n} item${n === 1 ? '' : 's'}.` : 'This group is empty.' }))) return; mutateBoard(boardId, (bd) => { bd.groups = bd.groups.filter((x) => x.id !== groupId); bd.items = bd.items.filter((i) => i.group_id !== groupId); }); await serverCall(boardId, api.del(`/api/groups/${groupId}`)); } // ─── Columns ───────────────────────────────────────────── export async function addColumn(boardId, type, { after, title } = {}) { const r = await serverCall(boardId, api.post(`/api/boards/${boardId}/columns`, { type, after, title })); await loadBoard(boardId); return r.id; } export function patchColumn(boardId, colId, patch) { mutateBoard(boardId, (b) => { const c = b.columns.find((x) => x.id === colId); if (c) Object.assign(c, patch); }); return serverCall(boardId, api.patch(`/api/columns/${colId}`, patch)); } export async function deleteColumn(boardId, colId) { const c = B(boardId).columns.find((x) => x.id === colId); if (!(await confirmDialog({ title: `Delete column "${c.title}"?`, body: 'All values in this column are deleted.' }))) return; mutateBoard(boardId, (b) => { b.columns = b.columns.filter((x) => x.id !== colId); }); await serverCall(boardId, api.del(`/api/columns/${colId}`)); } // ─── Boards / workspace structure ──────────────────────── export async function renameBoard(boardId, name) { mutateBoard(boardId, (b) => { b.board.name = name; }); const tb = state.boards.find((b) => b.id === boardId); if (tb) tb.name = name; await serverCall(boardId, api.patch(`/api/boards/${boardId}`, { name })); } export async function deleteBoard(boardId) { const b = state.boards.find((x) => x.id === boardId); if (!(await confirmDialog({ title: `Delete board "${b?.name}"?`, body: 'Every item, update and file on it is deleted. This cannot be undone.' }))) return; await api.del(`/api/boards/${boardId}`); delete state.boardData[boardId]; if (state.route.boardId === boardId) navigate('/'); await loadTree(); toast('Board deleted'); } export async function duplicateBoard(boardId) { const r = await api.post(`/api/boards/${boardId}/duplicate`, {}); await loadTree(); navigate(boardPath(r.id)); } export async function newFolder(workspace_id, parent_id) { const name = await promptDialog({ title: 'New folder', placeholder: 'Folder name', confirm: 'Create' }); if (!name) return; await api.post('/api/folders', { workspace_id, parent_id, name }); await loadTree(); } export async function renameFolder(f) { const name = await promptDialog({ title: 'Rename folder', value: f.name }); if (!name) return; await api.patch(`/api/folders/${f.id}`, { name }); await loadTree(); } export async function deleteFolder(f) { if (!(await confirmDialog({ title: `Delete folder "${f.name}"?`, body: 'Boards inside are kept and moved up a level.' }))) return; await api.del(`/api/folders/${f.id}`); await loadTree(); } export async function toggleFavorite(boardId) { const on = !state.favorites.includes(boardId); state.favorites = on ? [...state.favorites, boardId] : state.favorites.filter((x) => x !== boardId); mutateBoard(boardId, (b) => { b.favorite = on; }); await api.post(`/api/boards/${boardId}/favorite`, { on }); }