From 092a84693fbaa1a6c8be9466d6febd9120eeae4e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 15 Nov 2025 11:11:42 +0200 Subject: [PATCH] feat(board/relation): basic support for editing relations in columns --- .../src/widgets/collections/board/card.tsx | 2 +- .../src/widgets/collections/board/column.tsx | 3 +- .../src/widgets/collections/board/index.tsx | 87 ++++++++++++------- .../src/widgets/react/NoteAutocomplete.tsx | 16 +++- 4 files changed, 69 insertions(+), 39 deletions(-) diff --git a/apps/client/src/widgets/collections/board/card.tsx b/apps/client/src/widgets/collections/board/card.tsx index fc81e9f0e..0ee92a11d 100644 --- a/apps/client/src/widgets/collections/board/card.tsx +++ b/apps/client/src/widgets/collections/board/card.tsx @@ -119,7 +119,7 @@ export default function Card({ setTitle(newTitle); }} dismiss={() => api.dismissEditingTitle()} - multiline + mode="multiline" /> )} diff --git a/apps/client/src/widgets/collections/board/column.tsx b/apps/client/src/widgets/collections/board/column.tsx index 791eb585e..f014b67bf 100644 --- a/apps/client/src/widgets/collections/board/column.tsx +++ b/apps/client/src/widgets/collections/board/column.tsx @@ -124,6 +124,7 @@ export default function Column({ currentValue={column} save={newTitle => api.renameColumn(column, newTitle)} dismiss={() => setColumnNameToEdit?.(undefined)} + mode={isInRelationMode ? "relation" : "normal"} /> )} @@ -187,7 +188,7 @@ function AddNewItem({ column, api }: { column: string, api: BoardApi }) { placeholder={t("board_view.new-item-placeholder")} save={(title) => api.createNewItem(column, title)} dismiss={() => setIsCreatingNewItem(false)} - multiline isNewItem + mode="multiline" isNewItem /> )} diff --git a/apps/client/src/widgets/collections/board/index.tsx b/apps/client/src/widgets/collections/board/index.tsx index effcb7fda..97b9632bf 100644 --- a/apps/client/src/widgets/collections/board/index.tsx +++ b/apps/client/src/widgets/collections/board/index.tsx @@ -13,6 +13,7 @@ import Column from "./column"; import BoardApi from "./api"; import FormTextArea from "../../react/FormTextArea"; import FNote from "../../../entities/fnote"; +import NoteAutocomplete from "../../react/NoteAutocomplete"; export interface BoardViewData { columns?: BoardColumnData[]; @@ -188,14 +189,14 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
)} - +
) } -function AddNewColumn({ api }: { api: BoardApi }) { +function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMode: boolean }) { const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false); const addColumnCallback = useCallback(() => { @@ -215,19 +216,20 @@ function AddNewColumn({ api }: { api: BoardApi }) { save={(columnName) => api.addNewColumn(columnName)} dismiss={() => setIsCreatingNewColumn(false)} isNewItem + mode={isInRelationMode ? "relation" : "normal"} /> )} ) } -export function TitleEditor({ currentValue, placeholder, save, dismiss, multiline, isNewItem }: { +export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: { currentValue?: string; placeholder?: string; save: (newValue: string) => void; dismiss: () => void; - multiline?: boolean; isNewItem?: boolean; + mode?: "normal" | "multiline" | "relation"; }) { const inputRef = useRef(null); const focusElRef = useRef(null); @@ -240,8 +242,6 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, multilin inputRef.current?.select(); }, [ inputRef ]); - const Element = multiline ? FormTextArea : FormTextBox; - useEffect(() => { if (dismissOnNextRefreshRef.current) { dismiss(); @@ -249,31 +249,52 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, multilin } }); - return ( - ) => { - if (e.key === "Enter" || e.key === "Escape") { - e.preventDefault(); - e.stopPropagation(); - shouldDismiss.current = (e.key === "Escape"); - if (focusElRef.current instanceof HTMLElement) { - focusElRef.current.focus(); - } - } - }} - onBlur={(newValue) => { - if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) { - save(newValue); - dismissOnNextRefreshRef.current = true; - } else { - dismiss(); - } - }} - /> - ); + const onKeyDown = (e: TargetedKeyboardEvent | KeyboardEvent) => { + if (e.key === "Enter" || e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + shouldDismiss.current = (e.key === "Escape"); + if (focusElRef.current instanceof HTMLElement) { + focusElRef.current.focus(); + } + } + }; + + const onBlur = (newValue) => { + if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) { + save(newValue); + dismissOnNextRefreshRef.current = true; + } else { + dismiss(); + } + }; + + if (mode !== "relation") { + const Element = mode === "multiline" ? FormTextArea : FormTextBox; + + return ( + + ); + } else { + return ( + + ) + } } diff --git a/apps/client/src/widgets/react/NoteAutocomplete.tsx b/apps/client/src/widgets/react/NoteAutocomplete.tsx index 223dbb5d4..198e69695 100644 --- a/apps/client/src/widgets/react/NoteAutocomplete.tsx +++ b/apps/client/src/widgets/react/NoteAutocomplete.tsx @@ -5,7 +5,7 @@ import type { RefObject } from "preact"; import type { CSSProperties } from "preact/compat"; import { useSyncedRef } from "./hooks"; -interface NoteAutocompleteProps { +interface NoteAutocompleteProps { id?: string; inputRef?: RefObject; text?: string; @@ -15,13 +15,15 @@ interface NoteAutocompleteProps { opts?: Omit; onChange?: (suggestion: Suggestion | null) => void; onTextChange?: (text: string) => void; + onKeyDown?: (e: KeyboardEvent) => void; + onBlur?: (newValue: string) => void; noteIdChanged?: (noteId: string) => void; noteId?: string; } -export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) { +export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged, onKeyDown, onBlur }: NoteAutocompleteProps) { const ref = useSyncedRef(externalInputRef); - + useEffect(() => { if (!ref.current) return; const $autoComplete = $(ref.current); @@ -57,6 +59,12 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, if (onTextChange) { $autoComplete.on("input", () => onTextChange($autoComplete[0].value)); } + if (onKeyDown) { + $autoComplete.on("keydown", (e) => e.originalEvent && onKeyDown(e.originalEvent)); + } + if (onBlur) { + $autoComplete.on("blur", () => onBlur($autoComplete.getSelectedNoteId() ?? "")); + } }, [opts, container?.current]); useEffect(() => { @@ -81,4 +89,4 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder={placeholder ?? t("add_link.search_note")} /> ); -} \ No newline at end of file +}