feat(print): report ignored notes

This commit is contained in:
Elian Doran 2025-12-24 18:42:13 +02:00
parent 84c40eb233
commit 60866c959f
No known key found for this signature in database
7 changed files with 95 additions and 51 deletions

View File

@ -9,10 +9,17 @@ import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList"
interface RendererProps {
note: FNote;
onReady: () => void;
onReady: (data: PrintReport) => void;
onProgressChanged?: (progress: number) => void;
}
export type PrintReport = {
type: "single-note";
} | {
type: "collection";
ignoredNoteIds: string[];
};
async function main() {
const notePath = window.location.hash.substring(1);
const noteId = notePath.split("/").at(-1);
@ -35,9 +42,11 @@ function App({ note, noteId }: { note: FNote | null | undefined, noteId: string
window.dispatchEvent(new CustomEvent("note-load-progress", { detail: { progress } }));
}
}, []);
const onReady = useCallback(() => {
const onReady = useCallback((detail: PrintReport) => {
if (sentReadyEvent.current) return;
window.dispatchEvent(new Event("note-ready"));
window.dispatchEvent(new CustomEvent("note-ready", {
detail
}));
window._noteReady = true;
sentReadyEvent.current = true;
}, []);
@ -92,7 +101,7 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
await loadCustomCss(note);
}
load().then(() => requestAnimationFrame(onReady));
load().then(() => requestAnimationFrame(() => onReady({})));
}, [ note ]);
return <>
@ -111,9 +120,9 @@ function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps)
ntxId="print"
highlightedTokens={null}
media="print"
onReady={async () => {
onReady={async (data: PrintReport) => {
await loadCustomCss(note);
onReady();
onReady(data);
}}
onProgressChanged={onProgressChanged}
/>;

View File

@ -1770,7 +1770,10 @@
"note_detail": {
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'",
"printing": "Printing in progress...",
"printing_pdf": "Exporting to PDF in progress..."
"printing_pdf": "Exporting to PDF in progress...",
"print_report_title": "Print report",
"print_report_collection_content_one": "{{count}} note in the collection could not be printed because they are not supported or they are protected.",
"print_report_collection_content_other": "{{count}} notes in the collection could not be printed because they are not supported or they are protected."
},
"note_title": {
"placeholder": "type note's title here...",

View File

@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../components/note_context";
import FNote from "../entities/fnote";
import type { PrintReport } from "../print";
import attributes from "../services/attributes";
import { t } from "../services/i18n";
import protected_session_holder from "../services/protected_session_holder";
@ -179,8 +180,21 @@ export default function NoteDetail() {
showToast("printing", e.detail.progress);
});
iframe.contentWindow.addEventListener("note-ready", () => {
iframe.contentWindow.addEventListener("note-ready", (e) => {
toast.closePersistent("printing");
if ("detail" in e) {
const printReport = e.detail as PrintReport;
if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
toast.showPersistent({
id: "print-report",
icon: "bx bx-collection",
title: t("note_detail.print_report_title"),
message: t("note_detail.print_report_collection_content", { count: printReport.ignoredNoteIds.length })
});
}
}
iframe.contentWindow?.print();
document.body.removeChild(iframe);
});

View File

@ -1,14 +1,17 @@
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, 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";
import { lazy, Suspense } from "preact/compat";
import { useEffect, useRef, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import type { PrintReport } from "../../print";
import froca from "../../services/froca";
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
import ViewModeStorage from "./view_mode_storage";
interface NoteListProps {
note: FNote | null | undefined;
notePath: string | null | undefined;
@ -19,7 +22,7 @@ interface NoteListProps {
ntxId: string | null | undefined;
media: ViewModeMedia;
viewType: ViewTypeOptions | undefined;
onReady?: () => void;
onReady?: (data: PrintReport) => void;
onProgressChanged?(progress: number): void;
}
@ -48,7 +51,7 @@ const ViewComponents: Record<ViewTypeOptions, { normal: LazyLoadedComponent, pri
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();
@ -57,13 +60,13 @@ export default function NoteList(props: Pick<NoteListProps, "displayOnlyCollecti
const [ enabled, setEnabled ] = useState(noteContext?.hasNoteList());
useEffect(() => {
setEnabled(noteContext?.hasNoteList());
}, [ note, noteContext, viewType, viewScope?.viewMode, noteType ])
return <CustomNoteList viewType={viewType} note={note} isEnabled={!!enabled} notePath={notePath} ntxId={ntxId} {...props} />
}, [ note, noteContext, viewType, viewScope?.viewMode, noteType ]);
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} />
return <CustomNoteList {...props} isEnabled={true} viewType={viewType} />;
}
export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId, onReady, onProgressChanged, ...restProps }: NoteListProps) {
@ -112,7 +115,7 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
onProgressChanged: onProgressChanged ?? (() => {}),
...restProps
}
};
}
const ComponentToRender = viewType && props && isEnabled && (
@ -140,9 +143,9 @@ export function useNoteViewType(note?: FNote | null): ViewTypeOptions | undefine
} 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;
}
return viewType as ViewTypeOptions;
}
export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) {
@ -161,26 +164,26 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
async function getNoteIds(note: FNote) {
if (directChildrenOnly) {
return await note.getChildNoteIdsWithArchiveFiltering(includeArchived);
} else {
return await note.getSubtreeNoteIds(includeArchived);
}
return await note.getSubtreeNoteIds(includeArchived);
}
// Refresh on note switch.
useEffect(() => {
refreshNoteIds()
refreshNoteIds();
}, [ note, includeArchived, directChildrenOnly ]);
// Refresh on alterations to the note subtree.
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().some(branch =>
branch.parentNoteId === note.noteId
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 }) => {
@ -201,13 +204,13 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
...noteIds,
...await getNoteIds(importedNote),
importedNoteId
])
]);
}
}
subscribeToMessages(onImport);
return () => unsubscribeFromMessage(onImport);
}, [ note, noteIds, setNoteIds ])
}, [ note, noteIds, setNoteIds ]);
return noteIds;
}

