feat(board/relation): basic support for editing relations in columns

This commit is contained in:
Elian Doran 2025-11-15 11:11:42 +02:00
parent d1e80815d5
commit 092a84693f
No known key found for this signature in database
4 changed files with 69 additions and 39 deletions

View File

@ -119,7 +119,7 @@ export default function Card({
setTitle(newTitle); setTitle(newTitle);
}} }}
dismiss={() => api.dismissEditingTitle()} dismiss={() => api.dismissEditingTitle()}
multiline mode="multiline"
/> />
)} )}
</div> </div>

View File

@ -124,6 +124,7 @@ export default function Column({
currentValue={column} currentValue={column}
save={newTitle => api.renameColumn(column, newTitle)} save={newTitle => api.renameColumn(column, newTitle)}
dismiss={() => setColumnNameToEdit?.(undefined)} dismiss={() => setColumnNameToEdit?.(undefined)}
mode={isInRelationMode ? "relation" : "normal"}
/> />
)} )}
</h3> </h3>
@ -187,7 +188,7 @@ function AddNewItem({ column, api }: { column: string, api: BoardApi }) {
placeholder={t("board_view.new-item-placeholder")} placeholder={t("board_view.new-item-placeholder")}
save={(title) => api.createNewItem(column, title)} save={(title) => api.createNewItem(column, title)}
dismiss={() => setIsCreatingNewItem(false)} dismiss={() => setIsCreatingNewItem(false)}
multiline isNewItem mode="multiline" isNewItem
/> />
)} )}
</div> </div>

View File

@ -13,6 +13,7 @@ import Column from "./column";
import BoardApi from "./api"; import BoardApi from "./api";
import FormTextArea from "../../react/FormTextArea"; import FormTextArea from "../../react/FormTextArea";
import FNote from "../../../entities/fnote"; import FNote from "../../../entities/fnote";
import NoteAutocomplete from "../../react/NoteAutocomplete";
export interface BoardViewData { export interface BoardViewData {
columns?: BoardColumnData[]; columns?: BoardColumnData[];
@ -188,14 +189,14 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
<div className="column-drop-placeholder show" /> <div className="column-drop-placeholder show" />
)} )}
<AddNewColumn api={api} /> <AddNewColumn api={api} isInRelationMode={isInRelationMode} />
</div> </div>
</BoardViewContext.Provider> </BoardViewContext.Provider>
</div> </div>
) )
} }
function AddNewColumn({ api }: { api: BoardApi }) { function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMode: boolean }) {
const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false); const [ isCreatingNewColumn, setIsCreatingNewColumn ] = useState(false);
const addColumnCallback = useCallback(() => { const addColumnCallback = useCallback(() => {
@ -215,19 +216,20 @@ function AddNewColumn({ api }: { api: BoardApi }) {
save={(columnName) => api.addNewColumn(columnName)} save={(columnName) => api.addNewColumn(columnName)}
dismiss={() => setIsCreatingNewColumn(false)} dismiss={() => setIsCreatingNewColumn(false)}
isNewItem isNewItem
mode={isInRelationMode ? "relation" : "normal"}
/> />
)} )}
</div> </div>
) )
} }
export function TitleEditor({ currentValue, placeholder, save, dismiss, multiline, isNewItem }: { export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
currentValue?: string; currentValue?: string;
placeholder?: string; placeholder?: string;
save: (newValue: string) => void; save: (newValue: string) => void;
dismiss: () => void; dismiss: () => void;
multiline?: boolean;
isNewItem?: boolean; isNewItem?: boolean;
mode?: "normal" | "multiline" | "relation";
}) { }) {
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
const focusElRef = useRef<Element>(null); const focusElRef = useRef<Element>(null);
@ -240,8 +242,6 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, multilin
inputRef.current?.select(); inputRef.current?.select();
}, [ inputRef ]); }, [ inputRef ]);
const Element = multiline ? FormTextArea : FormTextBox;
useEffect(() => { useEffect(() => {
if (dismissOnNextRefreshRef.current) { if (dismissOnNextRefreshRef.current) {
dismiss(); dismiss();
@ -249,31 +249,52 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, multilin
} }
}); });
return ( const onKeyDown = (e: TargetedKeyboardEvent<HTMLInputElement | HTMLTextAreaElement> | KeyboardEvent) => {
<Element if (e.key === "Enter" || e.key === "Escape") {
inputRef={inputRef} e.preventDefault();
currentValue={currentValue ?? ""} e.stopPropagation();
placeholder={placeholder} shouldDismiss.current = (e.key === "Escape");
autoComplete="trilium-title-entry" // forces the auto-fill off better than the "off" value. if (focusElRef.current instanceof HTMLElement) {
rows={multiline ? 4 : undefined} focusElRef.current.focus();
onKeyDown={(e: TargetedKeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => { }
if (e.key === "Enter" || e.key === "Escape") { }
e.preventDefault(); };
e.stopPropagation();
shouldDismiss.current = (e.key === "Escape"); const onBlur = (newValue) => {
if (focusElRef.current instanceof HTMLElement) { if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) {
focusElRef.current.focus(); save(newValue);
} dismissOnNextRefreshRef.current = true;
} } else {
}} dismiss();
onBlur={(newValue) => { }
if (!shouldDismiss.current && newValue.trim() && (newValue !== currentValue || isNewItem)) { };
save(newValue);
dismissOnNextRefreshRef.current = true; if (mode !== "relation") {
} else { const Element = mode === "multiline" ? FormTextArea : FormTextBox;
dismiss();
} return (
}} <Element
/> inputRef={inputRef}
); currentValue={currentValue ?? ""}
placeholder={placeholder}
autoComplete="trilium-title-entry" // forces the auto-fill off better than the "off" value.
rows={mode === "multiline" ? 4 : undefined}
onKeyDown={onKeyDown}
onBlur={onBlur}
/>
);
} else {
return (
<NoteAutocomplete
inputRef={inputRef}
noteId={currentValue ?? ""}
opts={{
hideAllButtons: true,
allowCreatingNotes: true
}}
onKeyDown={onKeyDown}
onBlur={onBlur}
/>
)
}
} }

