Skip to content

Commit 767a203

Browse files
authored
Merge pull request #1147 from mslourens/export-folder
export folder as md or text
2 parents c564c25 + 5d46adf commit 767a203

6 files changed

Lines changed: 205 additions & 26 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ node_modules/*
88
/compiled
99
/secret
1010
*.log
11-
.vscode
12-
.idea
11+
.idea
12+
.vscode

browser/main/SideNav/StorageItem.js

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import dataApi from 'browser/main/lib/dataApi'
1010
import StorageItemChild from 'browser/components/StorageItem'
1111
import eventEmitter from 'browser/main/lib/eventEmitter'
1212
import _ from 'lodash'
13+
import * as path from 'path'
1314

1415
const { remote } = require('electron')
1516
const { Menu, MenuItem, dialog } = remote
@@ -24,18 +25,20 @@ class StorageItem extends React.Component {
2425
}
2526

2627
handleHeaderContextMenu (e) {
27-
const menu = new Menu()
28-
menu.append(new MenuItem({
29-
label: 'Add Folder',
30-
click: (e) => this.handleAddFolderButtonClick(e)
31-
}))
32-
menu.append(new MenuItem({
33-
type: 'separator'
34-
}))
35-
menu.append(new MenuItem({
36-
label: 'Unlink Storage',
37-
click: (e) => this.handleUnlinkStorageClick(e)
38-
}))
28+
const menu = Menu.buildFromTemplate([
29+
{
30+
label: 'Add Folder',
31+
click: (e) => this.handleAddFolderButtonClick(e)
32+
},
33+
{
34+
type: 'separator'
35+
},
36+
{
37+
label: 'Unlink Storage',
38+
click: (e) => this.handleUnlinkStorageClick(e)
39+
}
40+
])
41+
3942
menu.popup()
4043
}
4144

@@ -89,18 +92,36 @@ class StorageItem extends React.Component {
8992
}
9093

9194
handleFolderButtonContextMenu (e, folder) {
92-
const menu = new Menu()
93-
menu.append(new MenuItem({
94-
label: 'Rename Folder',
95-
click: (e) => this.handleRenameFolderClick(e, folder)
96-
}))
97-
menu.append(new MenuItem({
98-
type: 'separator'
99-
}))
100-
menu.append(new MenuItem({
101-
label: 'Delete Folder',
102-
click: (e) => this.handleFolderDeleteClick(e, folder)
103-
}))
95+
const menu = Menu.buildFromTemplate([
96+
{
97+
label: 'Rename Folder',
98+
click: (e) => this.handleRenameFolderClick(e, folder)
99+
},
100+
{
101+
type: 'separator'
102+
},
103+
{
104+
label: 'Export Folder',
105+
submenu: [
106+
{
107+
label: 'Export as txt',
108+
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
109+
},
110+
{
111+
label: 'Export as md',
112+
click: (e) => this.handleExportFolderClick(e, folder, 'md')
113+
}
114+
]
115+
},
116+
{
117+
type: 'separator'
118+
},
119+
{
120+
label: 'Delete Folder',
121+
click: (e) => this.handleFolderDeleteClick(e, folder)
122+
}
123+
])
124+
104125
menu.popup()
105126
}
106127

@@ -112,6 +133,31 @@ class StorageItem extends React.Component {
112133
})
113134
}
114135

136+
handleExportFolderClick (e, folder, fileType) {
137+
const options = {
138+
properties: ['openDirectory', 'createDirectory'],
139+
buttonLabel: 'Select directory',
140+
title: 'Select a folder to export the files to',
141+
multiSelections: false
142+
}
143+
dialog.showOpenDialog(remote.getCurrentWindow(), options,
144+
(paths) => {
145+
if (paths && paths.length === 1) {
146+
const { storage, dispatch } = this.props
147+
dataApi
148+
.exportFolder(storage.key, folder.key, fileType, paths[0])
149+
.then((data) => {
150+
dispatch({
151+
type: 'EXPORT_FOLDER',
152+
storage: data.storage,
153+
folderKey: data.folderKey,
154+
fileType: data.fileType
155+
})
156+
})
157+
}
158+
})
159+
}
160+
115161
handleFolderDeleteClick (e, folder) {
116162
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
117163
type: 'warning',
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { findStorage } from 'browser/lib/findStorage'
2+
import resolveStorageData from './resolveStorageData'
3+
import resolveStorageNotes from './resolveStorageNotes'
4+
import * as path from 'path'
5+
import * as fs from 'fs'
6+
7+
/**
8+
* @param {String} storageKey
9+
* @param {String} folderKey
10+
* @param {String} fileType
11+
* @param {String} exportDir
12+
*
13+
* @return {Object}
14+
* ```
15+
* {
16+
* storage: Object,
17+
* folderKey: String,
18+
* fileType: String,
19+
* exportDir: String
20+
* }
21+
* ```
22+
*/
23+
24+
function exportFolder (storageKey, folderKey, fileType, exportDir) {
25+
let targetStorage
26+
try {
27+
targetStorage = findStorage(storageKey)
28+
} catch (e) {
29+
return Promise.reject(e)
30+
}
31+
32+
return resolveStorageData(targetStorage)
33+
.then(function assignNotes (storage) {
34+
return resolveStorageNotes(storage)
35+
.then((notes) => {
36+
return {
37+
storage,
38+
notes
39+
}
40+
})
41+
})
42+
.then(function exportNotes (data) {
43+
const { storage, notes } = data
44+
45+
notes
46+
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
47+
.forEach(snippet => {
48+
const notePath = path.join(exportDir, `${snippet.title}.${fileType}`)
49+
fs.writeFileSync(notePath, snippet.content, (err) => {
50+
if (err) throw err
51+
})
52+
})
53+
54+
return {
55+
storage,
56+
folderKey,
57+
fileType,
58+
exportDir
59+
}
60+
})
61+
}
62+
63+
module.exports = exportFolder

