1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-02 12:47:41 +02:00

Desktop, Cli: Resolves #4095: Allow exporting conflict notes

This commit is contained in:
Laurent Cozic 2020-11-17 11:50:46 +00:00
parent 7be93b1fbb
commit eb8284ecdb
11 changed files with 79 additions and 20 deletions

View File

@ -335,7 +335,6 @@
"web/env.php": true, "web/env.php": true,
"**/*.js": {"when": "$(basename).ts"}, "**/*.js": {"when": "$(basename).ts"},
"**/*?.js": { "when": "$(basename).tsx"}, "**/*?.js": { "when": "$(basename).tsx"},
}, }
"cSpell.enabled": true,
} }
} }

View File

@ -19,6 +19,12 @@ function exportDir() {
return `${__dirname}/export`; 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[]) { function fieldsEqual(model1: any, model2: any, fieldNames: string[]) {
for (let i = 0; i < fieldNames.length; i++) { for (let i = 0; i < fieldNames.length; i++) {
const f = fieldNames[i]; const f = fieldNames[i];
@ -31,10 +37,7 @@ describe('services_InteropService', function() {
beforeEach(async (done) => { beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1); await setupDatabaseAndSynchronizer(1);
await switchClient(1); await switchClient(1);
await recreateExportDir();
const dir = exportDir();
await fs.remove(dir);
await fs.mkdirp(dir);
done(); done();
}); });
@ -416,6 +419,36 @@ describe('services_InteropService', function() {
expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true); 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 () => { 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 // 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: // that doesn't exist. Can happen for example in this case:

View File

@ -19,6 +19,7 @@ interface ExportNoteOptions {
printBackground?: boolean; printBackground?: boolean;
pageSize?: string; pageSize?: string;
landscape?: boolean; landscape?: boolean;
includeConflicts?: boolean;
} }
export default class InteropServiceHelper { export default class InteropServiceHelper {
@ -162,6 +163,7 @@ export default class InteropServiceHelper {
exportOptions.format = module.format; exportOptions.format = module.format;
exportOptions.modulePath = module.path; exportOptions.modulePath = module.path;
exportOptions.target = module.target; exportOptions.target = module.target;
exportOptions.includeConflicts = !!options.includeConflicts;
if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds; if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds;
if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds; if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds;

View File

@ -12,6 +12,7 @@ interface MultiNoteActionsProps {
dispatch: Function; dispatch: Function;
watchedNoteFiles: string[]; watchedNoteFiles: string[];
plugins: PluginStates; plugins: PluginStates;
inConflictFolder: boolean;
} }
function styles_(props: MultiNoteActionsProps) { function styles_(props: MultiNoteActionsProps) {
@ -51,6 +52,7 @@ export default function MultiNoteActions(props: MultiNoteActionsProps) {
dispatch: props.dispatch, dispatch: props.dispatch,
watchedNoteFiles: props.watchedNoteFiles, watchedNoteFiles: props.watchedNoteFiles,
plugins: props.plugins, plugins: props.plugins,
inConflictFolder: props.inConflictFolder,
}); });
const itemComps = []; const itemComps = [];

View File

@ -36,6 +36,7 @@ const { substrWithEllipsis } = require('@joplin/lib/string-utils');
const NoteSearchBar = require('../NoteSearchBar.min.js'); const NoteSearchBar = require('../NoteSearchBar.min.js');
const { reg } = require('@joplin/lib/registry.js'); const { reg } = require('@joplin/lib/registry.js');
const Note = require('@joplin/lib/models/Note.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 bridge = require('electron').remote.require('./bridge').default;
const NoteRevisionViewer = require('../NoteRevisionViewer.min'); const NoteRevisionViewer = require('../NoteRevisionViewer.min');
@ -449,6 +450,7 @@ function NoteEditor(props: NoteEditorProps) {
dispatch={props.dispatch} dispatch={props.dispatch}
watchedNoteFiles={props.watchedNoteFiles} watchedNoteFiles={props.watchedNoteFiles}
plugins={props.plugins} plugins={props.plugins}
inConflictFolder={props.selectedFolderId === Folder.conflictFolderId()}
/>; />;
} }
@ -560,6 +562,7 @@ const mapStateToProps = (state: AppState) => {
notes: state.notes, notes: state.notes,
folders: state.folders, folders: state.folders,
selectedNoteIds: state.selectedNoteIds, selectedNoteIds: state.selectedNoteIds,
selectedFolderId: state.selectedFolderId,
isProvisional: state.provisionalNoteIds.includes(noteId), isProvisional: state.provisionalNoteIds.includes(noteId),
editorNoteStatuses: state.editorNoteStatuses, editorNoteStatuses: state.editorNoteStatuses,
syncStarted: state.syncStarted, syncStarted: state.syncStarted,

View File

@ -13,6 +13,7 @@ export interface NoteEditorProps {
themeId: number; themeId: number;
dispatch: Function; dispatch: Function;
selectedNoteIds: string[]; selectedNoteIds: string[];
selectedFolderId: string;
notes: any[]; notes: any[];
watchedNoteFiles: string[]; watchedNoteFiles: string[];
isProvisional: boolean; isProvisional: boolean;

View File

@ -2,19 +2,21 @@ import { AppState } from '../../app';
import eventManager from '@joplin/lib/eventManager'; import eventManager from '@joplin/lib/eventManager';
import NoteListUtils from '../utils/NoteListUtils'; import NoteListUtils from '../utils/NoteListUtils';
import { _ } from '@joplin/lib/locale'; 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 React = require('react');
const { ItemList } = require('../ItemList.min.js');
const { connect } = require('react-redux'); 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 Note = require('@joplin/lib/models/Note');
const Setting = require('@joplin/lib/models/Setting').default; const Folder = require('@joplin/lib/models/Folder');
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 commands = [ const commands = [
require('./commands/focusElementNoteList'), require('./commands/focusElementNoteList'),
@ -122,6 +124,7 @@ class NoteListComponent extends React.Component {
dispatch: this.props.dispatch, dispatch: this.props.dispatch,
watchedNoteFiles: this.props.watchedNoteFiles, watchedNoteFiles: this.props.watchedNoteFiles,
plugins: this.props.plugins, plugins: this.props.plugins,
inConflictFolder: this.props.selectedFolderId === Folder.conflictFolderId(),
}); });
menu.popup(bridge().window()); menu.popup(bridge().window());

View File

@ -19,6 +19,7 @@ interface ContextMenuProps {
dispatch: Function; dispatch: Function;
watchedNoteFiles: string[]; watchedNoteFiles: string[];
plugins: PluginStates; plugins: PluginStates;
inConflictFolder: boolean;
} }
export default class NoteListUtils { export default class NoteListUtils {
@ -149,7 +150,10 @@ export default class NoteListUtils {
new MenuItem({ new MenuItem({
label: module.fullLabel(), label: module.fullLabel(),
click: async () => { 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,
});
}, },
}) })
); );

View File

@ -31,9 +31,18 @@ class Folder extends BaseItem {
return field in fieldsToLabels ? fieldsToLabels[field] : field; 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() 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 => { .then(rows => {
const output = []; const output = [];
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {

View File

@ -321,6 +321,8 @@ export default class InteropService {
// Recursively get all the folders that have valid parents // Recursively get all the folders that have valid parents
const folderIds = await Folder.childrenIds('', true); const folderIds = await Folder.childrenIds('', true);
if (options.includeConflicts) folderIds.push(Folder.conflictFolderId());
let fullSourceFolderIds = sourceFolderIds.slice(); let fullSourceFolderIds = sourceFolderIds.slice();
for (let i = 0; i < sourceFolderIds.length; i++) { for (let i = 0; i < sourceFolderIds.length; i++) {
const id = sourceFolderIds[i]; const id = sourceFolderIds[i];
@ -335,7 +337,7 @@ export default class InteropService {
if (!sourceNoteIds.length) await queueExportItem(BaseModel.TYPE_FOLDER, folderId); 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++) { for (let noteIndex = 0; noteIndex < noteIds.length; noteIndex++) {
const noteId = noteIds[noteIndex]; const noteId = noteIds[noteIndex];

View File

@ -93,6 +93,7 @@ export interface ExportOptions {
sourceNoteIds?: string[]; sourceNoteIds?: string[];
modulePath?: string; modulePath?: string;
target?: FileSystemItem; target?: FileSystemItem;
includeConflicts?: boolean;
} }
export interface ImportExportResult { export interface ImportExportResult {