feat(react/ribbon): port edited notes

This commit is contained in:
Elian Doran 2025-08-22 17:31:06 +03:00
parent c3eca3b626
commit cee4714665
No known key found for this signature in database
9 changed files with 101 additions and 111 deletions

View File

@ -1,6 +1,7 @@
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
import { useEffect, useState } from "preact/hooks";
import keyboard_actions from "../../services/keyboard_actions";
import { separateByCommas } from "./react_utils";
interface KeyboardShortcutProps {
actionName: KeyboardActionNames;
@ -21,13 +22,13 @@ export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps)
<>
{action.effectiveShortcuts?.map((shortcut, i) => {
const keys = shortcut.split("+");
return keys
return separateByCommas(keys
.map((key, i) => (
<>
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
</>
))
}).reduce<any>((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), [])}
)))
})}
</>
);
}

View File

@ -0,0 +1,21 @@
import { useEffect, useMemo, useState } from "preact/hooks";
import link from "../../services/link";
import RawHtml from "./RawHtml";
interface NoteLinkOpts {
notePath: string | string[];
showNotePath?: boolean;
}
export default function NoteLink({ notePath, showNotePath }: NoteLinkOpts) {
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
useEffect(() => {
link.createLink(stringifiedNotePath, { showNotePath: true })
.then(setJqueryEl);
}, [ stringifiedNotePath, showNotePath ])
return <RawHtml html={jqueryEl} />
}

View File

@ -4,7 +4,7 @@ type HTMLElementLike = string | HTMLElement | JQuery<HTMLElement>;
interface RawHtmlProps {
className?: string;
html: HTMLElementLike;
html?: HTMLElementLike;
style?: CSSProperties;
}
@ -19,7 +19,7 @@ export function RawHtmlBlock(props: RawHtmlProps) {
function getProps({ className, html, style }: RawHtmlProps) {
return {
className: className,
dangerouslySetInnerHTML: getHtml(html),
dangerouslySetInnerHTML: getHtml(html ?? ""),
style
}
}

View File

@ -1,4 +1,4 @@
import { createContext, render, type JSX, type RefObject } from "preact";
import { ComponentChild, createContext, render, type JSX, type RefObject } from "preact";
import Component from "../../components/component";
export const ParentComponent = createContext<Component | null>(null);
@ -39,4 +39,9 @@ export function renderReactWidgetAtElement(parentComponent: Component, el: JSX.E
export function disposeReactWidget(container: Element) {
render(null, container);
}
export function separateByCommas(components: ComponentChild[]) {
return components.reduce<any>((acc, item) =>
(acc.length ? [...acc, ", ", item] : [item]), []);
}

View File

@ -0,0 +1,51 @@
import { useEffect, useState } from "preact/hooks";
import { TabContext } from "./ribbon-interface";
import { EditedNotesResponse } from "@triliumnext/commons";
import server from "../../services/server";
import { t } from "../../services/i18n";
import froca from "../../services/froca";
import NoteLink from "../react/NoteLink";
import { separateByCommas } from "../react/react_utils";
export default function EditedNotesTab({ note }: TabContext) {
const [ editedNotes, setEditedNotes ] = useState<EditedNotesResponse>();
useEffect(() => {
if (!note) return;
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(editedNotes);
});
}, [ note?.noteId ]);
return (
<div className="edited-notes-widget" style={{
padding: "12px",
maxHeight: "200px",
width: "100%",
overflow: "auto"
}}>
{editedNotes ? (
<div className="edited-notes-list use-tn-links">
{separateByCommas(editedNotes.map(editedNote => {
return (
<span className="edited-note-line">
{editedNote.isDeleted ? (
<i>{`${editedNote.title} ${t("edited_notes.deleted")}`}</i>
) : (
<>
{editedNote.notePath ? <NoteLink notePath={editedNote.notePath} showNotePath /> : <span>{editedNote.title}</span> }
</>
)}
</span>
)
}))}
</div>
) : (
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
)}
</div>
)
}

View File

