diff --git a/joplin.code-workspace b/joplin.code-workspace index 350011c68..45b7408c0 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -335,7 +335,6 @@ "web/env.php": true, "**/*.js": {"when": "$(basename).ts"}, "**/*?.js": { "when": "$(basename).tsx"}, - }, - "cSpell.enabled": true, + } } } \ No newline at end of file diff --git a/packages/app-cli/tests/services_InteropService.ts b/packages/app-cli/tests/services_InteropService.ts index beb593596..ee4a6c666 100644 --- a/packages/app-cli/tests/services_InteropService.ts +++ b/packages/app-cli/tests/services_InteropService.ts @@ -19,6 +19,12 @@ function exportDir() { return `${__dirname}/export`; } +async function recreateExportDir() { + const dir = exportDir(); + await fs.remove(dir); + await fs.mkdirp(dir); +} + function fieldsEqual(model1: any, model2: any, fieldNames: string[]) { for (let i = 0; i < fieldNames.length; i++) { const f = fieldNames[i]; @@ -31,10 +37,7 @@ describe('services_InteropService', function() { beforeEach(async (done) => { await setupDatabaseAndSynchronizer(1); await switchClient(1); - - const dir = exportDir(); - await fs.remove(dir); - await fs.mkdirp(dir); + await recreateExportDir(); done(); }); @@ -416,6 +419,36 @@ describe('services_InteropService', function() { expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true); })); + it('should export conflict notes', asyncTest(async () => { + const folder1 = await Folder.save({ title: 'testexportfolder' }); + await Note.save({ title: 'textexportnote1', parent_id: folder1.id, is_conflict: 1 }); + await Note.save({ title: 'textexportnote2', parent_id: folder1.id }); + + const service = InteropService.instance(); + + await service.export({ + path: exportDir(), + format: 'md', + sourceFolderIds: [folder1.id], + includeConflicts: false, + }); + + expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote1.md`)).toBe(false); + expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true); + + await recreateExportDir(); + + await service.export({ + path: exportDir(), + format: 'md', + sourceFolderIds: [folder1.id], + includeConflicts: true, + }); + + expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote1.md`)).toBe(true); + expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true); + })); + it('should not try to export folders with a non-existing parent', asyncTest(async () => { // Handles and edge case where user has a folder but this folder with a parent // that doesn't exist. Can happen for example in this case: diff --git a/packages/app-desktop/InteropServiceHelper.ts b/packages/app-desktop/InteropServiceHelper.ts index 10851d665..d42ea7f12 100644 --- a/packages/app-desktop/InteropServiceHelper.ts +++ b/packages/app-desktop/InteropServiceHelper.ts @@ -19,6 +19,7 @@ interface ExportNoteOptions { printBackground?: boolean; pageSize?: string; landscape?: boolean; + includeConflicts?: boolean; } export default class InteropServiceHelper { @@ -162,6 +163,7 @@ export default class InteropServiceHelper { exportOptions.format = module.format; exportOptions.modulePath = module.path; exportOptions.target = module.target; + exportOptions.includeConflicts = !!options.includeConflicts; if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds; if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds; diff --git a/packages/app-desktop/gui/MultiNoteActions.tsx b/packages/app-desktop/gui/MultiNoteActions.tsx index 2afb65f7a..8ead5e7d5 100644 --- a/packages/app-desktop/gui/MultiNoteActions.tsx +++ b/packages/app-desktop/gui/MultiNoteActions.tsx @@ -12,6 +12,7 @@ interface MultiNoteActionsProps { dispatch: Function; watchedNoteFiles: string[]; plugins: PluginStates; + inConflictFolder: boolean; } function styles_(props: MultiNoteActionsProps) { @@ -51,6 +52,7 @@ export default function MultiNoteActions(props: MultiNoteActionsProps) { dispatch: props.dispatch, watchedNoteFiles: props.watchedNoteFiles, plugins: props.plugins, + inConflictFolder: props.inConflictFolder, }); const itemComps = []; diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index 544655c1b..f384b6c15 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -36,6 +36,7 @@ const { substrWithEllipsis } = require('@joplin/lib/string-utils'); const NoteSearchBar = require('../NoteSearchBar.min.js'); const { reg } = require('@joplin/lib/registry.js'); const Note = require('@joplin/lib/models/Note.js'); +const Folder = require('@joplin/lib/models/Folder.js'); const bridge = require('electron').remote.require('./bridge').default; const NoteRevisionViewer = require('../NoteRevisionViewer.min'); @@ -449,6 +450,7 @@ function NoteEditor(props: NoteEditorProps) { dispatch={props.dispatch} watchedNoteFiles={props.watchedNoteFiles} plugins={props.plugins} + inConflictFolder={props.selectedFolderId === Folder.conflictFolderId()} />; } @@ -560,6 +562,7 @@ const mapStateToProps = (state: AppState) => { notes: state.notes, folders: state.folders, selectedNoteIds: state.selectedNoteIds, + selectedFolderId: state.selectedFolderId, isProvisional: state.provisionalNoteIds.includes(noteId), editorNoteStatuses: state.editorNoteStatuses, syncStarted: state.syncStarted, diff --git a/packages/app-desktop/gui/NoteEditor/utils/types.ts b/packages/app-desktop/gui/NoteEditor/utils/types.ts index f6e231e6e..94f015989 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/types.ts @@ -13,6 +13,7 @@ export interface NoteEditorProps { themeId: number; dispatch: Function; selectedNoteIds: string[]; + selectedFolderId: string; notes: any[]; watchedNoteFiles: string[]; isProvisional: boolean; diff --git a/packages/app-desktop/gui/NoteList/NoteList.tsx b/packages/app-desktop/gui/NoteList/NoteList.tsx index 2d70ff24e..624d49d00 100644 --- a/packages/app-desktop/gui/NoteList/NoteList.tsx +++ b/packages/app-desktop/gui/NoteList/NoteList.tsx @@ -2,19 +2,21 @@ import { AppState } from '../../app'; import eventManager from '@joplin/lib/eventManager'; import NoteListUtils from '../utils/NoteListUtils'; import { _ } from '@joplin/lib/locale'; -const { ItemList } = require('../ItemList.min.js'); +import time from '@joplin/lib/time'; +import BaseModel from '@joplin/lib/BaseModel'; +import bridge from '../../services/bridge'; +import Setting from '@joplin/lib/models/Setting'; +import NoteListItem from '../NoteListItem'; +import CommandService from '@joplin/lib/services/CommandService.js'; +import shim from '@joplin/lib/shim'; +import styled from 'styled-components'; +import { themeStyle } from '@joplin/lib/theme'; const React = require('react'); + +const { ItemList } = require('../ItemList.min.js'); const { connect } = require('react-redux'); -const time = require('@joplin/lib/time').default; -const { themeStyle } = require('@joplin/lib/theme'); -const BaseModel = require('@joplin/lib/BaseModel').default; -const bridge = require('electron').remote.require('./bridge').default; const Note = require('@joplin/lib/models/Note'); -const Setting = require('@joplin/lib/models/Setting').default; -const NoteListItem = require('../NoteListItem').default; -const CommandService = require('@joplin/lib/services/CommandService.js').default; -const styled = require('styled-components').default; -const shim = require('@joplin/lib/shim').default; +const Folder = require('@joplin/lib/models/Folder'); const commands = [ require('./commands/focusElementNoteList'), @@ -122,6 +124,7 @@ class NoteListComponent extends React.Component { dispatch: this.props.dispatch, watchedNoteFiles: this.props.watchedNoteFiles, plugins: this.props.plugins, + inConflictFolder: this.props.selectedFolderId === Folder.conflictFolderId(), }); menu.popup(bridge().window()); diff --git a/packages/app-desktop/gui/utils/NoteListUtils.ts b/packages/app-desktop/gui/utils/NoteListUtils.ts index 54c3441ed..21dd33b6c 100644 --- a/packages/app-desktop/gui/utils/NoteListUtils.ts +++ b/packages/app-desktop/gui/utils/NoteListUtils.ts @@ -19,6 +19,7 @@ interface ContextMenuProps { dispatch: Function; watchedNoteFiles: string[]; plugins: PluginStates; + inConflictFolder: boolean; } export default class NoteListUtils { @@ -149,7 +150,10 @@ export default class NoteListUtils { new MenuItem({ label: module.fullLabel(), click: async () => { - await InteropServiceHelper.export(props.dispatch.bind(this), module, { sourceNoteIds: noteIds }); + await InteropServiceHelper.export(props.dispatch.bind(this), module, { + sourceNoteIds: noteIds, + includeConflicts: props.inConflictFolder, + }); }, }) ); diff --git a/packages/lib/models/Folder.js b/packages/lib/models/Folder.js index 61d409528..b70714d14 100644 --- a/packages/lib/models/Folder.js +++ b/packages/lib/models/Folder.js @@ -31,9 +31,18 @@ class Folder extends BaseItem { return field in fieldsToLabels ? fieldsToLabels[field] : field; } - static noteIds(parentId) { + static noteIds(parentId, options = null) { + options = Object.assign({}, { + includeConflicts: false, + }, options); + + const where = ['parent_id = ?']; + if (!options.includeConflicts) { + where.push('is_conflict = 0'); + } + return this.db() - .selectAll('SELECT id FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]) + .selectAll(`SELECT id FROM notes WHERE ${where.join(' AND ')}`, [parentId]) .then(rows => { const output = []; for (let i = 0; i < rows.length; i++) { diff --git a/packages/lib/services/interop/InteropService.ts b/packages/lib/services/interop/InteropService.ts index 3014f71fb..ebd5d62a9 100644 --- a/packages/lib/services/interop/InteropService.ts +++ b/packages/lib/services/interop/InteropService.ts @@ -321,6 +321,8 @@ export default class InteropService { // Recursively get all the folders that have valid parents const folderIds = await Folder.childrenIds('', true); + if (options.includeConflicts) folderIds.push(Folder.conflictFolderId()); + let fullSourceFolderIds = sourceFolderIds.slice(); for (let i = 0; i < sourceFolderIds.length; i++) { const id = sourceFolderIds[i]; @@ -335,7 +337,7 @@ export default class InteropService { if (!sourceNoteIds.length) await queueExportItem(BaseModel.TYPE_FOLDER, folderId); - const noteIds = await Folder.noteIds(folderId); + const noteIds = await Folder.noteIds(folderId, { includeConflicts: !!options.includeConflicts }); for (let noteIndex = 0; noteIndex < noteIds.length; noteIndex++) { const noteId = noteIds[noteIndex]; diff --git a/packages/lib/services/interop/types.ts b/packages/lib/services/interop/types.ts index 9dfa600fc..b6d2eaf2c 100644 --- a/packages/lib/services/interop/types.ts +++ b/packages/lib/services/interop/types.ts @@ -93,6 +93,7 @@ export interface ExportOptions { sourceNoteIds?: string[]; modulePath?: string; target?: FileSystemItem; + includeConflicts?: boolean; } export interface ImportExportResult {