From cb84e4c7b6f842db74c71f51215508b6ac6c84bc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 11 Sep 2025 21:42:59 +0300 Subject: [PATCH] refactor(react/collections/table): split card/column --- .../src/widgets/collections/board/card.tsx | 88 +++++ .../src/widgets/collections/board/column.tsx | 234 +++++++++++++ .../src/widgets/collections/board/index.tsx | 312 +----------------- 3 files changed, 325 insertions(+), 309 deletions(-) create mode 100644 apps/client/src/widgets/collections/board/card.tsx create mode 100644 apps/client/src/widgets/collections/board/column.tsx diff --git a/apps/client/src/widgets/collections/board/card.tsx b/apps/client/src/widgets/collections/board/card.tsx new file mode 100644 index 000000000..3dda7fd89 --- /dev/null +++ b/apps/client/src/widgets/collections/board/card.tsx @@ -0,0 +1,88 @@ +import { useCallback, useContext, useEffect, useRef } from "preact/hooks"; +import FBranch from "../../../entities/fbranch"; +import FNote from "../../../entities/fnote"; +import BoardApi from "./api"; +import { BoardViewContext } from "."; +import { ContextMenuEvent } from "../../../menus/context_menu"; +import { openNoteContextMenu } from "./context_menu"; +import FormTextBox from "../../react/FormTextBox"; + +export default function Card({ + api, + note, + branch, + column, + index, + setDraggedCard, + isDragging +}: { + api: BoardApi, + note: FNote, + branch: FBranch, + column: string, + index: number, + setDraggedCard: (card: { noteId: string, branchId: string, fromColumn: string, index: number } | null) => void, + isDragging: boolean +}) { + const { branchIdToEdit } = useContext(BoardViewContext); + const isEditing = branch.branchId === branchIdToEdit; + const colorClass = note.getColorClass() || ''; + const editorRef = useRef(null); + + const handleDragStart = useCallback((e: DragEvent) => { + e.dataTransfer!.effectAllowed = 'move'; + e.dataTransfer!.setData('text/plain', note.noteId); + setDraggedCard({ noteId: note.noteId, branchId: branch.branchId, fromColumn: column, index }); + }, [note.noteId, branch.branchId, column, index, setDraggedCard]); + + const handleDragEnd = useCallback(() => { + setDraggedCard(null); + }, [setDraggedCard]); + + const handleContextMenu = useCallback((e: ContextMenuEvent) => { + openNoteContextMenu(api, e, note.noteId, branch.branchId, column); + }, [ api, note, branch, column ]); + + useEffect(() => { + editorRef.current?.focus(); + }, [ isEditing ]); + + return ( +
+ + {!isEditing ? ( + <>{note.title} + ) : ( + { + if (e.key === "Enter") { + const newTitle = e.currentTarget.value; + if (newTitle !== note.title) { + api.renameCard(note.noteId, newTitle); + } + api.dismissEditingTitle(); + } + + if (e.key === "Escape") { + api.dismissEditingTitle(); + } + }} + onBlur={(newTitle) => { + if (newTitle !== note.title) { + api.renameCard(note.noteId, newTitle); + } + api.dismissEditingTitle(); + }} + /> + )} +
+ ) +} diff --git a/apps/client/src/widgets/collections/board/column.tsx b/apps/client/src/widgets/collections/board/column.tsx new file mode 100644 index 000000000..08da9c97b --- /dev/null +++ b/apps/client/src/widgets/collections/board/column.tsx @@ -0,0 +1,234 @@ +import { useCallback, useContext, useEffect, useRef } from "preact/hooks"; +import FBranch from "../../../entities/fbranch"; +import FNote from "../../../entities/fnote"; +import { BoardViewContext } from "."; +import branches from "../../../services/branches"; +import { openColumnContextMenu } from "./context_menu"; +import { ContextMenuEvent } from "../../../menus/context_menu"; +import FormTextBox from "../../react/FormTextBox"; +import Icon from "../../react/Icon"; +import { t } from "../../../services/i18n"; +import BoardApi from "./api"; +import Card from "./card"; + +export default function Column({ + column, + columnIndex, + columnItems, + statusAttribute, + draggedCard, + setDraggedCard, + dropTarget, + setDropTarget, + dropPosition, + setDropPosition, + onCardDrop, + draggedColumn, + setDraggedColumn, + isDraggingColumn, + api +}: { + column: string, + columnIndex: number, + columnItems?: { note: FNote, branch: FBranch }[], + statusAttribute: string, + draggedCard: { noteId: string, branchId: string, fromColumn: string, index: number } | null, + setDraggedCard: (card: { noteId: string, branchId: string, fromColumn: string, index: number } | null) => void, + dropTarget: string | null, + setDropTarget: (target: string | null) => void, + dropPosition: { column: string, index: number } | null, + setDropPosition: (position: { column: string, index: number } | null) => void, + onCardDrop: () => void, + draggedColumn: { column: string, index: number } | null, + setDraggedColumn: (column: { column: string, index: number } | null) => void, + isDraggingColumn: boolean, + api: BoardApi +}) { + const context = useContext(BoardViewContext); + const isEditing = (context.columnNameToEdit === column); + const editorRef = useRef(null); + + const handleColumnDragStart = useCallback((e: DragEvent) => { + e.dataTransfer!.effectAllowed = 'move'; + e.dataTransfer!.setData('text/plain', column); + setDraggedColumn({ column, index: columnIndex }); + e.stopPropagation(); // Prevent card drag from interfering + }, [column, columnIndex, setDraggedColumn]); + + const handleColumnDragEnd = useCallback(() => { + setDraggedColumn(null); + }, [setDraggedColumn]); + + const handleDragOver = useCallback((e: DragEvent) => { + if (draggedColumn) return; // Don't handle card drops when dragging columns + e.preventDefault(); + setDropTarget(column); + + // Calculate drop position based on mouse position + const cards = Array.from(e.currentTarget.querySelectorAll('.board-note')); + const mouseY = e.clientY; + + let newIndex = cards.length; + for (let i = 0; i < cards.length; i++) { + const card = cards[i] as HTMLElement; + const rect = card.getBoundingClientRect(); + const cardMiddle = rect.top + rect.height / 2; + + if (mouseY < cardMiddle) { + newIndex = i; + break; + } + } + + setDropPosition({ column, index: newIndex }); + }, [column, setDropTarget, setDropPosition]); + + const handleDragLeave = useCallback((e: DragEvent) => { + const relatedTarget = e.relatedTarget as HTMLElement; + const currentTarget = e.currentTarget as HTMLElement; + + if (!currentTarget.contains(relatedTarget)) { + setDropTarget(null); + setDropPosition(null); + } + }, [setDropTarget, setDropPosition]); + + const handleDrop = useCallback(async (e: DragEvent) => { + if (draggedColumn) return; // Don't handle card drops when dragging columns + e.preventDefault(); + setDropTarget(null); + setDropPosition(null); + + if (draggedCard && dropPosition) { + const targetIndex = dropPosition.index; + const targetItems = columnItems || []; + + if (draggedCard.fromColumn !== column) { + // Moving to a different column + await api.changeColumn(draggedCard.noteId, column); + + // If there are items in the target column, reorder + if (targetItems.length > 0 && targetIndex < targetItems.length) { + const targetBranch = targetItems[targetIndex].branch; + await branches.moveBeforeBranch([ draggedCard.branchId ], targetBranch.branchId); + } + } else if (draggedCard.index !== targetIndex) { + // Reordering within the same column + let targetBranchId: string | null = null; + + if (targetIndex < targetItems.length) { + // Moving before an existing item + const adjustedIndex = draggedCard.index < targetIndex ? targetIndex : targetIndex; + if (adjustedIndex < targetItems.length) { + targetBranchId = targetItems[adjustedIndex].branch.branchId; + await branches.moveBeforeBranch([ draggedCard.branchId ], targetBranchId); + } + } else if (targetIndex > 0) { + // Moving to the end - place after the last item + const lastItem = targetItems[targetItems.length - 1]; + await branches.moveAfterBranch([ draggedCard.branchId ], lastItem.branch.branchId); + } + } + + onCardDrop(); + } + setDraggedCard(null); + }, [draggedCard, draggedColumn, dropPosition, columnItems, column, statusAttribute, setDraggedCard, setDropTarget, setDropPosition, onCardDrop]); + + const handleEdit = useCallback(() => { + context.setColumnNameToEdit?.(column); + }, [column]); + + const handleContextMenu = useCallback((e: ContextMenuEvent) => { + openColumnContextMenu(api, e, column); + }, [ api, column ]); + + useEffect(() => { + editorRef.current?.focus(); + }, [ isEditing ]); + + return ( +
+

+ {!isEditing ? ( + <> + {column} + + + ) : ( + <> + { + if (e.key === "Enter") { + const newTitle = e.currentTarget.value; + if (newTitle !== column) { + api.renameColumn(column, newTitle); + } + context.setColumnNameToEdit?.(undefined); + } + + if (e.key === "Escape") { + context.setColumnNameToEdit?.(undefined); + } + }} + onBlur={(newTitle) => { + if (newTitle !== column) { + api.renameColumn(column, newTitle); + } + context.setColumnNameToEdit?.(undefined); + }} + /> + + )} +

+ + {(columnItems ?? []).map(({ note, branch }, index) => { + const showIndicatorBefore = dropPosition?.column === column && + dropPosition.index === index && + draggedCard?.noteId !== note.noteId; + + return ( + <> + {showIndicatorBefore && ( +
+ )} + + + ); + })} + {dropPosition?.column === column && dropPosition.index === (columnItems?.length ?? 0) && ( +
+ )} + +
api.createNewItem(column)}> + {" "} + {t("board_view.new-item")} +
+
+ ) +} diff --git a/apps/client/src/widgets/collections/board/index.tsx b/apps/client/src/widgets/collections/board/index.tsx index 2acbfe82d..e7b9a9322 100644 --- a/apps/client/src/widgets/collections/board/index.tsx +++ b/apps/client/src/widgets/collections/board/index.tsx @@ -1,19 +1,15 @@ -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { 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 FNote from "../../../entities/fnote"; -import FBranch from "../../../entities/fbranch"; import Icon from "../../react/Icon"; import { t } from "../../../services/i18n"; import Api from "./api"; import FormTextBox from "../../react/FormTextBox"; -import branchService from "../../../services/branches"; -import { openColumnContextMenu, openNoteContextMenu } from "./context_menu"; -import { ContextMenuEvent } from "../../../menus/context_menu"; import { createContext } from "preact"; import { onWheelHorizontalScroll } from "../../widget_utils"; +import Column from "./column"; export interface BoardViewData { columns?: BoardColumnData[]; @@ -29,7 +25,7 @@ interface BoardViewContextData { setColumnNameToEdit?: (column: string | undefined) => void; } -const BoardViewContext = createContext({}); +export const BoardViewContext = createContext({}); export default function BoardView({ note: parentNote, noteIds, viewConfig, saveConfig }: ViewModeProps) { const [ statusAttribute ] = useNoteLabelWithDefault(parentNote, "board:groupBy", "status"); @@ -186,308 +182,6 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC ) } -function Column({ - column, - columnIndex, - columnItems, - statusAttribute, - draggedCard, - setDraggedCard, - dropTarget, - setDropTarget, - dropPosition, - setDropPosition, - onCardDrop, - draggedColumn, - setDraggedColumn, - isDraggingColumn, - api -}: { - column: string, - columnIndex: number, - columnItems?: { note: FNote, branch: FBranch }[], - statusAttribute: string, - draggedCard: { noteId: string, branchId: string, fromColumn: string, index: number } | null, - setDraggedCard: (card: { noteId: string, branchId: string, fromColumn: string, index: number } | null) => void, - dropTarget: string | null, - setDropTarget: (target: string | null) => void, - dropPosition: { column: string, index: number } | null, - setDropPosition: (position: { column: string, index: number } | null) => void, - onCardDrop: () => void, - draggedColumn: { column: string, index: number } | null, - setDraggedColumn: (column: { column: string, index: number } | null) => void, - isDraggingColumn: boolean, - api: Api -}) { - const context = useContext(BoardViewContext); - const isEditing = (context.columnNameToEdit === column); - const editorRef = useRef(null); - - const handleColumnDragStart = useCallback((e: DragEvent) => { - e.dataTransfer!.effectAllowed = 'move'; - e.dataTransfer!.setData('text/plain', column); - setDraggedColumn({ column, index: columnIndex }); - e.stopPropagation(); // Prevent card drag from interfering - }, [column, columnIndex, setDraggedColumn]); - - const handleColumnDragEnd = useCallback(() => { - setDraggedColumn(null); - }, [setDraggedColumn]); - - const handleDragOver = useCallback((e: DragEvent) => { - if (draggedColumn) return; // Don't handle card drops when dragging columns - e.preventDefault(); - setDropTarget(column); - - // Calculate drop position based on mouse position - const cards = Array.from(e.currentTarget.querySelectorAll('.board-note')); - const mouseY = e.clientY; - - let newIndex = cards.length; - for (let i = 0; i < cards.length; i++) { - const card = cards[i] as HTMLElement; - const rect = card.getBoundingClientRect(); - const cardMiddle = rect.top + rect.height / 2; - - if (mouseY < cardMiddle) { - newIndex = i; - break; - } - } - - setDropPosition({ column, index: newIndex }); - }, [column, setDropTarget, setDropPosition]); - - const handleDragLeave = useCallback((e: DragEvent) => { - const relatedTarget = e.relatedTarget as HTMLElement; - const currentTarget = e.currentTarget as HTMLElement; - - if (!currentTarget.contains(relatedTarget)) { - setDropTarget(null); - setDropPosition(null); - } - }, [setDropTarget, setDropPosition]); - - const handleDrop = useCallback(async (e: DragEvent) => { - if (draggedColumn) return; // Don't handle card drops when dragging columns - e.preventDefault(); - setDropTarget(null); - setDropPosition(null); - - if (draggedCard && dropPosition) { - const targetIndex = dropPosition.index; - const targetItems = columnItems || []; - - if (draggedCard.fromColumn !== column) { - // Moving to a different column - await api.changeColumn(draggedCard.noteId, column); - - // If there are items in the target column, reorder - if (targetItems.length > 0 && targetIndex < targetItems.length) { - const targetBranch = targetItems[targetIndex].branch; - await branchService.moveBeforeBranch([ draggedCard.branchId ], targetBranch.branchId); - } - } else if (draggedCard.index !== targetIndex) { - // Reordering within the same column - let targetBranchId: string | null = null; - - if (targetIndex < targetItems.length) { - // Moving before an existing item - const adjustedIndex = draggedCard.index < targetIndex ? targetIndex : targetIndex; - if (adjustedIndex < targetItems.length) { - targetBranchId = targetItems[adjustedIndex].branch.branchId; - await branchService.moveBeforeBranch([ draggedCard.branchId ], targetBranchId); - } - } else if (targetIndex > 0) { - // Moving to the end - place after the last item - const lastItem = targetItems[targetItems.length - 1]; - await branchService.moveAfterBranch([ draggedCard.branchId ], lastItem.branch.branchId); - } - } - - onCardDrop(); - } - setDraggedCard(null); - }, [draggedCard, draggedColumn, dropPosition, columnItems, column, statusAttribute, setDraggedCard, setDropTarget, setDropPosition, onCardDrop]); - - const handleEdit = useCallback(() => { - context.setColumnNameToEdit?.(column); - }, [column]); - - const handleContextMenu = useCallback((e: ContextMenuEvent) => { - openColumnContextMenu(api, e, column); - }, [ api, column ]); - - useEffect(() => { - editorRef.current?.focus(); - }, [ isEditing ]); - - return ( -
-

- {!isEditing ? ( - <> - {column} - - - ) : ( - <> - { - if (e.key === "Enter") { - const newTitle = e.currentTarget.value; - if (newTitle !== column) { - api.renameColumn(column, newTitle); - } - context.setColumnNameToEdit?.(undefined); - } - - if (e.key === "Escape") { - context.setColumnNameToEdit?.(undefined); - } - }} - onBlur={(newTitle) => { - if (newTitle !== column) { - api.renameColumn(column, newTitle); - } - context.setColumnNameToEdit?.(undefined); - }} - /> - - )} -

- - {(columnItems ?? []).map(({ note, branch }, index) => { - const showIndicatorBefore = dropPosition?.column === column && - dropPosition.index === index && - draggedCard?.noteId !== note.noteId; - - return ( - <> - {showIndicatorBefore && ( -
- )} - - - ); - })} - {dropPosition?.column === column && dropPosition.index === (columnItems?.length ?? 0) && ( -
- )} - -
api.createNewItem(column)}> - {" "} - {t("board_view.new-item")} -
-
- ) -} - -function Card({ - api, - note, - branch, - column, - index, - setDraggedCard, - isDragging -}: { - api: Api, - note: FNote, - branch: FBranch, - column: string, - index: number, - setDraggedCard: (card: { noteId: string, branchId: string, fromColumn: string, index: number } | null) => void, - isDragging: boolean -}) { - const { branchIdToEdit } = useContext(BoardViewContext); - const isEditing = branch.branchId === branchIdToEdit; - const colorClass = note.getColorClass() || ''; - const editorRef = useRef(null); - - const handleDragStart = useCallback((e: DragEvent) => { - e.dataTransfer!.effectAllowed = 'move'; - e.dataTransfer!.setData('text/plain', note.noteId); - setDraggedCard({ noteId: note.noteId, branchId: branch.branchId, fromColumn: column, index }); - }, [note.noteId, branch.branchId, column, index, setDraggedCard]); - - const handleDragEnd = useCallback(() => { - setDraggedCard(null); - }, [setDraggedCard]); - - const handleContextMenu = useCallback((e: ContextMenuEvent) => { - openNoteContextMenu(api, e, note.noteId, branch.branchId, column); - }, [ api, note, branch, column ]); - - useEffect(() => { - editorRef.current?.focus(); - }, [ isEditing ]); - - return ( -
- - {!isEditing ? ( - <>{note.title} - ) : ( - { - if (e.key === "Enter") { - const newTitle = e.currentTarget.value; - if (newTitle !== note.title) { - api.renameCard(note.noteId, newTitle); - } - api.dismissEditingTitle(); - } - - if (e.key === "Escape") { - api.dismissEditingTitle(); - } - }} - onBlur={(newTitle) => { - if (newTitle !== note.title) { - api.renameCard(note.noteId, newTitle); - } - api.dismissEditingTitle(); - }} - /> - )} -
- ) -} - function AddNewColumn({ viewConfig, saveConfig }: { viewConfig?: BoardViewData, saveConfig: (data: BoardViewData) => void }) { const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false); const columnNameRef = useRef(null);