feat(react/dialogs): port clone_to

This commit is contained in:
Elian Doran 2025-08-04 21:17:35 +03:00
parent 18eb704b81
commit aa9ffb8f6b
No known key found for this signature in database
12 changed files with 171 additions and 171 deletions

View File

@ -68,7 +68,7 @@
"search_for_note_by_its_name": "按名称搜索笔记",
"cloned_note_prefix_title": "克隆的笔记将在笔记树中显示给定的前缀",
"prefix_optional": "前缀(可选)",
"clone_to_selected_note": "克隆到选定的笔记 <kbd>回车</kbd>",
"clone_to_selected_note": "克隆到选定的笔记",
"no_path_to_clone_to": "没有克隆路径。",
"note_cloned": "笔记 \"{{clonedTitle}}\" 已克隆到 \"{{targetTitle}}\""
},

View File

@ -68,7 +68,7 @@
"search_for_note_by_its_name": "Suche nach einer Notiz anhand ihres Namens",
"cloned_note_prefix_title": "Die geklonte Notiz wird im Notizbaum mit dem angegebenen Präfix angezeigt",
"prefix_optional": "Präfix (optional)",
"clone_to_selected_note": "Auf ausgewählte Notiz klonen <kbd>Eingabe</kbd>",
"clone_to_selected_note": "Auf ausgewählte Notiz klonen",
"no_path_to_clone_to": "Kein Pfad zum Klonen.",
"note_cloned": "Die Notiz \"{{clonedTitle}}\" wurde in \"{{targetTitle}}\" hinein geklont"
},

View File

@ -68,7 +68,7 @@
"search_for_note_by_its_name": "search for note by its name",
"cloned_note_prefix_title": "Cloned note will be shown in note tree with given prefix",
"prefix_optional": "Prefix (optional)",
"clone_to_selected_note": "Clone to selected note <kbd>enter</kbd>",
"clone_to_selected_note": "Clone to selected note",
"no_path_to_clone_to": "No path to clone to.",
"note_cloned": "Note \"{{clonedTitle}}\" has been cloned into \"{{targetTitle}}\""
},

View File

@ -68,7 +68,7 @@
"search_for_note_by_its_name": "buscar nota por su nombre",
"cloned_note_prefix_title": "La nota clonada se mostrará en el árbol de notas con el prefijo dado",
"prefix_optional": "Prefijo (opcional)",
"clone_to_selected_note": "Clonar a nota seleccionada <kbd>enter</kbd>",
"clone_to_selected_note": "Clonar a nota seleccionada",
"no_path_to_clone_to": "No hay ruta para clonar.",
"note_cloned": "La nota \"{{clonedTitle}}\" a sido clonada en \"{{targetTitle}}\""
},

View File

@ -68,7 +68,7 @@
"search_for_note_by_its_name": "rechercher une note par son nom",
"cloned_note_prefix_title": "La note clonée sera affichée dans l'arbre des notes avec le préfixe donné",
"prefix_optional": "Préfixe (facultatif)",
"clone_to_selected_note": "Cloner vers la note sélectionnée <kbd>entrer</kbd>",
"clone_to_selected_note": "Cloner vers la note sélectionnée",
"no_path_to_clone_to": "Aucun chemin vers lequel cloner.",
"note_cloned": "La note \"{{clonedTitle}}\" a été clonée dans \"{{targetTitle}}\""
},

View File

@ -342,7 +342,7 @@
},
"clone_to": {
"clone_notes_to": "Clonează notițele către...",
"clone_to_selected_note": "Clonează notița selectată <kbd>enter</kbd>",
"clone_to_selected_note": "Clonează notița selectată",
"cloned_note_prefix_title": "Notița clonată va fi afișată în ierarhia notiței utilizând prefixul dat",
"help_on_links": "Informații despre legături",
"no_path_to_clone_to": "Nicio cale de clonat.",

View File

@ -66,7 +66,7 @@
"search_for_note_by_its_name": "按名稱搜尋筆記",
"cloned_note_prefix_title": "複製的筆記將在筆記樹中顯示給定的前綴",
"prefix_optional": "前綴(可選)",
"clone_to_selected_note": "複製到選定的筆記 <kbd>Enter</kbd>",
"clone_to_selected_note": "複製到選定的筆記",
"no_path_to_clone_to": "沒有複製路徑。",
"note_cloned": "筆記 \"{{clonedTitle}}\" 已複製到 \"{{targetTitle}}\""
},

View File

