From 45f7dbfc313821a4d1240d5559696dcb3c15eccb Mon Sep 17 00:00:00 2001 From: Andrew P Date: Mon, 5 Oct 2020 16:48:44 +0200 Subject: [PATCH 1/2] Create export - import --- README.md | 5 + common/ipcMessages.ts | 3 + main/events.ts | 19 ++++ main/fileManager.ts | 83 +++++++++++++- main/menu/import.ts | 41 +++++++ main/menu/index.ts | 2 + main/pluginManager.ts | 56 ++++++++- main/plugins/json/index.ts | 2 +- main/windowManager.ts | 8 ++ package.json | 3 +- src/App.vue | 3 + src/export/Export.vue | 170 ++++++++++++++++++++++++++++ src/export/store.ts | 37 ++++++ src/folder/store.ts | 1 + src/store/index.ts | 2 + src/store/plugins/ipc.ts | 11 ++ testData/import/import_example.xlsx | Bin 0 -> 13404 bytes yarn.lock | 87 +++++++++++++- 18 files changed, 525 insertions(+), 8 deletions(-) create mode 100644 main/menu/import.ts create mode 100644 src/export/Export.vue create mode 100644 src/export/store.ts create mode 100644 testData/import/import_example.xlsx diff --git a/README.md b/README.md index 97c5fd2e..0a7bc008 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,11 @@ Any type of contributions are welcome. * json - [.json, .arb (Flutter Internationalization)] * yaml - [.yaml, .yml] +#### Import / export translations + +* Import translations from XLSX [.xlsx, .xls] files +* Export translations to XLSX or Comma-separated [.xlsx, .csv] files + **Feature requests and/or pull requests with new plugins are welcomed 🙂** **If you want to test the features, you can open the testData folder!** diff --git a/common/ipcMessages.ts b/common/ipcMessages.ts index 96eb439c..daa6b357 100644 --- a/common/ipcMessages.ts +++ b/common/ipcMessages.ts @@ -13,3 +13,6 @@ export const saveSettings = 'saveSettings'; export const recentFolders = 'recentFolders'; export const closeFolder = 'closeFolder'; export const refreshFolder = 'refreshFolder'; +export const showExport = 'showExport'; +export const createXls = 'createXls' +export const exportComplete = 'exportComplete' diff --git a/main/events.ts b/main/events.ts index 81772c4e..ffca6df4 100644 --- a/main/events.ts +++ b/main/events.ts @@ -5,6 +5,7 @@ import { ParsedFile } from '../common/types'; import * as fileManager from './fileManager'; import * as settings from './Settings'; import * as windowManager from './windowManager'; +import { getCurrentWindow } from './windowManager' const onSave = async (e: any, data: any) => { const window = BrowserWindow.fromWebContents(e.sender); @@ -38,6 +39,23 @@ const onSave = async (e: any, data: any) => { } }; +const createXLS = async (e: any, data: Object) => { + const window = getCurrentWindow(); + if (!window) { + return false; + } + try { + const res = await fileManager.saveXls(data, window); + if (res) { + dialog.showMessageBox(window, { message: 'Export complete' }); + } + } catch (e) { + dialog.showErrorBox('Failed export', 'Failed to create xls or csv file'); + } finally { + windowManager.sendExportComplete(window) + } +} + const onOpen = (e: any, data: string) => { const window = BrowserWindow.fromWebContents(e.sender); if (!window) return; @@ -81,6 +99,7 @@ const registerAppEvents = () => { ipcMain.on(ipcMessages.saveSettings, onSaveSettings); ipcMain.on(ipcMessages.settings, onGetSettings); ipcMain.on(ipcMessages.recentFolders, onRecentFolders); + ipcMain.on(ipcMessages.createXls, createXLS); app.on('open-file', onOpenFile); app.on('will-finish-launching', () => { diff --git a/main/fileManager.ts b/main/fileManager.ts index a5668193..cb45b4eb 100644 --- a/main/fileManager.ts +++ b/main/fileManager.ts @@ -3,18 +3,21 @@ import { exists } from 'fs'; import { promisify } from 'util'; import nodeWatch from 'node-watch'; import * as _ from 'lodash/fp'; +import * as xlsx from 'xlsx'; +import * as path from 'path'; import { LoadedFolder, LoadedGroup, LoadedPath, ParsedFile } from '../common/types'; -import { loadFolder, saveFile } from './pluginManager'; +import { loadFolder, saveFile, parseXlsx } from './pluginManager'; import * as settings from './Settings'; import { createWindow, - getAvailableWindow, + getAvailableWindow, getCurrentWindow, sendClose, sendOpen, + sendSave, sendRecentFolders, - sendRefreshFolder, -} from './windowManager'; + sendRefreshFolder +} from './windowManager' const existsAsync = promisify(exists); @@ -23,6 +26,38 @@ export const openFolder = async (folderPath: string) => { await openFolderInWindow(folderPath, window); }; +export const openFile = async (filePath: string) => { + const window = getAvailableWindow() || createWindow(); + const isValidPath = await existsAsync(filePath); + if (!isValidPath) { + dialog.showMessageBox(window, { + type: 'error', + message: `File not found in the given path "${filePath}"`, + }); + } else { + const { canceled, filePaths } = await dialog.showOpenDialog({ + title: 'Select save directory', + properties: ['openDirectory'], + }); + if (canceled) { return; } + const savePath = filePaths[0]; + settings.removeRecentFolder(savePath); + const parsedData = await parseXlsx(filePath, savePath); + if (parsedData.length) { + await sendOpen(window, path.parse(filePath).dir, parsedData); + app.addRecentDocument(savePath); + const recentFolders = settings.addRecentFolder(savePath); + sendRecentFolders(window, recentFolders); + sendSave(window); + } else { + dialog.showMessageBox(window, { + type: 'error', + message: `Data not found in the given file "${filePath}"`, + }); + } + } +}; + export const openFolderInWindow = async (folderPath: string, window: Electron.BrowserWindow) => { let recentFolders: string[]; @@ -60,6 +95,46 @@ export const saveFolder = async (data: LoadedPath[]): Promise => { ); }; +export const saveXls = async (data: any, window: Electron.BrowserWindow) => { + if (!window) { + return false + } + const file = await dialog.showSaveDialog(window, { + filters: [ + { name: 'Microsoft Excel (xlsx)', extensions: ['xlsx', 'xls'] }, + { name: 'Comma-separated values (csv)', extensions: ['csv'] }, + ], + }); + if (file.canceled) { + return false; + } + const { selectedLanguages, sheetData } = data; + const wb = xlsx.utils.book_new(); + const ws = xlsx.utils.aoa_to_sheet([['filename', 'label', ...selectedLanguages]]); + for (const fileName in sheetData) { + for (const label in sheetData[fileName]) { + let row = [fileName, label]; + selectedLanguages.map((lang: string) => { + const val = sheetData[fileName][label][lang] || ''; + row.push(val); + }); + xlsx.utils.sheet_add_aoa(ws, [ + [...row] + ], { + origin: -1, + }); + } + } + const filePath = file.filePath || '' + xlsx.utils.book_append_sheet(wb, ws); + try { + await xlsx.writeFile(wb, filePath); + return true; + } catch (e) { + throw e; + } +}; + const getParsedFiles = (data: LoadedPath[]): ParsedFile[] => data .map((it) => diff --git a/main/menu/import.ts b/main/menu/import.ts new file mode 100644 index 00000000..a90a6cf3 --- /dev/null +++ b/main/menu/import.ts @@ -0,0 +1,41 @@ +import { dialog } from 'electron'; +import { openFile } from '../fileManager'; +import * as windowManager from '../windowManager'; + +const openDirectory = async () => { + const { canceled, filePaths } = await dialog.showOpenDialog({ + filters: [ + { name: 'Supported files xls / xlsx', extensions: ['xls', 'xlsx'] }, + { name: 'All Files', extensions: ['*'] }, + ], + properties: ['openFile'], + }); + if (!canceled) { + openFile(filePaths[0]); + } +}; + +const openExport = async () => { + const window = windowManager.getCurrentWindow(); + if (!window) { + return; + } + + windowManager.sendShowExport(window); +}; + +const importMenu: Electron.MenuItemConstructorOptions = { + label: 'Import / Export', + submenu: [ + { + label: 'Import From XLSX', + click: openDirectory, + }, + { + label: 'Export To XLSX or CSV', + click: openExport, + }, + ], +}; + +export default importMenu; diff --git a/main/menu/index.ts b/main/menu/index.ts index 729bd852..2543b8a3 100644 --- a/main/menu/index.ts +++ b/main/menu/index.ts @@ -6,6 +6,7 @@ import fileMenu from './file'; import helpMenu from './help'; import viewMenu from './view'; import windowMenu from './window'; +import importMenu from './import' import MenuItem = Electron.MenuItem; @@ -18,6 +19,7 @@ if (Object.keys(appMenu).length > 0) { } menuTemplate.push(fileMenu); menuTemplate.push(editMenu); +menuTemplate.push(importMenu); menuTemplate.push(viewMenu); menuTemplate.push(windowMenu); menuTemplate.push(helpMenu); diff --git a/main/pluginManager.ts b/main/pluginManager.ts index 67a8860e..fc1d95ff 100644 --- a/main/pluginManager.ts +++ b/main/pluginManager.ts @@ -2,10 +2,12 @@ import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; import * as util from 'util'; +import * as xlsx from 'xlsx'; import { getLocale } from '../common/language'; import { LoadedFolder, LoadedGroup, LoadedPath, ParsedFile } from '../common/types'; import getPlugins, { IPlugin } from './plugins'; +import { setWith, omit } from 'lodash'; const readdirAsync = util.promisify(fs.readdir); const readFileAsync = util.promisify(fs.readFile); @@ -27,6 +29,59 @@ export const loadFolder = async (folderPath: string): Promise => { return groupedFiles.concat(groupedLanguageFolders).concat(subFolders); }; +export const parseXlsx = async (filePath: string, savePath: string): Promise => { + try { + const workbook = await xlsx.readFile(filePath, { + cellHTML: false, + }); + const sheet = workbook.Sheets[workbook.SheetNames[0]]; + const sheetJson = xlsx.utils.sheet_to_json(sheet); + const sheetObject: { [key: string]: { [key: string]: ParsedFile } } = {}; + await sheetJson.map((row: any) => { + let filename: any = row.filename; + let label: any = row.label; + const languages: any = omit(row, ['filename', 'label']); + for (const lang in languages) { + if (!sheetObject[filename]) { + sheetObject[filename] = {}; + } + if (!sheetObject[filename][lang]) { + sheetObject[filename][lang] = { + fileName: filename + '_' + lang, + filePath: savePath + '\\' + filename + '_' + lang + '.json', + prefix: filename, + language: lang, + extension: '.json', + data: {}, + } as ParsedFile; + + try { + writeFileAsync(savePath + '\\' + filename + '_' + lang + '.json', '{}'); + } catch (e) {} + + } + setWith(sheetObject[filename][lang].data, label, languages[lang], Object); + } + }); + const parsedFiles: LoadedGroup[] = []; + const files = Object.keys(sheetObject); + await files.map((filename) => { + const items: ParsedFile[] = []; + for (const k in sheetObject[filename]) { + items.push(sheetObject[filename][k]); + } + parsedFiles.push({ + type: 'file', + name: filename, + items: items, + } as LoadedGroup); + }); + return parsedFiles; + } catch (e) { + return []; + } +}; + export const parseFile = async (filePath: string): Promise => { try { const fileContent = await readFileAsync(filePath); @@ -53,7 +108,6 @@ export const saveFile = async (parsedFile: ParsedFile): Promise => { const data = await plugin.parse(fileContent.toString()); const updatedData = mergeDrop(data, parsedFile.data); - const serializedContent = await plugin.serialize(updatedData); if (serializedContent === null) { return false; diff --git a/main/plugins/json/index.ts b/main/plugins/json/index.ts index e5cf6598..a9ddd543 100644 --- a/main/plugins/json/index.ts +++ b/main/plugins/json/index.ts @@ -10,7 +10,7 @@ export const parse = (content: string): Promise => { export const serialize = async (data: object): Promise => { try { - return JSON.stringify(data, Object.keys(data).sort(), 2); + return JSON.stringify(data, undefined, 2); } catch (e) { return undefined; } diff --git a/main/windowManager.ts b/main/windowManager.ts index c46a8b08..aaf10d14 100644 --- a/main/windowManager.ts +++ b/main/windowManager.ts @@ -86,6 +86,14 @@ const sendToIpc = (window: BrowserWindow, message: string, data?: any) => { } }; +export const sendShowExport = (window: BrowserWindow) => { + sendToIpc(window, ipcMessages.showExport); +}; + +export const sendExportComplete = (window: BrowserWindow) => { + sendToIpc(window, ipcMessages.exportComplete) +}; + export enum SaveResponse { Save, Cancel, diff --git a/package.json b/package.json index a52e4278..3f88614a 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "node-watch": "^0.6.3", "rimraf": "^3.0.2", "roboto-fontface": "*", + "snyk": "^1.316.1", "vue": "^2.6.11", "vue-class-component": "^7.2.3", "vue-router": "^3.1.6", @@ -81,7 +82,7 @@ "vuex": "^3.1.3", "vuex-class": "^0.3.2", "vuex-module-decorators": "^0.16.1", - "snyk": "^1.316.1" + "xlsx": "^0.16.7" }, "browserslist": [ "last 2 Chrome versions" diff --git a/src/App.vue b/src/App.vue index 8bce9fd9..d13ec255 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,6 +5,7 @@ + @@ -12,11 +13,13 @@ import { provideStore } from '@/store/utils'; import { defineComponent } from '@vue/composition-api'; import Settings from '@/settings/views/Settings.vue'; + import Export from '@/export/Export.vue'; export default defineComponent({ name: 'App', components: { Settings, + Export }, setup(props, ctx) { provideStore(ctx.root.$store); diff --git a/src/export/Export.vue b/src/export/Export.vue new file mode 100644 index 00000000..fca9d605 --- /dev/null +++ b/src/export/Export.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/src/export/store.ts b/src/export/store.ts new file mode 100644 index 00000000..752b534f --- /dev/null +++ b/src/export/store.ts @@ -0,0 +1,37 @@ +import { Action, Module, Mutation, VuexModule } from 'vuex-module-decorators'; + +import { sendIpc } from '@/store/plugins/ipc'; +import * as ipcMessages from '@common/ipcMessages'; + +@Module({ + namespaced: true, +}) +export default class ExportModule extends VuexModule { + isExportVisible = false; + isLoading = false; + + @Action + createXLS(data: Object) { + sendIpc(ipcMessages.createXls, data); + } + + @Mutation + showExport() { + this.isExportVisible = true; + } + + @Mutation + hideExport() { + this.isExportVisible = false; + } + + @Mutation + showLoading() { + this.isLoading = true; + } + + @Mutation + hideLoading() { + this.isLoading = false + } +} diff --git a/src/folder/store.ts b/src/folder/store.ts index 9680c5b0..3ea4a3ff 100644 --- a/src/folder/store.ts +++ b/src/folder/store.ts @@ -68,6 +68,7 @@ export default class FolderModule extends VuexModule { commit('setSelectedItem', null); commit('setModifiedContent', false); commit('setClipboard', { item: null, action: null }); + await dispatch('createLanguageList'); await dispatch('sendModifiedContent'); } diff --git a/src/store/index.ts b/src/store/index.ts index e8d53fb4..60444c61 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,6 +7,7 @@ import global from './modules/global'; import home from '@/home/store'; import folder from '@/folder/store'; import settings from '@/settings/store'; +import exportModule from '@/export/store' Vue.use(Vuex); @@ -20,6 +21,7 @@ export default new Vuex.Store({ home, folder, settings, + export: exportModule }, plugins: [ipcPlugin], }); diff --git a/src/store/plugins/ipc.ts b/src/store/plugins/ipc.ts index 2481a954..cd9125e0 100644 --- a/src/store/plugins/ipc.ts +++ b/src/store/plugins/ipc.ts @@ -85,10 +85,21 @@ export class IpcModule extends VuexModule { @Action closeFolder() { this.context.dispatch('folder/closeFolder', undefined, { root: true }); + this.context.commit('export/hideExport', undefined, { root: true }); } @Action refreshFolder(data: LoadedPath[]) { this.context.dispatch('folder/refreshFolder', data, { root: true }); } + + @Action + showExport() { + this.context.commit('export/showExport', undefined, { root: true }); + } + + @Action + exportComplete() { + this.context.commit('export/hideLoading', undefined, { root: true }); + } } diff --git a/testData/import/import_example.xlsx b/testData/import/import_example.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..18fbc177a72e662fd9721f9bd73505ca70e8073a GIT binary patch literal 13404 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 literal 0 HcmV?d00001 diff --git a/yarn.lock b/yarn.lock index 81c143aa..3f262d59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1181,6 +1181,14 @@ address@^1.1.2: resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== +adler-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" + integrity sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU= + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -2117,6 +2125,15 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +cfb@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.0.tgz#6a4d0872b525ed60349e1ef51fb4b0bf73eca9a8" + integrity sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ== + dependencies: + adler-32 "~1.2.0" + crc-32 "~1.2.0" + printj "~1.1.2" + chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2402,6 +2419,14 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codepage@~1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" + integrity sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k= + dependencies: + commander "~2.14.1" + exit-on-epipe "~1.0.1" + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -2462,7 +2487,7 @@ commander@2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== -commander@2.17.x: +commander@2.17.x, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== @@ -2472,6 +2497,11 @@ commander@^2.12.1, commander@^2.18.0, commander@^2.20.0, commander@^2.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@~2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + integrity sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw== + commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -2661,6 +2691,14 @@ cosmiconfig@^5.0.0: js-yaml "^3.13.1" parse-json "^4.0.0" +crc-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" + integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + create-ecdh@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" @@ -3903,6 +3941,11 @@ execa@^3.3.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +exit-on-epipe@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -4294,6 +4337,11 @@ forwarded@~0.1.2: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -7953,6 +8001,11 @@ pretty-error@^2.0.2: renderkid "^2.0.1" utila "~0.4" +printj@~1.1.0, printj@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -9463,6 +9516,13 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" @@ -10850,11 +10910,21 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" @@ -10929,6 +10999,21 @@ xdg-basedir@^4.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xlsx@^0.16.7: + version "0.16.7" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.16.7.tgz#62fd6590addac7c4419daaaa2b0c5388015d5f69" + integrity sha512-Xc4NRjci2Grbh9NDk/XoaWycJurxEug1wwn0aJCmB0NvIMyQuHYq2muWLWGidYNZPf94aUbqm6K8Fbjd7gKTZg== + dependencies: + adler-32 "~1.2.0" + cfb "^1.1.4" + codepage "~1.14.0" + commander "~2.17.1" + crc-32 "~1.2.0" + exit-on-epipe "~1.0.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + xml2js@0.4.23, xml2js@^0.4.17, xml2js@^0.4.9: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" From 056d6d908915691589b96b246d281bfd1d9bd910 Mon Sep 17 00:00:00 2001 From: Andrew P Date: Wed, 7 Oct 2020 08:05:57 +0200 Subject: [PATCH 2/2] Handle folder watcher closing --- common/ipcMessages.ts | 4 ++-- main/fileManager.ts | 20 +++++++++++++++----- main/windowManager.ts | 2 ++ src/export/store.ts | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/common/ipcMessages.ts b/common/ipcMessages.ts index daa6b357..c3d074e7 100644 --- a/common/ipcMessages.ts +++ b/common/ipcMessages.ts @@ -14,5 +14,5 @@ export const recentFolders = 'recentFolders'; export const closeFolder = 'closeFolder'; export const refreshFolder = 'refreshFolder'; export const showExport = 'showExport'; -export const createXls = 'createXls' -export const exportComplete = 'exportComplete' +export const createXls = 'createXls'; +export const exportComplete = 'exportComplete'; diff --git a/main/fileManager.ts b/main/fileManager.ts index cb45b4eb..46015254 100644 --- a/main/fileManager.ts +++ b/main/fileManager.ts @@ -4,7 +4,6 @@ import { promisify } from 'util'; import nodeWatch from 'node-watch'; import * as _ from 'lodash/fp'; import * as xlsx from 'xlsx'; -import * as path from 'path'; import { LoadedFolder, LoadedGroup, LoadedPath, ParsedFile } from '../common/types'; import { loadFolder, saveFile, parseXlsx } from './pluginManager'; @@ -20,6 +19,7 @@ import { } from './windowManager' const existsAsync = promisify(exists); +let watcher: any; export const openFolder = async (folderPath: string) => { const window = getAvailableWindow() || createWindow(); @@ -41,14 +41,15 @@ export const openFile = async (filePath: string) => { }); if (canceled) { return; } const savePath = filePaths[0]; - settings.removeRecentFolder(savePath); const parsedData = await parseXlsx(filePath, savePath); if (parsedData.length) { - await sendOpen(window, path.parse(filePath).dir, parsedData); + await sendOpen(window, savePath, parsedData); + sendSave(window); + watchFolder(window, savePath); + app.addRecentDocument(savePath); const recentFolders = settings.addRecentFolder(savePath); sendRecentFolders(window, recentFolders); - sendSave(window); } else { dialog.showMessageBox(window, { type: 'error', @@ -142,11 +143,20 @@ const getParsedFiles = (data: LoadedPath[]): ParsedFile[] => ) .flat(); +export const closeFolderWatcher = function () { + if (typeof watcher !== undefined) { + if (watcher && typeof watcher.close !== undefined) { + watcher.close(); + } + } +} + const watchFolder = (window: Electron.BrowserWindow, folderPath: string) => { const handleFileUpdate = _.debounce(1000, async () => { const parsedFiles = await loadFolder(folderPath); sendRefreshFolder(window, parsedFiles); }); - nodeWatch(folderPath, { recursive: true }, handleFileUpdate); + closeFolderWatcher(); + watcher = nodeWatch(folderPath, { recursive: true }, handleFileUpdate); }; diff --git a/main/windowManager.ts b/main/windowManager.ts index aaf10d14..58d6f251 100644 --- a/main/windowManager.ts +++ b/main/windowManager.ts @@ -6,6 +6,7 @@ import * as ipcMessages from '../common/ipcMessages'; import { LoadedPath } from '../common/types'; import { getFormattedFoldersPaths } from './pathUtils'; import * as settings from './Settings'; +import { closeFolderWatcher } from './fileManager' export const hasWindows = (): boolean => BrowserWindow.getAllWindows().length > 0; @@ -73,6 +74,7 @@ export const sendRecentFolders = (window: BrowserWindow, data: string[]) => { }; export const sendClose = (window: BrowserWindow) => { + closeFolderWatcher(); sendToIpc(window, ipcMessages.closeFolder, {}); }; diff --git a/src/export/store.ts b/src/export/store.ts index 752b534f..ee001453 100644 --- a/src/export/store.ts +++ b/src/export/store.ts @@ -32,6 +32,6 @@ export default class ExportModule extends VuexModule { @Mutation hideLoading() { - this.isLoading = false + this.isLoading = false; } }