// Left navigation: global nav rail + workspace tree (folders, boards, favorites). import { html, useState } from '../vendor/preact-htm.mjs'; import { api } from './api.js'; import { state, update, useStore, navigate, boardPath, loadTree, localSet, localGet } from './store.js'; import { Icon, Avatar, menu, openModal, closeModal, ModalHeader, promptDialog, confirmDialog, openPopover, ColorPicker, GROUP_COLORS } from './ui.js'; import * as A from './actions.js'; import { UserMenu } from './pages.js'; export function Sidebar() { const s = useStore(); const [q, setQ] = useState(''); const [openFolders, setOpenFolders] = useState(() => { try { return JSON.parse(localGet('wb.openFolders') || '{}'); } catch { return {}; } }); const ws = s.workspaces.find((w) => w.id === s.currentWorkspace) || s.workspaces[0]; const r = s.route; const toggleFolder = (id) => { const n = { ...openFolders, [id]: !openFolders[id] }; setOpenFolders(n); localSet('wb.openFolders', JSON.stringify(n)); }; const pickWs = (id) => { localSet('wb.workspace', id); update({ currentWorkspace: id }); }; const favs = s.boards.filter((b) => s.favorites.includes(b.id)); const filter = q.trim().toLowerCase(); const wsBoards = s.boards.filter((b) => b.workspace_id === ws?.id && (!filter || b.name.toLowerCase().includes(filter))); const wsFolders = s.folders.filter((f) => f.workspace_id === ws?.id); const wsMenu = (e) => menu(e, [ ...s.workspaces.map((w) => ({ label: w.name, icon: 'workspace', active: w.id === ws?.id, onClick: () => pickWs(w.id) })), { divider: true }, { label: 'Add workspace', icon: 'plus', onClick: async () => { const name = await promptDialog({ title: 'New workspace', placeholder: 'e.g. Production, Creative, Facilities', confirm: 'Create' }); if (name) { const w = await api.post('/api/workspaces', { name, color: GROUP_COLORS[s.workspaces.length % GROUP_COLORS.length] }); await loadTree(); pickWs(w.id); } } }, ws && { label: 'Workspace overview', icon: 'info', onClick: () => navigate(`/workspaces/${ws.id}`) }, ws && { label: 'Rename workspace', icon: 'edit', onClick: async () => { const n = await promptDialog({ title: 'Rename workspace', value: ws.name }); if (n) { await api.patch(`/api/workspaces/${ws.id}`, { name: n }); loadTree(); } } }, ws && s.me.role === 'admin' && s.workspaces.length > 1 && { label: 'Delete workspace', icon: 'trash', danger: true, onClick: async () => { if (await confirmDialog({ title: `Delete workspace "${ws.name}"?`, body: 'Every board, item and file in it is deleted permanently.' })) { await api.del(`/api/workspaces/${ws.id}`); await loadTree(); pickWs(s.workspaces[0]?.id); } } }, ]); const addMenu = (e) => menu(e, [ { label: 'New board', icon: 'board', onClick: () => openModal(NewBoardModal, { workspaceId: ws.id }) }, { label: 'New folder', icon: 'folder', onClick: () => A.newFolder(ws.id) }, ]); const BoardLink = ({ b, depth = 0 }) => html``; const boardMenu = (e, b) => menu(e, [ { label: 'Open in new tab', icon: 'expand', onClick: () => window.open(boardPath(b.id), '_blank') }, { label: 'Rename', icon: 'edit', onClick: async () => { const n = await promptDialog({ title: 'Rename board', value: b.name }); if (n) { await A.renameBoard(b.id, n); loadTree(); } } }, { label: s.favorites.includes(b.id) ? 'Remove from favorites' : 'Add to favorites', icon: 'star', onClick: () => A.toggleFavorite(b.id).then(loadTree) }, { label: 'Duplicate', icon: 'copy', onClick: () => A.duplicateBoard(b.id) }, { label: 'Move to folder', icon: 'folder', keepOpen: true, onClick: (ev) => menu(ev, [{ label: '(no folder)', onClick: () => moveBoard(b, null) }, ...wsFolders.map((f) => ({ label: f.name, onClick: () => moveBoard(b, f.id) }))]) }, { divider: true }, { label: 'Delete', icon: 'trash', danger: true, onClick: () => A.deleteBoard(b.id) }, ]); const moveBoard = async (b, folderId) => { await api.patch(`/api/boards/${b.id}`, { folder_id: folderId }); loadTree(); }; const Folder = ({ f, depth = 0 }) => { const open = openFolders[f.id] || !!filter; const kids = wsFolders.filter((x) => x.parent_id === f.id); const boards = wsBoards.filter((b) => b.folder_id === f.id); if (filter && !boards.length && !kids.length) return null; const fMenu = (e) => menu(e, [ { label: 'New board here', icon: 'board', onClick: () => openModal(NewBoardModal, { workspaceId: ws.id, folderId: f.id }) }, { label: 'New subfolder', icon: 'folder', onClick: () => A.newFolder(ws.id, f.id) }, { label: 'Rename', icon: 'edit', onClick: () => A.renameFolder(f) }, { label: 'Color', icon: 'gear', keepOpen: true, onClick: (ev) => openPopover(ev.currentTarget, ColorPicker, { value: f.color, onPick: async (c) => { await api.patch(`/api/folders/${f.id}`, { color: c }); loadTree(); } }) }, { divider: true }, { label: 'Delete folder', icon: 'trash', danger: true, onClick: () => A.deleteFolder(f) }, ]); return html``; }; return html` ${s.sidebarOpen ? html`
update({ sidebarOpen: false })} />` : null}`; } function RailBtn({ icon, label, active, onClick, badge }) { return html``; } const TEMPLATES = [ ['blank', 'Blank board', 'Owner, Status and Due date to start', 'board'], ['project', 'Project tracker', 'Owner, status, priority, timeline', 'timeline'], ['inventory', 'Equipment inventory', 'Model, serial #, location, condition, files', 'archive'], ['content', 'Content calendar', 'Owner, status, publish date, channel, caption', 'calendar'], ['requests', 'Requests', 'Intake for requests with priority and due date', 'inbox'], ]; export function NewBoardModal({ workspaceId, folderId }) { const s = useStore(); const [name, setName] = useState(''); const [tpl, setTpl] = useState('blank'); const [ws, setWs] = useState(workspaceId); const [folder, setFolder] = useState(folderId || ''); const [busy, setBusy] = useState(false); const create = async (e) => { e.preventDefault(); setBusy(true); try { const r = await api.post('/api/boards', { workspace_id: ws, folder_id: folder || null, name: name.trim() || 'New board', template: tpl }); await loadTree(); closeModal(); navigate(boardPath(r.id)); } finally { setBusy(false); } }; return html`
<${ModalHeader} title="Create board" /> setName(e.target.value)} placeholder="New board" autofocus />
${TEMPLATES.map(([k, l, d, i]) => html``)}
`; }