View File

@ -5,7 +5,7 @@ import type { RefObject } from "preact";
import type { CSSProperties } from "preact/compat"; import type { CSSProperties } from "preact/compat";
import { useSyncedRef } from "./hooks"; import { useSyncedRef } from "./hooks";
interface NoteAutocompleteProps { interface NoteAutocompleteProps {
id?: string; id?: string;
inputRef?: RefObject<HTMLInputElement>; inputRef?: RefObject<HTMLInputElement>;
text?: string; text?: string;
@ -15,13 +15,15 @@ interface NoteAutocompleteProps {
opts?: Omit<Options, "container">; opts?: Omit<Options, "container">;
onChange?: (suggestion: Suggestion | null) => void; onChange?: (suggestion: Suggestion | null) => void;
onTextChange?: (text: string) => void; onTextChange?: (text: string) => void;
onKeyDown?: (e: KeyboardEvent) => void;
onBlur?: (newValue: string) => void;
noteIdChanged?: (noteId: string) => void; noteIdChanged?: (noteId: string) => void;
noteId?: string; 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<HTMLInputElement>(externalInputRef); const ref = useSyncedRef<HTMLInputElement>(externalInputRef);
useEffect(() => { useEffect(() => {
if (!ref.current) return; if (!ref.current) return;
const $autoComplete = $(ref.current); const $autoComplete = $(ref.current);
@ -57,6 +59,12 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
if (onTextChange) { if (onTextChange) {
$autoComplete.on("input", () => onTextChange($autoComplete[0].value)); $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]); }, [opts, container?.current]);
useEffect(() => { useEffect(() => {
@ -81,4 +89,4 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
placeholder={placeholder ?? t("add_link.search_note")} /> placeholder={placeholder ?? t("add_link.search_note")} />
</div> </div>
); );
} }