import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { ViewModeProps } from "../interface"; import "./index.css"; import { ColumnMap, getBoardData } from "./data"; import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks"; import Icon from "../../react/Icon"; import { t } from "../../../services/i18n"; import Api from "./api"; import FormTextBox from "../../react/FormTextBox"; import { createContext } from "preact"; import { onWheelHorizontalScroll } from "../../widget_utils"; import Column from "./column"; import BoardApi from "./api"; import FormTextArea from "../../react/FormTextArea"; export interface BoardViewData { columns?: BoardColumnData[]; } export interface BoardColumnData { value: string; } interface BoardViewContextData { api?: BoardApi; branchIdToEdit?: string; columnNameToEdit?: string; setColumnNameToEdit?: Dispatch>; setBranchIdToEdit?: Dispatch>; draggedColumn: { column: string, index: number } | null; setDraggedColumn: (column: { column: string, index: number } | null) => void; dropPosition: { column: string, index: number } | null; setDropPosition: (position: { column: string, index: number } | null) => void; draggedCard: { noteId: string, branchId: string, fromColumn: string, index: number } | null; setDraggedCard: (card: { noteId: string, branchId: string, fromColumn: string, index: number } | null) => void; setDropTarget: (target: string | null) => void, dropTarget: string | null } export const BoardViewContext = createContext({}); export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps) { const [ statusAttribute ] = useNoteLabelWithDefault(parentNote, "board:groupBy", "status"); const [ byColumn, setByColumn ] = useState(); const [ columns, setColumns ] = useState(); const [ draggedCard, setDraggedCard ] = useState<{ noteId: string, branchId: string, fromColumn: string, index: number } | null>(null); const [ dropTarget, setDropTarget ] = useState(null); const [ dropPosition, setDropPosition ] = useState<{ column: string, index: number } | null>(null); const [ draggedColumn, setDraggedColumn ] = useState<{ column: string, index: number } | null>(null); const [ columnDropPosition, setColumnDropPosition ] = useState(null); const [ branchIdToEdit, setBranchIdToEdit ] = useState(); const [ columnNameToEdit, setColumnNameToEdit ] = useState(); const api = useMemo(() => { return new Api(byColumn, columns ?? [], parentNote, statusAttribute, viewConfig ?? {}, saveConfig, setBranchIdToEdit ); }, [ byColumn, columns, parentNote, statusAttribute, viewConfig, saveConfig, setBranchIdToEdit ]); const boardViewContext = useMemo(() => ({ api, branchIdToEdit, setBranchIdToEdit, columnNameToEdit, setColumnNameToEdit, draggedColumn, setDraggedColumn, dropPosition, setDropPosition, draggedCard, setDraggedCard, dropTarget, setDropTarget }), [ api, branchIdToEdit, setBranchIdToEdit, columnNameToEdit, setColumnNameToEdit, draggedColumn, setDraggedColumn, dropPosition, setDropPosition, draggedCard, setDraggedCard, dropTarget, setDropTarget ]); function refresh() { getBoardData(parentNote, statusAttribute, viewConfig ?? {}).then(({ byColumn, newPersistedData }) => { setByColumn(byColumn); if (newPersistedData) { viewConfig = { ...newPersistedData }; saveConfig(newPersistedData); } // Use the order from persistedData.columns, then add any new columns found const orderedColumns = viewConfig?.columns?.map(col => col.value) || []; const allColumns = Array.from(byColumn.keys()); const newColumns = allColumns.filter(col => !orderedColumns.includes(col)); setColumns([...orderedColumns, ...newColumns]); }); } useEffect(refresh, [ parentNote, noteIds, viewConfig ]); const handleColumnDrop = useCallback((fromIndex: number, toIndex: number) => { const newColumns = api.reorderColumn(fromIndex, toIndex); setColumns(newColumns); setDraggedColumn(null); setColumnDropPosition(null); }, [api]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { // Check if any changes affect our board const hasRelevantChanges = // React to changes in status attribute for notes in this board loadResults.getAttributeRows().some(attr => attr.name === statusAttribute && noteIds.includes(attr.noteId!)) || // React to changes in note title loadResults.getNoteIds().some(noteId => noteIds.includes(noteId)) || // React to changes in branches for subchildren (e.g., moved, added, or removed notes) loadResults.getBranchRows().some(branch => noteIds.includes(branch.noteId!)) || // React to changes in note icon or color. loadResults.getAttributeRows().some(attr => [ "iconClass", "color" ].includes(attr.name ?? "") && noteIds.includes(attr.noteId ?? "")) || // React to attachment change loadResults.getAttachmentRows().some(att => att.ownerId === parentNote.noteId && att.title === "board.json") || // React to changes in "groupBy" loadResults.getAttributeRows().some(attr => attr.name === "board:groupBy" && attr.noteId === parentNote.noteId); if (hasRelevantChanges) { refresh(); } }); const handleColumnDragOver = useCallback((e: DragEvent) => { if (!draggedColumn) return; e.preventDefault(); const container = e.currentTarget as HTMLElement; const columns = Array.from(container.querySelectorAll('.board-column')); const mouseX = e.clientX; let newIndex = columns.length; for (let i = 0; i < columns.length; i++) { const col = columns[i] as HTMLElement; const rect = col.getBoundingClientRect(); const colMiddle = rect.left + rect.width / 2; if (mouseX < colMiddle) { newIndex = i; break; } } setColumnDropPosition(newIndex); }, [draggedColumn]); const handleContainerDrop = useCallback((e: DragEvent) => { e.preventDefault(); if (draggedColumn && columnDropPosition !== null) { handleColumnDrop(draggedColumn.index, columnDropPosition); } }, [draggedColumn, columnDropPosition, handleColumnDrop]); return (
{byColumn && columns?.map((column, index) => ( <> {columnDropPosition === index && draggedColumn?.column !== column && (
)} ))} {columnDropPosition === columns?.length && draggedColumn && (
)}
) } function AddNewColumn({ api }: { api: BoardApi }) { const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false); const addColumnCallback = useCallback(() => { setIsCreatingNewColumn(true); }, []); return (
{!isCreatingNewColumn ? <> {" "} {t("board_view.add-column")} : ( api.addNewColumn(columnName)} dismiss={() => setIsCreatingNewColumn(false)} isNewItem /> )}
) } export function TitleEditor({ currentValue, placeholder, save, dismiss, multiline, isNewItem }: { currentValue?: string; placeholder?: string; save: (newValue: string) => void; dismiss: () => void; multiline?: boolean; isNewItem?: boolean; }) { const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); inputRef.current?.select(); }, [ inputRef ]); const Element = multiline ? FormTextArea : FormTextBox; return ( { if (e.key === "Enter") { const newValue = e.currentTarget.value; if (newValue !== currentValue || isNewItem) { save(newValue); } dismiss(); } if (e.key === "Escape") { dismiss(); } }} onBlur={(newValue) => { if (newValue !== currentValue) { save(newValue); } dismiss(); }} /> ); }