@ -11,6 +11,7 @@ import options from "../../services/options";
import { CommandNames } from "../../components/app_context";
import FNote from "../../entities/fnote";
import ScriptTab from "./ScriptTab";
import EditedNotesTab from "./EditedNotesTab";
interface TitleContext {
note: FNote | null | undefined;
@ -21,9 +22,9 @@ interface TabConfiguration {
icon: string;
// TODO: Mark as required after porting them all.
content?: (context: TabContext) => VNode;
show?: (context: TitleContext) => boolean;
show?: (context: TitleContext) => boolean | null | undefined;
toggleCommand?: CommandNames;
activate?: boolean;
activate?: boolean | ((context: TitleContext) => boolean);
/**
* By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar).
*/
@ -44,7 +45,7 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
icon: "bx bx-play",
content: ScriptTab,
activate: true,
show: ({ note }) => !!note &&
show: ({ note }) => note &&
(note.isTriliumScript() || note.isTriliumSqlite()) &&
(note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
},
@ -54,9 +55,11 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
icon: "bx bx-search"
},
{
// Edited NotesWidget
title: t("edited_notes.title"),
icon: "bx bx-calendar-edit"
icon: "bx bx-calendar-edit",
content: EditedNotesTab,
show: ({ note }) => note?.hasOwnedLabel("dateNote"),
activate: ({ note }) => (note?.getPromotedDefinitionAttributes().length === 0 || !options.is("promotedAttributesOpenInRibbon")) && options.is("editedNotesOpenInRibbon")
},
{
// BookPropertiesWidget

View File

@ -1,98 +0,0 @@
import linkService from "../../services/link.js";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import options from "../../services/options.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
const TPL = /*html*/`
<div class="edited-notes-widget">
<style>
.edited-notes-widget {
padding: 12px;
max-height: 200px;
width: 100%;
overflow: auto;
}
</style>
<div class="no-edited-notes-found">${t("edited_notes.no_edited_notes_found")}</div>
<div class="edited-notes-list use-tn-links"></div>
</div>
`;
// TODO: Deduplicate with server.
interface EditedNotesResponse {
noteId: string;
isDeleted: boolean;
title: string;
notePath: string[];
}
export default class EditedNotesWidget extends NoteContextAwareWidget {
private $list!: JQuery<HTMLElement>;
private $noneFound!: JQuery<HTMLElement>;
get name() {
return "editedNotes";
}
isEnabled() {
return super.isEnabled() && this.note?.hasOwnedLabel("dateNote");
}
getTitle() {
return {
show: this.isEnabled(),
// promoted attributes have priority over edited notes
activate: (this.note?.getPromotedDefinitionAttributes().length === 0 || !options.is("promotedAttributesOpenInRibbon")) && options.is("editedNotesOpenInRibbon"),
};
}
async doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$list = this.$widget.find(".edited-notes-list");
this.$noneFound = this.$widget.find(".no-edited-notes-found");
}
async refreshWithNote(note: FNote) {
let editedNotes = await server.get<EditedNotesResponse[]>(`edited-notes/${note.getLabelValue("dateNote")}`);
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
this.$list.empty();
this.$noneFound.hide();
if (editedNotes.length === 0) {
this.$noneFound.show();
return;
}
const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
for (let i = 0; i < editedNotes.length; i++) {
const editedNote = editedNotes[i];
const $item = $('<span class="edited-note-line">');
if (editedNote.isDeleted) {
const title = `${editedNote.title} ${t("edited_notes.deleted")}`;
$item.append($("<i>").text(title).attr("title", title));
} else {
$item.append(editedNote.notePath ? await linkService.createLink(editedNote.notePath.join("/"), { showNotePath: true }) : $("<span>").text(editedNote.title));
}
if (i < editedNotes.length - 1) {
$item.append(", ");
}
this.$list.append($item);
}
}
}

View File

@ -12,7 +12,7 @@ import type { Request, Response } from "express";
import type BRevision from "../../becca/entities/brevision.js";
import type BNote from "../../becca/entities/bnote.js";
import type { NotePojo } from "../../becca/becca-interface.js";
import { RevisionItem, RevisionPojo, RevisionRow } from "@triliumnext/commons";
import { EditedNotesResponse, RevisionItem, RevisionPojo, RevisionRow } from "@triliumnext/commons";
interface NotePath {
noteId: string;
@ -184,7 +184,7 @@ function getEditedNotesOnDate(req: Request) {
notePojo.notePath = notePath ? notePath.notePath : null;
return notePojo;
});
}) satisfies EditedNotesResponse;
}
function getNotePathData(note: BNote): NotePath | undefined {

View File

@ -162,3 +162,10 @@ export type ToggleInParentResponse = {
success: false;
message: string;
}
export type EditedNotesResponse = {
noteId: string;
isDeleted: boolean;
title?: string;
notePath?: string[] | null;
}[];