mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(react/ribbon): port edited notes
This commit is contained in:
parent
c3eca3b626
commit
cee4714665
@ -1,6 +1,7 @@
|
|||||||
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
|
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import keyboard_actions from "../../services/keyboard_actions";
|
import keyboard_actions from "../../services/keyboard_actions";
|
||||||
|
import { separateByCommas } from "./react_utils";
|
||||||
|
|
||||||
interface KeyboardShortcutProps {
|
interface KeyboardShortcutProps {
|
||||||
actionName: KeyboardActionNames;
|
actionName: KeyboardActionNames;
|
||||||
@ -21,13 +22,13 @@ export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps)
|
|||||||
<>
|
<>
|
||||||
{action.effectiveShortcuts?.map((shortcut, i) => {
|
{action.effectiveShortcuts?.map((shortcut, i) => {
|
||||||
const keys = shortcut.split("+");
|
const keys = shortcut.split("+");
|
||||||
return keys
|
return separateByCommas(keys
|
||||||
.map((key, i) => (
|
.map((key, i) => (
|
||||||
<>
|
<>
|
||||||
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
||||||
</>
|
</>
|
||||||
))
|
)))
|
||||||
}).reduce<any>((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), [])}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
21
apps/client/src/widgets/react/NoteLink.tsx
Normal file
21
apps/client/src/widgets/react/NoteLink.tsx
Normal 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} />
|
||||||
|
|
||||||
|
}
|
@ -4,7 +4,7 @@ type HTMLElementLike = string | HTMLElement | JQuery<HTMLElement>;
|
|||||||
|
|
||||||
interface RawHtmlProps {
|
interface RawHtmlProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
html: HTMLElementLike;
|
html?: HTMLElementLike;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ export function RawHtmlBlock(props: RawHtmlProps) {
|
|||||||
function getProps({ className, html, style }: RawHtmlProps) {
|
function getProps({ className, html, style }: RawHtmlProps) {
|
||||||
return {
|
return {
|
||||||
className: className,
|
className: className,
|
||||||
dangerouslySetInnerHTML: getHtml(html),
|
dangerouslySetInnerHTML: getHtml(html ?? ""),
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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";
|
import Component from "../../components/component";
|
||||||
|
|
||||||
export const ParentComponent = createContext<Component | null>(null);
|
export const ParentComponent = createContext<Component | null>(null);
|
||||||
@ -40,3 +40,8 @@ export function renderReactWidgetAtElement(parentComponent: Component, el: JSX.E
|
|||||||
export function disposeReactWidget(container: Element) {
|
export function disposeReactWidget(container: Element) {
|
||||||
render(null, container);
|
render(null, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function separateByCommas(components: ComponentChild[]) {
|
||||||
|
return components.reduce<any>((acc, item) =>
|
||||||
|
(acc.length ? [...acc, ", ", item] : [item]), []);
|
||||||
|
}
|
51
apps/client/src/widgets/ribbon/EditedNotesTab.tsx
Normal file
51
apps/client/src/widgets/ribbon/EditedNotesTab.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -11,6 +11,7 @@ import options from "../../services/options";
|
|||||||
import { CommandNames } from "../../components/app_context";
|
import { CommandNames } from "../../components/app_context";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import ScriptTab from "./ScriptTab";
|
import ScriptTab from "./ScriptTab";
|
||||||
|
import EditedNotesTab from "./EditedNotesTab";
|
||||||
|
|
||||||
interface TitleContext {
|
interface TitleContext {
|
||||||
note: FNote | null | undefined;
|
note: FNote | null | undefined;
|
||||||
@ -21,9 +22,9 @@ interface TabConfiguration {
|
|||||||
icon: string;
|
icon: string;
|
||||||
// TODO: Mark as required after porting them all.
|
// TODO: Mark as required after porting them all.
|
||||||
content?: (context: TabContext) => VNode;
|
content?: (context: TabContext) => VNode;
|
||||||
show?: (context: TitleContext) => boolean;
|
show?: (context: TitleContext) => boolean | null | undefined;
|
||||||
toggleCommand?: CommandNames;
|
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).
|
* 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",
|
icon: "bx bx-play",
|
||||||
content: ScriptTab,
|
content: ScriptTab,
|
||||||
activate: true,
|
activate: true,
|
||||||
show: ({ note }) => !!note &&
|
show: ({ note }) => note &&
|
||||||
(note.isTriliumScript() || note.isTriliumSqlite()) &&
|
(note.isTriliumScript() || note.isTriliumSqlite()) &&
|
||||||
(note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
|
(note.hasLabel("executeDescription") || note.hasLabel("executeButton"))
|
||||||
},
|
},
|
||||||
@ -54,9 +55,11 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
|
|||||||
icon: "bx bx-search"
|
icon: "bx bx-search"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Edited NotesWidget
|
|
||||||
title: t("edited_notes.title"),
|
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
|
// BookPropertiesWidget
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,7 +12,7 @@ import type { Request, Response } from "express";
|
|||||||
import type BRevision from "../../becca/entities/brevision.js";
|
import type BRevision from "../../becca/entities/brevision.js";
|
||||||
import type BNote from "../../becca/entities/bnote.js";
|
import type BNote from "../../becca/entities/bnote.js";
|
||||||
import type { NotePojo } from "../../becca/becca-interface.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 {
|
interface NotePath {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
@ -184,7 +184,7 @@ function getEditedNotesOnDate(req: Request) {
|
|||||||
notePojo.notePath = notePath ? notePath.notePath : null;
|
notePojo.notePath = notePath ? notePath.notePath : null;
|
||||||
|
|
||||||
return notePojo;
|
return notePojo;
|
||||||
});
|
}) satisfies EditedNotesResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNotePathData(note: BNote): NotePath | undefined {
|
function getNotePathData(note: BNote): NotePath | undefined {
|
||||||
|
@ -162,3 +162,10 @@ export type ToggleInParentResponse = {
|
|||||||
success: false;
|
success: false;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EditedNotesResponse = {
|
||||||
|
noteId: string;
|
||||||
|
isDeleted: boolean;
|
||||||
|
title?: string;
|
||||||
|
notePath?: string[] | null;
|
||||||
|
}[];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user