refactor(react/dialogs): integrate self-triggering modal in more dialogs

This commit is contained in:
Elian Doran 2025-08-10 00:32:26 +03:00
parent cd5467bf5c
commit fa97ec6c72
No known key found for this signature in database
14 changed files with 241 additions and 244 deletions

View File

@ -1,4 +1,3 @@
import { openDialog } from "../../services/dialog.js";
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import Modal from "../react/Modal.js";
import { t } from "../../services/i18n.js";
@ -9,19 +8,26 @@ import openService from "../../services/open.js";
import { useState } from "preact/hooks";
import type { CSSProperties } from "preact/compat";
import type { AppInfo } from "@triliumnext/commons";
import useTriliumEvent from "../react/hooks.jsx";
function AboutDialogComponent() {
let [appInfo, setAppInfo] = useState<AppInfo | null>(null);
async function onShown() {
const appInfo = await server.get<AppInfo>("app-info");
setAppInfo(appInfo);
}
let [shown, setShown] = useState(false);
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
useTriliumEvent("openAboutDialog", () => setShown(true));
return (
<Modal className="about-dialog" size="lg" title={t("about.title")} onShown={onShown}>
<Modal className="about-dialog"
size="lg"
title={t("about.title")}
show={shown}
onShown={async () => {
const appInfo = await server.get<AppInfo>("app-info");
setAppInfo(appInfo);
}}
onHidden={() => setShown(false)}
>
<table className="table table-borderless">
<tbody>
<tr>
@ -83,7 +89,4 @@ export default class AboutDialog extends ReactBasicWidget {
return <AboutDialogComponent />;
}
async openAboutDialogEvent() {
openDialog(this.$widget);
}
}

View File

@ -1,5 +1,4 @@
import { EventData } from "../../components/app_context";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
@ -10,24 +9,28 @@ import { useRef, useState } from "preact/hooks";
import tree from "../../services/tree";
import { useEffect } from "react";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import { logError } from "../../services/ws";
import FormGroup from "../react/FormGroup.js";
import { refToJQuerySelector } from "../react/react_utils";
import useTriliumEvent from "../react/hooks";
type LinkType = "reference-link" | "external-link" | "hyper-link";
interface AddLinkDialogProps {
text?: string;
textTypeWidget?: TextTypeWidget;
}
function AddLinkDialogComponent({ text: _text, textTypeWidget }: AddLinkDialogProps) {
const [ text, setText ] = useState(_text ?? "");
function AddLinkDialogComponent() {
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
const [ text, setText ] = useState<string>();
const [ linkTitle, setLinkTitle ] = useState("");
const hasSelection = textTypeWidget?.hasSelection();
const [ linkType, setLinkType ] = useState<LinkType>(hasSelection ? "hyper-link" : "reference-link");
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ shown, setShown ] = useState(false);
useTriliumEvent("showAddLinkDialog", ( { textTypeWidget, text }) => {
setTextTypeWidget(textTypeWidget);
setText(text);
setShown(true);
});
async function setDefaultLinkTitle(noteId: string) {
const noteTitle = await tree.getNoteTitle(noteId);
@ -100,7 +103,11 @@ function AddLinkDialogComponent({ text: _text, textTypeWidget }: AddLinkDialogPr
footer={<Button text={t("add_link.button_add_link")} keyboardShortcut="Enter" />}
onSubmit={onSubmit}
onShown={onShown}
onHidden={() => setSuggestion(null)}
onHidden={() => {
setSuggestion(null);
setShown(false);
}}
show={shown}
>
<FormGroup label={t("add_link.note")}>
<NoteAutocomplete
@ -150,18 +157,9 @@ function AddLinkDialogComponent({ text: _text, textTypeWidget }: AddLinkDialogPr
}
export default class AddLinkDialog extends ReactBasicWidget {
private props: AddLinkDialogProps = {};
get component() {
return <AddLinkDialogComponent {...this.props} />;
}
async showAddLinkDialogEvent({ textTypeWidget, text = "" }: EventData<"showAddLinkDialog">) {
this.props.text = text;
this.props.textTypeWidget = textTypeWidget;
this.doRender();
await openDialog(this.$widget);
return <AddLinkDialogComponent />;
}
}

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from "preact/hooks";
import { EventData } from "../../components/app_context";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
@ -12,50 +11,52 @@ import Button from "../react/Button";
import bulk_action from "../../services/bulk_action";
import toast from "../../services/toast";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import { RawHtmlBlock } from "../react/RawHtml";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import useTriliumEvent from "../react/hooks";
interface BulkActionProps {
bulkActionNote?: FNote | null;
selectedOrActiveNoteIds?: string[];
}
function BulkActionComponent({ selectedOrActiveNoteIds, bulkActionNote }: BulkActionProps) {
function BulkActionComponent() {
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
const [ bulkActionNote, setBulkActionNote ] = useState<FNote | null>();
const [ includeDescendants, setIncludeDescendants ] = useState(false);
const [ affectedNoteCount, setAffectedNoteCount ] = useState(0);
const [ existingActions, setExistingActions ] = useState<RenameNoteBulkAction[]>([]);
const [ shown, setShown ] = useState(false);
if (!selectedOrActiveNoteIds || !bulkActionNote) {
return;
}
useEffect(() => {
server.post<BulkActionAffectedNotes>("bulk-action/affected-notes", {
noteIds: selectedOrActiveNoteIds,
includeDescendants
}).then(({ affectedNoteCount }) => setAffectedNoteCount(affectedNoteCount));
}, [ selectedOrActiveNoteIds, includeDescendants ]);
function refreshExistingActions() {
setExistingActions(bulk_action.parseActions(bulkActionNote!));
}
useEffect(() => refreshExistingActions(), []);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((row) =>
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
refreshExistingActions();
}
useTriliumEvent("openBulkActionsDialog", async ({ selectedOrActiveNoteIds }) => {
setSelectedOrActiveNoteIds(selectedOrActiveNoteIds);
setBulkActionNote(await froca.getNote("_bulkAction"));
setShown(true);
});
return ( selectedOrActiveNoteIds &&
if (selectedOrActiveNoteIds && bulkActionNote) {
useEffect(() => {
server.post<BulkActionAffectedNotes>("bulk-action/affected-notes", {
noteIds: selectedOrActiveNoteIds,
includeDescendants
}).then(({ affectedNoteCount }) => setAffectedNoteCount(affectedNoteCount));
}, [ selectedOrActiveNoteIds, includeDescendants ]);
function refreshExistingActions() {
setExistingActions(bulk_action.parseActions(bulkActionNote!));
}
useEffect(() => refreshExistingActions(), []);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((row) =>
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
refreshExistingActions();
}
});
}
return (
<Modal
className="bulk-actions-dialog"
size="xl"
title={t("bulk_actions.bulk_actions")}
footer={<Button text={t("bulk_actions.execute_bulk_actions")} primary />}
show={shown}
onSubmit={async () => {
await server.post("bulk-action/execute", {
noteIds: selectedOrActiveNoteIds,
@ -65,6 +66,7 @@ function BulkActionComponent({ selectedOrActiveNoteIds, bulkActionNote }: BulkAc
toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
closeActiveDialog();
}}
onHidden={() => setShown(false)}
>
<h4>{t("bulk_actions.affected_notes")}: <span>{affectedNoteCount}</span></h4>
<FormCheckbox
@ -114,19 +116,8 @@ function ExistingActionsList({ existingActions }: { existingActions?: RenameNote
export default class BulkActionsDialog extends ReactBasicWidget {
private props: BulkActionProps = {};
get component() {
return <BulkActionComponent {...this.props} />
}
async openBulkActionsDialogEvent({ selectedOrActiveNoteIds }: EventData<"openBulkActionsDialog">) {
this.props = {
selectedOrActiveNoteIds,
bulkActionNote: await froca.getNote("_bulkAction")
};
this.doRender();
openDialog(this.$widget);
return <BulkActionComponent />
}
}

View File

@ -1,6 +1,6 @@
import { useRef, useState } from "preact/compat";
import appContext, { EventData } from "../../components/app_context";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import appContext from "../../components/app_context";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
@ -15,16 +15,32 @@ import tree from "../../services/tree";
import branches from "../../services/branches";
import toast from "../../services/toast";
import NoteList from "../react/NoteList";
import useTriliumEvent from "../react/hooks";
interface CloneToDialogProps {
clonedNoteIds?: string[];
}
function CloneToDialogComponent({ clonedNoteIds }: CloneToDialogProps) {
function CloneToDialogComponent() {
const [ clonedNoteIds, setClonedNoteIds ] = useState<string[]>();
const [ prefix, setPrefix ] = useState("");
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ shown, setShown ] = useState(false);
const autoCompleteRef = useRef<HTMLInputElement>(null);
useTriliumEvent("cloneNoteIdsTo", ({ noteIds }) => {
if (!noteIds || noteIds.length === 0) {
noteIds = [appContext.tabManager.getActiveContextNoteId() ?? ""];
}
const clonedNoteIds: string[] = [];
for (const noteId of noteIds) {
if (!clonedNoteIds.includes(noteId)) {
clonedNoteIds.push(noteId);
}
}
setClonedNoteIds(clonedNoteIds);
setShown(true);
});
function onSubmit() {
if (!clonedNoteIds) {
return;
@ -49,6 +65,8 @@ function CloneToDialogComponent({ clonedNoteIds }: CloneToDialogProps) {
footer={<Button text={t("clone_to.clone_to_selected_note")} keyboardShortcut="Enter" />}
onSubmit={onSubmit}
onShown={() => triggerRecentNotes(autoCompleteRef.current)}
onHidden={() => setShown(false)}
show={shown}
>
<h5>{t("clone_to.notes_to_clone")}</h5>
<NoteList style={{ maxHeight: "200px", overflow: "auto" }} noteIds={clonedNoteIds} />
@ -67,29 +85,9 @@ function CloneToDialogComponent({ clonedNoteIds }: CloneToDialogProps) {
}
export default class CloneToDialog extends ReactBasicWidget {
private props: CloneToDialogProps = {};
get component() {
return <CloneToDialogComponent {...this.props} />;
}
async cloneNoteIdsToEvent({ noteIds }: EventData<"cloneNoteIdsTo">) {
if (!noteIds || noteIds.length === 0) {
noteIds = [appContext.tabManager.getActiveContextNoteId() ?? ""];
}
const clonedNoteIds: string[] = [];
for (const noteId of noteIds) {
if (!clonedNoteIds.includes(noteId)) {
clonedNoteIds.push(noteId);
}
}
this.props = { clonedNoteIds };
this.doRender();
openDialog(this.$widget);
return <CloneToDialogComponent />;
}
}

View File

@ -1,4 +1,3 @@
import { openDialog } from "../../services/dialog.js";
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import Modal from "../react/Modal.jsx";
import { t } from "../../services/i18n.js";
@ -7,10 +6,18 @@ import { CommandNames } from "../../components/app_context.js";
import RawHtml from "../react/RawHtml.jsx";
import { useEffect, useState } from "preact/hooks";
import keyboard_actions from "../../services/keyboard_actions.js";
import useTriliumEvent from "../react/hooks.jsx";
function HelpDialogComponent() {
const [ shown, setShown ] = useState(false);
useTriliumEvent("showCheatsheet", () => setShown(true));
return (
<Modal title={t("help.title")} className="help-dialog use-tn-links" minWidth="90%" size="lg" scrollable>
<Modal
title={t("help.title")} className="help-dialog use-tn-links" minWidth="90%" size="lg" scrollable
onHidden={() => setShown(false)}
show={shown}
>
<div className="help-cards row row-cols-md-3 g-3">
<Card title={t("help.noteNavigation")}>
<ul>
@ -161,7 +168,4 @@ export default class HelpDialog extends ReactBasicWidget {
return <HelpDialogComponent />;
}
showCheatsheetEvent() {
openDialog(this.$widget);
}
}

View File

@ -1,13 +1,17 @@
import { useRef } from "react";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
import { closeActiveDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import Button from "../react/Button.js";
import Modal from "../react/Modal.js";
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import { useState } from "preact/hooks";
import useTriliumEvent from "../react/hooks.jsx";
function IncorrectCpuArchDialogComponent() {
const [ shown, setShown ] = useState(false);
const downloadButtonRef = useRef<HTMLButtonElement>(null);
useTriliumEvent("showCpuArchWarning", () => setShown(true));
return (
<Modal
@ -33,6 +37,8 @@ function IncorrectCpuArchDialogComponent() {
<Button text={t("cpu_arch_warning.continue_anyway")}
onClick={() => closeActiveDialog()} />
</>}
onHidden={() => setShown(false)}
show={shown}
>
<p>{utils.isMac() ? t("cpu_arch_warning.message_macos") : t("cpu_arch_warning.message_windows")}</p>
<p>{t("cpu_arch_warning.recommendation")}</p>
@ -46,8 +52,4 @@ export default class IncorrectCpuArchDialog extends ReactBasicWidget {
return <IncorrectCpuArchDialogComponent />
}
showCpuArchWarningEvent() {
openDialog(this.$widget);
}
}

View File

@ -1,6 +1,6 @@
import { useRef, useState } from "preact/compat";
import { useCallback, useRef, useState } from "preact/compat";
import appContext from "../../components/app_context";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
@ -8,6 +8,7 @@ import utils from "../../services/utils";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import Button from "../react/Button";
import useTriliumEvent from "../react/hooks";
interface RenderMarkdownResponse {
htmlContent: string;
@ -15,7 +16,26 @@ interface RenderMarkdownResponse {
function MarkdownImportDialogComponent() {
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
let [ text, setText ] = useState("");
let [ text, setText ] = useState("");
let [ shown, setShown ] = useState(false);
const triggerImport = useCallback(() => {
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
return;
}
if (utils.isElectron()) {
const { clipboard } = utils.dynamicRequire("electron");
const text = clipboard.readText();
convertMarkdownToHtml(text);
} else {
setShown(true);
}
}, []);
useTriliumEvent("importMarkdownInline", triggerImport);
useTriliumEvent("pasteMarkdownIntoText", triggerImport);
async function sendForm() {
await convertMarkdownToHtml(text);
@ -28,6 +48,8 @@ function MarkdownImportDialogComponent() {
className="markdown-import-dialog" title={t("markdown_import.dialog_title")} size="lg"
footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={sendForm} keyboardShortcut="Ctrl+Space" />}
onShown={() => markdownImportTextArea.current?.focus()}
onHidden={() => setShown(false) }
show={shown}
>
<p>{t("markdown_import.modal_body_text")}</p>
<textarea ref={markdownImportTextArea} value={text}
@ -49,26 +71,6 @@ export default class MarkdownImportDialog extends ReactBasicWidget {
return <MarkdownImportDialogComponent />;
}
async importMarkdownInlineEvent() {
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
return;
}
if (utils.isElectron()) {
const { clipboard } = utils.dynamicRequire("electron");
const text = clipboard.readText();
convertMarkdownToHtml(text);
} else {
openDialog(this.$widget);
}
}
async pasteMarkdownIntoTextEvent() {
// BC with keyboard shortcuts command
await this.importMarkdownInlineEvent();
}
}
async function convertMarkdownToHtml(markdownContent: string) {

View File

@ -1,8 +1,7 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import { t } from "../../services/i18n";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { EventData } from "../../components/app_context";
import { closeActiveDialog } from "../../services/dialog";
import NoteList from "../react/NoteList";
import FormGroup from "../react/FormGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
@ -13,15 +12,19 @@ import tree from "../../services/tree";
import froca from "../../services/froca";
import branches from "../../services/branches";
import toast from "../../services/toast";
import useTriliumEvent from "../react/hooks";
interface MoveToDialogProps {
movedBranchIds?: string[];
}
function MoveToDialogComponent({ movedBranchIds }: MoveToDialogProps) {
function MoveToDialogComponent() {
const [ movedBranchIds, setMovedBranchIds ] = useState<string[]>();
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ shown, setShown ] = useState(false);
const autoCompleteRef = useRef<HTMLInputElement>(null);
useTriliumEvent("moveBranchIdsTo", ({ branchIds }) => {
setMovedBranchIds(branchIds);
setShown(true);
});
async function onSubmit() {
const notePath = suggestion?.notePath;
if (!notePath) {
@ -49,6 +52,8 @@ function MoveToDialogComponent({ movedBranchIds }: MoveToDialogProps) {
footer={<Button text={t("move_to.move_button")} keyboardShortcut="Enter" />}
onSubmit={onSubmit}
onShown={() => triggerRecentNotes(autoCompleteRef.current)}
onHidden={() => setShown(false)}
show={shown}
>
<h5>{t("move_to.notes_to_move")}</h5>
<NoteList branchIds={movedBranchIds} />
@ -65,17 +70,8 @@ function MoveToDialogComponent({ movedBranchIds }: MoveToDialogProps) {
export default class MoveToDialog extends ReactBasicWidget {
private props: MoveToDialogProps = {};
get component() {
return <MoveToDialogComponent {...this.props} />;
}
async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
const movedBranchIds = branchIds;
this.props = { movedBranchIds };
this.doRender();
openDialog(this.$widget);
return <MoveToDialogComponent />;
}
}

View File

@ -1,11 +1,16 @@
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import appContext from "../../components/app_context";
import { useState } from "preact/hooks";
import useTriliumEvent from "../react/hooks";
function PasswordNotSetDialogComponent() {
const [ shown, setShown ] = useState(false);
useTriliumEvent("showPasswordNotSet", () => setShown(true));
return (
<Modal
size="md" className="password-not-set-dialog"
@ -14,6 +19,8 @@ function PasswordNotSetDialogComponent() {
closeActiveDialog();
appContext.triggerCommand("showOptions", { section: "_optionsPassword" });
}} />}
onHidden={() => setShown(false)}
show={shown}
>
<p>{t("password_not_set.body1")}</p>
<p>{t("password_not_set.body2")}</p>
@ -27,8 +34,4 @@ export default class PasswordNotSetDialog extends ReactBasicWidget {
return <PasswordNotSetDialogComponent />;
}
showPasswordNotSetEvent() {
openDialog(this.$widget);
}
}

View File

@ -1,16 +1,20 @@
import { useRef, useState } from "preact/hooks";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import FormTextBox from "../react/FormTextBox";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import protected_session from "../../services/protected_session";
import useTriliumEvent from "../react/hooks";
function ProtectedSessionPasswordDialogComponent() {
const [ shown, setShown ] = useState(false);
const [ password, setPassword ] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
useTriliumEvent("showProtectedSessionPasswordDialog", () => setShown(true));
return (
<Modal
className="protected-session-password-dialog"
@ -20,6 +24,8 @@ function ProtectedSessionPasswordDialogComponent() {
footer={<Button text={t("protected_session_password.start_button")} />}
onSubmit={() => protected_session.setupProtectedSession(password)}
onShown={() => inputRef.current?.focus()}
onHidden={() => setShown(false)}
show={shown}
>
<label htmlFor="protected-session-password" className="col-form-label">{t("protected_session_password.form_label")}</label>
<FormTextBox
@ -39,10 +45,6 @@ export default class ProtectedSessionPasswordDialog extends ReactBasicWidget {
return <ProtectedSessionPasswordDialogComponent />;
}
showProtectedSessionPasswordDialogEvent() {
openDialog(this.$widget);
}
closeProtectedSessionPasswordDialogEvent() {
closeActiveDialog();
}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "preact/hooks";
import appContext, { EventData } from "../../components/app_context";
import dialog, { closeActiveDialog, openDialog } from "../../services/dialog";
import appContext from "../../components/app_context";
import dialog, { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
@ -14,14 +14,19 @@ import { formatDateTime } from "../../utils/formatters";
import link from "../../services/link";
import RawHtml from "../react/RawHtml";
import ws from "../../services/ws";
import useTriliumEvent from "../react/hooks";
interface RecentChangesDialogProps {
ancestorNoteId?: string;
}
function RecentChangesDialogComponent({ ancestorNoteId }: RecentChangesDialogProps) {
function RecentChangesDialogComponent() {
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
const [ groupedByDate, setGroupedByDate ] = useState<Map<String, RecentChangesRow[]>>();
const [ needsRefresh, setNeedsRefresh ] = useState<boolean>(false);
const [ needsRefresh, setNeedsRefresh ] = useState(false);
const [ shown, setShown ] = useState(false);
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
setNeedsRefresh(true);
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
setShown(true);
});
if (!groupedByDate || needsRefresh) {
useEffect(() => {
@ -43,7 +48,7 @@ function RecentChangesDialogComponent({ ancestorNoteId }: RecentChangesDialogPro
})
}
return (ancestorNoteId &&
return (
<Modal
title={t("recent_changes.title")}
className="recent-changes-dialog"
@ -61,6 +66,8 @@ function RecentChangesDialogComponent({ ancestorNoteId }: RecentChangesDialogPro
}}
/>
}
onHidden={() => setShown(false)}
show={shown}
>
<div className="recent-changes-content">
{groupedByDate?.size
@ -152,18 +159,8 @@ function DeletedNoteLink({ change }: { change: RecentChangesRow }) {
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);
return <RecentChangesDialogComponent />
}
}

View File

@ -1,7 +1,7 @@
import type { RevisionPojo, RevisionItem } from "@triliumnext/commons";
import appContext, { EventData } from "../../components/app_context";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import dialog, { closeActiveDialog, openDialog } from "../../services/dialog";
import dialog, { closeActiveDialog } from "../../services/dialog";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import server from "../../services/server";
@ -18,26 +18,35 @@ import { CSSProperties } from "preact/compat";
import open from "../../services/open";
import ActionButton from "../react/ActionButton";
import options from "../../services/options";
import useTriliumEvent from "../react/hooks";
interface RevisionsDialogProps {
note?: FNote;
}
function RevisionsDialogComponent({ note }: RevisionsDialogProps) {
const [ revisions, setRevisions ] = useState<RevisionItem[]>([]);
function RevisionsDialogComponent() {
const [ note, setNote ] = useState<FNote>();
const [ revisions, setRevisions ] = useState<RevisionItem[]>();
const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
const [ shown, setShown ] = useState(false);
if (note) {
useEffect(() => {
useTriliumEvent("showRevisions", async ({ noteId }) => {
const note = await getNote(noteId);
if (note) {
setNote(note);
setShown(true);
}
});
useEffect(() => {
if (note?.noteId) {
server.get<RevisionItem[]>(`notes/${note.noteId}/revisions`).then(setRevisions);
}, [ note.noteId ]);
}
} else {
setRevisions(undefined);
}
}, [ note?.noteId ]);
if (revisions?.length && !currentRevision) {
setCurrentRevision(revisions[0]);
}
return (note &&
return (
<Modal
className="revisions-dialog"
size="xl"
@ -49,7 +58,7 @@ function RevisionsDialogComponent({ note }: RevisionsDialogProps) {
onClick={async () => {
const text = t("revisions.confirm_delete_all");
if (await dialog.confirm(text)) {
if (note && await dialog.confirm(text)) {
await server.remove(`notes/${note.noteId}/revisions`);
closeActiveDialog();
@ -59,11 +68,16 @@ function RevisionsDialogComponent({ note }: RevisionsDialogProps) {
}
footer={<RevisionFooter note={note} />}
footerStyle={{ paddingTop: 0, paddingBottom: 0 }}
onHidden={() => {
setShown(false);
setNote(undefined);
}}
show={shown}
>
<RevisionsList
revisions={revisions}
revisions={revisions ?? []}
onSelect={(revisionId) => {
const correspondingRevision = revisions.find((r) => r.revisionId === revisionId);
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
if (correspondingRevision) {
setCurrentRevision(correspondingRevision);
}
@ -239,7 +253,7 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
}
}
function RevisionFooter({ note }: { note: FNote }) {
function RevisionFooter({ note }: { note?: FNote }) {
if (!note) {
return <></>;
}
@ -268,18 +282,8 @@ function RevisionFooter({ note }: { note: FNote }) {
export default class RevisionsDialog extends ReactBasicWidget {
private props: RevisionsDialogProps = {};
get component() {
return <RevisionsDialogComponent {...this.props} />
}
async showRevisionsEvent({ noteId }: EventData<"showRevisions">) {
this.props = {
note: await getNote(noteId) ?? undefined
};
this.doRender();
openDialog(this.$widget);
return <RevisionsDialogComponent />
}
}

View File

@ -1,6 +1,5 @@
import { useState } from "preact/hooks";
import { EventData } from "../../components/app_context";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import FormCheckbox from "../react/FormCheckbox";
@ -10,13 +9,21 @@ import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import server from "../../services/server";
import FormGroup from "../react/FormGroup";
import useTriliumEvent from "../react/hooks";
function SortChildNotesDialogComponent({ parentNoteId }: { parentNoteId?: string }) {
function SortChildNotesDialogComponent() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
const [ sortBy, setSortBy ] = useState("title");
const [ sortDirection, setSortDirection ] = useState("asc");
const [ foldersFirst, setFoldersFirst ] = useState(false);
const [ sortNatural, setSortNatural ] = useState(false);
const [ sortLocale, setSortLocale ] = useState("");
const [ shown, setShown ] = useState(false);
useTriliumEvent("sortChildNotes", ({ node }) => {
setParentNoteId(node.data.noteId);
setShown(true);
});
async function onSubmit() {
await server.put(`notes/${parentNoteId}/sort-children`, {
@ -31,12 +38,14 @@ function SortChildNotesDialogComponent({ parentNoteId }: { parentNoteId?: string
closeActiveDialog();
}
return (parentNoteId &&
return (
<Modal
className="sort-child-notes-dialog"
title={t("sort_child_notes.sort_children_by")}
size="lg" maxWidth={500}
onSubmit={onSubmit}
onHidden={() => setShown(false)}
show={shown}
footer={<Button text={t("sort_child_notes.sort")} keyboardShortcut="Enter" />}
>
<h5>{t("sort_child_notes.sorting_criteria")}</h5>
@ -88,17 +97,8 @@ function SortChildNotesDialogComponent({ parentNoteId }: { parentNoteId?: string
export default class SortChildNotesDialog extends ReactBasicWidget {
private parentNoteId?: string;
get component() {
return <SortChildNotesDialogComponent parentNoteId={this.parentNoteId} />;
return <SortChildNotesDialogComponent />;
}
async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) {
this.parentNoteId = node.data.noteId;
this.doRender();
openDialog(this.$widget);
}
}

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "preact/compat";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import FormCheckbox from "../react/FormCheckbox";
@ -9,18 +9,21 @@ import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import options from "../../services/options";
import importService from "../../services/import.js";
import { EventData } from "../../components/app_context";
import tree from "../../services/tree";
import useTriliumEvent from "../react/hooks";
interface UploadAttachmentsDialogProps {
parentNoteId?: string;
}
function UploadAttachmentsDialogComponent({ parentNoteId }: UploadAttachmentsDialogProps) {
function UploadAttachmentsDialogComponent() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
const [ files, setFiles ] = useState<FileList | null>(null);
const [ shrinkImages, setShrinkImages ] = useState(options.is("compressImages"));
const [ isUploading, setIsUploading ] = useState(false);
const [ description, setDescription ] = useState<string | undefined>(undefined);
const [ shown, setShown ] = useState(false);
useTriliumEvent("showUploadAttachmentsDialog", ({ noteId }) => {
setParentNoteId(noteId);
setShown(true);
});
if (parentNoteId) {
useEffect(() => {
@ -29,23 +32,25 @@ function UploadAttachmentsDialogComponent({ parentNoteId }: UploadAttachmentsDia
}, [parentNoteId]);
}
return (parentNoteId &&
return (
<Modal
className="upload-attachments-dialog"
size="lg"
title={t("upload_attachments.upload_attachments_to_note")}
footer={<Button text={t("upload_attachments.upload")} primary disabled={!files || isUploading} />}
onSubmit={async () => {
if (!files) {
if (!files || !parentNoteId) {
return;
}
setIsUploading(true);
const filesCopy = Array.from(files);
await importService.uploadFiles("attachments", parentNoteId, filesCopy, { shrinkImages });
setIsUploading(false);
closeActiveDialog();
}}
onHidden={() => setShown(false)}
show={shown}
>
<FormGroup label={t("upload_attachments.choose_files")} description={description}>
<FormFileUpload onChange={setFiles} multiple />
@ -64,16 +69,8 @@ function UploadAttachmentsDialogComponent({ parentNoteId }: UploadAttachmentsDia
export default class UploadAttachmentsDialog extends ReactBasicWidget {
private props: UploadAttachmentsDialogProps = {};
get component() {
return <UploadAttachmentsDialogComponent {...this.props} />;
}
showUploadAttachmentsDialogEvent({ noteId }: EventData<"showUploadAttachmentsDialog">) {
this.props = { parentNoteId: noteId };
this.doRender();
openDialog(this.$widget);
return <UploadAttachmentsDialogComponent />;
}
}