Files
waters-boards/server/public/js/actions.js
T
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

169 lines
8.5 KiB
JavaScript

// 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 });
}