mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 23:29:02 +02:00
feat(react/floating_buttons): port backlinks
This commit is contained in:
parent
1766d28fc2
commit
a95e28c085
@ -110,4 +110,50 @@
|
||||
.close-floating-buttons-button:hover {
|
||||
border: 1px solid var(--button-border-color);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Backlinks */
|
||||
.backlinks-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backlinks-ticker {
|
||||
border-radius: 10px;
|
||||
border-color: var(--main-border-color);
|
||||
background-color: var(--more-accented-background-color);
|
||||
padding: 4px 10px 4px 10px;
|
||||
opacity: 90%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backlinks-count {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-items {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
width: 400px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
color: var(--main-text-color);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.backlink-excerpt {
|
||||
border-left: 2px solid var(--main-border-color);
|
||||
padding-left: 10px;
|
||||
opacity: 80%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.backlink-excerpt .backlink-link { /* the actual backlink */
|
||||
font-weight: bold;
|
||||
background-color: yellow;
|
||||
}
|
||||
/* #endregion */
|
@ -4,11 +4,11 @@ import Component from "../components/component";
|
||||
import NoteContext from "../components/note_context";
|
||||
import FNote from "../entities/fnote";
|
||||
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
|
||||
import { useNoteLabelBoolean, useTriliumOption } from "./react/hooks";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useNoteLabelBoolean, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
|
||||
import server from "../services/server";
|
||||
import { SaveSqlConsoleResponse } from "@triliumnext/commons";
|
||||
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
|
||||
import toast from "../services/toast";
|
||||
import { t } from "../services/i18n";
|
||||
import { copyImageReferenceToClipboard } from "../services/image";
|
||||
@ -16,6 +16,9 @@ import tree from "../services/tree";
|
||||
import protected_session_holder from "../services/protected_session_holder";
|
||||
import options from "../services/options";
|
||||
import { getHelpUrlForNote } from "../services/in_app_help";
|
||||
import froca from "../services/froca";
|
||||
import NoteLink from "./react/NoteLink";
|
||||
import RawHtml from "./react/RawHtml";
|
||||
|
||||
export interface FloatingButtonDefinition {
|
||||
component: (context: FloatingButtonContext) => VNode;
|
||||
@ -109,6 +112,10 @@ export const FLOATING_BUTTON_DEFINITIONS: FloatingButtonDefinition[] = [
|
||||
{
|
||||
component: InAppHelpButton,
|
||||
isEnabled: ({ note }) => !!getHelpUrlForNote(note)
|
||||
},
|
||||
{
|
||||
component: Backlinks,
|
||||
isEnabled: ({ noteContext }) => noteContext.viewScope?.viewMode === "default"
|
||||
}
|
||||
];
|
||||
|
||||
@ -320,4 +327,79 @@ function InAppHelpButton({ note }: FloatingButtonContext) {
|
||||
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Backlinks({ note }: FloatingButtonContext) {
|
||||
let [ backlinkCount, setBacklinkCount ] = useState(0);
|
||||
let [ popupOpen, setPopupOpen ] = useState(true);
|
||||
const backlinksContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
server.get<BacklinkCountResponse>(`note-map/${note.noteId}/backlink-count`).then(resp => {
|
||||
setBacklinkCount(resp.count);
|
||||
});
|
||||
}, [ note ]);
|
||||
|
||||
// Determine the max height of the container.
|
||||
const { windowHeight } = useWindowSize();
|
||||
useLayoutEffect(() => {
|
||||
const el = backlinksContainerRef.current;
|
||||
if (popupOpen && el) {
|
||||
const box = el.getBoundingClientRect();
|
||||
const maxHeight = windowHeight - box.top - 10;
|
||||
el.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
}, [ popupOpen, windowHeight ]);
|
||||
|
||||
return (
|
||||
<div className="backlinks-widget has-overflow">
|
||||
{backlinkCount > 0 && <>
|
||||
<div
|
||||
className="backlinks-ticker"
|
||||
onClick={() => setPopupOpen(!popupOpen)}
|
||||
>
|
||||
<span className="backlinks-count">{t("zpetne_odkazy.backlink", { count: backlinkCount })}</span>
|
||||
</div>
|
||||
|
||||
{popupOpen && (
|
||||
<div ref={backlinksContainerRef} className="backlinks-items dropdown-menu" style={{ display: "block" }}>
|
||||
<BacklinksList noteId={note.noteId} />
|
||||
</div>
|
||||
)}
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksList({ noteId }: { noteId: string }) {
|
||||
const [ backlinks, setBacklinks ] = useState<BacklinksResponse>([]);
|
||||
|
||||
useEffect(() => {
|
||||
server.get<BacklinksResponse>(`note-map/${noteId}/backlinks`).then(async (backlinks) => {
|
||||
// prefetch all
|
||||
const noteIds = backlinks
|
||||
.filter(bl => "noteId" in bl)
|
||||
.map((bl) => bl.noteId);
|
||||
await froca.getNotes(noteIds);
|
||||
setBacklinks(backlinks);
|
||||
});
|
||||
}, [ noteId ]);
|
||||
|
||||
return backlinks.map(backlink => (
|
||||
<div>
|
||||
<NoteLink
|
||||
notePath={backlink.noteId}
|
||||
showNotePath showNoteIcon
|
||||
noPreview
|
||||
/>
|
||||
|
||||
{"relationName" in backlink ? (
|
||||
<p>{backlink.relationName}</p>
|
||||
) : (
|
||||
backlink.excerpts.map(excerpt => (
|
||||
<RawHtml html={excerpt} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
/**
|
||||
* !!! Filename is intentionally mangled, because some adblockers don't like the word "backlinks".
|
||||
*/
|
||||
|
||||
import { t } from "../../services/i18n.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import server from "../../services/server.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="backlinks-widget has-overflow">
|
||||
<style>
|
||||
.backlinks-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backlinks-ticker {
|
||||
border-radius: 10px;
|
||||
border-color: var(--main-border-color);
|
||||
background-color: var(--more-accented-background-color);
|
||||
padding: 4px 10px 4px 10px;
|
||||
opacity: 90%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backlinks-count {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-items {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
width: 400px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
color: var(--main-text-color);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.backlink-excerpt {
|
||||
border-left: 2px solid var(--main-border-color);
|
||||
padding-left: 10px;
|
||||
opacity: 80%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.backlink-excerpt .backlink-link { /* the actual backlink */
|
||||
font-weight: bold;
|
||||
background-color: yellow;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="backlinks-ticker">
|
||||
<span class="backlinks-count"></span>
|
||||
</div>
|
||||
|
||||
<div class="backlinks-items dropdown-menu" style="display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// TODO: Deduplicate with server
|
||||
interface Backlink {
|
||||
noteId: string;
|
||||
relationName?: string;
|
||||
excerpts?: string[];
|
||||
}
|
||||
|
||||
export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $count!: JQuery<HTMLElement>;
|
||||
private $items!: JQuery<HTMLElement>;
|
||||
private $ticker!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$count = this.$widget.find(".backlinks-count");
|
||||
this.$items = this.$widget.find(".backlinks-items");
|
||||
this.$ticker = this.$widget.find(".backlinks-ticker");
|
||||
|
||||
this.$count.on("click", () => {
|
||||
this.$items.toggle();
|
||||
this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10);
|
||||
|
||||
if (this.$items.is(":visible")) {
|
||||
this.renderBacklinks();
|
||||
}
|
||||
});
|
||||
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.clearItems();
|
||||
|
||||
if (this.noteContext?.viewScope?.viewMode !== "default") {
|
||||
this.toggle(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// can't use froca since that would count only relations from loaded notes
|
||||
// TODO: Deduplicate response type
|
||||
const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`);
|
||||
|
||||
if (!resp || !resp.count) {
|
||||
this.toggle(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggle(true);
|
||||
this.$count.text(
|
||||
// i18next plural
|
||||
`${t("zpetne_odkazy.backlink", { count: resp.count })}`
|
||||
);
|
||||
}
|
||||
|
||||
toggle(show: boolean) {
|
||||
this.$widget.toggleClass("hidden-no-content", !show)
|
||||
.toggleClass("visible", !!show);
|
||||
}
|
||||
|
||||
clearItems() {
|
||||
this.$items.empty().hide();
|
||||
}
|
||||
|
||||
async renderBacklinks() {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$items.empty();
|
||||
|
||||
const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`);
|
||||
|
||||
if (!backlinks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await froca.getNotes(backlinks.map((bl) => bl.noteId)); // prefetch all
|
||||
|
||||
for (const backlink of backlinks) {
|
||||
const $item = $("<div>");
|
||||
|
||||
$item.append(
|
||||
await linkService.createLink(backlink.noteId, {
|
||||
showNoteIcon: true,
|
||||
showNotePath: true,
|
||||
showTooltip: false
|
||||
})
|
||||
);
|
||||
|
||||
if (backlink.relationName) {
|
||||
$item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
|
||||
} else {
|
||||
$item.append(...(backlink.excerpts ?? []));
|
||||
}
|
||||
|
||||
this.$items.append($item);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,17 +5,18 @@ import RawHtml from "./RawHtml";
|
||||
interface NoteLinkOpts {
|
||||
notePath: string | string[];
|
||||
showNotePath?: boolean;
|
||||
showNoteIcon?: boolean;
|
||||
style?: Record<string, string | number>;
|
||||
noPreview?: boolean;
|
||||
noTnLink?: boolean;
|
||||
}
|
||||
|
||||
export default function NoteLink({ notePath, showNotePath, style, noPreview, noTnLink }: NoteLinkOpts) {
|
||||
export default function NoteLink({ notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink }: NoteLinkOpts) {
|
||||
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
||||
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
||||
|
||||
useEffect(() => {
|
||||
link.createLink(stringifiedNotePath, { showNotePath })
|
||||
link.createLink(stringifiedNotePath, { showNotePath, showNoteIcon })
|
||||
.then(setJqueryEl);
|
||||
}, [ stringifiedNotePath, showNotePath ]);
|
||||
|
||||
|
@ -5,12 +5,7 @@ import { JSDOM } from "jsdom";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import type BAttribute from "../../becca/entities/battribute.js";
|
||||
import type { Request } from "express";
|
||||
|
||||
interface Backlink {
|
||||
noteId: string;
|
||||
relationName?: string;
|
||||
excerpts?: string[];
|
||||
}
|
||||
import { BacklinkCountResponse, BacklinksResponse } from "@triliumnext/commons";
|
||||
|
||||
interface TreeLink {
|
||||
sourceNoteId: string;
|
||||
@ -361,10 +356,10 @@ function getBacklinkCount(req: Request) {
|
||||
|
||||
return {
|
||||
count: getFilteredBacklinks(note).length
|
||||
};
|
||||
} satisfies BacklinkCountResponse;
|
||||
}
|
||||
|
||||
function getBacklinks(req: Request): Backlink[] {
|
||||
function getBacklinks(req: Request): BacklinksResponse {
|
||||
const { noteId } = req.params;
|
||||
const note = becca.getNoteOrThrow(noteId);
|
||||
|
||||
@ -377,17 +372,16 @@ function getBacklinks(req: Request): Backlink[] {
|
||||
return {
|
||||
noteId: sourceNote.noteId,
|
||||
relationName: backlink.name
|
||||
};
|
||||
} satisfies BacklinksResponse[number];
|
||||
}
|
||||
|
||||
backlinksWithExcerptCount++;
|
||||
|
||||
const excerpts = findExcerpts(sourceNote, noteId);
|
||||
|
||||
return {
|
||||
noteId: sourceNote.noteId,
|
||||
excerpts
|
||||
};
|
||||
} satisfies BacklinksResponse[number];
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -208,3 +208,15 @@ export interface ConvertToAttachmentResponse {
|
||||
}
|
||||
|
||||
export type SaveSqlConsoleResponse = CloneResponse;
|
||||
|
||||
export interface BacklinkCountResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type BacklinksResponse = ({
|
||||
noteId: string;
|
||||
relationName: string;
|
||||
} | {
|
||||
noteId: string;
|
||||
excerpts: string[]
|
||||
})[];
|
||||
|
Loading…
x
Reference in New Issue
Block a user