diff --git a/package.json b/package.json index 8c7df897..577cd7f0 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@vue/cli-plugin-router": "^4.5.0", "@vue/cli-service": "^4.5.0", "autoprefixer": "^9", - "electron": "^8.0.0", + "electron": "^8.5.5", "electron-devtools-installer": "^3.2.0", "electron-notarize": "^1.1.1", "electron-updater": "^4.3.9", @@ -53,8 +53,7 @@ "raw-loader": "^4.0.2", "tailwindcss": "npm:@tailwindcss/postcss7-compat", "vue-cli-plugin-electron-builder": "^2.0.0", - "vue-template-compiler": "^2.6.10", - "webpack": "^5.61.0" + "vue-template-compiler": "^2.6.10" }, "gitHooks": { "pre-commit": "lint-staged" diff --git a/src/App.vue b/src/App.vue index 5befd873..f00053c9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -30,43 +30,43 @@ import SetupWizard from './pages/SetupWizard/SetupWizard'; import DatabaseSelector from './pages/DatabaseSelector'; import Settings from '@/pages/Settings/Settings.vue'; import WindowsTitleBar from '@/components/WindowsTitleBar'; -import { remote } from 'electron'; +import { ipcRenderer } from 'electron'; import config from '@/config'; import { connectToLocalDatabase } from '@/utils'; -import { getMainWindowSize } from '@/screenSize'; +import { IPC_MESSAGES, IPC_ACTIONS } from '@/messages'; export default { name: 'App', data() { return { - activeScreen: null + activeScreen: null, }; }, watch: { - activeScreen(value) { + async activeScreen(value) { if (!value) return; - let { width, height } = getMainWindowSize(); + const { width, height } = ipcRenderer.invoke( + IPC_ACTIONS.GET_PRIMARY_DISPLAY_SIZE + ); let size = { Desk: [width, height], DatabaseSelector: [600, 600], SetupWizard: [600, 600], - Settings: [460, 577] + Settings: [460, 577], }[value]; let resizable = value === 'Desk'; - let win = remote.getCurrentWindow(); - if (size.length) { - win.setSize(...size); - win.setResizable(resizable); + if (size.length && value != 'Settings') { + ipcRenderer.send(IPC_MESSAGES.RESIZE_MAIN_WINDOW, size, resizable); } - } + }, }, components: { Desk, SetupWizard, DatabaseSelector, Settings, - WindowsTitleBar + WindowsTitleBar, }, async mounted() { let lastSelectedFilePath = config.get('lastSelectedFilePath', null); @@ -99,7 +99,7 @@ export default { }, checkForUpdates() { frappe.events.trigger('check-for-updates'); - } - } + }, + }, }; diff --git a/src/background.js b/src/background.js index d30a7560..a9a22c4f 100644 --- a/src/background.js +++ b/src/background.js @@ -1,32 +1,54 @@ 'use strict'; -import { app, protocol, BrowserWindow, ipcMain } from 'electron'; +import electron, { + app, + dialog, + protocol, + BrowserWindow, + ipcMain, + Menu, +} from 'electron'; import { autoUpdater } from 'electron-updater'; import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'; -import { - createProtocol - // installVueDevtools -} from 'vue-cli-plugin-electron-builder/lib'; +import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'; + +import saveHtmlAsPdf from './saveHtmlAsPdf'; +import { IPC_MESSAGES, IPC_ACTIONS } from './messages'; import theme from '@/theme'; -import { getMainWindowSize } from './screenSize'; const isDevelopment = process.env.NODE_ENV !== 'production'; const isMac = process.platform === 'darwin'; const isLinux = process.platform === 'linux'; -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. +// Global ref to prevent garbage collection. let mainWindow; let winURL; let checkedForUpdate = false; // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ - { scheme: 'app', privileges: { secure: true, standard: true } } + { scheme: 'app', privileges: { secure: true, standard: true } }, ]); +/* ----------------------------- + * Main process helper functions + * -----------------------------*/ + +function getMainWindowSize() { + let height; + if (app.isReady()) { + // const screen = require('electron').screen; + const screen = electron.screen; + height = screen.getPrimaryDisplay().workAreaSize.height; + height = height > 907 ? 907 : height; + } else { + height = 907; + } + const width = Math.ceil(1.323 * height); + return { height, width }; +} + function createWindow() { - // Create the browser window. let { width, height } = getMainWindowSize(); mainWindow = new BrowserWindow({ vibrancy: 'sidebar', @@ -35,10 +57,10 @@ function createWindow() { width, height, webPreferences: { - nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION + nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, }, frame: isLinux, - resizable: true + resizable: true, }); if (process.env.WEBPACK_DEV_SERVER_URL) { @@ -58,6 +80,12 @@ function createWindow() { mainWindow.on('closed', () => { mainWindow = null; }); + + mainWindow.webContents.on('did-finish-load', () => { + mainWindow.webContents.send('store-on-window', { + appVersion: app.getVersion(), + }); + }); } function createSettingsWindow(tab = 'General') { @@ -68,49 +96,111 @@ function createSettingsWindow(tab = 'General') { height: 577, backgroundColor: theme.backgroundColor.gray['200'], webPreferences: { - nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION + nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, }, - resizable: false + resizable: false, }); settingsWindow.loadURL(`${winURL}#/settings/${tab}`); + settingsWindow.on('close', () => { + mainWindow.reload(); + }); } -ipcMain.on('check-for-updates', () => { +/* --------------------------------- + * Register ipcMain message handlers + * ---------------------------------*/ + +ipcMain.on(IPC_MESSAGES.CHECK_FOR_UPDATES, () => { if (!isDevelopment && !checkedForUpdate) { autoUpdater.checkForUpdatesAndNotify(); checkedForUpdate = true; } }); -ipcMain.on('open-settings-window', (event, tab) => { +ipcMain.on(IPC_MESSAGES.OPEN_SETTINGS, (event, tab) => { createSettingsWindow(tab); }); -ipcMain.on('reload-main-window', () => { +ipcMain.on(IPC_MESSAGES.OPEN_SETTINGS, (event) => { + const window = event.sender.getOwnerBrowserWindow(); + const menu = Menu.getApplicationMenu(); + menu.popup({ window }); +}); + +ipcMain.on(IPC_MESSAGES.RELOAD_MAIN_WINDOW, () => { mainWindow.reload(); }); -// Quit when all windows are closed. +ipcMain.on(IPC_MESSAGES.RESIZE_MAIN_WINDOW, (event, size, resizable) => { + const [width, height] = size; + if (!width || !height) return; + mainWindow.setSize(width, height); + mainWindow.setResizable(resizable); +}); + +ipcMain.on(IPC_MESSAGES.CLOSE_CURRENT_WINDOW, (event) => { + event.sender.getOwnerBrowserWindow().close(); +}); + +ipcMain.on(IPC_MESSAGES.MINIMIZE_CURRENT_WINDOW, (event) => { + event.sender.getOwnerBrowserWindow().minimize(); +}); + +/* ---------------------------------- + * Register ipcMain function handlers + * ----------------------------------*/ + +ipcMain.handle(IPC_ACTIONS.TOGGLE_MAXIMIZE_CURRENT_WINDOW, (event) => { + const window = event.sender.getOwnerBrowserWindow(); + const maximizing = !window.isMaximized(); + if (maximizing) { + window.maximize(); + } else { + window.unmaximize(); + } + return maximizing; +}); + +ipcMain.handle(IPC_ACTIONS.GET_OPEN_FILEPATH, async (event, options) => { + const window = event.sender.getOwnerBrowserWindow(); + return await dialog.showOpenDialog(window, options); +}); + +ipcMain.handle(IPC_ACTIONS.GET_SAVE_FILEPATH, async (event, options) => { + const window = event.sender.getOwnerBrowserWindow(); + return await dialog.showSaveDialog(window, options); +}); + +ipcMain.handle(IPC_ACTIONS.GET_PRIMARY_DISPLAY_SIZE, (event) => { + return getMainWindowSize(); +}); + +ipcMain.handle(IPC_ACTIONS.GET_DIALOG_RESPONSE, async (event, options) => { + const window = event.sender.getOwnerBrowserWindow(); + return await dialog.showMessageBox(window, options); +}); + +ipcMain.handle(IPC_ACTIONS.SAVE_HTML_AS_PDF, async (event, html, savePath) => { + return await saveHtmlAsPdf(html, savePath); +}); + +/* ------------------------------ + * Register app lifecycle methods + * ------------------------------*/ + app.on('window-all-closed', () => { - // On macOS it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. if (mainWindow === null) { createWindow(); } }); -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. app.on('ready', async () => { if (isDevelopment && !process.env.IS_TEST) { // Install Vue Devtools @@ -128,10 +218,13 @@ app.on('ready', async () => { createWindow(); }); -// Exit cleanly on request from parent process in development mode. +/* ------------------------------ + * Register node#process messages + * ------------------------------*/ + if (isDevelopment) { if (process.platform === 'win32') { - process.on('message', data => { + process.on('message', (data) => { if (data === 'graceful-exit') { app.quit(); } diff --git a/src/components/Controls/AttachImage.vue b/src/components/Controls/AttachImage.vue index 40ebe250..e8d6e100 100644 --- a/src/components/Controls/AttachImage.vue +++ b/src/components/Controls/AttachImage.vue @@ -41,8 +41,9 @@ diff --git a/src/components/WindowsTitleBar.vue b/src/components/WindowsTitleBar.vue index e98514bf..3f054115 100644 --- a/src/components/WindowsTitleBar.vue +++ b/src/components/WindowsTitleBar.vue @@ -30,23 +30,20 @@ diff --git a/src/main.js b/src/main.js index e18f3f23..a8a05d04 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,7 @@ import router from './router'; // other imports import { ipcRenderer } from 'electron'; +import { IPC_MESSAGES } from './messages'; import Store from 'electron-store'; (async () => { @@ -26,17 +27,22 @@ import Store from 'electron-store'; frappe.fetch = window.fetch.bind(); frappe.events.on('reload-main-window', () => { - ipcRenderer.send('reload-main-window'); + ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW); }); frappe.events.on('check-for-updates', () => { let { autoUpdate } = frappe.AccountingSettings; if (autoUpdate == null || autoUpdate === 1) { - ipcRenderer.send('check-for-updates'); + ipcRenderer.send(IPC_MESSAGES.CHECK_FOR_UPDATES); } }); window.frappe = frappe; + window.frappe.store = {}; + + ipcRenderer.on('store-on-window', (event, message) => { + Object.assign(window.frappe.store, message); + }); Vue.config.productionTip = false; Vue.component('feather-icon', FeatherIcon); @@ -51,22 +57,22 @@ import Store from 'electron-store'; return { win32: 'Windows', darwin: 'Mac', - linux: 'Linux' + linux: 'Linux', }[process.platform]; - } + }, }, methods: { _(...args) { return frappe._(...args); - } - } + }, + }, }); Vue.config.errorHandler = (err, vm, info) => { console.error(err, vm, info); }; - process.on('unhandledRejection', error => { + process.on('unhandledRejection', (error) => { console.error(error); }); @@ -75,8 +81,8 @@ import Store from 'electron-store'; el: '#app', router, components: { - App + App, }, - template: '' + template: '', }); })(); diff --git a/src/menu.js b/src/menu.js deleted file mode 100644 index 0e87332f..00000000 --- a/src/menu.js +++ /dev/null @@ -1,11 +0,0 @@ -const { remote } = require('electron'); -const { Menu } = remote; - -function openMenu() { - const menu = Menu.getApplicationMenu(); - menu.popup({ window: remote.getCurrentWindow() }); -} - -module.exports = { - openMenu -}; diff --git a/src/messages.js b/src/messages.js new file mode 100644 index 00000000..fb2d258f --- /dev/null +++ b/src/messages.js @@ -0,0 +1,18 @@ +export const IPC_MESSAGES = { + OPEN_MENU: 'open-menu', + OPEN_SETTINGS: 'open-settings', + CHECK_FOR_UPDATES: 'check-for-updates', + RELOAD_MAIN_WINDOW: 'reload-main-window', + RESIZE_MAIN_WINDOW: 'resize-main-window', + CLOSE_CURRENT_WINDOW: 'close-current-window', + MINIMIZE_CURRENT_WINDOW: 'minimize-current-window', +}; + +export const IPC_ACTIONS = { + TOGGLE_MAXIMIZE_CURRENT_WINDOW: 'toggle-maximize-current-window', + GET_OPEN_FILEPATH: 'open-dialog', + GET_SAVE_FILEPATH: 'save-dialog', + GET_DIALOG_RESPONSE: 'show-message-box', + GET_PRIMARY_DISPLAY_SIZE: 'get-primary-display-size', + SAVE_HTML_AS_PDF: 'save-html-as-pdf' +}; diff --git a/src/pages/DataImport/index.vue b/src/pages/DataImport/index.vue index bb1d6ed9..bf5064f0 100644 --- a/src/pages/DataImport/index.vue +++ b/src/pages/DataImport/index.vue @@ -4,24 +4,28 @@

