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:
parent
7be93b1fbb
commit
eb8284ecdb
@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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:
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 = [];
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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());
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -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++) {
|
||||||
|
@ -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];
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user