mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 07:08:55 +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 { 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]), [])}
|
||||
)))
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
@ -40,3 +40,8 @@ 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]), []);
|
||||
}
|
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 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
|
||||
|
@ -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 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 {
|
||||
|
@ -162,3 +162,10 @@ export type ToggleInParentResponse = {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EditedNotesResponse = {
|
||||
noteId: string;
|
||||
isDeleted: boolean;
|
||||
title?: string;
|
||||
notePath?: string[] | null;
|
||||
}[];
|
||||
|
Loading…
x
Reference in New Issue
Block a user