View File

@ -1,4 +1,5 @@
import FNote from "../../entities/fnote";
import type { PrintReport } from "../../print";
export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board", "presentation"] as const;
export type ViewTypeOptions = typeof allViewTypes[number];
@ -18,6 +19,6 @@ export interface ViewModeProps<T extends object> {
viewConfig: T | undefined;
saveConfig(newConfig: T): void;
media: ViewModeMedia;
onReady(): void;
onReady(data: PrintReport): void;
onProgressChanged?: ProgressChangedFn;
}

View File

@ -1,6 +1,7 @@
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
import type FNote from "../../../entities/fnote";
import type { PrintReport } from "../../../print";
import content_renderer from "../../../services/content_renderer";
import froca from "../../../services/froca";
import type { ViewModeProps } from "../interface";
@ -13,13 +14,17 @@ interface NotesWithContent {
export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady, onProgressChanged }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const [ notesWithContent, setNotesWithContent ] = useState<NotesWithContent[]>();
const [ state, setState ] = useState<{
notesWithContent?: NotesWithContent[],
data?: PrintReport
}>({});
useLayoutEffect(() => {
const noteIdsSet = new Set<string>();
froca.getNotes(noteIds).then(async (notes) => {
const noteIdsWithChildren = await note.getSubtreeNoteIds(true);
const ignoredNoteIds: string[] = [];
const notesWithContent: NotesWithContent[] = [];
async function processNote(note: FNote, depth: number) {
@ -35,6 +40,8 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady, onPro
rewriteHeadings(contentEl, depth);
noteIdsSet.add(note.noteId);
notesWithContent.push({ note, contentEl });
} else {
ignoredNoteIds.push(note.noteId);
}
if (onProgressChanged) {
@ -58,22 +65,28 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady, onPro
rewriteLinks(contentEl, noteIdsSet);
}
setNotesWithContent(notesWithContent);
setState({
notesWithContent,
data: {
type: "collection",
ignoredNoteIds
}
});
});
}, [noteIds]);
useEffect(() => {
if (notesWithContent && onReady) {
onReady();
if (onReady && state?.data) {
onReady(state.data);
}
}, [ notesWithContent, onReady ]);
}, [ state, onReady ]);
return (
<div class="note-list list-print-view">
<div class="note-list-container use-tn-links">
<h1>{note.title}</h1>
{notesWithContent?.map(({ note: childNote, contentEl }) => (
{state.notesWithContent?.map(({ note: childNote, contentEl }) => (
<section id={`note-${childNote.noteId}`} class="note" dangerouslySetInnerHTML={{ __html: contentEl.innerHTML }} />
))}
</div>

View File

@ -1,17 +1,18 @@
import type { App, BrowserWindow, BrowserWindowConstructorOptions, IpcMainEvent,WebContents } from "electron";
import electron, { ipcMain } from "electron";
import fs from "fs/promises";
import { t } from "i18next";
import path from "path";
import url from "url";
import port from "./port.js";
import optionService from "./options.js";
import log from "./log.js";
import sqlInit from "./sql_init.js";
import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js";
import electron, { ipcMain } from "electron";
import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents, IpcMainEvent } from "electron";
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
import { t } from "i18next";
import log from "./log.js";
import optionService from "./options.js";
import port from "./port.js";
import { RESOURCE_DIR } from "./resource_dir.js";
import sqlInit from "./sql_init.js";
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
// Prevent the window being garbage collected
let mainWindow: BrowserWindow | null;
@ -160,8 +161,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
await browserWindow.loadURL(`http://127.0.0.1:${port}/?print#${notePath}`);
await browserWindow.webContents.executeJavaScript(`
new Promise(resolve => {
if (window._noteReady) return resolve();
window.addEventListener("note-ready", () => resolve());
if (window._noteReady) return resolve(window._noteReady);
window.addEventListener("note-ready", (data) => resolve(data));
});
`);
ipcMain.off("print-progress", progressCallback);
@ -289,9 +290,9 @@ async function configureWebContents(webContents: WebContents, spellcheckEnabled:
function getIcon() {
if (process.env.NODE_ENV === "development") {
return path.join(__dirname, "../../../desktop/electron-forge/app-icon/png/256x256-dev.png");
} else {
return path.join(RESOURCE_DIR, "../public/assets/icon.png");
}
}
return path.join(RESOURCE_DIR, "../public/assets/icon.png");
}
async function createSetupWindow() {