mirror of
https://github.com/zadam/trilium.git
synced 2025-11-30 12:34:24 +01:00
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Documentation / Build and Deploy Documentation (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run
238 lines
9.5 KiB
TypeScript
238 lines
9.5 KiB
TypeScript
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
|
|
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
|
|
import FNote from "../../entities/fnote";
|
|
import "./NoteList.css";
|
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
import ViewModeStorage from "./view_mode_storage";
|
|
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
|
|
import { WebSocketMessage } from "@triliumnext/commons";
|
|
import froca from "../../services/froca";
|
|
import { lazy, Suspense } from "preact/compat";
|
|
import { VNode } from "preact";
|
|
interface NoteListProps {
|
|
note: FNote | null | undefined;
|
|
notePath: string | null | undefined;
|
|
highlightedTokens?: string[] | null;
|
|
/** if set to `true` then only collection-type views are displayed such as geo-map and the calendar. The original book types grid and list will be ignored. */
|
|
displayOnlyCollections?: boolean;
|
|
isEnabled: boolean;
|
|
ntxId: string | null | undefined;
|
|
media: ViewModeMedia;
|
|
viewType: ViewTypeOptions | undefined;
|
|
onReady?: () => void;
|
|
onProgressChanged?(progress: number): void;
|
|
}
|
|
|
|
type LazyLoadedComponent = ((props: ViewModeProps<any>) => VNode<any> | undefined);
|
|
const ViewComponents: Record<ViewTypeOptions, { normal: LazyLoadedComponent, print?: LazyLoadedComponent }> = {
|
|
list: {
|
|
normal: lazy(() => import("./legacy/ListOrGridView.js").then(i => i.ListView)),
|
|
print: lazy(() => import("./legacy/ListPrintView.js").then(i => i.ListPrintView))
|
|
},
|
|
grid: {
|
|
normal: lazy(() => import("./legacy/ListOrGridView.js").then(i => i.GridView)),
|
|
},
|
|
geoMap: {
|
|
normal: lazy(() => import("./geomap/index.js")),
|
|
},
|
|
calendar: {
|
|
normal: lazy(() => import("./calendar/index.js"))
|
|
},
|
|
table: {
|
|
normal: lazy(() => import("./table/index.js")),
|
|
print: lazy(() => import("./table/TablePrintView.js"))
|
|
},
|
|
board: {
|
|
normal: lazy(() => import("./board/index.js"))
|
|
},
|
|
presentation: {
|
|
normal: lazy(() => import("./presentation/index.js"))
|
|
}
|
|
}
|
|
|
|
export default function NoteList(props: Pick<NoteListProps, "displayOnlyCollections" | "media" | "onReady" | "onProgressChanged">) {
|
|
const { note, noteContext, notePath, ntxId, viewScope } = useNoteContext();
|
|
const viewType = useNoteViewType(note);
|
|
const [ enabled, setEnabled ] = useState(noteContext?.hasNoteList());
|
|
useEffect(() => {
|
|
setEnabled(noteContext?.hasNoteList());
|
|
}, [ note, noteContext, viewType, viewScope?.viewMode ])
|
|
return <CustomNoteList viewType={viewType} note={note} isEnabled={!!enabled} notePath={notePath} ntxId={ntxId} {...props} />
|
|
}
|
|
|
|
export function SearchNoteList(props: Omit<NoteListProps, "isEnabled" | "viewType">) {
|
|
const viewType = useNoteViewType(props.note);
|
|
return <CustomNoteList {...props} isEnabled={true} viewType={viewType} />
|
|
}
|
|
|
|
export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId, onReady, onProgressChanged, ...restProps }: NoteListProps) {
|
|
const widgetRef = useRef<HTMLDivElement>(null);
|
|
const noteIds = useNoteIds(shouldEnable ? note : null, viewType, ntxId);
|
|
const isFullHeight = (viewType && viewType !== "list" && viewType !== "grid");
|
|
const [ isIntersecting, setIsIntersecting ] = useState(false);
|
|
const shouldRender = (isFullHeight || isIntersecting || note?.type === "book");
|
|
const isEnabled = (note && shouldEnable && !!viewType && shouldRender);
|
|
|
|
useEffect(() => {
|
|
if (isFullHeight || displayOnlyCollections || note?.type === "book") {
|
|
// Double role: no need to check if the note list is visible if the view is full-height or book, but also prevent legacy views if `displayOnlyCollections` is true.
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (!isIntersecting) {
|
|
setIsIntersecting(entries[0].isIntersecting);
|
|
observer.disconnect();
|
|
}
|
|
},
|
|
{
|
|
rootMargin: "50px",
|
|
threshold: 0.1
|
|
}
|
|
);
|
|
|
|
// there seems to be a race condition on Firefox which triggers the observer only before the widget is visible
|
|
// (intersection is false). https://github.com/zadam/trilium/issues/4165
|
|
setTimeout(() => widgetRef.current && observer.observe(widgetRef.current), 10);
|
|
return () => observer.disconnect();
|
|
}, [ widgetRef, isFullHeight, displayOnlyCollections, note ]);
|
|
|
|
// Preload the configuration.
|
|
let props: ViewModeProps<any> | undefined | null = null;
|
|
const viewModeConfig = useViewModeConfig(note, viewType);
|
|
if (note && notePath && viewModeConfig) {
|
|
props = {
|
|
note, noteIds, notePath,
|
|
highlightedTokens,
|
|
viewConfig: viewModeConfig.config,
|
|
saveConfig: viewModeConfig.storeFn,
|
|
onReady: onReady ?? (() => {}),
|
|
onProgressChanged: onProgressChanged ?? (() => {}),
|
|
|
|
...restProps
|
|
}
|
|
}
|
|
|
|
const ComponentToRender = viewType && props && isEnabled && (
|
|
props.media === "print" ? ViewComponents[viewType].print : ViewComponents[viewType].normal
|
|
);
|
|
|
|
return (
|
|
<div ref={widgetRef} className={`note-list-widget component ${isFullHeight && isEnabled ? "full-height" : ""}`}>
|
|
{ComponentToRender && props && (
|
|
<div className="note-list-widget-content">
|
|
<Suspense fallback="">
|
|
<ComponentToRender {...props} />
|
|
</Suspense>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function useNoteViewType(note?: FNote | null): ViewTypeOptions | undefined {
|
|
const [ viewType ] = useNoteLabel(note, "viewType");
|
|
|
|
if (!note) {
|
|
return undefined;
|
|
} else if (!(allViewTypes as readonly string[]).includes(viewType || "")) {
|
|
// when not explicitly set, decide based on the note type
|
|
return note.type === "search" ? "list" : "grid";
|
|
} else {
|
|
return viewType as ViewTypeOptions;
|
|
}
|
|
}
|
|
|
|
export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) {
|
|
const [ noteIds, setNoteIds ] = useState<string[]>([]);
|
|
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
|
|
const directChildrenOnly = (viewType === "list" || viewType === "grid" || viewType === "table" || note?.type === "search");
|
|
|
|
async function refreshNoteIds() {
|
|
if (!note) {
|
|
setNoteIds([]);
|
|
} else {
|
|
setNoteIds(await getNoteIds(note));
|
|
}
|
|
}
|
|
|
|
async function getNoteIds(note: FNote) {
|
|
if (directChildrenOnly) {
|
|
return await note.getChildNoteIdsWithArchiveFiltering(includeArchived);
|
|
} else {
|
|
return await note.getSubtreeNoteIds(includeArchived);
|
|
}
|
|
}
|
|
|
|
// Refresh on note switch.
|
|
useEffect(() => {
|
|
refreshNoteIds()
|
|
}, [ note, includeArchived, directChildrenOnly ]);
|
|
|
|
// Refresh on alterations to the note subtree.
|
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
|
if (note && loadResults.getBranchRows().some(branch =>
|
|
branch.parentNoteId === note.noteId
|
|
|| noteIds.includes(branch.parentNoteId ?? ""))
|
|
|| loadResults.getAttributeRows().some(attr => attr.name === "archived" && attr.noteId && noteIds.includes(attr.noteId))
|
|
) {
|
|
refreshNoteIds();
|
|
}
|
|
})
|
|
|
|
// Refresh on search.
|
|
useTriliumEvent("searchRefreshed", ({ ntxId: eventNtxId }) => {
|
|
if (eventNtxId !== ntxId) return;
|
|
refreshNoteIds();
|
|
});
|
|
|
|
// Refresh on import.
|
|
useEffect(() => {
|
|
async function onImport(message: WebSocketMessage) {
|
|
if (!("taskType" in message) || message.taskType !== "importNotes" || message.type !== "taskSucceeded") return;
|
|
const { parentNoteId, importedNoteId } = message.result;
|
|
if (!parentNoteId || !importedNoteId) return;
|
|
if (importedNoteId && (parentNoteId === note?.noteId || noteIds.includes(parentNoteId))) {
|
|
const importedNote = await froca.getNote(importedNoteId);
|
|
if (!importedNote) return;
|
|
setNoteIds([
|
|
...noteIds,
|
|
...await getNoteIds(importedNote),
|
|
importedNoteId
|
|
])
|
|
}
|
|
}
|
|
|
|
subscribeToMessages(onImport);
|
|
return () => unsubscribeFromMessage(onImport);
|
|
}, [ note, noteIds, setNoteIds ])
|
|
|
|
return noteIds;
|
|
}
|
|
|
|
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
|
|
const [ viewConfig, setViewConfig ] = useState<{
|
|
config: T | undefined;
|
|
storeFn: (data: T) => void;
|
|
note: FNote;
|
|
}>();
|
|
|
|
useEffect(() => {
|
|
if (!note || !viewType) return;
|
|
setViewConfig(undefined);
|
|
const viewStorage = new ViewModeStorage<T>(note, viewType);
|
|
viewStorage.restore().then(config => {
|
|
const storeFn = (config: T) => {
|
|
setViewConfig({ note, config, storeFn });
|
|
viewStorage.store(config);
|
|
};
|
|
setViewConfig({ note, config, storeFn });
|
|
});
|
|
}, [ note, viewType ]);
|
|
|
|
// Only expose config for the current note, avoid leaking notes when switching between them.
|
|
if (viewConfig?.note !== note) return undefined;
|
|
return viewConfig;
|
|
}
|