mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
fix(react/dialogs): port recent_changes
This commit is contained in:
parent
c0d3027e65
commit
31fbf2cb57
@ -242,7 +242,7 @@ div.tn-tool-dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Item title for deleted notes */
|
/* Item title for deleted notes */
|
||||||
.recent-changes-content ul li.deleted-note .note-title > .note-title {
|
.recent-changes-content ul li.deleted-note .note-title {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,177 +0,0 @@
|
|||||||
import { formatDateTime } from "../../utils/formatters.js";
|
|
||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import appContext, { type EventData } from "../../components/app_context.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
|
||||||
import dialogService, { openDialog } from "../../services/dialog.js";
|
|
||||||
import froca from "../../services/froca.js";
|
|
||||||
import hoistedNoteService from "../../services/hoisted_note.js";
|
|
||||||
import linkService from "../../services/link.js";
|
|
||||||
import server from "../../services/server.js";
|
|
||||||
import toastService from "../../services/toast.js";
|
|
||||||
import ws from "../../services/ws.js";
|
|
||||||
import { Modal } from "bootstrap";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="recent-changes-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
|
||||||
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title flex-grow-1">${t("recent_changes.title")}</h5>
|
|
||||||
<button class="erase-deleted-notes-now-button btn btn-sm" style="padding: 0 10px">${t("recent_changes.erase_notes_button")}</button>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("recent_changes.close")}"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="recent-changes-content"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
|
||||||
interface RecentChangesRow {
|
|
||||||
noteId: string;
|
|
||||||
date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RecentChangesDialog extends BasicWidget {
|
|
||||||
|
|
||||||
private ancestorNoteId?: string;
|
|
||||||
|
|
||||||
private modal!: bootstrap.Modal;
|
|
||||||
private $content!: JQuery<HTMLElement>;
|
|
||||||
private $eraseDeletedNotesNow!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
|
||||||
|
|
||||||
this.$content = this.$widget.find(".recent-changes-content");
|
|
||||||
this.$eraseDeletedNotesNow = this.$widget.find(".erase-deleted-notes-now-button");
|
|
||||||
this.$eraseDeletedNotesNow.on("click", () => {
|
|
||||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
|
||||||
this.refresh();
|
|
||||||
|
|
||||||
toastService.showMessage(t("recent_changes.deleted_notes_message"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async showRecentChangesEvent({ ancestorNoteId }: EventData<"showRecentChanges">) {
|
|
||||||
this.ancestorNoteId = ancestorNoteId;
|
|
||||||
|
|
||||||
await this.refresh();
|
|
||||||
|
|
||||||
openDialog(this.$widget);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refresh() {
|
|
||||||
if (!this.ancestorNoteId) {
|
|
||||||
this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
|
|
||||||
}
|
|
||||||
|
|
||||||
const recentChangesRows = await server.get<RecentChangesRow[]>(`recent-changes/${this.ancestorNoteId}`);
|
|
||||||
|
|
||||||
// preload all notes into cache
|
|
||||||
await froca.getNotes(
|
|
||||||
recentChangesRows.map((r) => r.noteId),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
this.$content.empty();
|
|
||||||
|
|
||||||
if (recentChangesRows.length === 0) {
|
|
||||||
this.$content.append(t("recent_changes.no_changes_message"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedByDate = this.groupByDate(recentChangesRows);
|
|
||||||
|
|
||||||
for (const [dateDay, dayChanges] of groupedByDate) {
|
|
||||||
const $changesList = $("<ul>");
|
|
||||||
|
|
||||||
const formattedDate = formatDateTime(dateDay, "full", "none");
|
|
||||||
const dayEl = $("<div>").append($("<b>").text(formattedDate)).append($changesList);
|
|
||||||
|
|
||||||
for (const change of dayChanges) {
|
|
||||||
const formattedTime = formatDateTime(change.date, "none", "short");
|
|
||||||
|
|
||||||
let $noteLink;
|
|
||||||
|
|
||||||
if (change.current_isDeleted) {
|
|
||||||
$noteLink = $("<span>");
|
|
||||||
|
|
||||||
$noteLink.append($("<span>").addClass("note-title").text(change.current_title));
|
|
||||||
|
|
||||||
if (change.canBeUndeleted) {
|
|
||||||
const $undeleteLink = $(`<a href="javascript:">`)
|
|
||||||
.text(t("recent_changes.undelete_link"))
|
|
||||||
.on("click", async () => {
|
|
||||||
const text = t("recent_changes.confirm_undelete");
|
|
||||||
|
|
||||||
if (await dialogService.confirm(text)) {
|
|
||||||
await server.put(`notes/${change.noteId}/undelete`);
|
|
||||||
|
|
||||||
this.modal.hide();
|
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
|
||||||
|
|
||||||
const activeContext = appContext.tabManager.getActiveContext();
|
|
||||||
if (activeContext) {
|
|
||||||
activeContext.setNote(change.noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$noteLink.append(" (").append($undeleteLink).append(")");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const note = await froca.getNote(change.noteId);
|
|
||||||
const notePath = note?.getBestNotePathString();
|
|
||||||
|
|
||||||
if (notePath) {
|
|
||||||
$noteLink = await linkService.createLink(notePath, {
|
|
||||||
title: change.title,
|
|
||||||
showNotePath: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$noteLink = $("<span>").text(note?.title ?? "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$changesList.append(
|
|
||||||
$("<li>")
|
|
||||||
.on("click", (e) => {
|
|
||||||
// Skip clicks on the link or deleted notes
|
|
||||||
if (e.target?.nodeName !== "A" && !change.current_isDeleted) {
|
|
||||||
// Open the current note
|
|
||||||
const activeContext = appContext.tabManager.getActiveContext();
|
|
||||||
if (activeContext) {
|
|
||||||
activeContext.setNote(change.noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toggleClass("deleted-note", !!change.current_isDeleted)
|
|
||||||
.append($("<span>").text(formattedTime).attr("title", change.date))
|
|
||||||
.append($noteLink.addClass("note-title"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$content.append(dayEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
groupByDate(rows: RecentChangesRow[]) {
|
|
||||||
const groupedByDate = new Map();
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const dateDay = row.date.substr(0, 10);
|
|
||||||
|
|
||||||
if (!groupedByDate.has(dateDay)) {
|
|
||||||
groupedByDate.set(dateDay, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
groupedByDate.get(dateDay).push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return groupedByDate;
|
|
||||||
}
|
|
||||||
}
|
|
156
apps/client/src/widgets/dialogs/recent_changes.tsx
Normal file
156
apps/client/src/widgets/dialogs/recent_changes.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { EventData } from "../../components/app_context";
|
||||||
|
import { openDialog } from "../../services/dialog";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import server from "../../services/server";
|
||||||
|
import toast from "../../services/toast";
|
||||||
|
import Button from "../react/Button";
|
||||||
|
import Modal from "../react/Modal";
|
||||||
|
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||||
|
import hoisted_note from "../../services/hoisted_note";
|
||||||
|
import { RecentChangesRow } from "@triliumnext/commons";
|
||||||
|
import froca from "../../services/froca";
|
||||||
|
import { formatDateTime } from "../../utils/formatters";
|
||||||
|
import link from "../../services/link";
|
||||||
|
import RawHtml from "../react/RawHtml";
|
||||||
|
|
||||||
|
interface RecentChangesDialogProps {
|
||||||
|
ancestorNoteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecentChangesDialogComponent({ ancestorNoteId }: RecentChangesDialogProps) {
|
||||||
|
const [ groupedByDate, setGroupedByDate ] = useState<Map<String, RecentChangesRow[]>>();
|
||||||
|
const [ needsRefresh, setNeedsRefresh ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
if (!groupedByDate || needsRefresh) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (needsRefresh) {
|
||||||
|
setNeedsRefresh(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.get<RecentChangesRow[]>(`recent-changes/${ancestorNoteId}`)
|
||||||
|
.then(async (recentChanges) => {
|
||||||
|
// preload all notes into cache
|
||||||
|
await froca.getNotes(
|
||||||
|
recentChanges.map((r) => r.noteId),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedByDate = groupByDate(recentChanges);
|
||||||
|
setGroupedByDate(groupedByDate);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ancestorNoteId &&
|
||||||
|
<Modal
|
||||||
|
title={t("recent_changes.title")}
|
||||||
|
className="recent-changes-dialog"
|
||||||
|
size="lg"
|
||||||
|
scrollable
|
||||||
|
header={
|
||||||
|
<Button
|
||||||
|
text={t("recent_changes.erase_notes_button")}
|
||||||
|
small style={{ padding: "0 10px" }}
|
||||||
|
onClick={() => {
|
||||||
|
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||||
|
setNeedsRefresh(true);
|
||||||
|
toast.showMessage(t("recent_changes.deleted_notes_message"));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="recent-changes-content">
|
||||||
|
{groupedByDate?.size
|
||||||
|
? <RecentChangesTimeline groupedByDate={groupedByDate} />
|
||||||
|
: <>{t("recent_changes.no_changes_message")}</>}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecentChangesTimeline({ groupedByDate }: { groupedByDate: Map<String, RecentChangesRow[]> }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ Array.from(groupedByDate.entries()).map(([dateDay, dayChanges]) => {
|
||||||
|
const formattedDate = formatDateTime(dateDay as string, "full", "none");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<b>{formattedDate}</b>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{ dayChanges.map((change) => {
|
||||||
|
const isDeleted = change.current_isDeleted;
|
||||||
|
const formattedTime = formatDateTime(change.date, "none", "short");
|
||||||
|
const note = froca.getNoteFromCache(change.noteId);
|
||||||
|
const notePath = note?.getBestNotePathString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={isDeleted ? "deleted-note" : ""}>
|
||||||
|
<span title={change.date}>{formattedTime}</span>
|
||||||
|
{ !isDeleted && notePath
|
||||||
|
? <NoteLink notePath={notePath} title={change.current_title} />
|
||||||
|
: <span className="note-title">{change.current_title}</span> }
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoteLink({ notePath, title }: { notePath: string, title: string }) {
|
||||||
|
if (!notePath || !title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
link.createLink(notePath, {
|
||||||
|
title,
|
||||||
|
showNotePath: true
|
||||||
|
}).then(setNoteLink);
|
||||||
|
}, [notePath, title]);
|
||||||
|
return (
|
||||||
|
noteLink ? <RawHtml className="note-title" html={noteLink[0].innerHTML} /> : <span className="note-title">Foo {title}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RecentChangesDialog extends ReactBasicWidget {
|
||||||
|
|
||||||
|
private props: RecentChangesDialogProps = {};
|
||||||
|
|
||||||
|
get component() {
|
||||||
|
return <RecentChangesDialogComponent {...this.props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
async showRecentChangesEvent({ ancestorNoteId }: EventData<"showRecentChanges">) {
|
||||||
|
this.props = {
|
||||||
|
ancestorNoteId: ancestorNoteId ?? hoisted_note.getHoistedNoteId()
|
||||||
|
};
|
||||||
|
this.doRender();
|
||||||
|
openDialog(this.$widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByDate(rows: RecentChangesRow[]) {
|
||||||
|
const groupedByDate = new Map<String, RecentChangesRow[]>();
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const dateDay = row.date.substr(0, 10);
|
||||||
|
|
||||||
|
if (!groupedByDate.has(dateDay)) {
|
||||||
|
groupedByDate.set(dateDay, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedByDate.get(dateDay)!.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedByDate;
|
||||||
|
}
|
@ -45,3 +45,8 @@ export interface RevisionPojo {
|
|||||||
content?: string | Buffer<ArrayBufferLike>;
|
content?: string | Buffer<ArrayBufferLike>;
|
||||||
contentLength?: number;
|
contentLength?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RecentChangesRow {
|
||||||
|
noteId: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user