{{ _('Data Import') }}

- Upload CSV - Download CSV Template + Upload CSV + Download CSV Template Submit
@@ -35,13 +39,12 @@ import { convertFieldsToDatatableColumns } from 'frappejs/client/ui/utils'; import { writeFile } from 'frappejs/server/utils'; import path from 'path'; import csv2json from 'csvjson-csv2json'; -const { remote } = require('electron'); export default { data() { return { doctype: undefined, - fileUploaded: false + fileUploaded: false, }; }, methods: { @@ -59,7 +62,7 @@ export default { this.datatable = new DataTable(this.$refs.datatable, { columns, data: [[]], - pasteFromClipboard: true + pasteFromClipboard: true, }); }, async downloadCSV() { @@ -72,19 +75,26 @@ export default { const documentsPath = process.env.NODE_ENV === 'development' ? path.resolve('.') - : remote.getGlobal('documentsPath'); + : frappe.store.documentsPath; - await writeFile( - path.resolve(documentsPath + `/frappe-accounting/${this.doctype}.csv`), - csvString - ); + let title = frappe._('Message'); + let message = frappe._('Template saved successfully.'); + + if (documentsPath === undefined) { + title = frappe._('Error'); + message = frappe._('Template could not be saved.'); + } else { + await writeFile( + path.resolve( + documentsPath + `/frappe-accounting/${this.doctype}.csv` + ), + csvString + ); + } frappe.call({ method: 'show-dialog', - args: { - title: 'Message', - message: `Template Saved Successfully` - } + args: { title, message }, }); }, uploadCSV(file) { @@ -93,9 +103,9 @@ export default { reader.onload = () => { const meta = frappe.getMeta(this.doctype); let header = reader.result.split('\n')[0]; - header = header.split(',').map(label => { + header = header.split(',').map((label) => { let fieldname; - meta.fields.some(field => { + meta.fields.some((field) => { if (field.label === label.trim()) { fieldname = field.fieldname; return true; @@ -120,19 +130,19 @@ export default { importData() { const rows = this.datatable.datamanager.getRows(); - const data = rows.map(row => { + const data = rows.map((row) => { return row.slice(1).reduce((prev, curr) => { prev[curr.column.field.fieldname] = curr.content; return prev; }, {}); }); - data.forEach(async d => { + data.forEach(async (d) => { try { await frappe .newDoc( Object.assign(d, { - doctype: this.doctype + doctype: this.doctype, }) ) .insert(); @@ -144,10 +154,10 @@ export default { method: 'show-dialog', args: { title: 'Message', - message: `Data Imported Successfully` - } + message: `Data Imported Successfully`, + }, }); - } - } + }, + }, }; diff --git a/src/pages/PrintView/PrintView.vue b/src/pages/PrintView/PrintView.vue index a279b4aa..96cf4461 100644 --- a/src/pages/PrintView/PrintView.vue +++ b/src/pages/PrintView/PrintView.vue @@ -21,7 +21,12 @@ >
diff --git a/src/pages/Settings/Settings.vue b/src/pages/Settings/Settings.vue index 9b065454..1f307aac 100644 --- a/src/pages/Settings/Settings.vue +++ b/src/pages/Settings/Settings.vue @@ -27,8 +27,6 @@
diff --git a/src/pages/Settings/TabSystem.vue b/src/pages/Settings/TabSystem.vue index 0a80dac3..446fdec1 100644 --- a/src/pages/Settings/TabSystem.vue +++ b/src/pages/Settings/TabSystem.vue @@ -51,7 +51,8 @@ import TwoColumnForm from '@/components/TwoColumnForm'; import FormControl from '@/components/Controls/FormControl'; import Button from '@/components/Button'; import config from '@/config'; -import { remote } from 'electron'; +import { ipcRenderer } from 'electron'; +import { IPC_MESSAGES } from '@/messages'; export default { name: 'TabSystem', @@ -74,7 +75,7 @@ export default { changeFile() { config.set('lastSelectedFilePath', null); frappe.events.trigger('reload-main-window'); - remote.getCurrentWindow().close(); + ipcRenderer.send(IPC_MESSAGES.CLOSE_CURRENT_WINDOW); } }, computed: { diff --git a/src/saveHtmlAsPdf.js b/src/saveHtmlAsPdf.js new file mode 100644 index 00000000..e12bbbeb --- /dev/null +++ b/src/saveHtmlAsPdf.js @@ -0,0 +1,54 @@ +import fs from 'fs'; +import { shell, BrowserWindow } from 'electron'; + +const PRINT_OPTIONS = { + marginsType: 1, // no margin + pageSize: 'A4', + printBackground: true, + printBackgrounds: true, + printSelectionOnly: false, +}; + +export default async function makePDF(html, savePath) { + const printWindow = getInitializedPrintWindow(); + + printWindow.webContents.executeJavaScript(` + document.body.innerHTML = \`${html}\`; + `); + + const sleep = (m) => new Promise((r) => setTimeout(r, m)); + // TODO: Check if event 'paint' works after bumping electron. + printWindow.webContents.on('did-finish-load', async () => { + await sleep(1000); // Required else pdf'll be blank. + printWindow.webContents.printToPDF(PRINT_OPTIONS).then((data) => { + printWindow.destroy(); + fs.writeFile(savePath, data, (error) => { + if (error) throw error; + return shell.openItem(savePath); + }); + }); + }); +} + +function getInitializedPrintWindow() { + const printWindow = new BrowserWindow({ + width: 595, + height: 842, + show: false, + webPreferences: { + nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, + }, + }); + printWindow.loadURL(getPrintWindowUrl()); + return printWindow; +} + +function getPrintWindowUrl() { + let url = global.WEBPACK_DEV_SERVER_URL; + if (url) { + url = url + 'print'; + } else { + url = 'app://./print.html'; + } + return url; +} diff --git a/src/screenSize.js b/src/screenSize.js deleted file mode 100644 index 70892f1f..00000000 --- a/src/screenSize.js +++ /dev/null @@ -1,14 +0,0 @@ -import electron from 'electron'; - -export function getMainWindowSize() { - let screen = electron.screen || electron.remote.screen; - let { height } = screen.getPrimaryDisplay().workAreaSize; - let width; - - if (height > 907) { - height = 907; - } - width = Math.ceil(1.323 * height); - - return { width, height }; -} diff --git a/src/utils.js b/src/utils.js index 1a07d009..86d05945 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,7 +2,8 @@ import frappe from 'frappejs'; import fs from 'fs'; import { _ } from 'frappejs/utils'; import migrate from './migrate'; -import { remote, shell, ipcRenderer } from 'electron'; +import { ipcRenderer } from 'electron'; +import { IPC_MESSAGES, IPC_ACTIONS } from './messages'; import SQLite from 'frappejs/backends/sqlite'; import postStart from '../server/postStart'; import router from '@/router'; @@ -12,10 +13,13 @@ import config from '@/config'; export async function createNewDatabase() { const options = { title: _('Select folder'), - defaultPath: 'frappe-books.db' + defaultPath: 'frappe-books.db', }; - let { filePath } = await remote.dialog.showSaveDialog(options); + let { filePath } = await ipcRenderer.invoke( + IPC_ACTIONS.GET_SAVE_FILEPATH, + options + ); if (filePath) { if (!filePath.endsWith('.db')) { filePath = filePath + '.db'; @@ -30,10 +34,10 @@ export async function createNewDatabase() { action() { fs.unlinkSync(filePath); return filePath; - } + }, }, - { label: _('Cancel'), action() {} } - ] + { label: _('Cancel'), action() {} }, + ], }); } else { return filePath; @@ -45,10 +49,13 @@ export async function loadExistingDatabase() { const options = { title: _('Select file'), properties: ['openFile'], - filters: [{ name: 'SQLite DB File', extensions: ['db'] }] + filters: [{ name: 'SQLite DB File', extensions: ['db'] }], }; - let { filePaths } = await remote.dialog.showOpenDialog(options); + const { filePaths } = await ipcRenderer.invoke( + IPC_ACTIONS.GET_OPEN_FILEPATH, + options + ); if (filePaths && filePaths[0]) { return filePaths[0]; @@ -58,7 +65,7 @@ export async function loadExistingDatabase() { export async function connectToLocalDatabase(filepath) { frappe.login('Administrator'); frappe.db = new SQLite({ - dbPath: filepath + dbPath: filepath, }); await frappe.db.connect(); await migrate(); @@ -66,13 +73,13 @@ export async function connectToLocalDatabase(filepath) { // set file info in config let files = config.get('files') || []; - if (!files.find(file => file.filePath === filepath)) { + if (!files.find((file) => file.filePath === filepath)) { files = [ { companyName: frappe.AccountingSettings.companyName, - filePath: filepath + filePath: filepath, }, - ...files + ...files, ]; config.set('files', files); } @@ -84,16 +91,17 @@ export async function connectToLocalDatabase(filepath) { export async function showMessageDialog({ message, description, - buttons = [] + buttons = [], }) { - let buttonLabels = buttons.map(a => a.label); - const { response } = await remote.dialog.showMessageBox( - remote.getCurrentWindow(), - { - message, - detail: description, - buttons: buttonLabels - } + const options = { + message, + detail: description, + buttons: buttons.map((a) => a.label), + }; + + const { response } = await ipcRenderer.invoke( + IPC_ACTIONS.GET_DIALOG_RESPONSE, + options ); let button = buttons[response]; @@ -103,11 +111,11 @@ export async function showMessageDialog({ } export function deleteDocWithPrompt(doc) { - return new Promise(resolve => { + return new Promise((resolve) => { showMessageDialog({ message: _('Are you sure you want to delete {0} "{1}"?', [ doc.doctype, - doc.name + doc.name, ]), description: _('This action is permanent'), buttons: [ @@ -117,18 +125,18 @@ export function deleteDocWithPrompt(doc) { doc .delete() .then(() => resolve(true)) - .catch(e => { + .catch((e) => { handleErrorWithDialog(e, doc); }); - } + }, }, { label: _('Cancel'), action() { resolve(false); - } - } - ] + }, + }, + ], }); }); } @@ -138,11 +146,11 @@ export function partyWithAvatar(party) { data() { return { imageURL: null, - label: null + label: null, }; }, components: { - Avatar + Avatar, }, async mounted() { this.imageURL = await frappe.db.getValue('Party', party, 'image'); @@ -153,7 +161,7 @@ export function partyWithAvatar(party) { {{ label }} - ` + `, }; } @@ -173,8 +181,8 @@ export function openQuickEdit({ doctype, name, hideFields, defaults = {} }) { name, hideFields, values: defaults, - lastRoute: currentRoute - } + lastRoute: currentRoute, + }, }); } @@ -183,7 +191,7 @@ export function getErrorMessage(e, doc) { if (e.type === frappe.errors.LinkValidationError) { errorMessage = _('{0} {1} is linked with existing records.', [ doc.doctype, - doc.name + doc.name, ]); } else if (e.type === frappe.errors.DuplicateEntryError) { errorMessage = _('{0} {1} already exists.', [doc.doctype, doc.name]); @@ -197,69 +205,8 @@ export function handleErrorWithDialog(e, doc) { throw e; } -// NOTE: a hack to find all the css from the current document and inject it to the print version -// remove this if you are able to fix and get the default css loading on the page -function injectCSS(contents) { - const styles = document.getElementsByTagName('style'); - - for (let style of styles) { - contents.insertCSS(style.innerHTML); - } -} - -export async function makePDF(html, destination) { - const { BrowserWindow } = remote; - - let printWindow = new BrowserWindow({ - width: 595, - height: 842, - show: false, - webPreferences: { - nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, - enableRemoteModule: true - } - }); - - let webpackDevServerURL = remote.getGlobal('WEBPACK_DEV_SERVER_URL'); - if (webpackDevServerURL) { - // Load the url of the dev server if in development mode - printWindow.loadURL(webpackDevServerURL + 'print'); - } else { - // Load the index.html when not in development - printWindow.loadURL(`app://./print.html`); - } - - printWindow.on('closed', () => { - printWindow = null; - }); - - const code = ` - document.body.innerHTML = \`${html}\`; - `; - - printWindow.webContents.executeJavaScript(code); - - const printOptions = { - marginsType: 1, // no margin - pageSize: 'A4', - printBackground: true, - printBackgrounds: true, - printSelectionOnly: false - }; - - const sleep = m => new Promise(r => setTimeout(r, m)); - - printWindow.webContents.on('did-finish-load', async () => { - injectCSS(printWindow.webContents); - await sleep(1000); - printWindow.webContents.printToPDF(printOptions).then(data => { - printWindow.close(); - fs.writeFile(destination, data, error => { - if (error) throw error; - return (shell.openItem(destination)); - }); - }); - }); +export async function makePDF(html, savePath) { + ipcRenderer.invoke(IPC_ACTIONS.SAVE_HTML_AS_PDF, html, savePath); } export function getActionsForDocument(doc) { @@ -267,24 +214,24 @@ export function getActionsForDocument(doc) { let deleteAction = { component: { - template: `{{ _('Delete') }}` + template: `{{ _('Delete') }}`, }, - condition: doc => !doc.isNew() && !doc.submitted && !doc.meta.isSingle, + condition: (doc) => !doc.isNew() && !doc.submitted && !doc.meta.isSingle, action: () => - deleteDocWithPrompt(doc).then(res => { + deleteDocWithPrompt(doc).then((res) => { if (res) { router.push(`/list/${doc.doctype}`); } - }) + }), }; let actions = [...(doc.meta.actions || []), deleteAction] - .filter(d => (d.condition ? d.condition(doc) : true)) - .map(d => { + .filter((d) => (d.condition ? d.condition(doc) : true)) + .map((d) => { return { label: d.label, component: d.component, - action: d.action.bind(this, doc, router) + action: d.action.bind(this, doc, router), }; }); @@ -292,5 +239,23 @@ export function getActionsForDocument(doc) { } export function openSettings(tab = 'General') { - ipcRenderer.send('open-settings-window', tab); + ipcRenderer.send(IPC_MESSAGES.OPEN_SETTINGS, tab); +} + +export async function runWindowAction(name) { + switch (name) { + case 'close': + ipcRenderer.send(IPC_MESSAGES.CLOSE_CURRENT_WINDOW); + break; + case 'minimize': + ipcRenderer.send(IPC_MESSAGES.MINIMIZE_CURRENT_WINDOW); + break; + case 'maximize': + const maximizing = await ipcRenderer.invoke( + IPC_ACTIONS.TOGGLE_MAXIMIZE_CURRENT_WINDOW + ); + name = maximizing ? name : 'unmaximize'; + break; + } + return name; } diff --git a/vue.config.js b/vue.config.js index 7e0901f1..afa8f5d3 100644 --- a/vue.config.js +++ b/vue.config.js @@ -4,18 +4,21 @@ const webpack = require('webpack'); module.exports = { pluginOptions: { electronBuilder: { - nodeIntegration: true - } + nodeIntegration: true, + chainWebpackRendererProcess: (config) => { + config.target('electron-renderer'); + }, + }, }, pages: { index: { entry: 'src/main.js', - filename: 'index.html' + filename: 'index.html', }, print: { entry: 'src/print.js', - filename: 'print.html' - } + filename: 'print.html', + }, }, runtimeCompiler: true, lintOnSave: process.env.NODE_ENV !== 'production', @@ -23,7 +26,7 @@ module.exports = { Object.assign(config.resolve.alias, { deepmerge$: 'deepmerge/dist/umd.js', 'frappe-charts$': 'frappe-charts/dist/frappe-charts.esm.js', - '~': path.resolve('.') + '~': path.resolve('.'), }); config.plugins.push( @@ -36,10 +39,10 @@ module.exports = { config.module.rules.push({ test: /\.txt$/i, - use: 'raw-loader' + use: 'raw-loader', }); config.devtool = 'source-map'; }, - transpileDependencies: ['frappejs'] + transpileDependencies: ['frappejs'], }; diff --git a/yarn.lock b/yarn.lock index c9196801..2d2ccdb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5024,7 +5024,7 @@ electron@5.0.0: electron-download "^4.1.0" extract-zip "^1.0.3" -electron@^8.0.0: +electron@^8.5.5: version "8.5.5" resolved "https://registry.yarnpkg.com/electron/-/electron-8.5.5.tgz#17b12bd70139c0099f750fc5de0d480bf03acb96" integrity sha512-e355H+tRDial0m+X2v+l+0SnaATAPw4sNjv9qmdk/6MJz/glteVJwVJEnxTjPfEELIJSChrBWDBVpjdDvoBF4Q== @@ -12759,7 +12759,7 @@ webpack@^4.0.0, webpack@^4.16.1, webpack@^4.18.0: watchpack "^1.7.4" webpack-sources "^1.4.1" -webpack@^5.22.0, webpack@^5.61.0: +webpack@^5.22.0: version "5.61.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.61.0.tgz#fa827f0ee9bdfd141dd73c3e891e955ebd52fe7f" integrity sha512-fPdTuaYZ/GMGFm4WrPi2KRCqS1vDp773kj9S0iI5Uc//5cszsFEDgHNaX4Rj1vobUiU1dFIV3mA9k1eHeluFpw==