@ -1,140 +0,0 @@
import noteAutocompleteService from "../../services/note_autocomplete.js";
import treeService from "../../services/tree.js";
import toastService from "../../services/toast.js";
import froca from "../../services/froca.js";
import branchService from "../../services/branches.js";
import appContext from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/`
<div class="clone-to-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title flex-grow-1">${t("clone_to.clone_notes_to")}</h5>
<button type="button" class="help-button" title="${t("clone_to.help_on_links")}" data-help-page="cloning-notes.html">?</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("clone_to.close")}"></button>
</div>
<form class="clone-to-form">
<div class="modal-body">
<h5>${t("clone_to.notes_to_clone")}</h5>
<ul class="clone-to-note-list" style="max-height: 200px; overflow: auto;"></ul>
<div class="form-group">
<label style="width: 100%">
${t("clone_to.target_parent_note")}
<div class="input-group">
<input class="clone-to-note-autocomplete form-control" placeholder="${t("clone_to.search_for_note_by_its_name")}">
</div>
</label>
</div>
<div class="form-group" title="${t("clone_to.cloned_note_prefix_title")}">
<label style="width: 100%">
${t("clone_to.prefix_optional")}
<input class="clone-prefix form-control" style="width: 100%;">
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">${t("clone_to.clone_to_selected_note")}</button>
</div>
</form>
</div>
</div>
</div>`;
export default class CloneToDialog extends BasicWidget {
private $form!: JQuery<HTMLElement>;
private $noteAutoComplete!: JQuery<HTMLElement>;
private $clonePrefix!: JQuery<HTMLElement>;
private $noteList!: JQuery<HTMLElement>;
private clonedNoteIds: string[] | null = null;
constructor() {
super();
}
doRender() {
this.$widget = $(TPL);
this.$form = this.$widget.find(".clone-to-form");
this.$noteAutoComplete = this.$widget.find(".clone-to-note-autocomplete");
this.$clonePrefix = this.$widget.find(".clone-prefix");
this.$noteList = this.$widget.find(".clone-to-note-list");
this.$form.on("submit", () => {
const notePath = this.$noteAutoComplete.getSelectedNotePath();
if (notePath) {
this.$widget.modal("hide");
this.cloneNotesTo(notePath);
} else {
logError(t("clone_to.no_path_to_clone_to"));
}
return false;
});
}
async cloneNoteIdsToEvent({ noteIds }: EventData<"cloneNoteIdsTo">) {
if (!noteIds || noteIds.length === 0) {
noteIds = [appContext.tabManager.getActiveContextNoteId() ?? ""];
}
this.clonedNoteIds = [];
for (const noteId of noteIds) {
if (!this.clonedNoteIds.includes(noteId)) {
this.clonedNoteIds.push(noteId);
}
}
openDialog(this.$widget);
this.$noteAutoComplete.val("").trigger("focus");
this.$noteList.empty();
for (const noteId of this.clonedNoteIds) {
const note = await froca.getNote(noteId);
if (!note) {
continue;
}
this.$noteList.append($("<li>").text(note.title));
}
noteAutocompleteService.initNoteAutocomplete(this.$noteAutoComplete);
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
}
async cloneNotesTo(notePath: string) {
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {
return;
}
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
if (!targetBranchId || !this.clonedNoteIds) {
return;
}
for (const cloneNoteId of this.clonedNoteIds) {
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, this.$clonePrefix.val() as string);
const clonedNote = await froca.getNote(cloneNoteId);
const targetBranch = froca.getBranch(targetBranchId);
if (!clonedNote || !targetBranch) {
continue;
}
const targetNote = await targetBranch.getNote();
if (!targetNote) {
continue;
}
toastService.showMessage(t("clone_to.note_cloned", { clonedTitle: clonedNote.title, targetTitle: targetNote.title }));
}
}
}

View File

