Skip to content

Commit 1d48506

Browse files
feat(main: db): migrate from SnippetsLab (#30)
1 parent 724dfc3 commit 1d48506

File tree

7 files changed

+207
-21
lines changed

7 files changed

+207
-21
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"fs-extra": "^10.0.1",
4444
"highlight.js": "^11.5.1",
4545
"interactjs": "^1.10.11",
46+
"lodash": "^4.17.21",
4647
"lowdb": "^3.0.0",
4748
"markdown-it": "^12.3.2",
4849
"markdown-it-link-attributes": "^4.0.0",

src/main/preload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { contextBridge, ipcRenderer } from 'electron'
2-
import { isDbExist, migrate, move } from './services/db'
2+
import { isDbExist, migrate, migrateFromSnippetsLab, move } from './services/db'
33
import { store } from './store'
44
import type { ElectronBridge } from '@shared/types/main'
55
import { version } from '../../package.json'
@@ -29,6 +29,7 @@ contextBridge.exposeInMainWorld('electron', {
2929
},
3030
db: {
3131
migrate: path => migrate(path),
32+
migrateFromSnippetsLab: path => migrateFromSnippetsLab(path),
3233
move: (from, to) => move(from, to),
3334
isExist: path => isDbExist(path)
3435
},

src/main/services/db/index.ts

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import fs from 'fs-extra'
33
import readline from 'readline'
44
import { nestedToFlat } from '../../utils'
55
import { nanoid } from 'nanoid'
6-
import type { Folder, Snippet, Tag } from '@shared/types/main/db'
7-
import { oldLanguageMap } from '../../../renderer/components/editor/languages'
6+
import type { DB, Folder, Snippet, Tag } from '@shared/types/main/db'
7+
import {
8+
oldLanguageMap,
9+
languages
10+
} from '../../../renderer/components/editor/languages'
11+
import { snakeCase } from 'lodash'
812

913
const DB_NAME = 'db.json'
1014

@@ -207,3 +211,113 @@ export const migrate = async (path: string) => {
207211
writeToFile(db)
208212
console.log('Migrate is done')
209213
}
214+
215+
export const migrateFromSnippetsLab = (path: string) => {
216+
interface SLFragment {
217+
Content: string
218+
'Date Created': string
219+
'Date Modified': string
220+
Note: string
221+
Title: string
222+
Language: string
223+
}
224+
interface SLSnippet {
225+
'Date Created': string
226+
'Date Modified': string
227+
Folder: string
228+
Title: string
229+
Fragments: SLFragment[]
230+
Tags: string[]
231+
}
232+
233+
interface SnippetsLabDbJSON {
234+
Snippets: SLSnippet[]
235+
}
236+
237+
const INBOX = 'Uncategorized'
238+
239+
const file = fs.readFileSync(path, 'utf-8')
240+
const json = JSON.parse(file) as SnippetsLabDbJSON
241+
242+
const folders = new Set<string>()
243+
const tags = new Set<string>()
244+
245+
const db: DB = {
246+
folders: [...DEFAULT_SYSTEM_FOLDERS],
247+
snippets: [],
248+
tags: []
249+
}
250+
251+
json.Snippets.forEach(i => {
252+
if (i.Folder) folders.add(i.Folder)
253+
254+
if (i.Tags.length) {
255+
i.Tags.forEach(t => tags.add(t))
256+
}
257+
})
258+
259+
folders.forEach(i => {
260+
if (i === INBOX) return
261+
db.folders.push({
262+
id: nanoid(8),
263+
name: i,
264+
defaultLanguage: 'plain_text',
265+
parentId: null,
266+
isOpen: false,
267+
isSystem: false,
268+
createdAt: new Date().valueOf(),
269+
updatedAt: new Date().valueOf()
270+
})
271+
})
272+
273+
tags.forEach(i => {
274+
db.tags.push({
275+
id: nanoid(8),
276+
name: i,
277+
createdAt: new Date().valueOf(),
278+
updatedAt: new Date().valueOf()
279+
})
280+
})
281+
282+
json.Snippets.forEach(i => {
283+
const folderId = db.folders.find(f => f.name === i.Folder)?.id || ''
284+
const tagsIds: string[] = []
285+
286+
if (i.Tags.length) {
287+
i.Tags.forEach(t => {
288+
const id = db.tags.find(_t => _t.name === t)?.id
289+
if (id) tagsIds.push(id)
290+
})
291+
}
292+
293+
const snippet: Snippet = {
294+
id: nanoid(8),
295+
name: i.Title,
296+
content: [],
297+
folderId,
298+
tagsIds,
299+
isDeleted: false,
300+
isFavorites: false,
301+
createdAt: new Date(i['Date Created']).valueOf(),
302+
updatedAt: new Date(i['Date Modified']).valueOf()
303+
}
304+
305+
if (i.Fragments.length) {
306+
i.Fragments.forEach(f => {
307+
const _language = snakeCase(f.Language.toLowerCase())
308+
const language = languages.find(i => i.value === _language)?.value
309+
310+
snippet.content.push({
311+
label: f.Title,
312+
value: f.Content,
313+
language: language || 'plain_text'
314+
})
315+
})
316+
}
317+
318+
db.snippets.push(snippet)
319+
})
320+
321+
writeToFile(db)
322+
console.log('Migrate is done')
323+
}

src/main/services/ipc/dialog.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import type { MessageBoxRequest } from '@shared/types/main'
1+
import type { DialogRequest, MessageBoxRequest } from '@shared/types/main'
22
import { dialog, ipcMain } from 'electron'
33

44
export const subscribeToDialog = () => {
5-
ipcMain.handle('main:open-dialog', () => {
5+
ipcMain.handle<DialogRequest, any>('main:open-dialog', (event, payload) => {
66
return new Promise<string>(resolve => {
7+
const { properties, filters } = payload
8+
79
const dir = dialog.showOpenDialogSync({
8-
properties: ['openDirectory', 'createDirectory']
10+
properties: properties || ['openDirectory', 'createDirectory'],
11+
filters: filters || [{ name: '*', extensions: ['json'] }]
912
})
1013

1114
if (dir) {

src/renderer/components/preferences/Storage.vue renamed to src/renderer/components/preferences/StoragePreferences.vue

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,21 @@
1919
</AppFormItem>
2020
<AppFormItem label="Migrate">
2121
<AppButton @click="onClickMigrate">
22-
Open folder
22+
From massCode v1.0
23+
</AppButton>
24+
<AppButton @click="onClickMigrateFromSnippetsLab">
25+
From SnippetsLab
2326
</AppButton>
2427
<template #desc>
25-
To migrate from v1 select the folder containing the database files.
28+
To migrate from massCode v1.0 select the folder containing the
29+
database files.
30+
<p>To migrate from SnippetsLab select JSON file.</p>
31+
<p>
32+
Some Limitations. During migration from SnippetsLab, snippets with
33+
unsupported languages will be set to default Plain Text. Also since
34+
JSON file does not represent nesting for folders, all folders will
35+
be first level.
36+
</p>
2637
</template>
2738
</AppFormItem>
2839
<AppFormItem label="Count">
@@ -36,7 +47,7 @@
3647
import { ipc, store, db, track } from '@/electron'
3748
import { useFolderStore } from '@/store/folders'
3849
import { useSnippetStore } from '@/store/snippets'
39-
import type { MessageBoxRequest } from '@shared/types/main'
50+
import type { MessageBoxRequest, DialogRequest } from '@shared/types/main'
4051
import { ref } from 'vue'
4152
4253
const snippetStore = useSnippetStore()
@@ -95,12 +106,20 @@ const onClickMigrate = async () => {
95106
96107
try {
97108
const path = await ipc.invoke<any, string>('main:open-dialog', {})
109+
110+
if (!path) return
111+
98112
await db.migrate(path)
113+
99114
ipc.invoke('main:restart-api', {})
115+
116+
resetStore()
117+
await snippetStore.getSnippets()
118+
100119
ipc.invoke('main:notification', {
101120
body: 'DB successfully migrated.'
102121
})
103-
snippetStore.getSnippets()
122+
104123
track('app/migrate')
105124
} catch (err) {
106125
const e = err as Error
@@ -111,22 +130,64 @@ const onClickMigrate = async () => {
111130
}
112131
}
113132
133+
const onClickMigrateFromSnippetsLab = async () => {
134+
const state = await ipc.invoke<MessageBoxRequest, boolean>(
135+
'main:open-message-box',
136+
{
137+
message: 'Are you sure you want to migrate from SnippetsLab',
138+
detail: 'During migrate, the current library will be overwritten.',
139+
buttons: ['Confirm', 'Cancel']
140+
}
141+
)
142+
143+
if (!state) return
144+
145+
try {
146+
const path = await ipc.invoke<DialogRequest, string>('main:open-dialog', {
147+
properties: ['openFile']
148+
})
149+
150+
if (!path) return
151+
152+
db.migrateFromSnippetsLab(path)
153+
154+
ipc.invoke('main:restart-api', {})
155+
156+
resetStore()
157+
await snippetStore.getSnippets()
158+
159+
ipc.invoke('main:notification', {
160+
body: 'DB successfully migrated.'
161+
})
162+
163+
track('app/migrate', 'from-snippets-lab')
164+
} catch (err) {
165+
const e = err as Error
166+
ipc.invoke('main:notification', {
167+
body: e.message
168+
})
169+
console.error(err)
170+
}
171+
}
172+
114173
const setStorageAndRestartApi = (path: string, reset?: boolean) => {
115174
storagePath.value = path
116175
store.preferences.set('storagePath', path)
117176
118-
if (reset) {
119-
store.app.delete('selectedFolderAlias')
120-
store.app.delete('selectedFolderId')
121-
store.app.delete('selectedFolderIds')
122-
store.app.delete('selectedSnippetId')
123-
124-
snippetStore.$reset()
125-
folderStore.$reset()
126-
}
177+
if (reset) resetStore()
127178
128179
ipc.invoke('main:restart-api', {})
129180
}
181+
182+
const resetStore = () => {
183+
store.app.delete('selectedFolderAlias')
184+
store.app.delete('selectedFolderId')
185+
store.app.delete('selectedFolderIds')
186+
store.app.delete('selectedSnippetId')
187+
188+
snippetStore.$reset()
189+
folderStore.$reset()
190+
}
130191
</script>
131192

132193
<style lang="scss" scoped></style>

src/renderer/views/Preferences.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
name="Storage"
1313
value="storage"
1414
>
15-
<Storage />
15+
<StoragePreferences />
1616
</AppMenuItem>
1717
<AppMenuItem
1818
name="Editor"

src/shared/types/main/index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IpcRendererEvent } from 'electron'
1+
import type { IpcRendererEvent, OpenDialogOptions } from 'electron'
22
import type { TrackEvents } from '../main/analytics'
33
import type { AppStore, PreferencesStore } from './store'
44

@@ -77,6 +77,11 @@ export interface MessageBoxRequest {
7777
buttons: string[]
7878
}
7979

80+
export interface DialogRequest {
81+
properties?: OpenDialogOptions['properties']
82+
filters?: OpenDialogOptions['filters']
83+
}
84+
8085
interface EventCallback {
8186
(event?: IpcRendererEvent, ...args: any[]): void
8287
}
@@ -107,6 +112,7 @@ export interface ElectronBridge {
107112
}
108113
db: {
109114
migrate: (path: string) => Promise<void>
115+
migrateFromSnippetsLab: (path: string) => void
110116
move: (from: string, to: string) => Promise<void>
111117
isExist: (path: string) => boolean
112118
}

0 commit comments

Comments
 (0)