refactor(react/dialogs): use shown everywhere

This commit is contained in:
Elian Doran
2025-08-10 11:38:12 +03:00
parent fa97ec6c72
commit b7482f2a6a
11 changed files with 215 additions and 242 deletions

View File

@@ -30,6 +30,7 @@ import type CodeMirror from "@triliumnext/codemirror";
import { StartupChecks } from "./startup_checks.js";
import type { CreateNoteOpts } from "../services/note_create.js";
import { ColumnComponent } from "tabulator-tables";
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@@ -370,6 +371,9 @@ export type CommandMappings = {
};
refreshTouchBar: CommandData;
reloadTextEditor: CommandData;
chooseNoteType: CommandData & {
callback: ChooseNoteTypeCallback
}
};
type EventMappings = {

View File

@@ -109,8 +109,6 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
async function chooseNoteType() {
return new Promise<ChooseNoteTypeResponse>((res) => {
// TODO: Remove ignore after callback for chooseNoteType is defined in app_context.ts
//@ts-ignore
appContext.triggerCommand("chooseNoteType", { callback: res });
});
}

View File

@@ -1,36 +1,51 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import Button from "../react/Button";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import { useState } from "react";
import FormCheckbox from "../react/FormCheckbox";
import useTriliumEvent from "../react/hooks";
interface ConfirmDialogProps {
title?: string;
message?: string | HTMLElement;
callback?: ConfirmDialogCallback;
lastElementToFocus?: HTMLElement | null;
isConfirmDeleteNoteBox?: boolean;
}
function ConfirmDialogComponent({ title, message, callback, lastElementToFocus, isConfirmDeleteNoteBox }: ConfirmDialogProps) {
function ConfirmDialogComponent() {
const [ opts, setOpts ] = useState<ConfirmDialogProps>();
const [ confirmed, setConfirmed ] = useState<boolean>(false);
const [ isDeleteNoteChecked, setIsDeleteNoteChecked ] = useState<boolean>(false);
const [ isDeleteNoteChecked, setIsDeleteNoteChecked ] = useState(false);
const [ shown, setShown ] = useState(false);
function showDialog(title: string | null, message: MessageType, callback: ConfirmDialogCallback, isConfirmDeleteNoteBox: boolean) {
setOpts({
title: title ?? undefined,
message: (typeof message === "object" && "length" in message ? message[0] : message),
callback,
isConfirmDeleteNoteBox
});
setShown(true);
}
useTriliumEvent("showConfirmDialog", ({ message, callback }) => showDialog(null, message, callback, false));
useTriliumEvent("showConfirmDeleteNoteBoxWithNoteDialog", ({ title, callback }) => showDialog(title, t("confirm.are_you_sure_remove_note", { title: title }), callback, true));
return (
<Modal
className="confirm-dialog"
title={title ?? t("confirm.confirmation")}
title={opts?.title ?? t("confirm.confirmation")}
size="md"
zIndex={2000}
scrollable={true}
onHidden={() => {
callback?.({
opts?.callback?.({
confirmed,
isDeleteNoteChecked
});
lastElementToFocus?.focus();
setShown(false);
}}
footer={<>
<Button text={t("confirm.cancel")} onClick={() => closeActiveDialog()} />
@@ -39,12 +54,13 @@ function ConfirmDialogComponent({ title, message, callback, lastElementToFocus,
closeActiveDialog();
}} />
</>}
show={shown}
>
{!message || typeof message === "string"
? <div>{(message as string) ?? ""}</div>
: <div dangerouslySetInnerHTML={{ __html: message.outerHTML ?? "" }} />}
{!opts?.message || typeof opts?.message === "string"
? <div>{(opts?.message as string) ?? ""}</div>
: <div dangerouslySetInnerHTML={{ __html: opts?.message.outerHTML ?? "" }} />}
{isConfirmDeleteNoteBox && (
{opts?.isConfirmDeleteNoteBox && (
<FormCheckbox
name="confirm-dialog-delete-note"
label={t("confirm.also_delete_note")}
@@ -77,31 +93,8 @@ export interface ConfirmWithTitleOptions {
export default class ConfirmDialog extends ReactBasicWidget {
private props: ConfirmDialogProps = {};
get component() {
return <ConfirmDialogComponent {...this.props} />;
}
showConfirmDialogEvent({ message, callback }: ConfirmWithMessageOptions) {
this.showDialog(null, message, callback, false);
}
showConfirmDeleteNoteBoxWithNoteDialogEvent({ title, callback }: ConfirmWithTitleOptions) {
const message = t("confirm.are_you_sure_remove_note", { title: title });
this.showDialog(title, message, callback, true);
}
private showDialog(title: string | null, message: MessageType, callback: ConfirmDialogCallback, isConfirmDeleteNoteBox: boolean) {
this.props = {
title: title ?? undefined,
message: (typeof message === "object" && "length" in message ? message[0] : message),
lastElementToFocus: (document.activeElement as HTMLElement),
callback,
isConfirmDeleteNoteBox
};
this.doRender();
openDialog(this.$widget);
return <ConfirmDialogComponent />;
}
}

View File

@@ -1,5 +1,5 @@
import { useRef, useState } from "preact/hooks";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
import { closeActiveDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js";
import FormCheckbox from "../react/FormCheckbox.js";
import Modal from "../react/Modal.js";
@@ -12,6 +12,7 @@ import FNote from "../../entities/fnote.js";
import link from "../../services/link.js";
import Button from "../react/Button.jsx";
import Alert from "../react/Alert.jsx";
import useTriliumEvent from "../react/hooks.jsx";
export interface ResolveOptions {
proceed: boolean;
@@ -25,22 +26,28 @@ interface ShowDeleteNotesDialogOpts {
forceDeleteAllClones?: boolean;
}
interface ShowDeleteNotesDialogProps extends ShowDeleteNotesDialogOpts { }
interface BrokenRelationData {
note: string;
relation: string;
source: string;
}
function DeleteNotesDialogComponent({ forceDeleteAllClones, branchIdsToDelete, callback }: ShowDeleteNotesDialogProps) {
function DeleteNotesDialogComponent() {
const [ opts, setOpts ] = useState<ShowDeleteNotesDialogOpts>({});
const [ deleteAllClones, setDeleteAllClones ] = useState(false);
const [ eraseNotes, setEraseNotes ] = useState(!!forceDeleteAllClones);
const [ eraseNotes, setEraseNotes ] = useState(!!opts.forceDeleteAllClones);
const [ brokenRelations, setBrokenRelations ] = useState<DeleteNotesPreview["brokenRelations"]>([]);
const [ noteIdsToBeDeleted, setNoteIdsToBeDeleted ] = useState<DeleteNotesPreview["noteIdsToBeDeleted"]>([]);
const [ shown, setShown ] = useState(false);
const okButtonRef = useRef<HTMLButtonElement>(null);
useTriliumEvent("showDeleteNotesDialog", (opts) => {
setOpts(opts);
setShown(true);
})
useEffect(() => {
const { branchIdsToDelete, forceDeleteAllClones } = opts;
if (!branchIdsToDelete || branchIdsToDelete.length === 0) {
return;
}
@@ -52,7 +59,7 @@ function DeleteNotesDialogComponent({ forceDeleteAllClones, branchIdsToDelete, c
setBrokenRelations(response.brokenRelations);
setNoteIdsToBeDeleted(response.noteIdsToBeDeleted);
});
}, [ branchIdsToDelete, deleteAllClones, forceDeleteAllClones ]);
}, [ opts ]);
return (
<Modal
@@ -61,24 +68,28 @@ function DeleteNotesDialogComponent({ forceDeleteAllClones, branchIdsToDelete, c
scrollable
title={t("delete_notes.delete_notes_preview")}
onShown={() => okButtonRef.current?.focus()}
onHidden={() => callback?.({ proceed: false })}
onHidden={() => {
opts.callback?.({ proceed: false })
setShown(false);
}}
footer={<>
<Button text={t("delete_notes.cancel")}
onClick={() => closeActiveDialog()} />
<Button text={t("delete_notes.ok")} primary
buttonRef={okButtonRef}
onClick={() => {
callback?.({ proceed: true, deleteAllClones, eraseNotes });
opts.callback?.({ proceed: true, deleteAllClones, eraseNotes });
closeActiveDialog();
}} />
</>}
show={shown}
>
<FormCheckbox name="delete-all-clones" label={t("delete_notes.delete_all_clones_description")}
currentValue={deleteAllClones} onChange={setDeleteAllClones}
/>
<FormCheckbox
name="erase-notes" label={t("delete_notes.erase_notes_warning")}
disabled={forceDeleteAllClones}
disabled={opts.forceDeleteAllClones}
currentValue={eraseNotes} onChange={setEraseNotes}
/>
@@ -161,20 +172,8 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
export default class DeleteNotesDialog extends ReactBasicWidget {
private props: ShowDeleteNotesDialogProps = {};
get component() {
return <DeleteNotesDialogComponent {...this.props} />;
}
async showDeleteNotesDialogEvent({ branchIdsToDelete, callback, forceDeleteAllClones }: ShowDeleteNotesDialogOpts) {
this.props = {
branchIdsToDelete,
callback,
forceDeleteAllClones
};
this.doRender();
openDialog(this.$widget);
return <DeleteNotesDialogComponent />;
}
}

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 tree from "../../services/tree";
import Button from "../react/Button";
@@ -13,6 +12,7 @@ import toastService, { ToastOptions } from "../../services/toast";
import utils from "../../services/utils";
import open from "../../services/open";
import froca from "../../services/froca";
import useTriliumEvent from "../react/hooks";
interface ExportDialogProps {
branchId?: string | null;
@@ -20,24 +20,48 @@ interface ExportDialogProps {
defaultType?: "subtree" | "single";
}
function ExportDialogComponent({ branchId, noteTitle, defaultType }: ExportDialogProps) {
const [ exportType, setExportType ] = useState<string>(defaultType ?? "subtree");
const [ subtreeFormat, setSubtreeFormat ] = useState<string>("html");
const [ singleFormat, setSingleFormat ] = useState<string>("html");
const [ opmlVersion, setOpmlVersion ] = useState<string>("2.0");
function ExportDialogComponent() {
const [ opts, setOpts ] = useState<ExportDialogProps>();
const [ exportType, setExportType ] = useState(opts?.defaultType ?? "subtree");
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
const [ singleFormat, setSingleFormat ] = useState("html");
const [ opmlVersion, setOpmlVersion ] = useState("2.0");
const [ shown, setShown ] = useState(false);
return (branchId &&
useTriliumEvent("showExportDialog", async ({ notePath, defaultType }) => {
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
if (!parentNoteId) {
return;
}
const branchId = await froca.getBranchId(parentNoteId, noteId);
setOpts({
noteTitle: noteId && await tree.getNoteTitle(noteId),
defaultType,
branchId
});
setShown(true);
});
return (
<Modal
className="export-dialog"
title={`${t("export.export_note_title")} ${noteTitle ?? ""}`}
title={`${t("export.export_note_title")} ${opts?.noteTitle ?? ""}`}
size="lg"
onSubmit={() => {
if (!opts || !opts.branchId) {
return;
}
const format = (exportType === "subtree" ? subtreeFormat : singleFormat);
const version = (format === "opml" ? opmlVersion : "1.0");
exportBranch(branchId, exportType, format, version);
exportBranch(opts.branchId, exportType, format, version);
closeActiveDialog();
}}
onHidden={() => setShown(false)}
footer={<Button className="export-button" text={t("export.export")} primary />}
show={shown}
>
<FormRadioGroup
@@ -104,29 +128,10 @@ function ExportDialogComponent({ branchId, noteTitle, defaultType }: ExportDialo
export default class ExportDialog extends ReactBasicWidget {
private props: ExportDialogProps = {};
get component() {
return <ExportDialogComponent {...this.props} />
return <ExportDialogComponent />
}
async showExportDialogEvent({ notePath, defaultType }: EventData<"showExportDialog">) {
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
if (!parentNoteId) {
return;
}
const branchId = await froca.getBranchId(parentNoteId, noteId);
this.props = {
noteTitle: noteId && await tree.getNoteTitle(noteId),
defaultType,
branchId
};
this.doRender();
openDialog(this.$widget);
}
}
function exportBranch(branchId: string, type: string, format: string, version: string) {

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 tree from "../../services/tree";
import Button from "../react/Button";
@@ -11,13 +10,11 @@ import Modal from "../react/Modal";
import RawHtml from "../react/RawHtml";
import ReactBasicWidget from "../react/ReactBasicWidget";
import importService, { UploadFilesOptions } from "../../services/import";
import useTriliumEvent from "../react/hooks";
interface ImportDialogComponentProps {
parentNoteId?: string;
noteTitle?: string;
}
function ImportDialogComponent({ parentNoteId, noteTitle }: ImportDialogComponentProps) {
function ImportDialogComponent() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
const [ noteTitle, setNoteTitle ] = useState<string>();
const [ files, setFiles ] = useState<FileList | null>(null);
const [ safeImport, setSafeImport ] = useState(true);
const [ explodeArchives, setExplodeArchives ] = useState(true);
@@ -25,14 +22,21 @@ function ImportDialogComponent({ parentNoteId, noteTitle }: ImportDialogCompone
const [ textImportedAsText, setTextImportedAsText ] = useState(true);
const [ codeImportedAsCode, setCodeImportedAsCode ] = useState(true);
const [ replaceUnderscoresWithSpaces, setReplaceUnderscoresWithSpaces ] = useState(true);
const [ shown, setShown ] = useState(false);
return (parentNoteId &&
useTriliumEvent("showImportDialog", ({ noteId }) => {
setParentNoteId(noteId);
tree.getNoteTitle(noteId).then(setNoteTitle);
setShown(true);
});
return (
<Modal
className="import-dialog"
size="lg"
title={t("import.importIntoNote")}
onSubmit={async () => {
if (!files) {
if (!files || !parentNoteId) {
return;
}
@@ -48,7 +52,9 @@ function ImportDialogComponent({ parentNoteId, noteTitle }: ImportDialogCompone
closeActiveDialog();
await importService.uploadFiles("notes", parentNoteId, Array.from(files), options);
}}
onHidden={() => setShown(false)}
footer={<Button text={t("import.import")} primary disabled={!files} />}
show={shown}
>
<FormGroup label={t("import.chooseImportFile")} description={<>{t("import.importDescription")} <strong>{ noteTitle }</strong></>}>
<FormFileUpload multiple onChange={setFiles} />
@@ -86,20 +92,8 @@ function ImportDialogComponent({ parentNoteId, noteTitle }: ImportDialogCompone
export default class ImportDialog extends ReactBasicWidget {
private props?: ImportDialogComponentProps = {};
get component() {
return <ImportDialogComponent {...this.props} />
}
async showImportDialogEvent({ noteId }: EventData<"showImportDialog">) {
this.props = {
parentNoteId: noteId,
noteTitle: await tree.getNoteTitle(noteId)
}
this.doRender();
openDialog(this.$widget);
return <ImportDialogComponent />
}
}

View File

@@ -1,6 +1,5 @@
import { useRef, useState } from "preact/compat";
import type { EventData } from "../../components/app_context";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import FormGroup from "../react/FormGroup";
import FormRadioGroup from "../react/FormRadioGroup";
@@ -12,24 +11,30 @@ import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete
import tree from "../../services/tree";
import froca from "../../services/froca";
import EditableTextTypeWidget from "../type_widgets/editable_text";
import useTriliumEvent from "../react/hooks";
interface IncludeNoteDialogProps {
textTypeWidget?: EditableTextTypeWidget;
}
function IncludeNoteDialogComponent({ textTypeWidget }: IncludeNoteDialogProps) {
function IncludeNoteDialogComponent() {
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
const [suggestion, setSuggestion] = useState<Suggestion | null>(null);
const [boxSize, setBoxSize] = useState("medium");
const [shown, setShown] = useState(false);
useTriliumEvent("showIncludeNoteDialog", ({ textTypeWidget }) => {
setTextTypeWidget(textTypeWidget);
setShown(true);
});
const autoCompleteRef = useRef<HTMLInputElement>(null);
return (textTypeWidget &&
return (
<Modal
className="include-note-dialog"
title={t("include_note.dialog_title")}
size="lg"
onShown={() => triggerRecentNotes(autoCompleteRef.current)}
onHidden={() => setShown(false)}
onSubmit={() => {
if (!suggestion?.notePath) {
if (!suggestion?.notePath || !textTypeWidget) {
return;
}
@@ -37,6 +42,7 @@ function IncludeNoteDialogComponent({ textTypeWidget }: IncludeNoteDialogProps)
includeNote(suggestion.notePath, textTypeWidget);
}}
footer={<Button text={t("include_note.button_include")} keyboardShortcut="Enter" />}
show={shown}
>
<FormGroup label={t("include_note.label_note")}>
<NoteAutocomplete
@@ -66,16 +72,8 @@ function IncludeNoteDialogComponent({ textTypeWidget }: IncludeNoteDialogProps)
export default class IncludeNoteDialog extends ReactBasicWidget {
private props: IncludeNoteDialogProps = {};
get component() {
return <IncludeNoteDialogComponent {...this.props} />;
}
async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
this.props = { textTypeWidget };
this.doRender();
openDialog(this.$widget);
return <IncludeNoteDialogComponent />;
}
}

View File

@@ -1,29 +1,30 @@
import { EventData } from "../../components/app_context";
import ReactBasicWidget from "../react/ReactBasicWidget";
import { ConfirmDialogCallback } from "./confirm";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import Modal from "../react/Modal";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { useRef } from "preact/compat";
import { useRef, useState } from "preact/hooks";
import { RawHtmlBlock } from "../react/RawHtml";
import useTriliumEvent from "../react/hooks";
interface ShowInfoDialogProps {
message?: string | HTMLElement;
callback?: ConfirmDialogCallback;
lastElementToFocus?: HTMLElement | null;
}
function ShowInfoDialogComponent({ message, callback, lastElementToFocus }: ShowInfoDialogProps) {
function ShowInfoDialogComponent() {
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
const [ shown, setShown ] = useState(false);
const okButtonRef = useRef<HTMLButtonElement>(null);
return (message && <Modal
useTriliumEvent("showInfoDialog", (opts) => {
setOpts(opts);
setShown(true);
});
return (<Modal
className="info-dialog"
size="sm"
title={t("info.modalTitle")}
onHidden={() => {
callback?.();
lastElementToFocus?.focus();
opts?.callback?.();
setShown(false);
}}
onShown={() => okButtonRef.current?.focus?.()}
footer={<Button
@@ -31,27 +32,16 @@ function ShowInfoDialogComponent({ message, callback, lastElementToFocus }: Show
text={t("info.okButton")}
onClick={() => closeActiveDialog()}
/>}
show={shown}
>
<RawHtmlBlock className="info-dialog-content" html={message} />
<RawHtmlBlock className="info-dialog-content" html={opts?.message ?? ""} />
</Modal>);
}
export default class InfoDialog extends ReactBasicWidget {
private props: ShowInfoDialogProps = {};
get component() {
return <ShowInfoDialogComponent {...this.props} />;
}
showInfoDialogEvent({ message, callback }: EventData<"showInfoDialog">) {
this.props = {
message: Array.isArray(message) ? message[0] : message,
callback,
lastElementToFocus: (document.activeElement as HTMLElement)
};
this.doRender();
openDialog(this.$widget);
return <ShowInfoDialogComponent />;
}
}

View File

@@ -1,4 +1,4 @@
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import Button from "../react/Button";
@@ -9,20 +9,45 @@ import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"
import appContext from "../../components/app_context";
import commandRegistry from "../../services/command_registry";
import { refToJQuerySelector } from "../react/react_utils";
import useTriliumEvent from "../react/hooks";
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
type Mode = "last-search" | "recent-notes" | "commands";
interface JumpToNoteDialogProps {
mode: Mode;
}
function JumpToNoteDialogComponent({ mode }: JumpToNoteDialogProps) {
function JumpToNoteDialogComponent() {
const [ mode, setMode ] = useState<Mode>("last-search");
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const autocompleteRef = useRef<HTMLInputElement>(null);
const [ isCommandMode, setIsCommandMode ] = useState(mode === "commands");
const [ text, setText ] = useState(isCommandMode ? "> " : "");
const [ shown, setShown ] = useState(false);
async function openDialog(commandMode: boolean) {
let newMode: Mode;
if (commandMode) {
newMode = "commands";
} else if (Date.now() - lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) {
// if you open the Jump To dialog soon after using it previously, it can often mean that you
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
// so we'll keep the content.
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
newMode = "recent-notes";
} else {
newMode = "last-search";
}
if (mode !== newMode) {
setMode(newMode);
}
setShown(true);
setLastOpenedTs(Date.now());
}
useTriliumEvent("jumpToNote", () => openDialog(false));
useTriliumEvent("commandPalette", () => openDialog(true));
useEffect(() => {
setIsCommandMode(text.startsWith(">"));
@@ -77,7 +102,9 @@ function JumpToNoteDialogComponent({ mode }: JumpToNoteDialogProps) {
onChange={onItemSelected}
/>}
onShown={onShown}
onHidden={() => setShown(false)}
footer={!isCommandMode && <Button className="show-in-full-text-button" text={t("jump_to_note.search_button")} keyboardShortcut="Ctrl+Enter" />}
show={shown}
>
<div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div>
</Modal>
@@ -86,45 +113,8 @@ function JumpToNoteDialogComponent({ mode }: JumpToNoteDialogProps) {
export default class JumpToNoteDialog extends ReactBasicWidget {
private lastOpenedTs?: number;
private props: JumpToNoteDialogProps = {
mode: "last-search"
};
get component() {
return <JumpToNoteDialogComponent {...this.props} />;
}
async openDialog(commandMode = false) {
this.lastOpenedTs = Date.now();
let newMode: Mode;
if (commandMode) {
newMode = "commands";
} else if (Date.now() - this.lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) {
// if you open the Jump To dialog soon after using it previously, it can often mean that you
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
// so we'll keep the content.
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
newMode = "recent-notes";
} else {
newMode = "last-search";
}
if (this.props.mode !== newMode) {
this.props.mode = newMode;
this.doRender();
}
openDialog(this.$widget);
}
async jumpToNoteEvent() {
await this.openDialog();
}
async commandPaletteEvent() {
await this.openDialog(true);
return <JumpToNoteDialogComponent />;
}
}

View File

@@ -1,6 +1,6 @@
import ReactBasicWidget from "../react/ReactBasicWidget";
import Modal from "../react/Modal";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { closeActiveDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import FormGroup from "../react/FormGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
@@ -11,6 +11,7 @@ import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
import { TreeCommandNames } from "../../menus/tree_context_menu";
import { Suggestion } from "../../services/note_autocomplete";
import Badge from "../react/Badge";
import useTriliumEvent from "../react/hooks";
export interface ChooseNoteTypeResponse {
success: boolean;
@@ -19,20 +20,24 @@ export interface ChooseNoteTypeResponse {
notePath?: string;
}
type Callback = (data: ChooseNoteTypeResponse) => void;
export type ChooseNoteTypeCallback = (data: ChooseNoteTypeResponse) => void;
const SEPARATOR_TITLE_REPLACEMENTS = [
t("note_type_chooser.builtin_templates"),
t("note_type_chooser.templates")
];
interface NoteTypeChooserDialogProps {
callback?: Callback;
}
function NoteTypeChooserDialogComponent({ callback }: NoteTypeChooserDialogProps) {
const [ parentNote, setParentNote ] = useState<Suggestion>();
function NoteTypeChooserDialogComponent() {
const [ callback, setCallback ] = useState<ChooseNoteTypeCallback>();
const [ shown, setShown ] = useState(false);
const [ parentNote, setParentNote ] = useState<Suggestion | null>();
const [ noteTypes, setNoteTypes ] = useState<MenuItem<TreeCommandNames>[]>([]);
useTriliumEvent("chooseNoteType", ({ callback }) => {
setCallback(() => callback);
setShown(true);
});
if (!noteTypes.length) {
useEffect(() => {
note_types.getNoteTypeItems().then(noteTypes => {
@@ -72,7 +77,11 @@ function NoteTypeChooserDialogComponent({ callback }: NoteTypeChooserDialogProps
size="md"
zIndex={1100} // note type chooser needs to be higher than other dialogs from which it is triggered, e.g. "add link"
scrollable
onHidden={() => callback?.({ success: false })}
onHidden={() => {
callback?.({ success: false });
setShown(false);
}}
show={shown}
>
<FormGroup label={t("note_type_chooser.change_path_prompt")}>
<NoteAutocomplete
@@ -114,16 +123,8 @@ function NoteTypeChooserDialogComponent({ callback }: NoteTypeChooserDialogProps
export default class NoteTypeChooserDialog extends ReactBasicWidget {
private props: NoteTypeChooserDialogProps = {};
get component() {
return <NoteTypeChooserDialogComponent {...this.props} />
}
async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
this.props = { callback };
this.doRender();
openDialog(this.$widget);
return <NoteTypeChooserDialogComponent />
}
}

View File

@@ -1,5 +1,4 @@
import { useRef, useState } from "preact/hooks";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import Modal from "../react/Modal";
@@ -8,6 +7,7 @@ import ReactBasicWidget from "../react/ReactBasicWidget";
import FormTextBox from "../react/FormTextBox";
import FormGroup from "../react/FormGroup";
import { refToJQuerySelector } from "../react/react_utils";
import useTriliumEvent from "../react/hooks";
// JQuery here is maintained for compatibility with existing code.
interface ShownCallbackData {
@@ -27,24 +27,29 @@ export interface PromptDialogOptions {
callback?: (value: string | null) => void;
}
interface PromptDialogProps extends PromptDialogOptions { }
function PromptDialogComponent({ title, message, shown: shownCallback, callback }: PromptDialogProps) {
function PromptDialogComponent() {
const modalRef = useRef<HTMLDivElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const labelRef = useRef<HTMLLabelElement>(null);
const answerRef = useRef<HTMLInputElement>(null);
const [ opts, setOpts ] = useState<PromptDialogOptions>();
const [ value, setValue ] = useState("");
const [ shown, setShown ] = useState(false);
useTriliumEvent("showPromptDialog", (opts) => {
setOpts(opts);
setShown(true);
})
return (
<Modal
className="prompt-dialog"
title={title ?? t("prompt.title")}
title={opts?.title ?? t("prompt.title")}
size="lg"
zIndex={2000}
modalRef={modalRef} formRef={formRef}
onShown={() => {
shownCallback?.({
opts?.shown?.({
$dialog: refToJQuerySelector(modalRef),
$question: refToJQuerySelector(labelRef),
$answer: refToJQuerySelector(answerRef),
@@ -56,12 +61,16 @@ function PromptDialogComponent({ title, message, shown: shownCallback, callback
const modal = BootstrapModal.getOrCreateInstance(modalRef.current!);
modal.hide();
callback?.(value);
opts?.callback?.(value);
}}
onHidden={() => {
opts?.callback?.(null);
setShown(false);
}}
onHidden={() => callback?.(null)}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
show={shown}
>
<FormGroup label={message} labelRef={labelRef}>
<FormGroup label={opts?.message} labelRef={labelRef}>
<FormTextBox
name="prompt-dialog-answer"
inputRef={answerRef}
@@ -73,16 +82,8 @@ function PromptDialogComponent({ title, message, shown: shownCallback, callback
export default class PromptDialog extends ReactBasicWidget {
private props: PromptDialogProps = {};
get component() {
return <PromptDialogComponent {...this.props} />;
}
showPromptDialogEvent(props: PromptDialogOptions) {
this.props = props;
this.doRender();
openDialog(this.$widget, false);
return <PromptDialogComponent />;
}
}