From aa9ffb8f6bfb8f42c5cc000a4d601f27c633f32c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 4 Aug 2025 21:17:35 +0300 Subject: [PATCH] feat(react/dialogs): port clone_to --- .../src/translations/cn/translation.json | 2 +- .../src/translations/de/translation.json | 2 +- .../src/translations/en/translation.json | 2 +- .../src/translations/es/translation.json | 2 +- .../src/translations/fr/translation.json | 2 +- .../src/translations/ro/translation.json | 2 +- .../src/translations/tw/translation.json | 2 +- apps/client/src/widgets/dialogs/clone_to.ts | 140 ----------------- apps/client/src/widgets/dialogs/clone_to.tsx | 143 ++++++++++++++++++ .../src/widgets/dialogs/sort_child_notes.tsx | 14 +- apps/client/src/widgets/react/FormGroup.tsx | 9 +- apps/client/src/widgets/react/FormTextBox.tsx | 22 +-- 12 files changed, 171 insertions(+), 171 deletions(-) delete mode 100644 apps/client/src/widgets/dialogs/clone_to.ts create mode 100644 apps/client/src/widgets/dialogs/clone_to.tsx diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index 40cecaf6e..06cbe505b 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -68,7 +68,7 @@ "search_for_note_by_its_name": "按名称搜索笔记", "cloned_note_prefix_title": "克隆的笔记将在笔记树中显示给定的前缀", "prefix_optional": "前缀(可选)", - "clone_to_selected_note": "克隆到选定的笔记 回车", + "clone_to_selected_note": "克隆到选定的笔记", "no_path_to_clone_to": "没有克隆路径。", "note_cloned": "笔记 \"{{clonedTitle}}\" 已克隆到 \"{{targetTitle}}\"" }, diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index 6d3136844..5c4cba217 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -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 Eingabe", + "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" }, diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 1d104739a..6084e007c 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -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 enter", + "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}}\"" }, diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index 7aeae1655..1161e521a 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -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 enter", + "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}}\"" }, diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index 85e1bb0fc..671d44f27 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -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 entrer", + "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}}\"" }, diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index 101b9e779..c121badc3 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -342,7 +342,7 @@ }, "clone_to": { "clone_notes_to": "Clonează notițele către...", - "clone_to_selected_note": "Clonează notița selectată enter", + "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.", diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json index 5e10258d7..b327471cf 100644 --- a/apps/client/src/translations/tw/translation.json +++ b/apps/client/src/translations/tw/translation.json @@ -66,7 +66,7 @@ "search_for_note_by_its_name": "按名稱搜尋筆記", "cloned_note_prefix_title": "複製的筆記將在筆記樹中顯示給定的前綴", "prefix_optional": "前綴(可選)", - "clone_to_selected_note": "複製到選定的筆記 Enter", + "clone_to_selected_note": "複製到選定的筆記", "no_path_to_clone_to": "沒有複製路徑。", "note_cloned": "筆記 \"{{clonedTitle}}\" 已複製到 \"{{targetTitle}}\"" }, diff --git a/apps/client/src/widgets/dialogs/clone_to.ts b/apps/client/src/widgets/dialogs/clone_to.ts deleted file mode 100644 index 7c3a53071..000000000 --- a/apps/client/src/widgets/dialogs/clone_to.ts +++ /dev/null @@ -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*/` -`; - -export default class CloneToDialog extends BasicWidget { - private $form!: JQuery; - private $noteAutoComplete!: JQuery; - private $clonePrefix!: JQuery; - private $noteList!: JQuery; - 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($("
  • ").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 })); - } - } -} diff --git a/apps/client/src/widgets/dialogs/clone_to.tsx b/apps/client/src/widgets/dialogs/clone_to.tsx new file mode 100644 index 000000000..b86b231f4 --- /dev/null +++ b/apps/client/src/widgets/dialogs/clone_to.tsx @@ -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(null); + const autoCompleteRef = useRef(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 ( + } + onSubmit={onSubmit} + onShown={() => { + autoCompleteRef.current?.focus(); + note_autocomplete.showRecentNotes($(autoCompleteRef.current)); + }} + > +
    {t("clone_to.notes_to_clone")}
    + + + + + + + +
    + ) +} + +function NoteList({ noteIds, style }: { noteIds?: string[], style: CSSProperties }) { + const [ notes, setNotes ] = useState([]); + + useEffect(() => { + if (noteIds) { + froca.getNotes(noteIds).then((notes) => setNotes(notes)); + } + }, [noteIds]); + + return (notes && +
      + {notes.map(note => ( +
    • + {note.title} +
    • + ))} +
    + ); +} + +export default class CloneToDialog extends ReactBasicWidget { + + private props: CloneToDialogProps; + + get component() { + return ; + } + + 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 })); + } +} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/sort_child_notes.tsx b/apps/client/src/widgets/dialogs/sort_child_notes.tsx index b96e54dcf..acebc4359 100644 --- a/apps/client/src/widgets/dialogs/sort_child_notes.tsx +++ b/apps/client/src/widgets/dialogs/sort_child_notes.tsx @@ -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} /> - + + + ) } diff --git a/apps/client/src/widgets/react/FormGroup.tsx b/apps/client/src/widgets/react/FormGroup.tsx index 406515331..9c0709dfe 100644 --- a/apps/client/src/widgets/react/FormGroup.tsx +++ b/apps/client/src/widgets/react/FormGroup.tsx @@ -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 ( -
    +
    + + {description && {description}}
    ); } \ No newline at end of file diff --git a/apps/client/src/widgets/react/FormTextBox.tsx b/apps/client/src/widgets/react/FormTextBox.tsx index 0fe83f294..0a5610eb4 100644 --- a/apps/client/src/widgets/react/FormTextBox.tsx +++ b/apps/client/src/widgets/react/FormTextBox.tsx @@ -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 ( -
    - -
    + onChange?.(e.currentTarget.value)} /> ); } \ No newline at end of file