@ -0,0 +1,143 @@
import { CSSProperties, useRef, useState } from "preact/compat";
import appContext, { EventData } from "../../components/app_context";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import NoteAutocomplete from "../react/NoteAutocomplete";
import froca from "../../services/froca";
import { useEffect } from "react";
import FNote from "../../entities/fnote";
import FormGroup from "../react/FormGroup";
import FormTextBox from "../react/FormTextBox";
import Button from "../react/Button";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import { logError } from "../../services/ws";
import tree from "../../services/tree";
import branches from "../../services/branches";
import toast from "../../services/toast";
interface CloneToDialogProps {
clonedNoteIds: string[];
}
function CloneToDialogComponent({ clonedNoteIds }: CloneToDialogProps) {
const [ prefix, setPrefix ] = useState("");
const [ suggestion, setSuggestion ] = useState<Suggestion>(null);
const autoCompleteRef = useRef<HTMLInputElement>(null);
function onSubmit() {
const notePath = suggestion?.notePath;
if (!notePath) {
logError(t("clone_to.no_path_to_clone_to"));
return;
}
closeActiveDialog();
cloneNotesTo(notePath, clonedNoteIds, prefix);
}
return (
<Modal
className="clone-to-dialog"
title={t("clone_to.clone_notes_to")}
helpPageId="IakOLONlIfGI"
size="lg"
footer={<Button text={t("clone_to.clone_to_selected_note")} keyboardShortcut="Enter" />}
onSubmit={onSubmit}
onShown={() => {
autoCompleteRef.current?.focus();
note_autocomplete.showRecentNotes($(autoCompleteRef.current));
}}
>
<h5>{t("clone_to.notes_to_clone")}</h5>
<NoteList style={{ maxHeight: "200px", overflow: "auto" }} noteIds={clonedNoteIds} />
<FormGroup label={t("clone_to.target_parent_note")}>
<NoteAutocomplete
placeholder={t("clone_to.search_for_note_by_its_name")}
onChange={setSuggestion}
inputRef={autoCompleteRef}
/>
</FormGroup>
<FormGroup label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
<FormTextBox name="clone-prefix" onChange={setPrefix} />
</FormGroup>
</Modal>
)
}
function NoteList({ noteIds, style }: { noteIds?: string[], style: CSSProperties }) {
const [ notes, setNotes ] = useState<FNote[]>([]);
useEffect(() => {
if (noteIds) {
froca.getNotes(noteIds).then((notes) => setNotes(notes));
}
}, [noteIds]);
return (notes &&
<ul style={style}>
{notes.map(note => (
<li key={note.noteId}>
{note.title}
</li>
))}
</ul>
);
}
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 = [];
for (const noteId of noteIds) {
if (!clonedNoteIds.includes(noteId)) {
clonedNoteIds.push(noteId);
}
}
this.props = { clonedNoteIds };
this.doRender();
openDialog(this.$widget);
}
}
async function cloneNotesTo(notePath: string, clonedNoteIds: string[], prefix?: string) {
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {
return;
}
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
if (!targetBranchId || !clonedNoteIds) {
return;
}
for (const cloneNoteId of clonedNoteIds) {
await branches.cloneNoteToBranch(cloneNoteId, targetBranchId, prefix);
const clonedNote = await froca.getNote(cloneNoteId);
const targetBranch = froca.getBranch(targetBranchId);
if (!clonedNote || !targetBranch) {
continue;
}
const targetNote = await targetBranch.getNote();
if (!targetNote) {
continue;
}
toast.showMessage(t("clone_to.note_cloned", { clonedTitle: clonedNote.title, targetTitle: targetNote.title }));
}
}

View File

@ -9,6 +9,7 @@ import FormTextBox from "../react/FormTextBox";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import server from "../../services/server";
import FormGroup from "../react/FormGroup";
function SortChildNotesDialogComponent({ parentNoteId }: { parentNoteId?: string }) {
const [ sortBy, setSortBy ] = useState("title");
@ -75,13 +76,12 @@ function SortChildNotesDialogComponent({ parentNoteId }: { parentNoteId?: string
label={t("sort_child_notes.sort_with_respect_to_different_character_sorting")}
currentValue={sortNatural} onChange={setSortNatural}
/>
<FormTextBox
className="form-check"
name="sort-locale"
label={t("sort_child_notes.natural_sort_language")}
description={t("sort_child_notes.the_language_code_for_natural_sort")}
currentValue={sortLocale} onChange={setSortLocale}
/>
<FormGroup className="form-check" label={t("sort_child_notes.natural_sort_language")} description={t("sort_child_notes.the_language_code_for_natural_sort")}>
<FormTextBox
name="sort-locale"
currentValue={sortLocale} onChange={setSortLocale}
/>
</FormGroup>
</Modal>
)
}

View File

@ -2,16 +2,21 @@ import { ComponentChildren } from "preact";
interface FormGroupProps {
label: string;
title?: string;
className?: string;
children: ComponentChildren;
description?: string;
}
export default function FormGroup({ label, children }: FormGroupProps) {
export default function FormGroup({ label, title, className, children, description }: FormGroupProps) {
return (
<div className="form-group">
<div className={`form-group ${className}`} title={title}>
<label style={{ width: "100%" }}>
{label}
{children}
</label>
{description && <small className="form-text text-muted">{description}</small>}
</div>
);
}

View File

@ -1,25 +1,17 @@
interface FormTextBoxProps {
name: string;
label: string;
currentValue?: string;
className?: string;
description?: string;
onChange?(newValue: string): void;
}
export default function FormTextBox({ name, label, description, className, currentValue, onChange }: FormTextBoxProps) {
export default function FormTextBox({ name, className, currentValue, onChange }: FormTextBoxProps) {
return (
<div className={className}>
<label>
{label}
<input
type="text"
className="form-control"
name={name}
value={currentValue}
onInput={e => onChange?.(e.currentTarget.value)} />
{description && <small className="form-text text-muted">{description}</small>}
</label>
</div>
<input
type="text"
className={`form-control ${className}`}
name={name}
value={currentValue}
onInput={e => onChange?.(e.currentTarget.value)} />
);
}