2
0
mirror of https://github.com/frappe/books.git synced 2025-02-08 23:18:31 +00:00

Merge pull request #29 from netchampfaris/electronify

Electronify
This commit is contained in:
Rushabh Mehta 2018-04-02 12:38:11 +05:30 committed by GitHub
commit e4dd0a9954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 5065 additions and 932 deletions

3
.gitignore vendored
View File

@ -4,4 +4,5 @@ Thumbs.db
*test.db *test.db
*.log *.log
.cache .cache
/temp /temp
dist

68
electron/client.js Normal file
View File

@ -0,0 +1,68 @@
const path = require('path');
const electron = require('frappejs/client/electron');
const { writeFile } = require('frappejs/server/utils');
const appClient = require('../client');
const SetupWizard = require('../setup');
(async () => {
const configFilePath = path.join(require('os').homedir(), '.config', 'frappe-accounting', 'settings.json');
let settings, dbPath;
try {
settings = require(configFilePath);
} catch(e) {
settings = {}
}
if (settings.dbPath) {
dbPath = settings.dbPath;
electron.start({
dbPath,
models: require('../models')
}).then(() => {
appClient.start();
});
} else {
const setup = new SetupWizard();
window.setup = setup;
const values = await setup.start();
const {
companyName,
file,
country,
name,
email,
abbreviation,
bankName
} = values;
dbPath = path.join(file[0].path, companyName + '.db');
electron.start({
dbPath,
models: require('../models')
}).then(async () => {
await writeFile(configFilePath, JSON.stringify({
directory: path.dirname(dbPath),
dbPath: dbPath
}));
const doc = await frappe.getDoc('AccountingSettings');
await doc.set('companyName', companyName);
await doc.set('file', dbPath);
await doc.set('country', country);
await doc.set('fullname', name);
await doc.set('email', email);
await doc.set('abbreviation', abbreviation);
await doc.set('bankName', bankName);
await doc.update();
appClient.start();
})
}
})();

12
electron/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link href="../www/dist/css/style.css" rel="stylesheet">
</head>
<body>
<script src="./client.js"></script>
</body>
</html>

2710
fixtures/countryInfo.json Normal file

File diff suppressed because it is too large Load Diff

60
main.js Normal file
View File

@ -0,0 +1,60 @@
const electron = require('electron')
// Module to control application life.
const app = electron.app
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow
const path = require('path')
const url = require('url')
// 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.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, 'electron/index.html'),
protocol: 'file:',
slashes: true
}))
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// 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', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On OS X 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', function () {
// On OS X 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()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

View File

