diff --git a/apps/client/src/widgets/react/KeyboardShortcut.tsx b/apps/client/src/widgets/react/KeyboardShortcut.tsx index 049f54dd5..852fc8733 100644 --- a/apps/client/src/widgets/react/KeyboardShortcut.tsx +++ b/apps/client/src/widgets/react/KeyboardShortcut.tsx @@ -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) => ( <> {key} {i + 1 < keys.length && "+ "} - )) - }).reduce((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), [])} + ))) + })} ); } \ No newline at end of file diff --git a/apps/client/src/widgets/react/NoteLink.tsx b/apps/client/src/widgets/react/NoteLink.tsx new file mode 100644 index 000000000..31dd4fbeb --- /dev/null +++ b/apps/client/src/widgets/react/NoteLink.tsx @@ -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>(); + + useEffect(() => { + link.createLink(stringifiedNotePath, { showNotePath: true }) + .then(setJqueryEl); + }, [ stringifiedNotePath, showNotePath ]) + + return + +} \ No newline at end of file diff --git a/apps/client/src/widgets/react/RawHtml.tsx b/apps/client/src/widgets/react/RawHtml.tsx index 1e5aef648..d74fda43a 100644 --- a/apps/client/src/widgets/react/RawHtml.tsx +++ b/apps/client/src/widgets/react/RawHtml.tsx @@ -4,7 +4,7 @@ type HTMLElementLike = string | HTMLElement | JQuery; 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 } } diff --git a/apps/client/src/widgets/react/react_utils.tsx b/apps/client/src/widgets/react/react_utils.tsx index e8b0752b4..db451b21a 100644 --- a/apps/client/src/widgets/react/react_utils.tsx +++ b/apps/client/src/widgets/react/react_utils.tsx @@ -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(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((acc, item) => + (acc.length ? [...acc, ", ", item] : [item]), []); } \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/EditedNotesTab.tsx b/apps/client/src/widgets/ribbon/EditedNotesTab.tsx new file mode 100644 index 000000000..c85ac4ad1 --- /dev/null +++ b/apps/client/src/widgets/ribbon/EditedNotesTab.tsx @@ -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(); + + useEffect(() => { + if (!note) return; + server.get(`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 ( +
+ {editedNotes ? ( +
+ {separateByCommas(editedNotes.map(editedNote => { + return ( + + {editedNote.isDeleted ? ( + {`${editedNote.title} ${t("edited_notes.deleted")}`} + ) : ( + <> + {editedNote.notePath ? : {editedNote.title} } + + )} + + ) + }))} +
+ ) : ( +
{t("edited_notes.no_edited_notes_found")}
+ )} +
+ ) +} diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index e0bc25d9a..c043f84e3 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -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([ 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([ 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 diff --git a/apps/client/src/widgets/ribbon_widgets/edited_notes.ts b/apps/client/src/widgets/ribbon_widgets/edited_notes.ts deleted file mode 100644 index 768d48566..000000000 --- a/apps/client/src/widgets/ribbon_widgets/edited_notes.ts +++ /dev/null @@ -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*/` -
- - -
${t("edited_notes.no_edited_notes_found")}
- - -
-`; - -// TODO: Deduplicate with server. -interface EditedNotesResponse { - noteId: string; - isDeleted: boolean; - title: string; - notePath: string[]; -} - -export default class EditedNotesWidget extends NoteContextAwareWidget { - - private $list!: JQuery; - private $noneFound!: JQuery; - - 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(`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 = $(''); - - if (editedNote.isDeleted) { - const title = `${editedNote.title} ${t("edited_notes.deleted")}`; - $item.append($("").text(title).attr("title", title)); - } else { - $item.append(editedNote.notePath ? await linkService.createLink(editedNote.notePath.join("/"), { showNotePath: true }) : $("").text(editedNote.title)); - } - - if (i < editedNotes.length - 1) { - $item.append(", "); - } - - this.$list.append($item); - } - } -} diff --git a/apps/server/src/routes/api/revisions.ts b/apps/server/src/routes/api/revisions.ts index cdcabe6d2..055d0b75e 100644 --- a/apps/server/src/routes/api/revisions.ts +++ b/apps/server/src/routes/api/revisions.ts @@ -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 { diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index 311e45fb0..199ab164b 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -162,3 +162,10 @@ export type ToggleInParentResponse = { success: false; message: string; } + +export type EditedNotesResponse = { + noteId: string; + isDeleted: boolean; + title?: string; + notePath?: string[] | null; +}[];