browser/main/lib/dataApi/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const dataApi = {
77
updateFolder: require('./updateFolder'),
88
deleteFolder: require('./deleteFolder'),
99
reorderFolder: require('./reorderFolder'),
10+
exportFolder: require('./exportFolder'),
1011
createNote: require('./createNote'),
1112
updateNote: require('./updateNote'),
1213
deleteNote: require('./deleteNote'),

browser/main/store.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,13 @@ function data (state = defaultDataMap(), action) {
354354
state.storageMap = new Map(state.storageMap)
355355
state.storageMap.set(action.storage.key, action.storage)
356356
return state
357+
case 'EXPORT_FOLDER':
358+
{
359+
state = Object.assign({}, state)
360+
state.storageMap = new Map(state.storageMap)
361+
state.storageMap.set(action.storage.key, action.storage)
362+
}
363+
return state
357364
case 'DELETE_FOLDER':
358365
{
359366
state = Object.assign({}, state)

tests/dataApi/exportFolder-test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const test = require('ava')
2+
const exportFolder = require('browser/main/lib/dataApi/exportFolder')
3+
const createNote = require('browser/main/lib/dataApi/createNote')
4+
5+
global.document = require('jsdom').jsdom('<body></body>')
6+
global.window = document.defaultView
7+
global.navigator = window.navigator
8+
9+
const Storage = require('dom-storage')
10+
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
11+
const path = require('path')
12+
const TestDummy = require('../fixtures/TestDummy')
13+
const os = require('os')
14+
const faker = require('faker')
15+
const fs = require('fs')
16+
17+
const storagePath = path.join(os.tmpdir(), 'test/export-note')
18+
19+
test.beforeEach((t) => {
20+
t.context.storage = TestDummy.dummyStorage(storagePath)
21+
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
22+
})
23+
24+
test.serial('Export a folder', (t) => {
25+
const storageKey = t.context.storage.cache.key
26+
const folderKey = t.context.storage.json.folders[0].key
27+
28+
const input1 = {
29+
type: 'MARKDOWN_NOTE',
30+
description: '*Some* markdown text',
31+
tags: faker.lorem.words().split(' '),
32+
folder: folderKey
33+
}
34+
input1.title = 'input1'
35+
36+
const input2 = {
37+
type: 'SNIPPET_NOTE',
38+
description: 'Some normal text',
39+
snippets: [{
40+
name: faker.system.fileName(),
41+
mode: 'text',
42+
content: faker.lorem.lines()
43+
}],
44+
tags: faker.lorem.words().split(' '),
45+
folder: folderKey
46+
}
47+
input2.title = 'input2'
48+
49+
return createNote(storageKey, input1)
50+
.then(function () {
51+
return createNote(storageKey, input2)
52+
})
53+
.then(function () {
54+
return exportFolder(storageKey, folderKey, 'md', storagePath)
55+
})
56+
.then(function assert () {
57+
let filePath = path.join(storagePath, 'input1.md')
58+
t.true(fs.existsSync(filePath))
59+
filePath = path.join(storagePath, 'input2.md')
60+
t.false(fs.existsSync(filePath))
61+
})
62+
})

0 commit comments

Comments
 (0)