@ -1,3 +1,5 @@
const countryList = Object.keys(require('../../../fixtures/countryInfo.json')).sort();
module.exports = { module.exports = {
name: "AccountingSettings", name: "AccountingSettings",
label: "AccountingSettings", label: "AccountingSettings",
@ -9,15 +11,60 @@ module.exports = {
keywordFields: [], keywordFields: [],
fields: [ fields: [
{ {
fieldname: "Company Name", label: "Company Name",
label: "companyName", fieldname: "companyName",
fieldtype: "Data", fieldtype: "Data",
required: 1 required: 1
}, },
{ {
fieldname: "Writeoff Account", label: "Writeoff Account",
label: "writeOffAccount", fieldname: "writeOffAccount",
fieldtype: "Account" fieldtype: "Account"
},
{
"fieldname": "file",
"label": "File",
"fieldtype": "Data",
"required": 1,
"directory": 1
},
{
"fieldname": "country",
"label": "Country",
"fieldtype": "Autocomplete",
"required": 1,
getList: () => countryList
},
{
"fieldname": "fullname",
"label": "Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "email",
"label": "Email",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "abbreviation",
"label": "Abbreviation",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "bankName",
"label": "Bank Name",
"fieldtype": "Data",
"required": 1
} }
] ]

View File

@ -1,6 +1,7 @@
module.exports = { module.exports = {
models: { models: {
Account: require('./doctype/Account/Account.js'), Account: require('./doctype/Account/Account.js'),
AccountingSettings: require('./doctype/AccountingSettings/AccountingSettings'),
AccountingLedgerEntry: require('./doctype/AccountingLedgerEntry/AccountingLedgerEntry.js'), AccountingLedgerEntry: require('./doctype/AccountingLedgerEntry/AccountingLedgerEntry.js'),
Party: require('./doctype/Party/Party.js'), Party: require('./doctype/Party/Party.js'),

View File

@ -1,15 +1,31 @@
{ {
"name": "frappe-accounting", "name": "frappe-accounting",
"description": "Simple Accounting app for everyone",
"productName": "Frappe Accounting",
"version": "1.0.0", "version": "1.0.0",
"main": "start.js", "author": {
"name": "Frappe Technologies Pvt. Ltd.",
"email": "hello@frappe.io"
},
"build": {
"appId": "io.frappe.accounting"
},
"main": "main.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "mocha tests", "test": "mocha tests",
"start": "nodemon start.js" "start": "nodemon server.js",
"watch": "rollup -c --watch",
"electron": "electron main.js",
"electron-pack": "electron-packager . --overwrite",
"postinstall": "electron-builder install-app-deps"
}, },
"dependencies": { "dependencies": {
"frappejs": "link:../frappejs" "frappejs": "../frappejs"
}, },
"devDependencies": { "devDependencies": {
"electron": "1.8.4",
"electron-builder": "^20.6.2",
"electron-packager": "^11.2.0"
} }
} }

View File

@ -1,7 +1,7 @@
const server = require('frappejs/server'); const server = require('frappejs/server');
const frappe = require('frappejs'); const frappe = require('frappejs');
const GeneralLedger = require('../reports/generalLedger/GeneralLedger') const GeneralLedger = require('../reports/generalLedger/GeneralLedger');
const naming = require('frappejs/model/naming') const naming = require('frappejs/model/naming');
module.exports = { module.exports = {
async start() { async start() {

View File

@ -1,55 +1,86 @@
module.exports = [ const countryList = Object.keys(require('../fixtures/countryInfo.json')).sort();
{
fields: [
{
"fieldname": "country",
"label": "Country",
"fieldtype": "Data",
"required": 1
}
],
title: 'Select Country'
},
{ module.exports = {
fields: [ fields: [
{
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "email",
"label": "Email",
"fieldtype": "Data",
"required": 1
}
],
title: 'Add a Profile'
},
{ {
fields: [ "fieldname": "file",
{ "label": "File",
"fieldname": "companyName", "fieldtype": "File",
"label": "Company Name", "required": 1,
"fieldtype": "Data", "directory": 1
"required": 1 },
},
{ {
"fieldname": "abbreviation", "fieldname": "country",
"label": "Abbreviation", "label": "Country",
"fieldtype": "Data", "fieldtype": "Autocomplete",
"required": 1 "required": 1,
}, getList: () => countryList
{ },
"fieldname": "bankName",
"label": "Bank Name", {
"fieldtype": "Data", "fieldname": "name",
"required": 1 "label": "Name",
} "fieldtype": "Data",
], "required": 1
title: 'Add your Company' },
}
] {
"fieldname": "email",
"label": "Email",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "companyName",
"label": "Company Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "abbreviation",
"label": "Abbreviation",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "bankName",
"label": "Bank Name",
"fieldtype": "Data",
"required": 1
}
],
layout: [
{
title: 'Select File location',
fields: ['file']
},
{
title: 'Select Country',
fields: ['country']
},
{
title: 'Add a Profile',
fields: ['name', 'email']
},
{
title: 'Add your Company',
columns: [
{
fields: ['companyName', 'bankName']
},
{
fields: ['abbreviation']
},
]
}
]
}

View File

@ -3,20 +3,44 @@ const utils = require('frappejs/client/ui/utils');
const slideConfigs = require('./config'); const slideConfigs = require('./config');
const Tree = require('frappejs/client/ui/tree'); const Tree = require('frappejs/client/ui/tree');
const FormLayout = require('frappejs/client/view/formLayout'); const FormLayout = require('frappejs/client/view/formLayout');
const Observable = require('frappejs/utils/observable');
module.exports = class SetupWizard { module.exports = class SetupWizard {
constructor({postSetup = () => {}}) { constructor() {
this.slideList = []; this.slideCount = slideConfigs.layout.length;
this.indicatorList = []; this.indicatorList = [];
this.footerLinks = {};
this.currentIndex = 0; this.currentIndex = 0;
this.data = {}; this.doc = new Observable();
this.promise = new Promise(resolve => {
this.onComplete = resolve;
});
this.footerButtons = [
{
label: 'Prev', name: 'prev',
action: this.prevSlide.bind(this),
condition: index => index !== 0
},
{
label: 'Next', name: 'next',
action: this.nextSlide.bind(this),
condition: index => index !== this.slideCount - 1
},
{
label: 'Complete', name: 'complete',
action: this.onComplete.bind(this, this.doc),
condition: index => index === this.slideCount - 1
}
];
this.postSetup = postSetup;
this.make(); this.make();
}
this.showSlide(this.currentIndex); async start() {
this.showSlide(0);
return this.promise;
} }
make() { make() {
@ -25,60 +49,39 @@ module.exports = class SetupWizard {
this.$indicators = frappe.ui.add('div', 'indicators vertical-margin align-center', this.container); this.$indicators = frappe.ui.add('div', 'indicators vertical-margin align-center', this.container);
this.makeSlides(); this.makeSlides();
this.makeLinks(); this.makeButtons();
} }
makeSlides() { makeSlides() {
slideConfigs.forEach(config => { this.formLayout = new FormLayout(Object.assign(slideConfigs, {
this.formLayout = new FormLayout(config); doc: this.doc
this.slideList.push(this.formLayout); }));
let form = this.formLayout.form; this.container.appendChild(this.formLayout.form);
this.container.appendChild(form);
let title = frappe.ui.create('h3', {
className: 'text-extra-muted',
innerHTML: config.title
})
form.insertBefore(title, form.firstChild);
slideConfigs.layout.forEach(() => {
// indicator for each section
let indicator = frappe.ui.create('span', { let indicator = frappe.ui.create('span', {
inside: this.$indicators, inside: this.$indicators,
className: 'indicator gray' className: 'indicator gray'
}) });
this.indicatorList.push(indicator); this.indicatorList.push(indicator);
}); });
} }
makeLinks() { makeButtons() {
this.linkArea = frappe.ui.add('div', 'setup-link-area align-right', this.container); this.linkArea = frappe.ui.add('div', 'setup-link-area align-right', this.container);
// this.formLayout.on('change', () => { this.footerButtons.map(link => {
// const show = this.doc._dirty && !this.doc.submitted; link.element = utils.addButton(link.label, this.linkArea, link.action);
// this.saveButton.classList.toggle('hide', !show);
// });
this.getFooterLinks().map(link => {
let $link = utils.addLink(link.label, this.linkArea, () => {
this.buildData();
link.action(this.data);
});
this.footerLinks[link.name] = $link;
})
}
buildData() {
this.data = {};
this.slideList.forEach(slide => {
Object.assign(this.data, slide.doc);
}); });
} }
showSlide(index) { showSlide(index) {
utils.activate(this.container, this.slideList[index].form, 'form-body', 'active'); this.currentIndex = index;
this.slideList[index].controlList[0].input.blur(); utils.activate(this.container, `.form-section:nth-child(${index + 1})`, '.form-section', 'active');
this.activateIndicator(index); this.activateIndicator(index);
this.showFooterLinks(index); this.showFooterLinks(index);
this.currentIndex = index;
} }
prevSlide() { prevSlide() {
@ -90,46 +93,22 @@ module.exports = class SetupWizard {
} }
activateIndicator(index) { activateIndicator(index) {
this.indicatorList.forEach(indicator => {indicator.classList.add('gray')}); this.indicatorList.forEach(indicator => indicator.classList.add('gray'));
let indicator = this.indicatorList[index]; let indicator = this.indicatorList[index];
utils.activate(this.$indicators, indicator, 'gray', 'blue', index); utils.activate(this.$indicators, indicator, '.gray', 'blue', index);
frappe.ui.removeClass(indicator, 'gray'); frappe.ui.removeClass(indicator, 'gray');
indicator.classList.remove('gray'); indicator.classList.remove('gray');
} }
showFooterLinks(index) { showFooterLinks(index) {
let mat = [1, 1, 0] this.footerButtons.map(link => {
if(index === 0) { const show = link.condition(this.currentIndex);
mat = [0, 1, 0]; if (show) {
} else if (index === this.slideList.length - 1) { link.element.classList.remove('hide');
mat = [1, 0, 1]; } else {
} link.element.classList.add('hide');
this.showHideLinks(mat);
}
showHideLinks(matrix = [1, 1, 0]) {
let linkNames = this.getFooterLinks().map(link => link.name);
matrix.forEach((value, i) => {
const fn = value ? 'remove' : 'add';
this.footerLinks[linkNames[i]].classList[fn]('hide');
});
}
getFooterLinks() {
return [
{
label: 'Prev', name: 'prev',
action: this.prevSlide.bind(this)
},
{
label: 'Next', name: 'next',
action: this.nextSlide.bind(this)
},
{
label: 'Complete', name: 'complete',
action: this.postSetup.bind(this)
} }
]; })
} }
} }

View File

@ -7207,6 +7207,10 @@ span.CodeMirror-selectedtext {
border-bottom: 1px solid #d1d8dd; } border-bottom: 1px solid #d1d8dd; }
.body-scrollable.row-highlight-all .data-table-row:not(.row-unhighlight) { .body-scrollable.row-highlight-all .data-table-row:not(.row-unhighlight) {
background-color: #f5f7fa; } background-color: #f5f7fa; }
.body-scrollable .no-data td {
text-align: center;
padding: 8px;
padding: 0.5rem; }
.data-table-header { .data-table-header {
position: absolute; position: absolute;
top: 0; top: 0;
@ -7494,6 +7498,74 @@ mark {
margin: 0.25rem; } margin: 0.25rem; }
.vertical-margin { .vertical-margin {
margin: 1rem 0px; } margin: 1rem 0px; }
.tree {
padding: 15px; }
.tree li {
list-style: none;
margin: 2px 0px; }
ul.tree-children {
padding-left: 20px; }
.tree-link {
cursor: pointer;
display: inline-block;
padding: 1px; }
.tree-link .node-parent {
color: #6c757d;
font-size: 14px;
width: 10px;
text-align: center; }
.tree-link .node-leaf {
color: #ced4da; }
.tree-link .node-parent, .tree-link .node-leaf {
margin-right: 5px;
margin-left: 2px;
margin-top: 3px; }
.tree-link.active svg {
color: #007bff; }
.tree-link.active a {
color: #6c757d; }
.tree-hover {
background-color: #e9ecef;
min-height: 20px;
border: 1px solid #6c757d; }
.tree-node-toolbar {
display: inline-block;
padding: 0px 5px;
margin-left: 15px;
margin-bottom: -4px;
margin-top: -8px; }
.tree.with-skeleton, .tree.with-skeleton .tree-node {
position: relative; }
.tree.with-skeleton.opened::before, .tree.with-skeleton:last-child::after, .tree.with-skeleton .tree-node.opened::before, .tree.with-skeleton .tree-node:last-child::after {
content: '';
position: absolute;
top: 12px;
left: 7px;
height: calc(100% - 23px);
width: 1px;
background: #ced4da;
z-index: -1; }
.tree.with-skeleton:last-child::after, .tree.with-skeleton .tree-node:last-child::after {
top: 11px;
left: -13px;
height: calc(100% - 15px);
width: 3px;
background: #fff; }
.tree.with-skeleton.opened > .tree-children > .tree-node > .tree-link::before, .tree.with-skeleton .tree-node.opened > .tree-children > .tree-node > .tree-link::before {
content: '';
position: absolute;
width: 18px;
height: 1px;
top: 10px;
left: -12px;
z-index: -1;
background: #ced4da; }
.tree.with-skeleton.opened::before {
left: 22px;
top: 33px;
height: calc(100% - 67px); }
.tree-link.active ~ .balance-area {
color: #6c757d !important; }
.setup-container { .setup-container {
margin: 40px auto; margin: 40px auto;
padding: 20px 0px; padding: 20px 0px;
@ -7502,9 +7574,16 @@ mark {
border-radius: 4px; } border-radius: 4px; }
.setup-container h3 { .setup-container h3 {
text-align: center; } text-align: center; }
.setup-container .form-body { .setup-container .form-section {
display: none; } display: none; }
.setup-container .form-body.active { .setup-container .form-section.active {
display: block; } display: block; }
.setup-container .setup-link-area { .setup-container .setup-link-area {
margin: 0.25rem 2rem; } margin: 0.25rem 2rem; }
input[type=file] {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1; }

1569
www/dist/js/bundle.js vendored

File diff suppressed because it is too large Load Diff

1146
yarn.lock

File diff suppressed because it is too large Load Diff