mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	unified export dialog, WIP
This commit is contained in:
		
							
								
								
									
										67
									
								
								src/public/javascripts/dialogs/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/public/javascripts/dialogs/export.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| import treeService from '../services/tree.js'; | ||||
| import treeUtils from "../services/tree_utils.js"; | ||||
| import exportService from "../services/export.js"; | ||||
|  | ||||
| const $dialog = $("#export-dialog"); | ||||
| const $form = $("#export-form"); | ||||
| const $noteTitle = $dialog.find(".note-title"); | ||||
| const $subtreeFormats = $("#export-subtree-formats"); | ||||
| const $singleFormats = $("#export-single-formats"); | ||||
| const $subtreeType = $("#export-type-subtree"); | ||||
| const $singleType = $("#export-type-single"); | ||||
|  | ||||
| async function showDialog(defaultType) { | ||||
|     if (defaultType === 'subtree') { | ||||
|         $subtreeType.prop("checked", true).change(); | ||||
|     } | ||||
|     else if (defaultType === 'single') { | ||||
|         $singleType.prop("checked", true).change(); | ||||
|     } | ||||
|     else { | ||||
|         throw new Error("Unrecognized type " + defaultType); | ||||
|     } | ||||
|  | ||||
|     glob.activeDialog = $dialog; | ||||
|  | ||||
|     $dialog.modal(); | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|     const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId); | ||||
|  | ||||
|     $noteTitle.html(noteTitle); | ||||
| } | ||||
|  | ||||
| $form.submit(() => { | ||||
|     const exportType = $dialog.find("input[name='export-type']:checked").val(); | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|  | ||||
|     exportService.exportNote(currentNode.data.branchId, exportType); | ||||
|  | ||||
|     $dialog.modal('hide'); | ||||
|  | ||||
|     return false; | ||||
| }); | ||||
|  | ||||
| $('input[name=export-type]').change(function () { | ||||
|     if (this.value === 'subtree') { | ||||
|         if ($("input[name=export-subtree-format]:checked").length === 0) { | ||||
|             $("input[name=export-subtree-format]:first").prop("checked", true); | ||||
|         } | ||||
|  | ||||
|         $subtreeFormats.slideDown(); | ||||
|         $singleFormats.slideUp(); | ||||
|     } | ||||
|     else { | ||||
|         if ($("input[name=export-single-format]:checked").length === 0) { | ||||
|             $("input[name=export-single-format]:first").prop("checked", true); | ||||
|         } | ||||
|  | ||||
|         $subtreeFormats.slideUp(); | ||||
|         $singleFormats.slideDown(); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| export default { | ||||
|     showDialog | ||||
| }; | ||||
| @@ -1,35 +0,0 @@ | ||||
| import treeService from '../services/tree.js'; | ||||
| import server from '../services/server.js'; | ||||
| import treeUtils from "../services/tree_utils.js"; | ||||
| import exportService from "../services/export.js"; | ||||
|  | ||||
| const $dialog = $("#export-subtree-dialog"); | ||||
| const $form = $("#export-subtree-form"); | ||||
| const $noteTitle = $dialog.find(".note-title"); | ||||
|  | ||||
| async function showDialog() { | ||||
|     glob.activeDialog = $dialog; | ||||
|  | ||||
|     $dialog.modal(); | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|     const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId); | ||||
|  | ||||
|     $noteTitle.html(noteTitle); | ||||
| } | ||||
|  | ||||
| $form.submit(() => { | ||||
|     const exportFormat = $dialog.find("input[name='export-format']:checked").val(); | ||||
|  | ||||
|     const currentNode = treeService.getCurrentNode(); | ||||
|  | ||||
|     exportService.exportSubtree(currentNode.data.branchId, exportFormat); | ||||
|  | ||||
|     $dialog.modal('hide'); | ||||
|  | ||||
|     return false; | ||||
| }); | ||||
|  | ||||
| export default { | ||||
|     showDialog | ||||
| }; | ||||
							
								
								
									
										5
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							| @@ -7,6 +7,7 @@ import recentChangesDialog from '../dialogs/recent_changes.js'; | ||||
| import optionsDialog from '../dialogs/options.js'; | ||||
| import sqlConsoleDialog from '../dialogs/sql_console.js'; | ||||
| import markdownImportDialog from '../dialogs/markdown_import.js'; | ||||
| import exportDialog from '../dialogs/export.js'; | ||||
|  | ||||
| import cloning from './cloning.js'; | ||||
| import contextMenu from './tree_context_menu.js'; | ||||
| @@ -103,12 +104,12 @@ if (utils.isElectron()) { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| $("#export-note-to-markdown-button").click(function () { | ||||
| $("#export-note-button").click(function () { | ||||
|     if ($(this).hasClass("disabled")) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single') | ||||
|     exportDialog.showDialog('single'); | ||||
| }); | ||||
|  | ||||
| treeService.showTree(); | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import protectedSessionHolder from './protected_session_holder.js'; | ||||
| import utils from './utils.js'; | ||||
| import server from './server.js'; | ||||
|  | ||||
| function exportSubtree(noteId, format) { | ||||
| function exportNote(noteId, format) { | ||||
|     const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format + | ||||
|         "?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); | ||||
|  | ||||
| @@ -47,6 +47,6 @@ $("#import-upload").change(async function() { | ||||
| }); | ||||
|  | ||||
| export default { | ||||
|     exportSubtree, | ||||
|     exportNote, | ||||
|     importIntoNote | ||||
| }; | ||||
| @@ -564,8 +564,6 @@ async function createNote(node, parentNoteId, target, isProtected, saveSelection | ||||
|  | ||||
|     clearSelectedNodes(); // to unmark previously active node | ||||
|  | ||||
|     infoService.showMessage("Created!"); | ||||
|  | ||||
|     return {note, branch}; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import protectedSessionService from './protected_session.js'; | ||||
| import treeChangesService from './branches.js'; | ||||
| import treeUtils from './tree_utils.js'; | ||||
| import branchPrefixDialog from '../dialogs/branch_prefix.js'; | ||||
| import exportSubtreeDialog from '../dialogs/export_subtree.js'; | ||||
| import exportDialog from '../dialogs/export.js'; | ||||
| import infoService from "./info.js"; | ||||
| import treeCache from "./tree_cache.js"; | ||||
| import syncService from "./sync.js"; | ||||
| @@ -93,7 +93,7 @@ const contextMenuItems = [ | ||||
|     {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "clipboard"}, | ||||
|     {title: "Paste after", cmd: "pasteAfter", uiIcon: "clipboard"}, | ||||
|     {title: "----"}, | ||||
|     {title: "Export subtree", cmd: "exportSubtree", uiIcon: "arrow-up-right"}, | ||||
|     {title: "Export", cmd: "export", uiIcon: "arrow-up-right"}, | ||||
|     {title: "Import into note (tar, opml, md, enex)", cmd: "importIntoNote", uiIcon: "arrow-down-left"}, | ||||
|     {title: "----"}, | ||||
|     {title: "Collapse subtree <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "align-justify"}, | ||||
| @@ -127,7 +127,7 @@ async function getContextMenuItems(event) { | ||||
|     enableItem("pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search'); | ||||
|     enableItem("pasteInto", clipboardIds.length > 0 && note.type !== 'search'); | ||||
|     enableItem("importIntoNote", note.type !== 'search'); | ||||
|     enableItem("exportSubtree", note.type !== 'search'); | ||||
|     enableItem("export", note.type !== 'search'); | ||||
|     enableItem("editBranchPrefix", isNotRoot && parentNote.type !== 'search'); | ||||
|  | ||||
|     // Activate node on right-click | ||||
| @@ -179,8 +179,8 @@ function selectContextMenuItem(event, cmd) { | ||||
|     else if (cmd === "delete") { | ||||
|         treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); | ||||
|     } | ||||
|     else if (cmd === "exportSubtree") { | ||||
|         exportSubtreeDialog.showDialog(); | ||||
|     else if (cmd === "export") { | ||||
|         exportDialog.showDialog("subtree"); | ||||
|     } | ||||
|     else if (cmd === "importIntoNote") { | ||||
|         exportService.importIntoNote(node.data.noteId); | ||||
|   | ||||
| @@ -685,3 +685,13 @@ div[data-notify="container"] { | ||||
|     color: #777; | ||||
|     z-index: 100; | ||||
| } | ||||
|  | ||||
| #export-form .form-check { | ||||
|     padding-top: 10px; | ||||
|     padding-bottom: 10px; | ||||
| } | ||||
|  | ||||
| #export-form .format-choice { | ||||
|     padding-left: 40px; | ||||
|     display: none; | ||||
| } | ||||
| @@ -9,14 +9,15 @@ const repository = require("../../services/repository"); | ||||
| async function exportNote(req, res) { | ||||
|     // entityId maybe either noteId or branchId depending on format | ||||
|     const entityId = req.params.entityId; | ||||
|     const type = req.params.type; | ||||
|     const format = req.params.format; | ||||
|  | ||||
|     if (format === 'native-tar') { | ||||
|         await nativeTarExportService.exportToTar(await repository.getBranch(entityId), res); | ||||
|     } | ||||
|     else if (format === 'markdown-tar') { | ||||
|         await markdownTarExportService.exportToMarkdown(await repository.getBranch(entityId), res); | ||||
|     if (type === 'tar') { | ||||
|         await nativeTarExportService.exportToTar(await repository.getBranch(entityId), format, res); | ||||
|     } | ||||
|     // else if (format === 'tar') { | ||||
|     //     await markdownTarExportService.exportToMarkdown(await repository.getBranch(entityId), res); | ||||
|     // } | ||||
|     // export single note without subtree | ||||
|     else if (format === 'markdown-single') { | ||||
|         await markdownSingleExportService.exportSingleMarkdown(await repository.getNote(entityId), res); | ||||
|   | ||||
| @@ -3,8 +3,15 @@ | ||||
| const html = require('html'); | ||||
| const native_tar = require('tar-stream'); | ||||
| const sanitize = require("sanitize-filename"); | ||||
| const mimeTypes = require('mime-types'); | ||||
| const TurndownService = require('turndown'); | ||||
|  | ||||
| /** | ||||
|  * @param format - 'html' or 'markdown' | ||||
|  */ | ||||
| async function exportToTar(branch, format, res) { | ||||
|     const turndownService = new TurndownService(); | ||||
|  | ||||
| async function exportToTar(branch, res) { | ||||
|     const pack = native_tar.pack(); | ||||
|  | ||||
|     const exportedNoteIds = []; | ||||
| @@ -52,6 +59,10 @@ async function exportToTar(branch, res) { | ||||
|             }) | ||||
|         }; | ||||
|  | ||||
|         if (note.type === 'text') { | ||||
|             metadata.format = format; | ||||
|         } | ||||
|  | ||||
|         if (await note.hasLabel('excludeFromExport')) { | ||||
|             return; | ||||
|         } | ||||
| @@ -75,9 +86,35 @@ async function exportToTar(branch, res) { | ||||
|     } | ||||
|  | ||||
|     function saveDataFile(childFileName, note) { | ||||
|         const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; | ||||
|         let content = note.content; | ||||
|  | ||||
|         pack.entry({name: childFileName + ".dat", size: content.length}, content); | ||||
|         if (note.type === 'text') { | ||||
|             if (format === 'html') { | ||||
|                 content = html.prettyPrint(note.content, {indent_size: 2}); | ||||
|             } | ||||
|             else if (format === 'markdown') { | ||||
|                 content = turndownService.turndown(note.content); | ||||
|             } | ||||
|             else { | ||||
|                 throw new Error("Unknown format: " + format); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const extension = mimeTypes.extension(note.mime) | ||||
|             || getExceptionalExtension(note.mime) | ||||
|             || "dat"; | ||||
|  | ||||
|         if (!childFileName.toLowerCase().endsWith(extension)) { | ||||
|             childFileName += "." + extension; | ||||
|         } | ||||
|  | ||||
|         pack.entry({name: childFileName, size: content.length}, content); | ||||
|     } | ||||
|  | ||||
|     function getExceptionalExtension(mime) { | ||||
|         if (mime === 'application/x-javascript') { | ||||
|             return 'js'; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function saveMetadataFile(childFileName, metadata) { | ||||
|   | ||||
							
								
								
									
										67
									
								
								src/views/dialogs/export.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/views/dialogs/export.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| <div id="export-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog"> | ||||
|     <div class="modal-dialog modal-lg" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <div class="modal-header"> | ||||
|                 <h5 class="modal-title">Export note</h5> | ||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | ||||
|                     <span aria-hidden="true">×</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <form id="export-form"> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="export-type" id="export-type-subtree" value="subtree"> | ||||
|                         <label class="form-check-label" for="export-type-subtree">this note and all of its descendants</label> | ||||
|                     </div> | ||||
|  | ||||
|                     <div id="export-subtree-formats" class="format-choice"> | ||||
|                         <div class="form-check"> | ||||
|                             <input class="form-check-input" type="radio" name="export-subtree-format" id="export-subtree-format-html" | ||||
|                                    value="html"> | ||||
|                             <label class="form-check-label" for="export-subtree-format-html">HTML in TAR archiv - this is recommended since this preserves all the formatting.</label> | ||||
|                         </div> | ||||
|  | ||||
|                         <div class="form-check"> | ||||
|                             <input class="form-check-input" type="radio" name="export-subtree-format" id="export-subtree-format-markdown" | ||||
|                                    value="markdown-tar"> | ||||
|                             <label class="form-check-label" for="export-subtree-format-markdown"> | ||||
|                                 Markdown - this preserves most of the formatting. | ||||
|                             </label> | ||||
|                         </div> | ||||
|  | ||||
|                         <div class="form-check"> | ||||
|                             <input class="form-check-input" type="radio" name="export-subtree-format" id="export-subtree-format-opml" | ||||
|                                    value="opml"> | ||||
|                             <label class="form-check-label" for="export-subtree-format-opml"> | ||||
|                                 OPML - outliner interchange format for text only. Formatting, images and files are not included. | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="export-type" id="export-type-single" value="single"> | ||||
|                         <label class="form-check-label" for="export-type-single">only this note without its descendants</label> | ||||
|                     </div> | ||||
|  | ||||
|                     <div id="export-single-formats" class="format-choice"> | ||||
|                         <div class="form-check"> | ||||
|                             <input class="form-check-input" type="radio" name="export-single-format" id="export-single-format-html" value="html"> | ||||
|                             <label class="form-check-label" for="export-single-format-html">HTML - this is recommended since this preserves all the formatting.</label> | ||||
|                         </div> | ||||
|  | ||||
|                         <div class="form-check"> | ||||
|                             <input class="form-check-input" type="radio" name="export-single-format" id="export-single-format-markdown" | ||||
|                                    value="markdown-tar"> | ||||
|                             <label class="form-check-label" for="export-single-format-markdown"> | ||||
|                                 Markdown - this preserves most of the formatting. | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button class="btn btn-primary btn-sm">Export</button> | ||||
|                 </div> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -1,46 +0,0 @@ | ||||
| <div id="export-subtree-dialog" class="modal fade mx-auto" tabindex="-1" role="dialog"> | ||||
|     <div class="modal-dialog modal-lg" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <div class="modal-header"> | ||||
|                 <h5 class="modal-title">Export subtree</h5> | ||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | ||||
|                     <span aria-hidden="true">×</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <form id="export-subtree-form"> | ||||
|                 <div class="modal-body"> | ||||
|                     <div>Export note "<span class="note-title"></span>" and its subtree in the following format:</div> | ||||
|  | ||||
|                     <br/> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-tar" value="native-tar" checked> | ||||
|                         <label class="form-check-label" for="export-format-tar">Native TAR - this is Trilium's native format which preserves all notes' data & metadata.</label> | ||||
|                     </div> | ||||
|  | ||||
|                     <br/> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-opml" value="opml"> | ||||
|                         <label class="form-check-label" for="export-format-opml"> | ||||
|                             OPML - standard outliner interchange format for text only. Formatting, images, files are not included. | ||||
|                         </label> | ||||
|                     </div> | ||||
|  | ||||
|                     <br/> | ||||
|  | ||||
|                     <div class="form-check disabled"> | ||||
|                         <input class="form-check-input" type="radio" name="export-format" id="export-format-markdown" | ||||
|                                value="markdown-tar"> | ||||
|                         <label class="form-check-label" for="export-format-markdown"> | ||||
|                             Markdown - TAR archive of Markdown formatted notes | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button class="btn btn-primary btn-sm">Export</button> | ||||
|                 </div> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -157,9 +157,9 @@ | ||||
|                 <div class="dropdown-menu dropdown-menu-right"> | ||||
|                   <a class="dropdown-item" id="show-note-revisions-button" data-bind="css: { disabled: type() == 'file' || type() == 'image' }">Revisions</a> | ||||
|                   <a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a> | ||||
|                   <a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' }">HTML source</a> | ||||
|                   <a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }">Note source</a> | ||||
|                   <a class="dropdown-item" id="upload-file-button">Upload file</a> | ||||
|                   <a class="dropdown-item" id="export-note-to-markdown-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' }">Export as markdown</a> | ||||
|                   <a class="dropdown-item" id="export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
| @@ -173,7 +173,7 @@ | ||||
|       <% include dialogs/attributes.ejs %> | ||||
|       <% include dialogs/branch_prefix.ejs %> | ||||
|       <% include dialogs/event_log.ejs %> | ||||
|       <% include dialogs/export_subtree.ejs %> | ||||
|       <% include dialogs/export.ejs %> | ||||
|       <% include dialogs/jump_to_note.ejs %> | ||||
|       <% include dialogs/markdown_import.ejs %> | ||||
|       <% include dialogs/note_revisions.ejs %> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user