mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
Upstream merge
This commit is contained in:
commit
e1892c9f54
@ -195,8 +195,8 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
triggerChange(doctype, name) {
|
||||
this.trigger(`change:${doctype}`, { name: name }, 500);
|
||||
this.trigger(`change`, { doctype: name, name: name }, 500);
|
||||
this.trigger(`change:${doctype}`, { name }, 500);
|
||||
this.trigger(`change`, { doctype, name }, 500);
|
||||
}
|
||||
|
||||
async insert(doctype, doc) {
|
||||
@ -280,7 +280,7 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
async updateSingle(meta, doc, doctype) {
|
||||
await this.deleteSingleValues();
|
||||
await this.deleteSingleValues(doctype);
|
||||
for (let field of meta.getValidFields({ withChildren: false })) {
|
||||
let value = doc[field.fieldname];
|
||||
if (value) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const frappe = require('frappejs');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
const triggerEvent = name => frappe.events.trigger(`http:${name}`);
|
||||
|
||||
module.exports = class HTTPClient extends Observable {
|
||||
constructor({ server, protocol = 'http' }) {
|
||||
@ -105,19 +106,23 @@ module.exports = class HTTPClient extends Observable {
|
||||
}
|
||||
|
||||
async fetch(url, args) {
|
||||
triggerEvent('ajaxStart');
|
||||
|
||||
args.headers = this.getHeaders();
|
||||
let response = await frappe.fetch(url, args);
|
||||
|
||||
triggerEvent('ajaxStop');
|
||||
|
||||
if (response.status === 200) {
|
||||
let data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
frappe.events.trigger('Unauthorized');
|
||||
triggerEvent('unauthorized');
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw Error(data.error);
|
||||
}
|
||||
let data = await response.json();
|
||||
|
||||
return data;
|
||||
throw Error(await response.text());
|
||||
}
|
||||
|
||||
getFilesToUpload(doc) {
|
||||
|
@ -13,8 +13,13 @@ module.exports = class sqliteDatabase extends Database {
|
||||
if (dbPath) {
|
||||
this.dbPath = dbPath;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
this.conn = new sqlite3.Database(this.dbPath, () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn = new sqlite3.Database(this.dbPath, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
if (debug) {
|
||||
this.conn.on('trace', (trace) => console.log(trace));
|
||||
}
|
||||
@ -211,6 +216,7 @@ module.exports = class sqliteDatabase extends Database {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.run(query, params, (err) => {
|
||||
if (err) {
|
||||
console.error('Error in sql:', query);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
@ -220,8 +226,12 @@ module.exports = class sqliteDatabase extends Database {
|
||||
}
|
||||
|
||||
sql(query, params) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.all(query, params, (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error in sql:', query);
|
||||
reject(err)
|
||||
}
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
|
5
cli.js
5
cli.js
@ -13,6 +13,11 @@ program
|
||||
.description('Start development server')
|
||||
.action(require('./webpack/start'))
|
||||
|
||||
program
|
||||
.command('build [mode]')
|
||||
.description('Build assets for production')
|
||||
.action(require('./webpack/build'))
|
||||
|
||||
program
|
||||
.command('new-model <name>')
|
||||
.description('Create a new model in the `models/doctype` folder')
|
||||
|
@ -1,43 +0,0 @@
|
||||
let templates = {};
|
||||
|
||||
class BaseComponent extends HTMLElement {
|
||||
constructor(name) {
|
||||
super();
|
||||
|
||||
this._name = name;
|
||||
this._shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
|
||||
if (!templates[name]) {
|
||||
makeTemplate(name, this.templateHTML);
|
||||
}
|
||||
|
||||
let template = getTemplate(name);
|
||||
this._shadowRoot.appendChild(template);
|
||||
}
|
||||
|
||||
triggerEvent(eventName, detail = {}) {
|
||||
const event = new CustomEvent(eventName, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function makeTemplate(name, html) {
|
||||
if (!templates[name]) {
|
||||
let template = document.createElement('template');
|
||||
template.id = name;
|
||||
template.innerHTML = html;
|
||||
|
||||
templates[name] = template;
|
||||
}
|
||||
}
|
||||
|
||||
function getTemplate(name) {
|
||||
return templates[name].content.cloneNode(true);
|
||||
}
|
||||
|
||||
module.exports = BaseComponent;
|
@ -1,11 +0,0 @@
|
||||
<!-- styles -->
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- template -->
|
||||
<div class="tree">
|
||||
<slot></slot>
|
||||
</div>
|
@ -1,19 +0,0 @@
|
||||
const BaseComponent = require('../baseComponent');
|
||||
const TreeNode = require('./treeNode');
|
||||
|
||||
class Tree extends BaseComponent {
|
||||
get templateHTML() {
|
||||
return require('./index.html');
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super('Tree');
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('f-tree', Tree);
|
||||
|
||||
module.exports = {
|
||||
Tree,
|
||||
TreeNode
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
<!-- styles -->
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tree-node:hover {
|
||||
background-color: gray;
|
||||
background-color: var(--tree-node-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tree-node:hover .tree-node-actions {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tree-node-actions {
|
||||
display: none;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.tree-node-icon svg {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.tree-node-children {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- template -->
|
||||
<div class="tree-node">
|
||||
<div class="tree-node-content">
|
||||
<div class="tree-node-icon"></div>
|
||||
<div class="tree-node-label"></div>
|
||||
</div>
|
||||
<div class="tree-node-actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tree-node-children">
|
||||
<slot></slot>
|
||||
</div>
|
@ -1,94 +0,0 @@
|
||||
const octicons = require('octicons');
|
||||
const BaseComponent = require('../baseComponent');
|
||||
|
||||
const iconSet = {
|
||||
open: octicons["triangle-down"].toSVG({ width: "12", height: "12", "class": "tree-icon-open" }),
|
||||
close: octicons["triangle-right"].toSVG({ width: "12", height: "12", "class": "tree-icon-closed" })
|
||||
};
|
||||
|
||||
class TreeNode extends BaseComponent {
|
||||
static get observedAttributes() {
|
||||
return ['label', 'expanded'];
|
||||
}
|
||||
|
||||
get templateHTML() {
|
||||
return require('./treeNode.html');
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super('TreeNode');
|
||||
|
||||
let shadowRoot = this._shadowRoot;
|
||||
this.iconEl = shadowRoot.querySelector('.tree-node-icon');
|
||||
this.labelEl = shadowRoot.querySelector('.tree-node-label');
|
||||
this.actionsEl = shadowRoot.querySelector('.tree-node-actions');
|
||||
this.childrenEl = shadowRoot.querySelector('.tree-node-children');
|
||||
|
||||
this.addEventListener('click', e => {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if (e.target.matches('[slot="actions"]')) {
|
||||
this.triggerEvent('tree-node-action', {
|
||||
actionEl: e.target
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.expanded) {
|
||||
this.removeAttribute('expanded');
|
||||
} else {
|
||||
this.setAttribute('expanded', '');
|
||||
}
|
||||
});
|
||||
this.onExpand();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
switch(name) {
|
||||
case 'label': {
|
||||
this.labelEl.innerHTML = newValue || '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'expanded': {
|
||||
const isExpanded = this.hasAttribute('expanded');
|
||||
this.onExpand(isExpanded);
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
onExpand(isExpanded = false) {
|
||||
if (this.isLeaf) return;
|
||||
|
||||
if (isExpanded) {
|
||||
this.iconEl.innerHTML = iconSet.open;
|
||||
this.childrenEl.style.display = '';
|
||||
} else {
|
||||
this.iconEl.innerHTML = iconSet.close;
|
||||
this.childrenEl.style.display = 'none';
|
||||
}
|
||||
|
||||
this.triggerEvent('tree-node-expand', {
|
||||
expanded: isExpanded
|
||||
});
|
||||
}
|
||||
|
||||
get isRoot() {
|
||||
return this.hasAttribute('is-root');
|
||||
}
|
||||
|
||||
get expanded() {
|
||||
return this.hasAttribute('expanded');
|
||||
}
|
||||
|
||||
get isLeaf() {
|
||||
return this.hasAttribute('is-leaf');
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('f-tree-node', TreeNode);
|
||||
|
||||
module.exports = TreeNode;
|
@ -1,47 +0,0 @@
|
||||
const Modal = require('frappejs/client/ui/modal');
|
||||
const view = require('frappejs/client/view');
|
||||
|
||||
module.exports = class FormModal extends Modal {
|
||||
constructor(doctype, name) {
|
||||
super({title: doctype});
|
||||
this.doctype = doctype;
|
||||
}
|
||||
|
||||
async showWith(doctype, name) {
|
||||
if (!name) name = doctype;
|
||||
this.show();
|
||||
await this.setDoc(doctype, name);
|
||||
}
|
||||
|
||||
async setDoc(doctype, name) {
|
||||
if (!this.form) {
|
||||
this.makeForm();
|
||||
}
|
||||
await this.form.setDoc(doctype, name);
|
||||
let input = this.modal.querySelector('input') || this.modal.querySelector('select');
|
||||
input && input.focus();
|
||||
}
|
||||
|
||||
makeForm() {
|
||||
this.form = new (view.getFormClass(this.doctype))({
|
||||
doctype: this.doctype,
|
||||
parent: this.getBody(),
|
||||
container: this,
|
||||
actions: ['save']
|
||||
});
|
||||
|
||||
this.form.on('save', async () => {
|
||||
await this.trigger('save');
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
addButton(label, className, action) {
|
||||
if (className === 'primary') {
|
||||
return this.addPrimary(label, action).get(0);
|
||||
} else {
|
||||
return this.addSecondary(label, action).get(0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
const Page = require('frappejs/client/view/page');
|
||||
const view = require('frappejs/client/view');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
module.exports = class FormPage extends Page {
|
||||
constructor(doctype) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
super({title: `Edit ${meta.name}`, hasRoute: true});
|
||||
this.wrapper.classList.add('page-form');
|
||||
this.meta = meta;
|
||||
this.doctype = doctype;
|
||||
|
||||
this.form = new (view.getFormClass(doctype))({
|
||||
doctype: doctype,
|
||||
parent: this.body,
|
||||
container: this,
|
||||
actions: ['save', 'delete', 'duplicate', 'settings', 'print']
|
||||
});
|
||||
|
||||
if (this.meta.pageSettings && this.meta.pageSettings.hideTitle) {
|
||||
this.titleElement.classList.add('hide');
|
||||
}
|
||||
|
||||
// if name is different after saving, change the route
|
||||
this.form.on('save', async (params) => {
|
||||
let route = frappe.router.getRoute();
|
||||
if (this.form.doc.name && !(route && route[2] === this.form.doc.name)) {
|
||||
await frappe.router.setRoute('edit', this.form.doc.doctype, this.form.doc.name);
|
||||
frappe.ui.showAlert({message: 'Added', color: 'green'});
|
||||
}
|
||||
});
|
||||
|
||||
this.form.on('delete', async (params) => {
|
||||
this.hide();
|
||||
await frappe.router.setRoute('list', this.form.doctype);
|
||||
});
|
||||
}
|
||||
|
||||
async show(params) {
|
||||
super.show();
|
||||
try {
|
||||
await this.form.setDoc(params.doctype, params.name);
|
||||
frappe.desk.setActiveDoc(this.form.doc);
|
||||
} catch (e) {
|
||||
this.renderError(e.statusCode, e.message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
// const Search = require('./search');
|
||||
const Router = require('frappejs/common/router');
|
||||
const Page = require('frappejs/client/view/page');
|
||||
|
||||
const views = {};
|
||||
views.Form = require('./formpage');
|
||||
views.List = require('./listpage');
|
||||
views.Tree = require('./treepage');
|
||||
views.Print = require('./printpage');
|
||||
views.FormModal = require('./formmodal');
|
||||
views.Table = require('./tablepage');
|
||||
const DeskMenu = require('./menu');
|
||||
|
||||
module.exports = class Desk {
|
||||
constructor(columns=2) {
|
||||
frappe.router = new Router();
|
||||
frappe.router.listen();
|
||||
|
||||
let body = document.querySelector('body');
|
||||
//this.navbar = new Navbar();
|
||||
frappe.ui.empty(body);
|
||||
this.container = frappe.ui.add('div', '', body);
|
||||
this.containerRow = frappe.ui.add('div', 'row no-gutters', this.container)
|
||||
this.makeColumns(columns);
|
||||
|
||||
this.pages = {
|
||||
formModals: {},
|
||||
List: {}
|
||||
};
|
||||
|
||||
this.routeItems = {};
|
||||
|
||||
this.initRoutes();
|
||||
// this.search = new Search(this.nav);
|
||||
}
|
||||
|
||||
makeColumns(columns) {
|
||||
this.menu = null; this.center = null;
|
||||
this.columnCount = columns;
|
||||
if (columns === 3) {
|
||||
this.makeMenu();
|
||||
this.center = frappe.ui.add('div', 'col-md-4 desk-center', this.containerRow);
|
||||
this.body = frappe.ui.add('div', 'col-md-6 desk-body', this.containerRow);
|
||||
} else if (columns === 2) {
|
||||
this.makeMenu();
|
||||
this.body = frappe.ui.add('div', 'col-md-10 desk-body', this.containerRow);
|
||||
} else if (columns === 1) {
|
||||
this.makeMenuPage();
|
||||
this.body = frappe.ui.add('div', 'col-md-12 desk-body', this.containerRow);
|
||||
} else {
|
||||
throw 'columns can be 1, 2 or 3'
|
||||
}
|
||||
}
|
||||
|
||||
makeMenu() {
|
||||
this.menuColumn = frappe.ui.add('div', 'col-md-2 desk-menu', this.containerRow);
|
||||
this.menu = new DeskMenu(this.menuColumn);
|
||||
}
|
||||
|
||||
makeMenuPage() {
|
||||
// make menu page for 1 column layout
|
||||
this.menuPage = null;
|
||||
}
|
||||
|
||||
initRoutes() {
|
||||
frappe.router.add('not-found', async (params) => {
|
||||
if (!this.notFoundPage) {
|
||||
this.notFoundPage = new Page({title: 'Not Found'});
|
||||
}
|
||||
await this.notFoundPage.show();
|
||||
this.notFoundPage.renderError('Not Found', params ? params.route : '');
|
||||
})
|
||||
|
||||
frappe.router.add('list/:doctype', async (params) => {
|
||||
await this.showViewPage('List', params.doctype);
|
||||
});
|
||||
|
||||
frappe.router.add('tree/:doctype', async (params) => {
|
||||
await this.showViewPage('Tree', params.doctype);
|
||||
});
|
||||
|
||||
frappe.router.add('table/:doctype', async (params) => {
|
||||
await this.showViewPage('Table', params.doctype, params);
|
||||
})
|
||||
|
||||
frappe.router.add('edit/:doctype/:name', async (params) => {
|
||||
await this.showViewPage('Form', params.doctype, params);
|
||||
})
|
||||
|
||||
frappe.router.add('print/:doctype/:name', async (params) => {
|
||||
await this.showViewPage('Print', params.doctype, params);
|
||||
})
|
||||
|
||||
frappe.router.add('new/:doctype', async (params) => {
|
||||
let doc = await frappe.getNewDoc(params.doctype);
|
||||
// unset the name, its local
|
||||
await frappe.router.setRoute('edit', doc.doctype, doc.name);
|
||||
|
||||
// focus on new page
|
||||
frappe.desk.body.activePage.body.querySelector('input').focus();
|
||||
});
|
||||
|
||||
frappe.router.on('change', () => {
|
||||
if (this.menu) {
|
||||
this.menu.setActive();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleCenter(show) {
|
||||
const current = !frappe.desk.center.classList.contains('hide');
|
||||
if (show===undefined) {
|
||||
show = current;
|
||||
} else if (!!show===!!current) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
// add hide
|
||||
frappe.desk.center.classList.toggle('hide', !show);
|
||||
|
||||
if (show) {
|
||||
// set body to 6
|
||||
frappe.desk.body.classList.toggle('col-md-6', true);
|
||||
frappe.desk.body.classList.toggle('col-md-10', false);
|
||||
} else {
|
||||
// set body to 10
|
||||
frappe.desk.body.classList.toggle('col-md-6', false);
|
||||
frappe.desk.body.classList.toggle('col-md-10', true);
|
||||
}
|
||||
}
|
||||
|
||||
async showViewPage(view, doctype, params) {
|
||||
if (!params) params = doctype;
|
||||
if (!this.pages[view]) this.pages[view] = {};
|
||||
if (!this.pages[view][doctype]) this.pages[view][doctype] = new views[view](doctype);
|
||||
const page = this.pages[view][doctype];
|
||||
await page.show(params);
|
||||
}
|
||||
|
||||
async showFormModal(doctype, name) {
|
||||
if (!this.pages.formModals[doctype]) {
|
||||
this.pages.formModals[doctype] = new views.FormModal(doctype);
|
||||
}
|
||||
await this.pages.formModals[doctype].showWith(doctype, name);
|
||||
return this.pages.formModals[doctype];
|
||||
}
|
||||
|
||||
async setActiveDoc(doc) {
|
||||
this.activeDoc = doc;
|
||||
if (frappe.desk.center && !frappe.desk.center.activePage) {
|
||||
await frappe.desk.showViewPage('List', doc.doctype);
|
||||
}
|
||||
if (frappe.desk.pages.List[doc.doctype]) {
|
||||
frappe.desk.pages.List[doc.doctype].list.setActiveListRow(doc.name);
|
||||
}
|
||||
}
|
||||
|
||||
setActive(item) {
|
||||
let className = 'list-group-item-secondary';
|
||||
let activeItem = this.sidebarList.querySelector('.' + className);
|
||||
if (activeItem) {
|
||||
activeItem.classList.remove(className);
|
||||
}
|
||||
item.classList.add(className);
|
||||
}
|
||||
|
||||
addSidebarItem(label, action) {
|
||||
let item = frappe.ui.add('a', 'list-group-item list-group-item-action', this.sidebarList, label);
|
||||
if (typeof action === 'string') {
|
||||
item.href = action;
|
||||
this.routeItems[action] = item;
|
||||
} else {
|
||||
item.addEventHandler('click', () => {
|
||||
action();
|
||||
this.setActive(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const Page = require('frappejs/client/view/page');
|
||||
const view = require('frappejs/client/view');
|
||||
|
||||
module.exports = class ListPage extends Page {
|
||||
constructor(name) {
|
||||
|
||||
// if center column is present, list does not have its route
|
||||
const hasRoute = frappe.desk.center ? false : true;
|
||||
|
||||
super({
|
||||
title: frappe._("List"),
|
||||
parent: hasRoute ? frappe.desk.body : frappe.desk.center,
|
||||
hasRoute: hasRoute
|
||||
});
|
||||
|
||||
this.name = name;
|
||||
|
||||
this.list = new (view.getListClass(name))({
|
||||
doctype: name,
|
||||
parent: this.body,
|
||||
page: this
|
||||
});
|
||||
|
||||
frappe.docs.on('change', (params) => {
|
||||
if (params.doc.doctype === this.list.meta.name) {
|
||||
this.list.refreshRow(params.doc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async show(params) {
|
||||
super.show();
|
||||
|
||||
this.setTitle(this.name===this.list.meta.name ? (this.list.meta.label || this.list.meta.name) : this.name);
|
||||
if (frappe.desk.body.activePage && frappe.router.getRoute()[0]==='list') {
|
||||
frappe.desk.body.activePage.hide();
|
||||
}
|
||||
await this.list.refresh();
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
|
||||
module.exports = class DeskMenu {
|
||||
constructor(parent) {
|
||||
this.parent = parent;
|
||||
this.routeItems = {};
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.listGroup = frappe.ui.add('div', 'list-body', this.parent);
|
||||
}
|
||||
|
||||
addItem(label, action) {
|
||||
let item = frappe.ui.add('div', 'list-row', this.listGroup, label);
|
||||
if (typeof action === 'string') {
|
||||
this.routeItems[action] = item;
|
||||
}
|
||||
item.addEventListener('click', async () => {
|
||||
if (typeof action === 'string') {
|
||||
await frappe.router.setRoute(action);
|
||||
} else {
|
||||
action();
|
||||
}
|
||||
this.setActive(item);
|
||||
});
|
||||
}
|
||||
|
||||
setActive() {
|
||||
if (this.routeItems[window.location.hash]) {
|
||||
let item = this.routeItems[window.location.hash];
|
||||
let className = 'active';
|
||||
let activeItem = this.listGroup.querySelector('.' + className);
|
||||
|
||||
if (activeItem) {
|
||||
activeItem.classList.remove(className);
|
||||
}
|
||||
item.classList.add(className);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
|
||||
module.exports = class Navbar {
|
||||
constructor({brand_label = 'Home'} = {}) {
|
||||
Object.assign(this, arguments[0]);
|
||||
this.items = {};
|
||||
this.navbar = frappe.ui.add('div', 'navbar navbar-expand-md border-bottom navbar-dark bg-dark', document.querySelector('body'));
|
||||
|
||||
this.brand = frappe.ui.add('a', 'navbar-brand', this.navbar, brand_label);
|
||||
this.brand.href = '#';
|
||||
|
||||
this.toggler = frappe.ui.add('button', 'navbar-toggler', this.navbar);
|
||||
this.toggler.setAttribute('type', 'button');
|
||||
this.toggler.setAttribute('data-toggle', 'collapse');
|
||||
this.toggler.setAttribute('data-target', 'desk-navbar');
|
||||
this.toggler.innerHTML = `<span class="navbar-toggler-icon"></span>`;
|
||||
|
||||
this.navbar_collapse = frappe.ui.add('div', 'collapse navbar-collapse', this.navbar);
|
||||
this.navbar_collapse.setAttribute('id', 'desk-navbar');
|
||||
|
||||
this.nav = frappe.ui.add('ul', 'navbar-nav mr-auto', this.navbar_collapse);
|
||||
}
|
||||
|
||||
addItem(label, route) {
|
||||
let item = frappe.ui.add('li', 'nav-item', this.nav);
|
||||
item.link = frappe.ui.add('a', 'nav-link', item, label);
|
||||
item.link.href = route;
|
||||
this.items[label] = item;
|
||||
return item;
|
||||
}
|
||||
|
||||
add_dropdown(label) {
|
||||
|
||||
}
|
||||
|
||||
add_search() {
|
||||
let form = frappe.ui.add('form', 'form-inline my-2 my-md-0', this.nav);
|
||||
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const Page = require('frappejs/client/view/page');
|
||||
const { getHTML } = require('frappejs/common/print');
|
||||
const nunjucks = require('nunjucks/browser/nunjucks');
|
||||
|
||||
nunjucks.configure({ autoescape: false });
|
||||
|
||||
module.exports = class PrintPage extends Page {
|
||||
constructor(doctype) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
super({title: `${meta.name}`, hasRoute: true});
|
||||
this.meta = meta;
|
||||
this.doctype = doctype;
|
||||
this.titleElement.classList.add('hide');
|
||||
|
||||
this.addButton(frappe._('Edit'), 'primary', () => {
|
||||
frappe.router.setRoute('edit', this.doctype, this.name)
|
||||
});
|
||||
|
||||
this.addButton(frappe._('PDF'), 'secondary', async () => {
|
||||
frappe.getPDF(this.doctype, this.name);
|
||||
});
|
||||
}
|
||||
|
||||
async show(params) {
|
||||
super.show();
|
||||
this.name = params.name;
|
||||
if (this.meta.print) {
|
||||
// render
|
||||
this.renderTemplate();
|
||||
} else {
|
||||
this.renderError('No Print Settings');
|
||||
}
|
||||
}
|
||||
|
||||
async renderTemplate() {
|
||||
let doc = await frappe.getDoc(this.doctype, this.name);
|
||||
frappe.desk.setActiveDoc(doc);
|
||||
const html = await getHTML(this.doctype, this.name);
|
||||
try {
|
||||
this.body.innerHTML = html;
|
||||
// this.setTitle(doc.name);
|
||||
} catch (e) {
|
||||
this.renderError('Template Error', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
const Page = require('frappejs/client/view/page');
|
||||
const FormLayout = require('frappejs/client/view/formLayout');
|
||||
const DataTable = require('frappe-datatable');
|
||||
const frappe = require('frappejs');
|
||||
const utils = require('frappejs/client/ui/utils');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
|
||||
// baseclass for report
|
||||
// `url` url for report
|
||||
// `getColumns` return columns
|
||||
|
||||
module.exports = class ReportPage extends Page {
|
||||
constructor({title, filterFields = []}) {
|
||||
super({title: title, hasRoute: true});
|
||||
|
||||
this.fullPage = true;
|
||||
this.filterFields = filterFields;
|
||||
|
||||
this.filterWrapper = frappe.ui.add('div', 'filter-toolbar', this.body);
|
||||
this.tableWrapper = frappe.ui.add('div', 'table-page-wrapper', this.body);
|
||||
|
||||
this.btnNew = this.addButton(frappe._('Refresh'), 'btn-primary', async () => {
|
||||
await this.run();
|
||||
});
|
||||
|
||||
this.makeFilters();
|
||||
this.setDefaultFilterValues();
|
||||
}
|
||||
|
||||
getColumns() {
|
||||
// overrride
|
||||
}
|
||||
|
||||
getRowsForDataTable(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
makeFilters() {
|
||||
this.filters = new FormLayout({
|
||||
parent: this.filterWrapper,
|
||||
fields: this.filterFields,
|
||||
doc: new Observable(),
|
||||
inline: true
|
||||
});
|
||||
|
||||
this.filterWrapper.appendChild(this.filters.form);
|
||||
}
|
||||
|
||||
setDefaultFilterValues() {
|
||||
|
||||
}
|
||||
|
||||
getFilterValues() {
|
||||
const values = {};
|
||||
for (let control of this.filters.controlList) {
|
||||
values[control.fieldname] = control.getInputValue();
|
||||
if (control.required && !values[control.fieldname]) {
|
||||
frappe.ui.showAlert({message: frappe._('{0} is mandatory', control.label), color: 'red'});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
async show(params) {
|
||||
super.show();
|
||||
await this.run();
|
||||
}
|
||||
|
||||
async run() {
|
||||
if (frappe.params && frappe.params.filters) {
|
||||
for (let key in frappe.params.filters) {
|
||||
if (this.filters.controls[key]) {
|
||||
this.filters.controls[key].setInputValue(frappe.params.filters[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
frappe.params = null;
|
||||
|
||||
|
||||
if (!this.datatable) {
|
||||
this.makeDataTable();
|
||||
}
|
||||
|
||||
const filterValues = this.getFilterValues();
|
||||
if (filterValues === false) return;
|
||||
|
||||
let data = await frappe.call({
|
||||
method: this.method,
|
||||
args: filterValues
|
||||
});
|
||||
|
||||
const rows = this.getRowsForDataTable(data);
|
||||
const columns = utils.convertFieldsToDatatableColumns(this.getColumns(data), this.layout);
|
||||
this.datatable.refresh(rows, columns);
|
||||
}
|
||||
|
||||
makeDataTable() {
|
||||
this.datatable = new DataTable(this.tableWrapper, Object.assign({
|
||||
columns: utils.convertFieldsToDatatableColumns(this.getColumns(), this.layout),
|
||||
data: [],
|
||||
layout: this.layout || 'fluid',
|
||||
}, this.datatableOptions || {}));
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
|
||||
module.exports = class Search {
|
||||
constructor(parent) {
|
||||
this.input = frappe.ui.add('input', 'form-control nav-search', parent);
|
||||
this.input.addEventListener('keypress', function(event) {
|
||||
if (event.keyCode===13) {
|
||||
let list = frappe.router.current_page.list;
|
||||
if (list) {
|
||||
list.search_text = this.value;
|
||||
list.run();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
const Page = require('frappejs/client/view/page');
|
||||
const frappe = require('frappejs');
|
||||
const ModelTable = require('frappejs/client/ui/modelTable');
|
||||
|
||||
module.exports = class TablePage extends Page {
|
||||
constructor(doctype) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
super({title: `${meta.label || meta.name}`, hasRoute: true});
|
||||
this.filterWrapper = frappe.ui.add('div', 'filter-toolbar', this.body);
|
||||
this.fitlerButton = frappe.ui.add('button', 'btn btn-sm btn-outline-secondary', this.filterWrapper, 'Set Filters');
|
||||
this.tableWrapper = frappe.ui.add('div', 'table-page-wrapper', this.body);
|
||||
this.doctype = doctype;
|
||||
this.fullPage = true;
|
||||
|
||||
this.fitlerButton.addEventListener('click', async () => {
|
||||
const formModal = await frappe.desk.showFormModal('FilterSelector');
|
||||
formModal.form.once('apply-filters', () => {
|
||||
formModal.hide();
|
||||
this.run();
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async show(params) {
|
||||
super.show();
|
||||
|
||||
if (!this.filterSelector) {
|
||||
this.filterSelector = await frappe.getSingle('FilterSelector');
|
||||
this.filterSelector.reset(this.doctype);
|
||||
}
|
||||
|
||||
if (frappe.params && frappe.params.filters) {
|
||||
this.filterSelector.setFilters(frappe.params.filters);
|
||||
}
|
||||
frappe.params = null;
|
||||
|
||||
if (!this.modelTable) {
|
||||
this.modelTable = new ModelTable({
|
||||
doctype: this.doctype,
|
||||
parent: this.tableWrapper,
|
||||
layout: 'fluid',
|
||||
getRowData: async (rowIndex) => {
|
||||
return await frappe.getDoc(this.doctype, this.data[rowIndex].name);
|
||||
},
|
||||
setValue: async (control) => {
|
||||
await control.handleChange();
|
||||
await control.doc.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.run();
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.displayFilters();
|
||||
this.data = await frappe.db.getAll({
|
||||
doctype: this.doctype,
|
||||
fields: ['*'],
|
||||
filters: this.filterSelector.getFilters(),
|
||||
start: this.start,
|
||||
limit: 500
|
||||
});
|
||||
this.modelTable.refresh(this.data);
|
||||
}
|
||||
|
||||
displayFilters() {
|
||||
this.fitlerButton.textContent = this.filterSelector.getText();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const Page = require('frappejs/client/view/page');
|
||||
const view = require('frappejs/client/view');
|
||||
|
||||
module.exports = class TreePage extends Page {
|
||||
constructor(name) {
|
||||
const hasRoute = true;
|
||||
|
||||
super({
|
||||
title: frappe._("Tree"),
|
||||
parent: hasRoute ? frappe.desk.body : frappe.desk.center,
|
||||
hasRoute: hasRoute
|
||||
});
|
||||
|
||||
this.fullPage = true;
|
||||
|
||||
this.name = name;
|
||||
|
||||
this.tree = new (view.getTreeClass(name))({
|
||||
doctype: name,
|
||||
parent: this.body,
|
||||
page: this
|
||||
});
|
||||
}
|
||||
|
||||
async show(params) {
|
||||
super.show();
|
||||
this.setTitle(this.name===this.tree.meta.name ? (this.tree.meta.label || this.tree.meta.name) : this.name);
|
||||
await this.tree.refresh();
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
const common = require('frappejs/common');
|
||||
const sqlite = require('frappejs/backends/sqlite');
|
||||
const frappe = require('frappejs');
|
||||
frappe.ui = require('./ui');
|
||||
const Desk = require('./desk');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
|
||||
module.exports = {
|
||||
async start({dbPath, columns = 3, models}) {
|
||||
window.frappe = frappe;
|
||||
frappe.isServer = true;
|
||||
frappe.init();
|
||||
frappe.registerLibs(common);
|
||||
frappe.registerModels(require('frappejs/models'));
|
||||
|
||||
if (models) {
|
||||
frappe.registerModels(models);
|
||||
}
|
||||
|
||||
frappe.db = await new sqlite({ dbPath });
|
||||
await frappe.db.connect();
|
||||
await frappe.db.migrate();
|
||||
|
||||
frappe.fetch = window.fetch.bind();
|
||||
|
||||
frappe.docs = new Observable();
|
||||
|
||||
await frappe.getSingle('SystemSettings');
|
||||
|
||||
frappe.desk = new Desk(columns);
|
||||
await frappe.login('Administrator');
|
||||
}
|
||||
};
|
||||
|
@ -1,52 +0,0 @@
|
||||
const common = require('frappejs/common');
|
||||
const HTTPClient = require('frappejs/backends/http');
|
||||
const frappe = require('frappejs');
|
||||
frappe.ui = require('./ui');
|
||||
const Desk = require('./desk');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
const { getPDF } = require('frappejs/client/pdf');
|
||||
|
||||
module.exports = {
|
||||
async start({server, columns = 2, makeDesk = false}) {
|
||||
window.frappe = frappe;
|
||||
frappe.init();
|
||||
frappe.registerLibs(common);
|
||||
frappe.registerModels(require('frappejs/models'), 'client');
|
||||
frappe.fetch = window.fetch.bind();
|
||||
|
||||
frappe.db = await new HTTPClient({server: server});
|
||||
this.socket = io.connect(`http://${server}`); // eslint-disable-line
|
||||
frappe.db.bindSocketClient(this.socket);
|
||||
|
||||
frappe.docs = new Observable();
|
||||
await frappe.getSingle('SystemSettings');
|
||||
|
||||
if(makeDesk) {
|
||||
this.makeDesk(columns);
|
||||
}
|
||||
},
|
||||
|
||||
async makeDesk(columns) {
|
||||
frappe.desk = new Desk(columns);
|
||||
await frappe.login();
|
||||
},
|
||||
|
||||
setCall() {
|
||||
frappe.call = async (method, args) => {
|
||||
let url = `/api/method/${method}`;
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(args || {})
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
frappe.getPDF = getPDF;
|
||||
}
|
||||
};
|
||||
|
@ -1,30 +0,0 @@
|
||||
async function getPDF(doctype, name) {
|
||||
const headers = {
|
||||
'Accept': 'application/pdf',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
const res = await fetch('/api/method/pdf', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ doctype, name })
|
||||
});
|
||||
|
||||
const blob = await res.blob();
|
||||
showFile(blob);
|
||||
}
|
||||
|
||||
|
||||
function showFile(blob, filename='file.pdf') {
|
||||
const newBlob = new Blob([blob], { type: "application/pdf" })
|
||||
const data = window.URL.createObjectURL(newBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = data;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
setTimeout(() => window.URL.revokeObjectURL(data), 100);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPDF
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
@import "./variables.scss";
|
||||
@import "node_modules/frappe-datatable/dist/frappe-datatable";
|
||||
|
||||
.dt-header {
|
||||
background-color: $gray-200 !important;
|
||||
}
|
||||
|
||||
.dt-cell__edit {
|
||||
padding: 0px;
|
||||
|
||||
input, textarea {
|
||||
outline: none;
|
||||
border-radius: none;
|
||||
border: none;
|
||||
margin: none;
|
||||
padding: $spacer-2;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.awesomplete > ul {
|
||||
position: fixed;
|
||||
left: auto;
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-cell--highlight {
|
||||
background-color: $gray-200;
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
.indicator,
|
||||
.indicator-right {
|
||||
background: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.indicator::before,
|
||||
.indicator-right::after {
|
||||
content:'';
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 8px;
|
||||
background: $gray-300;
|
||||
}
|
||||
|
||||
.indicator::before {
|
||||
margin:0 $spacer-2 0 0;
|
||||
}
|
||||
|
||||
.indicator-right::after {
|
||||
margin:0 0 0 $spacer-2;
|
||||
}
|
||||
|
||||
.indicator.grey::before,
|
||||
.indicator-right.grey::after {
|
||||
background: $gray-300;
|
||||
}
|
||||
.indicator.blue::before,
|
||||
.indicator-right.blue::after {
|
||||
background: $blue;
|
||||
}
|
||||
.indicator.red::before,
|
||||
.indicator-right.red::after {
|
||||
background: $red;
|
||||
}
|
||||
.indicator.green::before,
|
||||
.indicator-right.green::after {
|
||||
background: $green;
|
||||
}
|
||||
.indicator.orange::before,
|
||||
.indicator-right.orange::after {
|
||||
background: $orange;
|
||||
}
|
||||
.indicator.purple::before,
|
||||
.indicator-right.purple::after {
|
||||
background: $purple;
|
||||
}
|
||||
.indicator.darkgrey::before,
|
||||
.indicator-right.darkgrey::after {
|
||||
background: $gray-600;
|
||||
}
|
||||
|
||||
.indicator.black::before,
|
||||
.indicator-right.black::after {
|
||||
background: $gray-800;
|
||||
}
|
||||
|
||||
.indicator.yellow::before,
|
||||
.indicator-right.yellow::after {
|
||||
background: $yellow;
|
||||
}
|
||||
|
||||
.modal-header .indicator {
|
||||
float: left;
|
||||
margin-top: 7.5px;
|
||||
margin-right: 3px;
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
@import "node_modules/awesomplete/awesomplete";
|
||||
@import "node_modules/flatpickr/dist/flatpickr";
|
||||
@import "node_modules/flatpickr/dist/themes/airbnb";
|
||||
@import "node_modules/codemirror/lib/codemirror";
|
||||
|
||||
@import "./variables.scss";
|
||||
@import "./indicators.scss";
|
||||
|
||||
html {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.desk-body {
|
||||
border-left: 1px solid $gray-300;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.desk-center {
|
||||
border-left: 1px solid $gray-300;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-bottom: $spacer-4;
|
||||
|
||||
.page-nav {
|
||||
padding: $spacer-2 $spacer-3;
|
||||
background-color: $gray-100;
|
||||
border-bottom: 1px solid $gray-300;
|
||||
|
||||
.btn {
|
||||
margin-left: $spacer-2;
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-weight: bold;
|
||||
padding: $spacer-4;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.page-links {
|
||||
padding: $spacer-3 $spacer-4;
|
||||
}
|
||||
|
||||
.page-error {
|
||||
text-align: center;
|
||||
padding: 200px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-body {
|
||||
padding: $spacer-3 $spacer-4;
|
||||
|
||||
.form-check {
|
||||
margin-bottom: $spacer-2;
|
||||
|
||||
.form-check-input {
|
||||
margin-top: $spacer-1;
|
||||
}
|
||||
.form-check-label {
|
||||
margin-left: $spacer-1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control.font-weight-bold {
|
||||
background-color: lightyellow;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-top: $spacer-3;
|
||||
}
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
.form-group {
|
||||
margin-right: $spacer-3;
|
||||
margin-bottom: $spacer-3;
|
||||
}
|
||||
}
|
||||
|
||||
.list-search {
|
||||
padding: $spacer-3 $spacer-4;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
.list-row {
|
||||
padding: $spacer-2 $spacer-4;
|
||||
border-bottom: 1px solid $gray-200;
|
||||
cursor: pointer;
|
||||
|
||||
.checkbox {
|
||||
margin-right: $spacer-2;
|
||||
}
|
||||
|
||||
a, a:hover, a:visited, a:active {
|
||||
color: $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
.list-row:hover {
|
||||
background-color: $gray-100;
|
||||
}
|
||||
|
||||
.list-row.active {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dropdown-item {
|
||||
padding: $spacer-2 $spacer-3;
|
||||
}
|
||||
|
||||
.bottom-right-float {
|
||||
position: fixed;
|
||||
margin-bottom: 0px;
|
||||
bottom: $spacer-3;
|
||||
right: $spacer-3;
|
||||
max-width: 200px;
|
||||
padding: $spacer-2 $spacer-3;
|
||||
}
|
||||
|
||||
.desk-menu {
|
||||
background-color: $gray-200;
|
||||
|
||||
.list-row {
|
||||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
.list-row:hover {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
.list-row.active {
|
||||
background-color: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.print-page {
|
||||
padding: $spacer-5;
|
||||
line-height: 1.8;
|
||||
|
||||
td, th {
|
||||
padding: $spacer-2;
|
||||
}
|
||||
}
|
||||
|
||||
.table-page-wrapper {
|
||||
width: 100%;
|
||||
padding: $spacer-3 $spacer-4;
|
||||
}
|
||||
|
||||
.filter-toolbar {
|
||||
padding: $spacer-3 $spacer-4;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
margin-top: $spacer-4;
|
||||
margin-bottom: $spacer-4;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-top: $spacer-2;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: 0.25rem;
|
||||
padding: $spacer-2;
|
||||
}
|
||||
|
||||
.awesomplete {
|
||||
display: block;
|
||||
|
||||
ul {
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
> ul > li {
|
||||
padding: .75rem .375rem;
|
||||
}
|
||||
|
||||
> ul > li:hover {
|
||||
background: $gray-300;
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
> ul > li[aria-selected="true"] {
|
||||
background: $gray-300;
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
> ul > li[aria-selected="true"]:hover {
|
||||
background: $gray-300;
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
li[aria-selected="true"] mark, li[aria-selected="false"] mark {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
mark {
|
||||
padding: none;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
margin: $spacer-1;
|
||||
}
|
||||
|
||||
.vertical-margin {
|
||||
margin: $spacer-3 0px;
|
||||
}
|
||||
|
||||
@import "./tree.scss";
|
||||
@import "./datatable.scss";
|
||||
|
||||
// just for accounting
|
||||
|
||||
.setup-container {
|
||||
margin: 40px auto;
|
||||
padding: 20px 0px;
|
||||
width: 450px;
|
||||
|
||||
border: 1px solid $gray-300;
|
||||
border-radius: 4px;
|
||||
|
||||
h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setup-link-area {
|
||||
margin: $spacer-1 $spacer-4;
|
||||
}
|
||||
}
|
||||
|
||||
// File Input
|
||||
|
||||
input[type=file] {
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.was-validated input[type=file]:invalid + button {
|
||||
border-color: $red;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
@import "./variables.scss";
|
||||
|
||||
.tree-body {
|
||||
padding: $spacer-3 $spacer-4;
|
||||
}
|
||||
|
||||
f-tree-node {
|
||||
--tree-node-hover: $gray-100;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
$spacer-1: 0.25rem;
|
||||
$spacer-2: 0.5rem;
|
||||
$spacer-3: 1rem;
|
||||
$spacer-4: 2rem;
|
||||
$spacer-5: 3rem;
|
||||
$page-width: 500px;
|
||||
|
||||
$shadow-width: 0.5rem;
|
@ -1,54 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const bootstrap = require('bootstrap');
|
||||
const $ = require('jquery');
|
||||
|
||||
class Dropdown {
|
||||
constructor({parent, label, items = [], right, cssClass='btn-secondary'}) {
|
||||
Object.assign(this, arguments[0]);
|
||||
Dropdown.instances += 1;
|
||||
this.id = 'dropdownMenuButton-' + Dropdown.instances;
|
||||
|
||||
this.make();
|
||||
|
||||
// init items
|
||||
if (this.items) {
|
||||
for (let item of this.items) {
|
||||
this.addItem(item.label, item.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
this.$dropdown = $(`<div class="dropdown ${this.right ? 'float-right' : ''}">
|
||||
<button class="btn ${this.cssClass} dropdown-toggle"
|
||||
type="button" id="${this.id}" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">${this.label}
|
||||
</button>
|
||||
<div class="dropdown-menu ${this.right ? 'dropdown-menu-right' : ''}" aria-labelledby="${this.id}"></div>
|
||||
</div>`).appendTo(this.parent)
|
||||
this.dropdown = this.$dropdown.get(0);
|
||||
this.dropdownMenu = this.dropdown.querySelector('.dropdown-menu');
|
||||
}
|
||||
|
||||
addItem(label, action) {
|
||||
let item = frappe.ui.add('button', 'dropdown-item', this.dropdownMenu, label);
|
||||
item.setAttribute('type', 'button');
|
||||
if (typeof action === 'string') {
|
||||
item.addEventListener('click', async () => {
|
||||
await frappe.router.setRoute(action);
|
||||
});
|
||||
} else {
|
||||
item.addEventListener('click', async () => {
|
||||
await action();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
floatRight() {
|
||||
frappe.ui.addClass(this.dropdown, 'float-right');
|
||||
}
|
||||
}
|
||||
|
||||
Dropdown.instances = 0;
|
||||
|
||||
module.exports = Dropdown;
|
@ -1,145 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const Dropdown = require('./dropdown');
|
||||
|
||||
module.exports = {
|
||||
create(tag, obj) {
|
||||
if(tag.includes('<')) {
|
||||
let div = document.createElement('div');
|
||||
div.innerHTML = tag.trim();
|
||||
return div.firstChild;
|
||||
}
|
||||
let element = document.createElement(tag);
|
||||
obj = obj || {};
|
||||
|
||||
let $ = (expr, con) => {
|
||||
return typeof expr === "string"
|
||||
? (con || document).querySelector(expr)
|
||||
: expr || null;
|
||||
}
|
||||
|
||||
for (var i in obj) {
|
||||
let val = obj[i];
|
||||
|
||||
if (i === "inside") {
|
||||
$(val).appendChild(element);
|
||||
}
|
||||
else if (i === "around") {
|
||||
let ref = $(val);
|
||||
ref.parentNode.insertBefore(element, ref);
|
||||
element.appendChild(ref);
|
||||
|
||||
} else if (i === "styles") {
|
||||
if(typeof val === "object") {
|
||||
Object.keys(val).map(prop => {
|
||||
element.style[prop] = val[prop];
|
||||
});
|
||||
}
|
||||
} else if (i in element ) {
|
||||
element[i] = val;
|
||||
}
|
||||
else {
|
||||
element.setAttribute(i, val);
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
add(tag, className, parent, textContent) {
|
||||
let element = document.createElement(tag);
|
||||
if (className) {
|
||||
for (let c of className.split(' ')) {
|
||||
this.addClass(element, c);
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
parent.appendChild(element);
|
||||
}
|
||||
if (textContent) {
|
||||
element.textContent = textContent;
|
||||
}
|
||||
return element;
|
||||
},
|
||||
|
||||
remove(element) {
|
||||
element.parentNode.removeChild(element);
|
||||
},
|
||||
|
||||
on(element, event, selector, handler) {
|
||||
if (!handler) {
|
||||
handler = selector;
|
||||
this.bind(element, event, handler);
|
||||
} else {
|
||||
this.delegate(element, event, selector, handler);
|
||||
}
|
||||
},
|
||||
|
||||
off(element, event, handler) {
|
||||
element.removeEventListener(event, handler);
|
||||
},
|
||||
|
||||
bind(element, event, callback) {
|
||||
event.split(/\s+/).forEach(function (event) {
|
||||
element.addEventListener(event, callback);
|
||||
});
|
||||
},
|
||||
|
||||
delegate(element, event, selector, callback) {
|
||||
element.addEventListener(event, function (e) {
|
||||
const delegatedTarget = e.target.closest(selector);
|
||||
if (delegatedTarget) {
|
||||
e.delegatedTarget = delegatedTarget;
|
||||
callback.call(this, e, delegatedTarget);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
empty(element) {
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
},
|
||||
|
||||
addClass(element, className) {
|
||||
if (element.classList) {
|
||||
element.classList.add(className);
|
||||
} else {
|
||||
element.className += " " + className;
|
||||
}
|
||||
},
|
||||
|
||||
removeClass(element, className) {
|
||||
if (element.classList) {
|
||||
element.classList.remove(className);
|
||||
} else {
|
||||
element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
},
|
||||
|
||||
toggleClass(element, className, flag) {
|
||||
if (flag === undefined) {
|
||||
flag = !element.classList.contains(className);
|
||||
}
|
||||
|
||||
if (!flag) {
|
||||
this.removeClass(element, className);
|
||||
} else {
|
||||
this.addClass(element, className);
|
||||
}
|
||||
},
|
||||
|
||||
toggle(element, default_display = '') {
|
||||
element.style.display = element.style.display === 'none' ? default_display : 'none';
|
||||
},
|
||||
|
||||
make_dropdown(label, parent, btn_class = 'btn-secondary') {
|
||||
return new Dropdown({parent: parent, label:label, btn_class:btn_class});
|
||||
},
|
||||
|
||||
showAlert({message, color='yellow', timeout=4}) {
|
||||
let alert = this.add('div', 'alert alert-warning bottom-right-float', document.body);
|
||||
alert.innerHTML = `<span class='indicator ${color}'>${message}</span>`;
|
||||
frappe.sleep(timeout).then(() => alert.remove());
|
||||
return alert;
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
module.exports = {
|
||||
bindKey(element, key, listener) {
|
||||
element.addEventListener('keydown', (e) => {
|
||||
if (key === this.getKey(e)) {
|
||||
listener(e);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getKey(e) {
|
||||
var keycode = e.keyCode || e.which;
|
||||
var key = this.keyMap[keycode] || String.fromCharCode(keycode);
|
||||
|
||||
if(e.ctrlKey || e.metaKey) {
|
||||
// add ctrl+ the key
|
||||
key = 'ctrl+' + key;
|
||||
}
|
||||
if(e.shiftKey) {
|
||||
// add ctrl+ the key
|
||||
key = 'shift+' + key;
|
||||
}
|
||||
return key.toLowerCase();
|
||||
},
|
||||
|
||||
keyMap: {
|
||||
8: 'backspace',
|
||||
9: 'tab',
|
||||
13: 'enter',
|
||||
16: 'shift',
|
||||
17: 'ctrl',
|
||||
91: 'meta',
|
||||
18: 'alt',
|
||||
27: 'escape',
|
||||
37: 'left',
|
||||
39: 'right',
|
||||
38: 'up',
|
||||
40: 'down',
|
||||
32: 'space'
|
||||
},
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
const $ = require('jquery');
|
||||
const bootstrap = require('bootstrap'); // eslint-disable-line
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
|
||||
module.exports = class Modal extends Observable {
|
||||
constructor({ title, body, primary, secondary }) {
|
||||
super();
|
||||
Object.assign(this, arguments[0]);
|
||||
this.make();
|
||||
this.show();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.$modal = $(`<div class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${this.title}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${this.getBodyHTML()}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).appendTo(document.body);
|
||||
this.modal = this.$modal.get(0);
|
||||
|
||||
if (this.primary) {
|
||||
this.addPrimary(this.primary.label, this.primary.action);
|
||||
}
|
||||
if (this.secondary) {
|
||||
this.addSecondary(this.secondary.label, this.secondary.action);
|
||||
}
|
||||
|
||||
this.$modal.on('hidden.bs.modal', () => this.trigger('hide'));
|
||||
this.$modal.on('shown.bs.modal', () => {
|
||||
this.trigger('show');
|
||||
});
|
||||
}
|
||||
|
||||
getBodyHTML() {
|
||||
return this.body || '';
|
||||
}
|
||||
|
||||
addPrimary(label, action) {
|
||||
return $(`<button type="button" class="btn btn-primary">
|
||||
${label}</button>`)
|
||||
.appendTo(this.$modal.find('.modal-footer'))
|
||||
.on('click', () => action(this));
|
||||
}
|
||||
|
||||
addSecondary(label, action) {
|
||||
return $(`<button type="button" class="btn btn-secondary">
|
||||
${label}</button>`)
|
||||
.appendTo(this.$modal.find('.modal-footer'))
|
||||
.on('click', () => action(this));
|
||||
}
|
||||
|
||||
setTitle(title) {
|
||||
this.$modal.find('.modal-title').text(title);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.$modal.modal('show');
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$modal.modal('hide');
|
||||
}
|
||||
|
||||
getBody() {
|
||||
return this.$modal.find('.modal-body').get(0);
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const DataTable = require('frappe-datatable');
|
||||
|
||||
const Modal = require('frappejs/client/ui/modal');
|
||||
const utils = require('./utils');
|
||||
|
||||
module.exports = class ModelTable {
|
||||
constructor({doctype, parent, layout, parentControl, getRowData,
|
||||
isDisabled, getTableData}) {
|
||||
Object.assign(this, arguments[0]);
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.datatable = new DataTable(this.parent, {
|
||||
columns: this.getColumns(),
|
||||
data: [],
|
||||
layout: this.meta.layout || this.layout || 'fluid',
|
||||
addCheckboxColumn: true,
|
||||
getEditor: this.getTableInput.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.datatable.setDimensions();
|
||||
}
|
||||
|
||||
getColumns() {
|
||||
return utils.convertFieldsToDatatableColumns(this.getTableFields(), this.layout);
|
||||
}
|
||||
|
||||
getTableFields() {
|
||||
return this.meta.fields.filter(f => f.hidden ? false : true);
|
||||
}
|
||||
|
||||
getTableInput(colIndex, rowIndex, value, parent) {
|
||||
let field = this.datatable.getColumn(colIndex).field;
|
||||
if (field.disabled || (this.isDisabled && this.isDisabled())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.fieldtype==='Text') {
|
||||
// text in modal
|
||||
parent = this.getControlModal(field).getBody();
|
||||
}
|
||||
const editor = this.getControl(field, parent);
|
||||
return editor;
|
||||
}
|
||||
|
||||
getControl(field, parent) {
|
||||
field.onlyInput = true;
|
||||
const controls = require('frappejs/client/view/controls');
|
||||
const control = controls.makeControl({field: field, parent: parent});
|
||||
|
||||
// change will be triggered by datatable
|
||||
control.skipChangeEvent = true;
|
||||
|
||||
return {
|
||||
initValue: async (value, rowIndex, column) => {
|
||||
column.activeControl = control;
|
||||
control.parentControl = this.parentControl;
|
||||
control.doc = await this.getRowData(rowIndex);
|
||||
control.setFocus();
|
||||
control.setInputValue(control.doc[column.id]);
|
||||
return control;
|
||||
},
|
||||
setValue: async (value, rowIndex, column) => {
|
||||
await this.setValue(control);
|
||||
},
|
||||
getValue: () => {
|
||||
return control.getInputValue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async setValue(control) {
|
||||
await control.handleChange();
|
||||
}
|
||||
|
||||
getControlModal(field) {
|
||||
this.modal = new Modal({
|
||||
title: frappe._('Edit {0}', field.label),
|
||||
body: '',
|
||||
primary: {
|
||||
label: frappe._('Submit'),
|
||||
action: (modal) => {
|
||||
this.datatable.cellmanager.submitEditing();
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.modal.on('hide', () => {
|
||||
this.datatable.cellmanager.deactivateEditing();
|
||||
this.datatable.cellmanager.$focusedCell.focus();
|
||||
});
|
||||
|
||||
return this.modal;
|
||||
}
|
||||
|
||||
checkValidity() {
|
||||
if (!this.datatable) {
|
||||
return true;
|
||||
}
|
||||
let data = this.getTableData();
|
||||
for (let rowIndex=0; rowIndex < data.length; rowIndex++) {
|
||||
let row = data[rowIndex];
|
||||
for (let column of this.datatable.datamanager.columns) {
|
||||
if (column.field && column.field.required) {
|
||||
let value = row[column.field.fieldname];
|
||||
if (value==='' || value===undefined || value===null) {
|
||||
let $cell = this.datatable.cellmanager.getCell$(column.colIndex, rowIndex);
|
||||
this.datatable.cellmanager.activateEditing($cell);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
refresh(data) {
|
||||
return this.datatable.refresh(data);
|
||||
}
|
||||
|
||||
getChecked() {
|
||||
return this.datatable.rowmanager.getCheckedRows();
|
||||
}
|
||||
|
||||
checkAll(check) {
|
||||
return this.datatable.rowmanager.checkAll(check);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
const Awesomplete = require('awesomplete');
|
||||
|
||||
class AutocompleteControl extends BaseControl {
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('type', 'text');
|
||||
this.setupAwesomplete();
|
||||
}
|
||||
|
||||
async setupAwesomplete() {
|
||||
this.awesomplete = new Awesomplete(this.input, {
|
||||
minChars: 0,
|
||||
maxItems: 99
|
||||
});
|
||||
|
||||
this.list = await this.getList();
|
||||
|
||||
// rebuild the list on input
|
||||
this.input.addEventListener('input', (event) => {
|
||||
this.awesomplete.list = this.list;
|
||||
});
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
if (this.list.includes(value)) {
|
||||
return value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutocompleteControl;
|
@ -1,206 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
|
||||
class BaseControl {
|
||||
constructor({field, parent, form}) {
|
||||
BaseControl.count++;
|
||||
|
||||
Object.assign(this, field);
|
||||
this.parent = parent;
|
||||
this.form = form;
|
||||
this.id = 'control-' + BaseControl.count;
|
||||
|
||||
if (!this.fieldname) {
|
||||
this.fieldname = frappe.slug(this.label);
|
||||
}
|
||||
if (!this.parent) {
|
||||
this.parent = this.form.form;
|
||||
}
|
||||
if (this.setup) {
|
||||
this.setup();
|
||||
}
|
||||
if (this.template) {
|
||||
this.wrapper = frappe.ui.add('div', 'field-template', this.parent);
|
||||
this.renderTemplate();
|
||||
} else {
|
||||
this.make();
|
||||
}
|
||||
}
|
||||
|
||||
bind(doc) {
|
||||
this.doc = doc;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this.template) {
|
||||
this.renderTemplate();
|
||||
} else {
|
||||
this.setDocValue();
|
||||
}
|
||||
this.setDisabled();
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
if (this.form && this.form.doc) {
|
||||
this.wrapper.innerHTML = this.template(this.form.doc, this.doc);
|
||||
} else {
|
||||
this.wrapper.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
setDocValue() {
|
||||
if (this.doc && !this.template) {
|
||||
this.setInputValue(this.doc.get(this.fieldname));
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
if (!this.onlyInput) {
|
||||
this.makeInputContainer();
|
||||
this.makeLabel();
|
||||
}
|
||||
this.makeInput();
|
||||
this.addChangeHandler();
|
||||
}
|
||||
|
||||
makeInputContainer(className = 'form-group') {
|
||||
this.inputContainer = frappe.ui.add('div', className, this.parent);
|
||||
}
|
||||
|
||||
makeLabel(labelClass = null) {
|
||||
this.labelElement = frappe.ui.add('label', labelClass, this.inputContainer, this.label);
|
||||
this.labelElement.setAttribute('for', this.id);
|
||||
if (this.inline) {
|
||||
this.labelElement.classList.add("sr-only");
|
||||
}
|
||||
}
|
||||
|
||||
makeInput(inputClass='form-control') {
|
||||
this.input = frappe.ui.add('input', inputClass, this.getInputParent());
|
||||
this.input.autocomplete = "off";
|
||||
this.input.id = this.id;
|
||||
|
||||
this.setInputName();
|
||||
this.setRequiredAttribute();
|
||||
this.setDisabled();
|
||||
if (!this.onlyInput) {
|
||||
this.makeDescription();
|
||||
}
|
||||
if (this.placeholder || this.inline) {
|
||||
this.input.setAttribute('placeholder', this.placeholder || this.label);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isDisabled() {
|
||||
let disabled = this.disabled;
|
||||
|
||||
if (this.doc && this.doc.submitted) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
if (this.formula && this.fieldtype !== 'Table') {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
}
|
||||
|
||||
setDisabled() {
|
||||
this.input.disabled = this.isDisabled();
|
||||
}
|
||||
|
||||
getInputParent() {
|
||||
return this.inputContainer || this.parent;
|
||||
}
|
||||
|
||||
setInputName() {
|
||||
this.input.setAttribute('name', this.fieldname);
|
||||
}
|
||||
|
||||
setRequiredAttribute() {
|
||||
if (this.required) {
|
||||
this.input.required = true;
|
||||
this.input.classList.add('font-weight-bold');
|
||||
}
|
||||
}
|
||||
|
||||
makeDescription() {
|
||||
if (this.description) {
|
||||
this.description_element = frappe.ui.add('small', 'form-text text-muted',
|
||||
this.inputContainer, this.description);
|
||||
}
|
||||
}
|
||||
|
||||
setInputValue(value) {
|
||||
this.input.value = this.format(value);
|
||||
}
|
||||
|
||||
format(value) {
|
||||
if (value === undefined || value === null) {
|
||||
value = '';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async getParsedValue() {
|
||||
return await this.parse(this.getInputValue());
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return this.input.value;
|
||||
}
|
||||
|
||||
async parse(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
async validate(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
addChangeHandler() {
|
||||
this.input.addEventListener('change', () => {
|
||||
if (this.skipChangeEvent) return;
|
||||
this.handleChange();
|
||||
});
|
||||
}
|
||||
|
||||
async handleChange(event) {
|
||||
let value = await this.parse(this.getInputValue());
|
||||
value = await this.validate(value);
|
||||
this.input.setCustomValidity(value === false ? 'error' : '');
|
||||
await this.updateDocValue(value);
|
||||
}
|
||||
|
||||
async updateDocValue(value) {
|
||||
if (!this.doc) return;
|
||||
if (this.doc[this.fieldname] !== value) {
|
||||
if (this.parentControl) {
|
||||
// its a child
|
||||
this.doc[this.fieldname] = value;
|
||||
this.parentControl.doc._dirty = true;
|
||||
await this.parentControl.doc.applyChange(this.fieldname);
|
||||
} else {
|
||||
// parent
|
||||
await this.doc.set(this.fieldname, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.input.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.input.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
setFocus() {
|
||||
this.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
BaseControl.count = 0;
|
||||
|
||||
module.exports = BaseControl;
|
@ -1,38 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class CheckControl extends BaseControl {
|
||||
make() {
|
||||
if (!this.onlyInput) {
|
||||
this.makeInputContainer();
|
||||
}
|
||||
this.makeInput();
|
||||
if (!this.onlyInput) {
|
||||
this.makeLabel();
|
||||
}
|
||||
this.addChangeHandler();
|
||||
}
|
||||
|
||||
makeInputContainer() {
|
||||
super.makeInputContainer('form-check');
|
||||
}
|
||||
|
||||
makeLabel() {
|
||||
super.makeLabel('form-check-label');
|
||||
}
|
||||
|
||||
makeInput() {
|
||||
super.makeInput('form-check-input');
|
||||
this.input.type = 'checkbox';
|
||||
}
|
||||
|
||||
setInputValue(value) {
|
||||
if (value === '0') value = 0;
|
||||
this.input.checked = value ? true : false;
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return this.input.checked ? 1 : 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CheckControl;
|
@ -1,35 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
// const frappe = require('frappejs');
|
||||
const CodeMirror = require('codemirror');
|
||||
const modeHTML = require('codemirror/mode/htmlmixed/htmlmixed'); // eslint-disable-line
|
||||
const modeJavascript = require('codemirror/mode/javascript/javascript'); // eslint-disable-line
|
||||
|
||||
class CodeControl extends BaseControl {
|
||||
makeInput() {
|
||||
if (!this.options) {
|
||||
this.options = {};
|
||||
}
|
||||
this.options.theme = 'default';
|
||||
this.input = new CodeMirror(this.getInputParent(), this.options);
|
||||
}
|
||||
|
||||
setInputValue(value) {
|
||||
if (value !== this.input.getValue()) {
|
||||
this.input.setValue(value || '');
|
||||
}
|
||||
}
|
||||
|
||||
getInputValue(value) {
|
||||
return this.input.getValue();
|
||||
}
|
||||
|
||||
addChangeHandler() {
|
||||
this.input.on('blur', () => {
|
||||
if (this.skipChangeEvent) return;
|
||||
this.handleChange();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = CodeControl;
|
@ -1,13 +0,0 @@
|
||||
const FloatControl = require('./float');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
class CurrencyControl extends FloatControl {
|
||||
parse(value) {
|
||||
return frappe.parse_number(value);
|
||||
}
|
||||
format(value) {
|
||||
return frappe.format_number(value);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CurrencyControl;
|
@ -1,14 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class DataControl extends BaseControl {
|
||||
make() {
|
||||
super.make();
|
||||
|
||||
if (!this.inputType) {
|
||||
this.inputType = 'text';
|
||||
}
|
||||
this.input.setAttribute('type', this.inputType);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DataControl;
|
@ -1,40 +0,0 @@
|
||||
const flatpickr = require('flatpickr');
|
||||
const BaseControl = require('./base');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
class DateControl extends BaseControl {
|
||||
make() {
|
||||
let dateFormat = {
|
||||
'yyyy-mm-dd': 'Y-m-d',
|
||||
'dd/mm/yyyy': 'd/m/Y',
|
||||
'dd-mm-yyyy': 'd-m-Y',
|
||||
'mm/dd/yyyy': 'm/d/Y',
|
||||
'mm-dd-yyyy': 'm-d-Y'
|
||||
}
|
||||
let altFormat = frappe.SystemSettings ?
|
||||
dateFormat[frappe.SystemSettings.dateFormat] :
|
||||
dateFormat['yyyy-mm-dd'];
|
||||
|
||||
super.make();
|
||||
this.input.setAttribute('type', 'text');
|
||||
this.flatpickr = flatpickr(this.input, {
|
||||
altInput: true,
|
||||
altFormat: altFormat,
|
||||
dateFormat:'Y-m-d'
|
||||
});
|
||||
}
|
||||
|
||||
setDisabled() {
|
||||
this.input.disabled = this.isDisabled();
|
||||
if (this.flatpickr && this.flatpickr.altInput) {
|
||||
this.flatpickr.altInput.disabled = this.isDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
setInputValue(value) {
|
||||
super.setInputValue(value);
|
||||
this.flatpickr.setDate(value);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DateControl;
|
@ -1,9 +0,0 @@
|
||||
const LinkControl = require('./link');
|
||||
|
||||
class DynamicLinkControl extends LinkControl {
|
||||
getTarget() {
|
||||
return this.doc[this.references];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DynamicLinkControl;
|
@ -1,53 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class FileControl extends BaseControl {
|
||||
make() {
|
||||
super.make();
|
||||
this.fileButton = frappe.ui.create('button', {
|
||||
className: 'btn btn-outline-secondary btn-block',
|
||||
inside: this.getInputParent(),
|
||||
type: 'button',
|
||||
textContent: 'Choose a file...',
|
||||
onclick: () => {
|
||||
this.input.click();
|
||||
}
|
||||
});
|
||||
|
||||
this.input.setAttribute('type', 'file');
|
||||
|
||||
if (this.directory) {
|
||||
this.input.setAttribute('webkitdirectory', '');
|
||||
}
|
||||
|
||||
if (this.allowMultiple) {
|
||||
this.input.setAttribute('multiple', '');
|
||||
}
|
||||
}
|
||||
|
||||
async handleChange() {
|
||||
await super.handleChange();
|
||||
this.setDocValue();
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return this.input.files;
|
||||
}
|
||||
|
||||
setInputValue(files) {
|
||||
|
||||
let label;
|
||||
if (!files || files.length === 0) {
|
||||
label = 'Choose a file...'
|
||||
} else if (files.length === 1) {
|
||||
label = files[0].name;
|
||||
} else {
|
||||
label = `${files.length} files selected`;
|
||||
}
|
||||
|
||||
this.fileButton.textContent = label;
|
||||
this.input.files = files;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FileControl;
|
@ -1,20 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class FloatControl extends BaseControl {
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('type', 'text');
|
||||
this.input.classList.add('text-right');
|
||||
this.input.addEventListener('focus', () => {
|
||||
setTimeout(() => {
|
||||
this.input.select();
|
||||
}, 100);
|
||||
})
|
||||
}
|
||||
parse(value) {
|
||||
value = parseFloat(value);
|
||||
return isNaN(value) ? 0 : value;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FloatControl;
|
@ -1,28 +0,0 @@
|
||||
const controlClasses = {
|
||||
Autocomplete: require('./autocomplete'),
|
||||
Check: require('./check'),
|
||||
Code: require('./code'),
|
||||
Data: require('./data'),
|
||||
Date: require('./date'),
|
||||
DynamicLink: require('./dynamicLink'),
|
||||
Currency: require('./currency'),
|
||||
Float: require('./float'),
|
||||
File: require('./file'),
|
||||
Int: require('./int'),
|
||||
Link: require('./link'),
|
||||
Password: require('./password'),
|
||||
Select: require('./select'),
|
||||
Table: require('./table'),
|
||||
Text: require('./text')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getControlClass(fieldtype) {
|
||||
return controlClasses[fieldtype];
|
||||
},
|
||||
makeControl({field, form, parent}) {
|
||||
const controlClass = this.getControlClass(field.fieldtype);
|
||||
let control = new controlClass({field:field, form:form, parent:parent});
|
||||
return control;
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
const FloatControl = require('./float');
|
||||
|
||||
class IntControl extends FloatControl {
|
||||
parse(value) {
|
||||
value = parseInt(value);
|
||||
return isNaN(value) ? 0 : value;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = IntControl;
|
@ -1,73 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const BaseControl = require('./base');
|
||||
const Awesomplete = require('awesomplete');
|
||||
|
||||
class LinkControl extends BaseControl {
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('type', 'text');
|
||||
this.setupAwesomplete();
|
||||
}
|
||||
|
||||
setupAwesomplete() {
|
||||
this.awesomplete = new Awesomplete(this.input, {
|
||||
minChars: 0,
|
||||
maxItems: 99,
|
||||
filter: () => true,
|
||||
sort: (a, b) => {
|
||||
if (a.value === '__newitem' || b.value === '__newitem') {
|
||||
return -1;
|
||||
}
|
||||
return a.value > b.value;
|
||||
}
|
||||
});
|
||||
|
||||
// rebuild the list on input
|
||||
this.input.addEventListener('input', async (event) => {
|
||||
let list = await this.getList(this.input.value);
|
||||
|
||||
// action to add new item
|
||||
list.push({
|
||||
label: frappe._('+ New {0}', this.label),
|
||||
value: '__newItem',
|
||||
});
|
||||
|
||||
this.awesomplete.list = list;
|
||||
});
|
||||
|
||||
// new item action
|
||||
this.input.addEventListener('awesomplete-select', async (e) => {
|
||||
if (e.text && e.text.value === '__newItem') {
|
||||
e.preventDefault();
|
||||
const newDoc = await frappe.getNewDoc(this.getTarget());
|
||||
const formModal = await frappe.desk.showFormModal(this.getTarget(), newDoc.name);
|
||||
if (formModal.form.doc.meta.hasField('name')) {
|
||||
formModal.form.doc.set('name', this.input.value);
|
||||
}
|
||||
|
||||
formModal.once('save', async () => {
|
||||
await this.updateDocValue(formModal.form.doc.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async getList(query) {
|
||||
return (await frappe.db.getAll({
|
||||
doctype: this.getTarget(),
|
||||
filters: this.getFilters(query, this),
|
||||
limit: 50
|
||||
})).map(d => d.name);
|
||||
}
|
||||
|
||||
getFilters(query) {
|
||||
return { keywords: ["like", query] }
|
||||
}
|
||||
|
||||
getTarget() {
|
||||
return this.target;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = LinkControl;
|
@ -1,10 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class PasswordControl extends BaseControl {
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('type', 'password');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PasswordControl;
|
@ -1,52 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
class SelectControl extends BaseControl {
|
||||
makeInput() {
|
||||
this.input = frappe.ui.add('select', 'form-control', this.getInputParent());
|
||||
this.addOptions();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.addOptions();
|
||||
super.refresh();
|
||||
}
|
||||
|
||||
addOptions() {
|
||||
const options = this.getOptions();
|
||||
if (this.areOptionsSame(options)) return;
|
||||
|
||||
frappe.ui.empty(this.input);
|
||||
for (let value of options) {
|
||||
let option = frappe.ui.add('option', null, this.input, value.label || value);
|
||||
option.setAttribute('value', value.value || value);
|
||||
}
|
||||
this.lastOptions = options;
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
let options = this.options;
|
||||
if (typeof options==='string') {
|
||||
options = options.split('\n');
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
areOptionsSame(options) {
|
||||
let same = false;
|
||||
if (this.lastOptions && options.length===this.lastOptions.length) {
|
||||
same = options.every((v ,i) => {
|
||||
const v1 = this.lastOptions[i];
|
||||
return (v.value || v) === (v1.value || v1)
|
||||
});
|
||||
}
|
||||
return same;
|
||||
}
|
||||
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('row', '3');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SelectControl;
|
@ -1,100 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const BaseControl = require('./base');
|
||||
const ModelTable = require('frappejs/client/ui/modelTable');
|
||||
|
||||
class TableControl extends BaseControl {
|
||||
make() {
|
||||
this.makeWrapper();
|
||||
this.modelTable = new ModelTable({
|
||||
doctype: this.childtype,
|
||||
parent: this.wrapper.querySelector('.datatable-wrapper'),
|
||||
parentControl: this,
|
||||
layout: this.layout || 'ratio',
|
||||
getTableData: () => this.getTableData(),
|
||||
getRowData: (rowIndex) => this.doc[this.fieldname][rowIndex],
|
||||
isDisabled: () => this.isDisabled(),
|
||||
});
|
||||
this.setupToolbar();
|
||||
}
|
||||
|
||||
makeWrapper() {
|
||||
this.wrapper = frappe.ui.add('div', 'table-wrapper', this.getInputParent());
|
||||
this.wrapper.innerHTML =
|
||||
`<div class="datatable-wrapper" style="width: 100%"></div>
|
||||
<div class="table-toolbar">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btn-add">
|
||||
${frappe._("Add")}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btn-remove">
|
||||
${frappe._("Remove")}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
setupToolbar() {
|
||||
this.wrapper.querySelector('.btn-add').addEventListener('click', async (event) => {
|
||||
this.doc[this.fieldname].push({});
|
||||
await this.doc.commit();
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.wrapper.querySelector('.btn-remove').addEventListener('click', async (event) => {
|
||||
let checked = this.modelTable.getChecked();
|
||||
this.doc[this.fieldname] = this.doc[this.fieldname].filter(d => !checked.includes(d.idx + ''));
|
||||
await this.doc.commit();
|
||||
this.refresh();
|
||||
this.modelTable.checkAll(false);
|
||||
});
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return this.doc[this.fieldname];
|
||||
}
|
||||
|
||||
setInputValue(value) {
|
||||
this.modelTable.refresh(this.getTableData(value));
|
||||
}
|
||||
|
||||
setDisabled() {
|
||||
this.refreshToolbar();
|
||||
}
|
||||
|
||||
getToolbar() {
|
||||
return this.wrapper.querySelector('.table-toolbar');
|
||||
}
|
||||
|
||||
refreshToolbar() {
|
||||
const toolbar = this.wrapper.querySelector('.table-toolbar');
|
||||
if (toolbar) {
|
||||
toolbar.classList.toggle('hide', this.isDisabled() ? true : false);
|
||||
}
|
||||
}
|
||||
|
||||
getTableData(value) {
|
||||
return (value && value.length) ? value : this.getDefaultData();
|
||||
}
|
||||
|
||||
getDefaultData() {
|
||||
// build flat table
|
||||
if (!this.doc) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.doc[this.fieldname]) {
|
||||
this.doc[this.fieldname] = [{idx: 0}];
|
||||
}
|
||||
|
||||
if (this.doc[this.fieldname].length === 0 && this.neverEmpty) {
|
||||
this.doc[this.fieldname] = [{idx: 0}];
|
||||
}
|
||||
|
||||
return this.doc[this.fieldname];
|
||||
}
|
||||
|
||||
checkValidity() {
|
||||
if (!this.modelTable) {
|
||||
return true;
|
||||
}
|
||||
return this.modelTable.checkValidity();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TableControl;
|
@ -1,14 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
class TextControl extends BaseControl {
|
||||
makeInput() {
|
||||
this.input = frappe.ui.add('textarea', 'form-control', this.getInputParent());
|
||||
}
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('rows', '8');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TextControl;
|
@ -1,336 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const controls = require('./controls');
|
||||
const FormLayout = require('./formLayout');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
const keyboard = require('frappejs/client/ui/keyboard');
|
||||
const utils = require('frappejs/client/ui/utils');
|
||||
|
||||
module.exports = class BaseForm extends Observable {
|
||||
constructor({doctype, parent, submit_label='Submit', container, meta, inline=false}) {
|
||||
super();
|
||||
Object.assign(this, arguments[0]);
|
||||
this.links = [];
|
||||
|
||||
if (!this.meta) {
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
}
|
||||
|
||||
if (this.setup) {
|
||||
this.setup();
|
||||
}
|
||||
|
||||
this.make();
|
||||
this.bindFormEvents();
|
||||
|
||||
if (this.doc) {
|
||||
// bootstrapped with a doc
|
||||
this.bindEvents(this.doc);
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
if (this.body || !this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inline) {
|
||||
this.body = this.parent
|
||||
} else {
|
||||
this.body = frappe.ui.add('div', 'form-body', this.parent);
|
||||
}
|
||||
|
||||
if (this.actions) {
|
||||
this.makeToolbar();
|
||||
}
|
||||
|
||||
this.form = frappe.ui.add('form', 'form-container', this.body);
|
||||
|
||||
if (this.inline) {
|
||||
this.form.classList.add('form-inline');
|
||||
}
|
||||
|
||||
this.form.onValidate = true;
|
||||
|
||||
this.formLayout = new FormLayout({
|
||||
fields: this.meta.fields,
|
||||
layout: this.meta.layout
|
||||
});
|
||||
|
||||
this.form.appendChild(this.formLayout.form);
|
||||
|
||||
this.bindKeyboard();
|
||||
}
|
||||
|
||||
bindFormEvents() {
|
||||
if (this.meta.formEvents) {
|
||||
for (let key in this.meta.formEvents) {
|
||||
this.on(key, this.meta.formEvents[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeToolbar() {
|
||||
if (this.actions.includes('save')) {
|
||||
this.makeSaveButton();
|
||||
|
||||
if (this.meta.isSubmittable) {
|
||||
this.makeSubmitButton();
|
||||
this.makeRevertButton();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.meta.print && this.actions.includes('print')) {
|
||||
let menu = this.container.getDropdown(frappe._('Menu'));
|
||||
menu.addItem(frappe._("Print"), async (e) => {
|
||||
await frappe.router.setRoute('print', this.doctype, this.doc.name);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.meta.isSingle && this.actions.includes('delete')) {
|
||||
let menu = this.container.getDropdown(frappe._('Menu'));
|
||||
menu.addItem(frappe._("Delete"), async (e) => {
|
||||
await this.delete();
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.meta.isSingle && this.actions.includes('duplicate')) {
|
||||
let menu = this.container.getDropdown(frappe._('Menu'));
|
||||
menu.addItem(frappe._('Duplicate'), async () => {
|
||||
let newDoc = await frappe.getDuplicate(this.doc);
|
||||
await frappe.router.setRoute('edit', newDoc.doctype, newDoc.name);
|
||||
newDoc.set('name', '');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.settings && this.actions.includes('settings')) {
|
||||
let menu = this.container.getDropdown(frappe._('Menu'));
|
||||
menu.addItem(frappe._('Settings...'), () => {
|
||||
frappe.desk.showFormModal(this.meta.settings, this.meta.settings);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
makeSaveButton() {
|
||||
this.saveButton = this.container.addButton(frappe._("Save"), 'primary', async (event) => {
|
||||
await this.save();
|
||||
});
|
||||
this.on('change', () => {
|
||||
const show = this.doc._dirty && !this.doc.submitted;
|
||||
this.saveButton.classList.toggle('hide', !show);
|
||||
});
|
||||
}
|
||||
|
||||
makeSubmitButton() {
|
||||
this.submitButton = this.container.addButton(frappe._("Submit"), 'primary', async (event) => {
|
||||
await this.submit();
|
||||
});
|
||||
this.on('change', () => {
|
||||
const show = this.meta.isSubmittable && !this.doc._dirty && !this.doc.submitted;
|
||||
this.submitButton.classList.toggle('hide', !show);
|
||||
});
|
||||
}
|
||||
|
||||
makeRevertButton() {
|
||||
this.revertButton = this.container.addButton(frappe._("Revert"), 'secondary', async (event) => {
|
||||
await this.revert();
|
||||
});
|
||||
this.on('change', () => {
|
||||
const show = this.meta.isSubmittable && !this.doc._dirty && this.doc.submitted;
|
||||
this.revertButton.classList.toggle('hide', !show);
|
||||
});
|
||||
}
|
||||
|
||||
bindKeyboard() {
|
||||
keyboard.bindKey(this.form, 'ctrl+s', (e) => {
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
e.preventDefault();
|
||||
if (this.doc._notInserted || this.doc._dirty) {
|
||||
this.save();
|
||||
} else {
|
||||
if (this.meta.isSubmittable && !this.doc.submitted) this.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setDoc(doctype, name) {
|
||||
this.doc = await frappe.getDoc(doctype, name);
|
||||
this.bindEvents(this.doc);
|
||||
if (this.doc._notInserted && !this.doc._nameCleared) {
|
||||
this.doc._nameCleared = true;
|
||||
// flag so that name is cleared only once
|
||||
await this.doc.set('name', '');
|
||||
}
|
||||
this.setTitle();
|
||||
frappe._curFrm = this;
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
if (!this.container) return;
|
||||
|
||||
const doctypeLabel = this.doc.meta.label || this.doc.meta.name;
|
||||
|
||||
if (this.doc.meta.isSingle || this.doc.meta.naming === 'random') {
|
||||
this.container.setTitle(doctypeLabel);
|
||||
} else if (this.doc._notInserted) {
|
||||
this.container.setTitle(frappe._('New {0}', doctypeLabel));
|
||||
} else {
|
||||
this.container.setTitle(this.doc.name);
|
||||
}
|
||||
if (this.doc.submitted) {
|
||||
// this.container.addTitleBadge('✓', frappe._('Submitted'));
|
||||
}
|
||||
}
|
||||
|
||||
setLinks(label, options) {
|
||||
// set links to helpful reports as identified by this.meta.links
|
||||
if (this.meta.links) {
|
||||
let links = this.getLinks();
|
||||
if (!links.equals(this.links)) {
|
||||
this.refreshLinks(links);
|
||||
this.links = links;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getLinks() {
|
||||
let links = [];
|
||||
for (let link of this.meta.links) {
|
||||
if (link.condition(this)) {
|
||||
links.push(link);
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
refreshLinks(links) {
|
||||
if (!(this.container && this.container.clearLinks)) return;
|
||||
|
||||
this.container.clearLinks();
|
||||
for(let link of links) {
|
||||
// make the link
|
||||
utils.addButton(link.label, this.container.linksElement, () => {
|
||||
let options = link.action(this);
|
||||
|
||||
if (options) {
|
||||
if (options.params) {
|
||||
// set route parameters
|
||||
frappe.params = options.params;
|
||||
}
|
||||
|
||||
if (options.route) {
|
||||
// go to the given route
|
||||
frappe.router.setRoute(...options.route);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async bindEvents(doc) {
|
||||
if (this.doc && this.docListener) {
|
||||
// stop listening to the old doc
|
||||
this.doc.off(this.docListener);
|
||||
}
|
||||
this.doc = doc;
|
||||
for (let control of this.formLayout.controlList) {
|
||||
control.bind(this.doc);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
this.setupDocListener();
|
||||
this.trigger('use', {doc:doc});
|
||||
}
|
||||
|
||||
setupDocListener() {
|
||||
// refresh value in control
|
||||
this.docListener = (params) => {
|
||||
if (params.fieldname) {
|
||||
// only single value changed
|
||||
let control = this.formLayout.controls[params.fieldname];
|
||||
if (control && control.getInputValue() !== control.format(params.fieldname)) {
|
||||
control.refresh();
|
||||
}
|
||||
} else {
|
||||
// multiple values changed
|
||||
this.refresh();
|
||||
}
|
||||
this.trigger('change');
|
||||
this.form.classList.remove('was-validated');
|
||||
};
|
||||
|
||||
this.doc.on('change', this.docListener);
|
||||
this.trigger('change');
|
||||
}
|
||||
|
||||
checkValidity() {
|
||||
let validity = this.form.checkValidity();
|
||||
if (validity) {
|
||||
for (let control of this.formLayout.controlList) {
|
||||
// check validity in table
|
||||
if (control.fieldtype==='Table') {
|
||||
validity = control.checkValidity();
|
||||
if (!validity) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return validity;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.formLayout.refresh();
|
||||
this.trigger('refresh', this);
|
||||
this.setLinks();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.doc.submitted = 1;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async revert() {
|
||||
this.doc.submitted = 0;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async save() {
|
||||
if (!this.checkValidity()) {
|
||||
this.form.classList.add('was-validated');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let oldName = this.doc.name;
|
||||
if (this.doc._notInserted) {
|
||||
await this.doc.insert();
|
||||
} else {
|
||||
await this.doc.update();
|
||||
}
|
||||
frappe.ui.showAlert({message: frappe._('Saved'), color: 'green'});
|
||||
if (oldName !== this.doc.name) {
|
||||
frappe.router.setRoute('edit', this.doctype, this.doc.name);
|
||||
return;
|
||||
}
|
||||
this.refresh();
|
||||
this.trigger('change');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
frappe.ui.showAlert({message: frappe._('Failed'), color: 'red'});
|
||||
return;
|
||||
}
|
||||
await this.trigger('save');
|
||||
}
|
||||
|
||||
async delete() {
|
||||
try {
|
||||
await this.doc.delete();
|
||||
frappe.ui.showAlert({message: frappe._('Deleted'), color: 'green'});
|
||||
this.trigger('delete');
|
||||
} catch (e) {
|
||||
frappe.ui.showAlert({message: e, color: 'red'});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const controls = require('./controls');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
|
||||
module.exports = class FormLayout extends Observable {
|
||||
constructor({fields, doc, layout, inline = false, events = []}) {
|
||||
super();
|
||||
Object.assign(this, arguments[0]);
|
||||
this.controls = {};
|
||||
this.controlList = [];
|
||||
this.sections = [];
|
||||
this.links = [];
|
||||
|
||||
this.form = document.createElement('div');
|
||||
this.form.classList.add('form-body');
|
||||
|
||||
if (this.inline) {
|
||||
this.form.classList.add('row');
|
||||
this.form.classList.add('p-0');
|
||||
}
|
||||
|
||||
this.makeLayout();
|
||||
|
||||
if (doc) {
|
||||
this.bindEvents(doc);
|
||||
}
|
||||
}
|
||||
|
||||
makeLayout() {
|
||||
if (this.layout) {
|
||||
for (let section of this.layout) {
|
||||
this.makeSection(section);
|
||||
}
|
||||
} else {
|
||||
this.makeControls(this.fields);
|
||||
}
|
||||
}
|
||||
|
||||
makeSection(section) {
|
||||
const sectionElement = frappe.ui.add('div', 'form-section', this.form);
|
||||
const sectionHead = frappe.ui.add('div', 'form-section-head', sectionElement);
|
||||
const sectionBody = frappe.ui.add('div', 'form-section-body', sectionElement);
|
||||
|
||||
if (section.title) {
|
||||
const head = frappe.ui.add('h6', 'uppercase', sectionHead);
|
||||
head.textContent = section.title;
|
||||
}
|
||||
|
||||
if (section.columns) {
|
||||
sectionBody.classList.add('row');
|
||||
for (let column of section.columns) {
|
||||
let columnElement = frappe.ui.add('div', 'col', sectionBody);
|
||||
this.makeControls(this.getFieldsFromLayoutElement(column.fields), columnElement);
|
||||
}
|
||||
} else {
|
||||
this.makeControls(this.getFieldsFromLayoutElement(section.fields), sectionBody);
|
||||
}
|
||||
this.sections.push(sectionBody);
|
||||
}
|
||||
|
||||
getFieldsFromLayoutElement(fields) {
|
||||
return this.fields.filter(d => fields.includes(d.fieldname));
|
||||
}
|
||||
|
||||
makeControls(fields, parent) {
|
||||
for(let field of fields) {
|
||||
if (!field.hidden && controls.getControlClass(field.fieldtype)) {
|
||||
let control = controls.makeControl({field: field, form: this, parent: parent});
|
||||
this.controlList.push(control);
|
||||
this.controls[field.fieldname] = control;
|
||||
if (this.inline) {
|
||||
control.inputContainer.classList.add('col');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async bindEvents(doc) {
|
||||
this.doc = doc;
|
||||
this.controlList.forEach(control => {
|
||||
control.bind(this.doc);
|
||||
});
|
||||
|
||||
this.doc.on('change', ({doc, fieldname}) => {
|
||||
this.controls[fieldname].refresh();
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
setValue(key, value) {
|
||||
if (!this.doc) return;
|
||||
this.doc.set(key, value);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.controlList.forEach(control => {
|
||||
control.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
bindFormEvents() {
|
||||
if (this.events) {
|
||||
for (let key in this.events) {
|
||||
this.on(key, this.events[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
const BaseList = require('frappejs/client/view/list');
|
||||
const BaseTree = require('frappejs/client/view/tree');
|
||||
const BaseForm = require('frappejs/client/view/form');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
module.exports = {
|
||||
getFormClass(doctype) {
|
||||
return (frappe.views['Form'] && frappe.views['Form'][doctype]) || BaseForm;
|
||||
},
|
||||
getListClass(doctype) {
|
||||
return (frappe.views['List'] && frappe.views['List'][doctype]) || BaseList;
|
||||
},
|
||||
getTreeClass(doctype) {
|
||||
return (frappe.views['Tree'] && frappe.views['Tree'][doctype] || BaseTree);
|
||||
}
|
||||
}
|
@ -1,319 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const keyboard = require('frappejs/client/ui/keyboard');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
|
||||
module.exports = class BaseList extends Observable {
|
||||
constructor({doctype, parent, fields=[], page}) {
|
||||
super();
|
||||
Object.assign(this, arguments[0]);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
|
||||
this.start = 0;
|
||||
this.pageLength = 20;
|
||||
|
||||
this.body = null;
|
||||
this.rows = [];
|
||||
this.data = [];
|
||||
|
||||
this.setupTreeSettings();
|
||||
|
||||
frappe.db.on(`change:${this.doctype}`, (params) => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
setupTreeSettings() {
|
||||
// list settings that can be overridden by meta
|
||||
this.listSettings = {
|
||||
getFields: list => list.fields,
|
||||
getRowHTML: (list, data) => {
|
||||
return `<div class="col-11">
|
||||
${list.getNameHTML(data)}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.meta.listSettings) {
|
||||
Object.assign(this.listSettings, this.meta.listSettings);
|
||||
}
|
||||
}
|
||||
|
||||
makeBody() {
|
||||
if (!this.body) {
|
||||
this.makeToolbar();
|
||||
this.parent.classList.add('list-page');
|
||||
this.body = frappe.ui.add('div', 'list-body', this.parent);
|
||||
this.body.setAttribute('data-doctype', this.doctype);
|
||||
this.makeMoreBtn();
|
||||
this.bindKeys();
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
return await this.run();
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.makeBody();
|
||||
this.dirty = false;
|
||||
|
||||
let data = await this.getData();
|
||||
|
||||
for (let i=0; i< Math.min(this.pageLength, data.length); i++) {
|
||||
let row = this.getRow(this.start + i);
|
||||
this.renderRow(row, data[i]);
|
||||
}
|
||||
|
||||
if (this.start > 0) {
|
||||
this.data = this.data.concat(data);
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
this.clearEmptyRows();
|
||||
this.updateMore(data.length > this.pageLength);
|
||||
this.selectDefaultRow();
|
||||
this.setActiveListRow();
|
||||
this.trigger('state-change');
|
||||
}
|
||||
|
||||
async getData() {
|
||||
let fields = this.listSettings.getFields(this) || [];
|
||||
this.updateStandardFields(fields);
|
||||
return await frappe.db.getAll({
|
||||
doctype: this.doctype,
|
||||
fields: fields,
|
||||
filters: this.getFilters(),
|
||||
start: this.start,
|
||||
limit: this.pageLength + 1
|
||||
});
|
||||
}
|
||||
|
||||
updateStandardFields(fields) {
|
||||
if (!fields.includes('name')) fields.push('name');
|
||||
if (!fields.includes('modified')) fields.push('modified');
|
||||
if (this.meta.isSubmittable && !fields.includes('submitted')) fields.push('submitted');
|
||||
}
|
||||
|
||||
async append() {
|
||||
this.start += this.pageLength;
|
||||
await this.run();
|
||||
}
|
||||
|
||||
getFilters() {
|
||||
let filters = {};
|
||||
if (this.searchInput.value) {
|
||||
filters.keywords = ['like', '%' + this.searchInput.value + '%'];
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
renderRow(row, data) {
|
||||
row.innerHTML = this.getRowBodyHTML(data);
|
||||
row.docName = data.name;
|
||||
row.setAttribute('data-name', data.name);
|
||||
}
|
||||
|
||||
getRowBodyHTML(data) {
|
||||
return `<div class="col-1">
|
||||
<input class="checkbox" type="checkbox" data-name="${data.name}">
|
||||
</div>` + this.listSettings.getRowHTML(this, data);
|
||||
}
|
||||
|
||||
|
||||
getNameHTML(data) {
|
||||
return `<span class="indicator ${this.meta.getIndicatorColor(data)}">${data[this.meta.titleField]}</span>`;
|
||||
}
|
||||
|
||||
getRow(i) {
|
||||
if (!this.rows[i]) {
|
||||
let row = frappe.ui.add('div', 'list-row row no-gutters', this.body);
|
||||
|
||||
// open on click
|
||||
let me = this;
|
||||
row.addEventListener('click', async function(e) {
|
||||
if (!e.target.tagName !== 'input') {
|
||||
await me.showItem(this.docName);
|
||||
}
|
||||
});
|
||||
row.style.display = 'flex';
|
||||
|
||||
// make element focusable
|
||||
row.setAttribute('tabindex', -1);
|
||||
this.rows[i] = row;
|
||||
}
|
||||
return this.rows[i];
|
||||
}
|
||||
|
||||
refreshRow(doc) {
|
||||
let row = this.getRowByName(doc.name);
|
||||
if (row) {
|
||||
this.renderRow(row, doc);
|
||||
}
|
||||
}
|
||||
|
||||
async showItem(name) {
|
||||
if (this.meta.print) {
|
||||
await frappe.router.setRoute('print', this.doctype, name);
|
||||
} else {
|
||||
await frappe.router.setRoute('edit', this.doctype, name);
|
||||
}
|
||||
}
|
||||
|
||||
getCheckedRowNames() {
|
||||
return [...this.body.querySelectorAll('.checkbox:checked')].map(check => check.getAttribute('data-name'));
|
||||
}
|
||||
|
||||
clearEmptyRows() {
|
||||
if (this.rows.length > this.data.length) {
|
||||
for (let i=this.data.length; i < this.rows.length; i++) {
|
||||
let row = this.getRow(i);
|
||||
row.innerHTML = '';
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectDefaultRow() {
|
||||
if (!frappe.desk.body.activePage && this.rows.length) {
|
||||
this.showItem(this.rows[0].docName);
|
||||
}
|
||||
}
|
||||
|
||||
makeToolbar() {
|
||||
this.makeSearch();
|
||||
|
||||
this.btnNew = this.page.addButton(frappe._('New'), 'btn-primary', async () => {
|
||||
await frappe.router.setRoute('new', this.doctype);
|
||||
});
|
||||
|
||||
this.btnDelete = this.page.addButton(frappe._('Delete'), 'btn-secondary hide', async () => {
|
||||
await frappe.db.deleteMany(this.doctype, this.getCheckedRowNames());
|
||||
await this.refresh();
|
||||
});
|
||||
|
||||
this.btnReport = this.page.addButton(frappe._('Report'), 'btn-outline-secondary hide', async () => {
|
||||
await frappe.router.setRoute('table', this.doctype);
|
||||
});
|
||||
|
||||
this.on('state-change', () => {
|
||||
const checkedCount = this.getCheckedRowNames().length;
|
||||
this.btnDelete.classList.toggle('hide', checkedCount ? false : true);
|
||||
this.btnNew.classList.toggle('hide', checkedCount ? true : false);
|
||||
this.btnReport.classList.toggle('hide', checkedCount ? true : false);
|
||||
});
|
||||
|
||||
this.page.body.addEventListener('click', (event) => {
|
||||
if(event.target.classList.contains('checkbox')) {
|
||||
this.trigger('state-change');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
makeSearch() {
|
||||
this.toolbar = frappe.ui.add('div', 'list-toolbar', this.parent);
|
||||
this.toolbar.innerHTML = `
|
||||
<div class="input-group list-search">
|
||||
<input class="form-control" type="text" placeholder="Search...">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary btn-search">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.searchInput = this.toolbar.querySelector('input');
|
||||
this.searchInput.addEventListener('keypress', (event) => {
|
||||
if (event.keyCode===13) {
|
||||
this.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
this.btnSearch = this.toolbar.querySelector('.btn-search');
|
||||
this.btnSearch.addEventListener('click', (event) => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
bindKeys() {
|
||||
keyboard.bindKey(this.body, 'up', () => this.move('up'));
|
||||
keyboard.bindKey(this.body, 'down', () => this.move('down'))
|
||||
|
||||
keyboard.bindKey(this.body, 'right', () => {
|
||||
if (frappe.desk.body.activePage) {
|
||||
frappe.desk.body.activePage.body.querySelector('input').focus();
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.bindKey(this.body, 'n', (e) => {
|
||||
frappe.router.setRoute('new', this.doctype);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
keyboard.bindKey(this.body, 'x', async (e) => {
|
||||
let activeListRow = this.getActiveListRow();
|
||||
if (activeListRow && activeListRow.docName) {
|
||||
e.preventDefault();
|
||||
await frappe.db.delete(this.doctype, activeListRow.docName);
|
||||
frappe.desk.body.activePage.hide();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async move(direction) {
|
||||
let elementRef = direction === 'up' ? 'previousSibling' : 'nextSibling';
|
||||
if (document.activeElement && document.activeElement.classList.contains('list-row')) {
|
||||
let next = document.activeElement[elementRef];
|
||||
if (next && next.docName) {
|
||||
await this.showItem(next.docName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeMoreBtn() {
|
||||
this.btnMore = frappe.ui.add('button', 'btn btn-secondary hide', this.parent, 'More');
|
||||
this.btnMore.addEventListener('click', () => {
|
||||
this.append();
|
||||
})
|
||||
}
|
||||
|
||||
updateMore(show) {
|
||||
if (show) {
|
||||
this.btnMore.classList.remove('hide');
|
||||
} else {
|
||||
this.btnMore.classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
setActiveListRow(name) {
|
||||
let activeListRow = this.getActiveListRow();
|
||||
if (activeListRow) {
|
||||
activeListRow.classList.remove('active');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
// get name from active page
|
||||
name = frappe.desk.activeDoc && frappe.desk.activeDoc.name;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
let row = this.getRowByName(name);
|
||||
if (row) {
|
||||
row.classList.add('active');
|
||||
row.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRowByName(name) {
|
||||
return this.body.querySelector(`.list-row[data-name="${name}"]`);
|
||||
}
|
||||
|
||||
getActiveListRow() {
|
||||
return this.body.querySelector('.list-row.active');
|
||||
}
|
||||
};
|
@ -1,100 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
const Dropdown = require('frappejs/client/ui/dropdown');
|
||||
|
||||
module.exports = class Page extends Observable {
|
||||
constructor({title, parent, hasRoute=true} = {}) {
|
||||
super();
|
||||
Object.assign(this, arguments[0]);
|
||||
if (!this.parent) {
|
||||
this.parent = frappe.desk.body;
|
||||
}
|
||||
this.make();
|
||||
this.dropdowns = {};
|
||||
if(this.title) {
|
||||
this.wrapper.setAttribute('title', this.title);
|
||||
this.setTitle(this.title);
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
this.wrapper = frappe.ui.add('div', 'page hide', this.parent);
|
||||
this.head = frappe.ui.add('div', 'page-nav clearfix hide', this.wrapper);
|
||||
this.titleElement = frappe.ui.add('h3', 'page-title', this.wrapper);
|
||||
this.linksElement = frappe.ui.add('div', 'btn-group page-links hide', this.wrapper);
|
||||
this.body = frappe.ui.add('div', 'page-body', this.wrapper);
|
||||
}
|
||||
|
||||
setTitle(title) {
|
||||
this.titleElement.textContent = title;
|
||||
if (this.hasRoute) {
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
addTitleBadge(message, title='', style='secondary') {
|
||||
this.titleElement.innerHTML += ` <span class='badge badge-${style}' title='${title}'>
|
||||
${message}</span>`;
|
||||
}
|
||||
|
||||
clearLinks() {
|
||||
frappe.ui.empty(this.linksElement);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.parent.activePage = null;
|
||||
this.wrapper.classList.add('hide');
|
||||
this.trigger('hide');
|
||||
}
|
||||
|
||||
addButton(label, className, action) {
|
||||
this.head.classList.remove('hide');
|
||||
this.button = frappe.ui.add('button', 'btn btn-sm float-right ' + this.getClassName(className), this.head);
|
||||
this.button.innerHTML = label;
|
||||
this.button.addEventListener('click', action);
|
||||
return this.button;
|
||||
}
|
||||
|
||||
getDropdown(label) {
|
||||
if (!this.dropdowns[label]) {
|
||||
this.dropdowns[label] = new Dropdown({parent: this.head, label: label,
|
||||
right: true, cssClass: 'btn-secondary btn-sm'});
|
||||
}
|
||||
return this.dropdowns[label];
|
||||
}
|
||||
|
||||
async show(params) {
|
||||
if (this.parent.activePage) {
|
||||
this.parent.activePage.hide();
|
||||
}
|
||||
|
||||
this.wrapper.classList.remove('hide');
|
||||
this.body.classList.remove('hide');
|
||||
|
||||
if (this.page_error) {
|
||||
this.page_error.classList.add('hide');
|
||||
}
|
||||
|
||||
this.parent.activePage = this;
|
||||
|
||||
frappe.desk.toggleCenter(this.fullPage ? false : true);
|
||||
}
|
||||
|
||||
renderError(title, message) {
|
||||
if (!this.page_error) {
|
||||
this.page_error = frappe.ui.add('div', 'page-error', this.wrapper);
|
||||
}
|
||||
this.body.classList.add('hide');
|
||||
this.page_error.classList.remove('hide');
|
||||
this.page_error.innerHTML = `<h3 class="text-extra-muted">${title ? title : ""}</h3><p class="text-muted">${message ? message : ""}</p>`;
|
||||
}
|
||||
|
||||
getClassName(className) {
|
||||
const newName = {
|
||||
'primary': 'btn-primary',
|
||||
'secondary': 'btn-outline-secondary'
|
||||
}[className];
|
||||
|
||||
return newName || className;
|
||||
}
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
const frappe = require('frappejs');
|
||||
const BaseList = require('./list');
|
||||
const Tree = require('frappejs/client/components/tree');
|
||||
// const keyboard = require('frappejs/client/ui/keyboard');
|
||||
|
||||
module.exports = class BaseTree extends BaseList {
|
||||
|
||||
init() {
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
|
||||
this.body = null;
|
||||
this.data = [];
|
||||
|
||||
this.setupTreeSettings();
|
||||
|
||||
frappe.db.on(`change:${this.doctype}`, (params) => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
setupTreeSettings() {
|
||||
// tree settings that can be overridden by meta
|
||||
this.treeSettings = {
|
||||
parentField: `parent${this.doctype}`
|
||||
}
|
||||
|
||||
if (this.meta.treeSettings) {
|
||||
Object.assign(this.treeSettings, this.meta.treeSettings);
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
return await this.run();
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.makeBody();
|
||||
this.body.innerHTML = '';
|
||||
this.dirty = false;
|
||||
|
||||
const rootLabel = this.treeSettings.getRootLabel ?
|
||||
await this.treeSettings.getRootLabel() :
|
||||
this.doctype;
|
||||
|
||||
this.renderTree(rootLabel);
|
||||
this.trigger('state-change');
|
||||
}
|
||||
|
||||
makeBody() {
|
||||
if (!this.body) {
|
||||
this.makeToolbar();
|
||||
this.parent.classList.add('tree-page');
|
||||
this.body = frappe.ui.add('div', 'tree-body', this.parent);
|
||||
this.body.setAttribute('data-doctype', this.doctype);
|
||||
this.bindKeys();
|
||||
}
|
||||
}
|
||||
|
||||
renderTree(rootLabel) {
|
||||
this.rootNode = {
|
||||
label: rootLabel,
|
||||
value: rootLabel,
|
||||
isRoot: true,
|
||||
isGroup: true,
|
||||
children: null
|
||||
}
|
||||
|
||||
this.treeWrapper = frappe.ui.create(`
|
||||
<f-tree>
|
||||
${this.getTreeNodeHTML(this.rootNode)}
|
||||
</f-tree>
|
||||
`);
|
||||
|
||||
const rootNode = this.treeWrapper.querySelector('f-tree-node[is-root]');
|
||||
rootNode.props = this.rootNode;
|
||||
|
||||
this.body.appendChild(this.treeWrapper);
|
||||
|
||||
frappe.ui.on(this.treeWrapper, 'tree-node-expand', 'f-tree-node', async (e, treeNode) => {
|
||||
if (!treeNode.expanded) return;
|
||||
|
||||
if (!treeNode.props.children) {
|
||||
const data = await this.getData(treeNode.props);
|
||||
const children = data.map(d => ({
|
||||
label: d.name,
|
||||
value: d.name,
|
||||
isGroup: d.isGroup,
|
||||
doc: d
|
||||
}));
|
||||
treeNode.props.children = children;
|
||||
|
||||
for (let child of children) {
|
||||
const childNode = frappe.ui.create(this.getTreeNodeHTML(child));
|
||||
childNode.props = child;
|
||||
treeNode.appendChild(childNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.on(this.treeWrapper, 'tree-node-action', 'f-tree-node', (e, treeNode) => {
|
||||
if (treeNode.isRoot) return;
|
||||
|
||||
const button = e.detail.actionEl;
|
||||
const action = button.getAttribute('data-action');
|
||||
|
||||
if (action === 'edit') {
|
||||
this.edit(treeNode.props.doc.name);
|
||||
} else if (action === 'addChild') {
|
||||
this.addChildNode(treeNode.props.doc.name);
|
||||
}
|
||||
});
|
||||
|
||||
rootNode.click(); // open the root node
|
||||
}
|
||||
|
||||
edit(name) {
|
||||
frappe.desk.showFormModal(this.doctype, name);
|
||||
}
|
||||
|
||||
async addChildNode(name) {
|
||||
const newDoc = await frappe.getNewDoc(this.doctype);
|
||||
const formModal = await frappe.desk.showFormModal(this.doctype, newDoc.name);
|
||||
const parentField = this.treeSettings.parentField;
|
||||
if (formModal.form.doc.meta.hasField(parentField)) {
|
||||
formModal.form.doc.set(parentField, name);
|
||||
}
|
||||
}
|
||||
|
||||
async getData(node) {
|
||||
let fields = this.getFields();
|
||||
let filters = {};
|
||||
|
||||
if (node.isRoot) {
|
||||
filters[this.treeSettings.parentField] = '';
|
||||
} else {
|
||||
filters[this.treeSettings.parentField] = node.value;
|
||||
}
|
||||
|
||||
return await frappe.db.getAll({
|
||||
doctype: this.doctype,
|
||||
fields,
|
||||
filters,
|
||||
order_by: 'name',
|
||||
order: 'asc'
|
||||
});
|
||||
}
|
||||
|
||||
getTreeNodeHTML(node) {
|
||||
return (
|
||||
`<f-tree-node
|
||||
label="${node.label}"
|
||||
value="${node.value}"
|
||||
${node.expanded ? 'expanded' : ''}
|
||||
${node.isRoot ? 'is-root' : ''}
|
||||
${node.isGroup ? '' : 'is-leaf'}
|
||||
>
|
||||
${this.getActionButtonsHTML()}
|
||||
</f-tree-node>`
|
||||
);
|
||||
}
|
||||
|
||||
getActionButtonsHTML() {
|
||||
return [
|
||||
{ id: 'edit', label: frappe._('Edit') },
|
||||
{ id: 'addChild', label: frappe._('Add Child') },
|
||||
// { id: 'delete', label: frappe._('Delete') },
|
||||
].map(button => {
|
||||
return `<button class="btn btn-link btn-sm m-0" slot="actions" data-action="${button.id}">
|
||||
${button.label}
|
||||
</button>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
getFields() {
|
||||
let fields = [this.treeSettings.parentField, 'isGroup']
|
||||
this.updateStandardFields(fields);
|
||||
return fields;
|
||||
}
|
||||
};
|
7
index.js
7
index.js
@ -56,8 +56,11 @@ module.exports = {
|
||||
if (this.app) {
|
||||
// add to router if client-server
|
||||
this.app.post(`/api/method/${method}`, this.asyncHandler(async function(request, response) {
|
||||
const data = await handler(request.body);
|
||||
response.json(data);
|
||||
let data = await handler(request.body);
|
||||
if (data === undefined) {
|
||||
data = {}
|
||||
}
|
||||
return response.json(data);
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
@ -52,27 +52,27 @@ module.exports = class BaseDocument extends Observable {
|
||||
async applyChange(fieldname) {
|
||||
if (await this.applyFormula()) {
|
||||
// multiple changes
|
||||
await this.trigger('change', { doc: this });
|
||||
await this.trigger('change', {
|
||||
doc: this
|
||||
});
|
||||
} else {
|
||||
// no other change, trigger control refresh
|
||||
await this.trigger('change', { doc: this, fieldname: fieldname });
|
||||
await this.trigger('change', {
|
||||
doc: this,
|
||||
fieldname: fieldname
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setDefaults() {
|
||||
for (let field of this.meta.fields) {
|
||||
if (this[field.fieldname]===null || this[field.fieldname]===undefined) {
|
||||
if (this[field.fieldname] === null || this[field.fieldname] === undefined) {
|
||||
|
||||
let defaultValue = null;
|
||||
|
||||
if (field.fieldtype === 'Date') {
|
||||
defaultValue = (new Date()).toISOString().substr(0, 10);
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Table') {
|
||||
defaultValue = [];
|
||||
}
|
||||
|
||||
if (field.default) {
|
||||
defaultValue = field.default;
|
||||
}
|
||||
@ -165,12 +165,14 @@ module.exports = class BaseDocument extends Observable {
|
||||
this.clearValues();
|
||||
Object.assign(this, data);
|
||||
this._dirty = false;
|
||||
this.trigger('change', {doc: this});
|
||||
this.trigger('change', {
|
||||
doc: this
|
||||
});
|
||||
}
|
||||
|
||||
clearValues() {
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
if(this[field.fieldname]) {
|
||||
if (this[field.fieldname]) {
|
||||
delete this[field.fieldname];
|
||||
}
|
||||
}
|
||||
@ -179,8 +181,8 @@ module.exports = class BaseDocument extends Observable {
|
||||
setChildIdx() {
|
||||
// renumber children
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
if (field.fieldtype==='Table') {
|
||||
for(let i=0; i < (this[field.fieldname] || []).length; i++) {
|
||||
if (field.fieldtype === 'Table') {
|
||||
for (let i = 0; i < (this[field.fieldname] || []).length; i++) {
|
||||
this[field.fieldname][i].idx = i;
|
||||
}
|
||||
}
|
||||
@ -188,7 +190,7 @@ module.exports = class BaseDocument extends Observable {
|
||||
}
|
||||
|
||||
async compareWithCurrentDoc() {
|
||||
if (frappe.isServer && !this._notInserted) {
|
||||
if (frappe.isServer && !this.isNew()) {
|
||||
let currentDoc = await frappe.db.get(this.doctype, this.name);
|
||||
|
||||
// check for conflict
|
||||
@ -227,12 +229,12 @@ module.exports = class BaseDocument extends Observable {
|
||||
// for each row
|
||||
for (let row of this[tablefield.fieldname]) {
|
||||
for (let field of formulaFields) {
|
||||
if (shouldApplyFormula(field, row)) {
|
||||
const val = await field.formula(row, doc);
|
||||
if (val !== false && val !== undefined) {
|
||||
row[field.fieldname] = val;
|
||||
if (shouldApplyFormula(field, row)) {
|
||||
const val = await field.formula(row, doc);
|
||||
if (val !== false && val !== undefined) {
|
||||
row[field.fieldname] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -240,27 +242,27 @@ module.exports = class BaseDocument extends Observable {
|
||||
|
||||
// parent
|
||||
for (let field of this.meta.getFormulaFields()) {
|
||||
if (shouldApplyFormula(field, doc)) {
|
||||
const val = await field.formula(doc);
|
||||
if (val !== false && val !== undefined) {
|
||||
doc[field.fieldname] = val;
|
||||
if (shouldApplyFormula(field, doc)) {
|
||||
const val = await field.formula(doc);
|
||||
if (val !== false && val !== undefined) {
|
||||
doc[field.fieldname] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
function shouldApplyFormula (field, doc) {
|
||||
if (field.readOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!frappe.isServer) {
|
||||
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
|
||||
return true;
|
||||
function shouldApplyFormula(field, doc) {
|
||||
if (field.readOnly) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
if (!frappe.isServer || frappe.isElectron) {
|
||||
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,4 +349,8 @@ module.exports = class BaseDocument extends Observable {
|
||||
}
|
||||
return _values[fieldname];
|
||||
}
|
||||
};
|
||||
|
||||
isNew() {
|
||||
return this._notInserted;
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frappejs",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.10",
|
||||
"description": "Frappe.js",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
@ -20,10 +20,13 @@
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"codemirror": "^5.35.0",
|
||||
"commander": "^2.13.0",
|
||||
"copy-webpack-plugin": "^4.5.4",
|
||||
"cors": "^2.8.4",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"deepmerge": "^2.1.0",
|
||||
"electron": "2.0.5",
|
||||
"electron": "2.0.12",
|
||||
"electron-builder": "^20.28.4",
|
||||
"electron-debug": "^2.0.0",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"express": "^4.16.2",
|
||||
@ -51,7 +54,7 @@
|
||||
"sharp": "^0.20.8",
|
||||
"showdown": "^1.8.6",
|
||||
"socket.io": "^2.0.4",
|
||||
"sqlite3": "^3.1.13",
|
||||
"sqlite3": "^4.0.2",
|
||||
"vue": "^2.5.16",
|
||||
"vue-flatpickr-component": "^7.0.4",
|
||||
"vue-loader": "^15.2.6",
|
||||
|
@ -10,6 +10,9 @@ async function makePDF(html, filepath) {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html);
|
||||
await page.addStyleTag({
|
||||
url: 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css'
|
||||
})
|
||||
await page.pdf({
|
||||
path: filepath,
|
||||
format: 'A4'
|
||||
@ -17,12 +20,49 @@ async function makePDF(html, filepath) {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
async function getPDFForElectron(doctype, name) {
|
||||
const { shell } = require('electron');
|
||||
const html = await getHTML(doctype, name);
|
||||
const filepath = path.join(frappe.electronSettings.directory, name + '.pdf');
|
||||
await makePDF(html, filepath);
|
||||
shell.openItem(filepath);
|
||||
async function getPDFForElectron(doctype, name, destination, htmlContent) {
|
||||
const { remote, shell } = require('electron');
|
||||
const { BrowserWindow } = remote;
|
||||
const html = htmlContent || await getHTML(doctype, name);
|
||||
const filepath = path.join(destination, name + '.pdf');
|
||||
|
||||
const fs = require('fs')
|
||||
let printWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 800,
|
||||
show: false
|
||||
})
|
||||
printWindow.loadURL(`file://${path.join(__static, 'print.html')}`);
|
||||
|
||||
printWindow.on('closed', () => {
|
||||
printWindow = null;
|
||||
});
|
||||
|
||||
const code = `
|
||||
document.body.innerHTML = \`${html}\`;
|
||||
`;
|
||||
|
||||
printWindow.webContents.executeJavaScript(code);
|
||||
|
||||
const printPromise = new Promise(resolve => {
|
||||
printWindow.webContents.on('did-finish-load', () => {
|
||||
printWindow.webContents.printToPDF({
|
||||
marginsType: 1, // no margin
|
||||
pageSize: 'A4',
|
||||
printBackground: true
|
||||
}, (error, data) => {
|
||||
if (error) throw error
|
||||
printWindow.close();
|
||||
fs.writeFile(filepath, data, (error) => {
|
||||
if (error) throw error
|
||||
resolve(shell.openItem(filepath));
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await printPromise;
|
||||
// await makePDF(html, filepath);
|
||||
}
|
||||
|
||||
function setupExpressRoute() {
|
||||
|
BIN
test.db-journal
BIN
test.db-journal
Binary file not shown.
@ -40,7 +40,7 @@ export default {
|
||||
</script>
|
||||
<style>
|
||||
.feather-icon {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -56,7 +56,7 @@ export default {
|
||||
try {
|
||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
||||
|
||||
if (this.doc._notInserted && this.meta.fields.map(df => df.fieldname).includes('name')) {
|
||||
if (this.doc.isNew() && this.meta.fields.map(df => df.fieldname).includes('name')) {
|
||||
// For a user editable name field,
|
||||
// it should be unset since it is autogenerated
|
||||
this.doc.set('name', '');
|
||||
@ -82,7 +82,7 @@ export default {
|
||||
if (this.invalid) return;
|
||||
|
||||
try {
|
||||
if (this.doc._notInserted) {
|
||||
if (this.doc.isNew()) {
|
||||
await this.doc.insert();
|
||||
} else {
|
||||
await this.doc.update();
|
||||
|
@ -5,7 +5,7 @@
|
||||
<f-button primary v-if="showSave" :disabled="disableSave" @click="$emit('save')">{{ _('Save') }}</f-button>
|
||||
<f-button primary v-if="showSubmit" @click="$emit('submit')">{{ _('Submit') }}</f-button>
|
||||
<f-button secondary v-if="showRevert" @click="$emit('revert')">{{ _('Revert') }}</f-button>
|
||||
<div class="ml-2">
|
||||
<div class="ml-2" v-if="showPrint">
|
||||
<f-button secondary v-if="showNextAction" @click="$emit('print')">{{ _('Print') }}</f-button>
|
||||
</div>
|
||||
<dropdown class="ml-2" v-if="showNextAction" :label="_('Actions')" :options="links"></dropdown>
|
||||
@ -29,45 +29,51 @@ export default {
|
||||
showSubmit: false,
|
||||
showRevert: false,
|
||||
showNextAction: false,
|
||||
showPrint: false,
|
||||
disableSave: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.doc.on('change', () => {
|
||||
this.isDirty = this.doc._dirty;
|
||||
this.updateShowSubmittable();
|
||||
});
|
||||
this.updateShowSubmittable();
|
||||
},
|
||||
methods: {
|
||||
updateShowSubmittable() {
|
||||
this.isDirty = this.doc._dirty;
|
||||
|
||||
this.showSubmit =
|
||||
this.meta.isSubmittable
|
||||
&& !this.isDirty
|
||||
&& !this.doc._notInserted
|
||||
&& !this.doc.isNew()
|
||||
&& this.doc.submitted === 0;
|
||||
|
||||
this.showRevert =
|
||||
this.meta.isSubmittable
|
||||
&& !this.isDirty
|
||||
&& !this.doc._notInserted
|
||||
&& !this.doc.isNew()
|
||||
&& this.doc.submitted === 1;
|
||||
|
||||
this.showNextAction = 1
|
||||
|
||||
this.showNextAction =
|
||||
!this.doc._notInserted
|
||||
!this.doc.isNew()
|
||||
&& this.links.length;
|
||||
|
||||
this.showPrint =
|
||||
this.doc.submitted === 1
|
||||
&& this.meta.print
|
||||
|
||||
this.showSave =
|
||||
this.doc._notInserted ?
|
||||
this.doc.isNew() ?
|
||||
true :
|
||||
this.meta.isSubmittable ?
|
||||
(this.isDirty ? true : false) :
|
||||
true;
|
||||
|
||||
this.disableSave =
|
||||
this.doc._notInserted ? false : !this.isDirty;
|
||||
this.doc.isNew() ? false : !this.isDirty;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -77,7 +83,7 @@ export default {
|
||||
title() {
|
||||
const _ = this._;
|
||||
|
||||
if (this.doc._notInserted) {
|
||||
if (this.doc.isNew()) {
|
||||
return _('New {0}', _(this.doc.doctype));
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fieldname === 'name' && !this.doc._notInserted) {
|
||||
if (fieldname === 'name' && !this.doc.isNew()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -66,6 +66,10 @@ export default {
|
||||
},
|
||||
updateDoc(fieldname, value) {
|
||||
this.doc.set(fieldname, value);
|
||||
this.$emit('updateDoc', {
|
||||
fieldname,
|
||||
value
|
||||
});
|
||||
},
|
||||
showSection(i) {
|
||||
if (this.layoutConfig.paginated) {
|
||||
|
@ -8,7 +8,7 @@
|
||||
class="form-control shadow-none w-100"
|
||||
:placeholder="_('Search...')">
|
||||
</form>
|
||||
<div class="navbar-text">.</div>
|
||||
<div class="navbar-text"> </div>
|
||||
</nav>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -35,7 +35,13 @@ export default {
|
||||
},
|
||||
|
||||
getPDF() {
|
||||
frappe.getPDF(this.doctype, this.name);
|
||||
frappe.call({
|
||||
method: 'print-pdf',
|
||||
args: {
|
||||
doctype: this.doctype,
|
||||
name: this.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="tree-node">
|
||||
<div class="tree-label px-3 py-2" @click.self="toggleChildren">
|
||||
<div @click="toggleChildren">
|
||||
<feather-icon :name="iconName" v-show="iconName" />
|
||||
<div class="d-flex align-items-center" @click="toggleChildren">
|
||||
<feather-icon class="mr-1" :name="iconName" v-show="iconName" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -43,9 +43,9 @@ const TreeNode = {
|
||||
},
|
||||
async getChildren() {
|
||||
if (this.children) return;
|
||||
|
||||
|
||||
this.children = [];
|
||||
|
||||
|
||||
let filters = {
|
||||
[this.settings.parentField]: this.parentValue
|
||||
};
|
||||
|
@ -13,6 +13,13 @@ export default {
|
||||
render(h) {
|
||||
return this.getWrapperElement(h);
|
||||
},
|
||||
watch: {
|
||||
// prop change does not change the value of input
|
||||
// this only happens for Autocomplete
|
||||
value(newValue) {
|
||||
this.$refs.input.value = newValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInputListeners() {
|
||||
return {
|
||||
@ -50,12 +57,16 @@ export default {
|
||||
if (this.highlightedItem > this.popupItems.length - 1) {
|
||||
this.highlightedItem = this.popupItems.length - 1;
|
||||
}
|
||||
|
||||
this.scrollToItem(this.highlightedItem);
|
||||
},
|
||||
highlightAboveItem() {
|
||||
this.highlightedItem -= 1;
|
||||
if (this.highlightedItem < 0) {
|
||||
this.highlightedItem = 0;
|
||||
}
|
||||
|
||||
this.scrollToItem(this.highlightedItem);
|
||||
},
|
||||
getChildrenElement(h) {
|
||||
return [
|
||||
@ -66,7 +77,8 @@ export default {
|
||||
},
|
||||
getDropdownElement(h) {
|
||||
return h('div', {
|
||||
class: ['dropdown-menu w-100', this.popupOpen ? 'show' : '']
|
||||
class: ['dropdown-menu w-100', this.popupOpen ? 'show' : ''],
|
||||
ref: 'dropdown-menu'
|
||||
}, this.getDropdownItems(h));
|
||||
},
|
||||
getDropdownItems(h) {
|
||||
@ -77,6 +89,7 @@ export default {
|
||||
href: '#',
|
||||
'data-value': item.value
|
||||
},
|
||||
ref: i,
|
||||
on: {
|
||||
click: e => {
|
||||
e.preventDefault();
|
||||
@ -93,6 +106,10 @@ export default {
|
||||
onItemClick(item) {
|
||||
this.handleChange(item.value);
|
||||
},
|
||||
scrollToItem(i) {
|
||||
const scrollTo = this.$refs[i].offsetTop - 5;
|
||||
this.$refs['dropdown-menu'].scrollTop = scrollTo;
|
||||
},
|
||||
async updateList(keyword) {
|
||||
this.popupItems = await this.getList(keyword);
|
||||
this.popupOpen = this.popupItems.length > 0;
|
||||
@ -117,3 +134,9 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.form-group[data-fieldtype="Link"] .dropdown-menu {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
@ -70,7 +70,14 @@ export default {
|
||||
|
||||
return Boolean(disabled);
|
||||
}
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
dynamicLinkTarget: reference => {
|
||||
return this.doc[reference];
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
|
@ -8,21 +8,24 @@ import { _ } from 'frappejs/utils';
|
||||
|
||||
export default {
|
||||
extends: Autocomplete,
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.$refs.input.value = newValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getList(query) {
|
||||
let filters = this.docfield.getFilters ?
|
||||
this.docfield.getFilters(query) :
|
||||
null;
|
||||
|
||||
if (query) {
|
||||
if (!filters) filters = {};
|
||||
filters.keywords = ['like', query];
|
||||
}
|
||||
|
||||
let target = this.getTarget();
|
||||
let titleField = frappe.getMeta(target).titleField;
|
||||
|
||||
const list = await frappe.db.getAll({
|
||||
doctype: this.getTarget(),
|
||||
filters: query
|
||||
? {
|
||||
keywords: ['like', query]
|
||||
}
|
||||
: null,
|
||||
fields: ['name'],
|
||||
doctype: target,
|
||||
filters,
|
||||
fields: ['name', titleField],
|
||||
limit: 50
|
||||
});
|
||||
|
||||
@ -34,7 +37,7 @@ export default {
|
||||
|
||||
return list
|
||||
.map(d => ({
|
||||
label: d.name,
|
||||
label: d[titleField],
|
||||
value: d.name
|
||||
}))
|
||||
.concat({
|
||||
|
@ -4,7 +4,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" width="60">
|
||||
<input class="mr-2" type="checkbox">
|
||||
<input class="mr-2" type="checkbox" @change="toggleCheckAll">
|
||||
<span>#</span>
|
||||
</th>
|
||||
<th scope="col" v-for="column in columns" :key="column.fieldname">
|
||||
@ -15,35 +15,51 @@
|
||||
<tbody v-if="rows.length">
|
||||
<tr v-for="(row, i) in rows" :key="i">
|
||||
<th scope="row">
|
||||
<input class="mr-2" type="checkbox" @change="e => onCheck(e, i)">
|
||||
<input
|
||||
class="mr-2"
|
||||
type="checkbox"
|
||||
:checked="checkedRows.includes(i)"
|
||||
@change="e => onCheck(e, i)"
|
||||
>
|
||||
<span>{{ i + 1 }}</span>
|
||||
</th>
|
||||
<td v-for="column in columns" :key="column.fieldname"
|
||||
tabindex="1"
|
||||
:ref="column.fieldname + i"
|
||||
@click="activateFocus(i, column.fieldname)"
|
||||
@dblclick="activateEditing(i, column.fieldname)"
|
||||
@click="deactivateEditing(i, column.fieldname)"
|
||||
@keydown.shift.tab="shiftTabPressOnCell(i, column.fieldname)"
|
||||
@keydown.tab="tabPressOnCell(i, column.fieldname)"
|
||||
@keydown.enter="enterPressOnCell(i, column.fieldname)"
|
||||
@keydown.enter="enterPressOnCell()"
|
||||
@keydown.tab.exact.prevent="focusNextCell()"
|
||||
@keydown.shift.tab.exact.prevent="focusPreviousCell()"
|
||||
@keydown.left="focusPreviousCell()"
|
||||
@keydown.right="focusNextCell()"
|
||||
@keydown.up="focusAboveCell(i, column.fieldname)"
|
||||
@keydown.down="focusBelowCell(i, column.fieldname)"
|
||||
@keydown.esc="escOnCell(i, column.fieldname)"
|
||||
>
|
||||
<frappe-control
|
||||
v-if="isEditing(i, column.fieldname)"
|
||||
:docfield="getDocfield(column.fieldname)"
|
||||
:value="row[column.fieldname]"
|
||||
:onlyInput="true"
|
||||
:doc="row"
|
||||
:autofocus="true"
|
||||
@change="onCellChange(i, column.fieldname, $event)"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ row[column.fieldname] }}
|
||||
</span>
|
||||
<div class="table-cell" :class="{'active': isFocused(i, column.fieldname)}">
|
||||
<frappe-control
|
||||
v-if="isEditing(i, column.fieldname)"
|
||||
:docfield="getDocfield(column.fieldname)"
|
||||
:value="row[column.fieldname]"
|
||||
:onlyInput="true"
|
||||
:doc="row"
|
||||
:autofocus="true"
|
||||
@change="onCellChange(i, column.fieldname, $event)"
|
||||
/>
|
||||
<div class="text-truncate" v-else>
|
||||
{{ row[column.fieldname] || ' ' }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td :colspan="columns.length + 1" class="text-center">
|
||||
No Data
|
||||
<div class="table-cell">
|
||||
No Data
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -56,7 +72,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import ModelTable from '../ModelTable';
|
||||
import Base from './Base';
|
||||
import Observable from 'frappejs/utils/observable';
|
||||
|
||||
@ -66,30 +81,83 @@ export default {
|
||||
return {
|
||||
columns: [],
|
||||
checkedRows: [],
|
||||
currentlyEditing: {}
|
||||
}
|
||||
currentlyEditing: {},
|
||||
currentlyFocused: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.columns = this.getColumns();
|
||||
},
|
||||
methods: {
|
||||
enterPressOnCell(i, fieldname) {
|
||||
escOnCell(i, fieldname) {
|
||||
this.deactivateEditing();
|
||||
this.activateFocus(i, fieldname);
|
||||
},
|
||||
shiftTabPressOnCell(i, fieldname) {
|
||||
if (this.isEditing(i, fieldname)) {
|
||||
let pos = this.columns.map(c => c.fieldname).indexOf(fieldname);
|
||||
pos = pos - 1;
|
||||
this.activateEditing(i, this.columns[pos].fieldname);
|
||||
enterPressOnCell() {
|
||||
const { index, fieldname } = this.currentlyFocused;
|
||||
if (this.isEditing(index, fieldname)) {
|
||||
this.deactivateEditing();
|
||||
this.activateFocus(index, fieldname);
|
||||
} else {
|
||||
this.activateEditing(index, fieldname);
|
||||
}
|
||||
},
|
||||
tabPressOnCell(i, fieldname) {
|
||||
if (this.isEditing(i, fieldname)) {
|
||||
let pos = this.columns.map(c => c.fieldname).indexOf(fieldname);
|
||||
pos = pos + 1;
|
||||
this.activateEditing(i, this.columns[pos].fieldname);
|
||||
focusPreviousCell() {
|
||||
let { index, fieldname } = this.currentlyFocused;
|
||||
if (this.isFocused(index, fieldname) && !this.isEditing(index, fieldname)) {
|
||||
let pos = this._getColumnIndex(fieldname);
|
||||
pos -= 1;
|
||||
if (pos < 0) {
|
||||
index -= 1;
|
||||
pos = this.columns.length - 1;
|
||||
}
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
pos = 0;
|
||||
}
|
||||
this.activateFocus(index, this.columns[pos].fieldname);
|
||||
}
|
||||
},
|
||||
focusNextCell() {
|
||||
let { index, fieldname } = this.currentlyFocused;
|
||||
if (this.isFocused(index, fieldname) && !this.isEditing(index, fieldname)) {
|
||||
|
||||
let pos = this._getColumnIndex(fieldname);
|
||||
pos += 1;
|
||||
if (pos > this.columns.length - 1) {
|
||||
index += 1;
|
||||
pos = 0;
|
||||
}
|
||||
if (index > this.rows.length - 1) {
|
||||
index = this.rows.length - 1;
|
||||
pos = this.columns.length - 1;
|
||||
}
|
||||
this.activateFocus(index, this.columns[pos].fieldname);
|
||||
}
|
||||
},
|
||||
focusAboveCell(i, fieldname) {
|
||||
if (this.isFocused(i, fieldname) && !this.isEditing(i, fieldname)) {
|
||||
let pos = this._getColumnIndex(fieldname);
|
||||
i -= 1;
|
||||
if (i < 0) {
|
||||
i = 0;
|
||||
}
|
||||
this.activateFocus(i, this.columns[pos].fieldname);
|
||||
}
|
||||
},
|
||||
focusBelowCell(i, fieldname) {
|
||||
if (this.isFocused(i, fieldname) && !this.isEditing(i, fieldname)) {
|
||||
let pos = this._getColumnIndex(fieldname);
|
||||
i += 1;
|
||||
if (i > this.rows.length - 1) {
|
||||
i = this.rows.length - 1;
|
||||
}
|
||||
this.activateFocus(i, this.columns[pos].fieldname);
|
||||
}
|
||||
},
|
||||
_getColumnIndex(fieldname) {
|
||||
return this.columns.map(c => c.fieldname).indexOf(fieldname);
|
||||
},
|
||||
onOutsideClick(e) {
|
||||
this.deactivateEditing();
|
||||
},
|
||||
@ -100,6 +168,13 @@ export default {
|
||||
this.checkedRows = this.checkedRows.filter(i => i !== idx);
|
||||
}
|
||||
},
|
||||
toggleCheckAll() {
|
||||
if (this.checkedRows.length === this.rows.length) {
|
||||
this.checkedRows = [];
|
||||
} else {
|
||||
this.checkedRows = this.rows.map((row, i) => i);
|
||||
}
|
||||
},
|
||||
getDocfield(fieldname) {
|
||||
return this.meta.getField(fieldname);
|
||||
},
|
||||
@ -107,8 +182,16 @@ export default {
|
||||
if (this.disabled) {
|
||||
return false;
|
||||
}
|
||||
return this.currentlyEditing.index === i &&
|
||||
this.currentlyEditing.fieldname === fieldname;
|
||||
return (
|
||||
this.currentlyEditing.index === i &&
|
||||
this.currentlyEditing.fieldname === fieldname
|
||||
);
|
||||
},
|
||||
isFocused(i, fieldname) {
|
||||
return (
|
||||
this.currentlyFocused.index === i &&
|
||||
this.currentlyFocused.fieldname === fieldname
|
||||
);
|
||||
},
|
||||
activateEditing(i, fieldname) {
|
||||
const docfield = this.columns.find(c => c.fieldname === fieldname);
|
||||
@ -120,12 +203,27 @@ export default {
|
||||
fieldname
|
||||
};
|
||||
},
|
||||
activateFocus(i, fieldname) {
|
||||
this.deactivateEditing();
|
||||
const docfield = this.columns.find(c => c.fieldname === fieldname);
|
||||
this.currentlyFocused = {
|
||||
index: i,
|
||||
fieldname
|
||||
};
|
||||
this.$refs[fieldname + i][0].focus();
|
||||
},
|
||||
deactivateEditing(i, _fieldname) {
|
||||
const { index, fieldname } = this.currentlyEditing;
|
||||
if (!(index === i && fieldname === _fieldname)) {
|
||||
this.currentlyEditing = {};
|
||||
}
|
||||
},
|
||||
deactivateFocus(i, _fieldname) {
|
||||
const { index, fieldname } = this.currentlyFocused;
|
||||
if (!(index === i && fieldname === _fieldname)) {
|
||||
this.currentlyFocused = {};
|
||||
}
|
||||
},
|
||||
addRow() {
|
||||
const rows = this.rows.slice();
|
||||
const newRow = {
|
||||
@ -154,7 +252,7 @@ export default {
|
||||
// make a copy
|
||||
let rows = this.rows.slice();
|
||||
rows = rows.filter((row, i) => {
|
||||
return !indices.includes(i)
|
||||
return !indices.includes(i);
|
||||
});
|
||||
|
||||
this.emitChange(rows);
|
||||
@ -190,17 +288,38 @@ export default {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.table .form-control {
|
||||
<style lang="scss" scoped>
|
||||
td {
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active {
|
||||
border: 1px solid var(--blue);
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.table [data-fieldtype="Link"] .input-group-append {
|
||||
.form-group /deep/ .form-control {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-fieldtype='Link'] .input-group-append {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="row pb-4">
|
||||
<frappe-control class="col-lg col-md-3 col-sm-6"
|
||||
<frappe-control class="col-4"
|
||||
v-for="docfield in filters"
|
||||
:key="docfield.fieldname"
|
||||
:docfield="docfield"
|
||||
:value="$data.filterValues[docfield.fieldname]"
|
||||
:doc="$data.filterValues"
|
||||
@change="updateValue(docfield.fieldname, $event)"/>
|
||||
</div>
|
||||
</template>
|
||||
@ -30,13 +31,6 @@ export default {
|
||||
this.$emit('change', this.filterValues);
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
dynamicLinkTarget: reference => {
|
||||
return this.filterValues[reference];
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateValue(fieldname, value) {
|
||||
this.filterValues[fieldname] = value;
|
||||
|
@ -18,11 +18,6 @@ export default {
|
||||
name: 'Report',
|
||||
props: ['reportName', 'reportConfig', 'filters'],
|
||||
computed: {
|
||||
reportColumns() {
|
||||
return utils.convertFieldsToDatatableColumns(
|
||||
this.reportConfig.getColumns()
|
||||
);
|
||||
},
|
||||
filtersExists() {
|
||||
return (this.reportConfig.filterFields || []).length;
|
||||
}
|
||||
@ -42,7 +37,7 @@ export default {
|
||||
}
|
||||
|
||||
if (data.columns) {
|
||||
columns = data.columns;
|
||||
columns = this.getColumns(data);
|
||||
}
|
||||
|
||||
if (!rows) {
|
||||
@ -50,7 +45,7 @@ export default {
|
||||
}
|
||||
|
||||
if (!columns) {
|
||||
columns = this.reportColumns;
|
||||
columns = this.getColumns();
|
||||
}
|
||||
|
||||
for(let column of columns) {
|
||||
@ -65,6 +60,10 @@ export default {
|
||||
data: rows
|
||||
});
|
||||
}
|
||||
},
|
||||
getColumns(data) {
|
||||
const columns = this.reportConfig.getColumns(data);
|
||||
return utils.convertFieldsToDatatableColumns(columns);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -8,7 +8,7 @@ export default function installFormModal(Vue) {
|
||||
let id;
|
||||
|
||||
const open = (doc, options = {}) => {
|
||||
const { defaultValues = null, onClose = null } = options;
|
||||
const { defaultValues = null, onClose = () => {} } = options;
|
||||
id = this.$modal.show({
|
||||
component: Form,
|
||||
props: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
const numberFormat = require('./numberFormat');
|
||||
const markdown = new (require('showdown').Converter)();
|
||||
// const markdown = new (require('showdown').Converter)();
|
||||
const luxon = require('luxon');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
@ -13,7 +13,7 @@ module.exports = {
|
||||
value = numberFormat.formatNumber(value);
|
||||
|
||||
} else if (field.fieldtype === 'Text') {
|
||||
value = markdown.makeHtml(value || '');
|
||||
// value = markdown.makeHtml(value || '');
|
||||
|
||||
} else if (field.fieldtype === 'Date') {
|
||||
let dateFormat;
|
||||
|
@ -15,7 +15,10 @@ module.exports = class Observable {
|
||||
|
||||
set(key, value) {
|
||||
this[key] = value;
|
||||
this.trigger('change', {doc: this, fieldname: key});
|
||||
this.trigger('change', {
|
||||
doc: this,
|
||||
fieldname: key
|
||||
});
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
@ -39,7 +42,7 @@ module.exports = class Observable {
|
||||
this._addListener('onceListeners', event, listener);
|
||||
}
|
||||
|
||||
async trigger(event, params, throttle=false) {
|
||||
async trigger(event, params, throttle = false) {
|
||||
if (throttle) {
|
||||
if (this._throttled(event, params, throttle)) return;
|
||||
params = [params]
|
||||
|
58
webpack/build.js
Normal file
58
webpack/build.js
Normal file
@ -0,0 +1,58 @@
|
||||
const webpack = require('webpack');
|
||||
const { getConfig, getElectronMainConfig } = require('./config');
|
||||
|
||||
module.exports = function build(mode) {
|
||||
const rendererConfig = getConfig();
|
||||
const mainConfig = getElectronMainConfig();
|
||||
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
if (mode === 'electron') {
|
||||
pack(rendererConfig)
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
}).catch(err => {
|
||||
console.log(`\n Failed to build renderer process`);
|
||||
console.error(`\n${err}\n`);
|
||||
process.exit(1)
|
||||
});
|
||||
|
||||
pack(mainConfig)
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
}).catch(err => {
|
||||
console.log(`\n Failed to build main process`);
|
||||
console.error(`\n${err}\n`);
|
||||
process.exit(1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pack(config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
webpack(config, (err, stats) => {
|
||||
if (err) reject(err.stack || err)
|
||||
|
||||
else if (stats.hasErrors()) {
|
||||
let err = ''
|
||||
|
||||
stats
|
||||
.toString({
|
||||
chunks: false,
|
||||
colors: true
|
||||
})
|
||||
.split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
err += ` ${line}\n`
|
||||
});
|
||||
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(stats.toString({
|
||||
chunks: false,
|
||||
colors: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -1,121 +1,213 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// plugins
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CaseSensitivePathsWebpackPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const { getAppConfig, resolveAppDir } = require('./utils');
|
||||
const appDependencies = require(resolveAppDir('./package.json')).dependencies;
|
||||
const frappeDependencies = require('../package.json').dependencies;
|
||||
|
||||
const plugins = {
|
||||
NamedModules: webpack.NamedModulesPlugin,
|
||||
HotModuleReplacement: webpack.HotModuleReplacementPlugin,
|
||||
Define: webpack.DefinePlugin,
|
||||
Progress: webpack.ProgressPlugin,
|
||||
VueLoader: require('vue-loader/lib/plugin'),
|
||||
Html: require('html-webpack-plugin'),
|
||||
CaseSensitivePaths: require('case-sensitive-paths-webpack-plugin'),
|
||||
FriendlyErrors: require('friendly-errors-webpack-plugin'),
|
||||
}
|
||||
let getConfig, getElectronMainConfig;
|
||||
|
||||
const appConfig = getAppConfig();
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
function makeConfig() {
|
||||
const appConfig = getAppConfig();
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const isElectron = process.env.ELECTRON === 'true';
|
||||
const isMonoRepo = process.env.MONO_REPO === 'true';
|
||||
|
||||
function getConfig() {
|
||||
const whiteListedModules = ['vue'];
|
||||
const allDependencies = Object.assign(frappeDependencies, appDependencies);
|
||||
const externals = Object.keys(allDependencies).filter(
|
||||
d => !whiteListedModules.includes(d)
|
||||
);
|
||||
|
||||
getConfig = function getConfig() {
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
context: resolveAppDir(),
|
||||
entry: appConfig.dev.entry,
|
||||
output: {
|
||||
path: path.resolve(appConfig.dev.outputDir),
|
||||
filename: '[name].js',
|
||||
publicPath: appConfig.dev.assetsPublicPath
|
||||
},
|
||||
devtool: 'cheap-module-eval-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: file => (
|
||||
/node_modules/.test(file) &&
|
||||
!/\.vue\.js/.test(file)
|
||||
)
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif)$/,
|
||||
use: [
|
||||
'file-loader'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'deepmerge$': 'deepmerge/dist/umd.js',
|
||||
'@': appConfig.dev.srcDir ? resolveAppDir(appConfig.dev.srcDir) : null
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new plugins.Define({
|
||||
'process.env': appConfig.dev.env
|
||||
}),
|
||||
new plugins.VueLoader(),
|
||||
new plugins.Html({
|
||||
template: resolveAppDir(appConfig.dev.entryHtml)
|
||||
}),
|
||||
new plugins.CaseSensitivePaths(),
|
||||
new plugins.NamedModules(),
|
||||
new plugins.HotModuleReplacement(),
|
||||
new plugins.FriendlyErrors({
|
||||
compilationSuccessInfo: {
|
||||
messages: [`FrappeJS server started at http://${appConfig.dev.devServerHost}:${appConfig.dev.devServerPort}`],
|
||||
},
|
||||
}),
|
||||
new plugins.Progress()
|
||||
],
|
||||
optimization: {
|
||||
noEmitOnErrors: false
|
||||
},
|
||||
devServer: {
|
||||
// contentBase: './dist', // dist path is directly configured in express
|
||||
hot: true,
|
||||
quiet: true
|
||||
},
|
||||
node: {
|
||||
// prevent webpack from injecting useless setImmediate polyfill because Vue
|
||||
// source contains it (although only uses it if it's native).
|
||||
setImmediate: false,
|
||||
// process is injected via DefinePlugin, although some 3rd party
|
||||
// libraries may require a mock to work properly (#934)
|
||||
process: 'mock',
|
||||
// prevent webpack from injecting mocks to Node native modules
|
||||
// that does not make sense for the client
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty'
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
context: resolveAppDir(),
|
||||
entry: isElectron ? appConfig.electron.entry : appConfig.dev.entry,
|
||||
externals: isElectron ? externals : undefined,
|
||||
target: isElectron ? 'electron-renderer' : 'web',
|
||||
output: {
|
||||
path: isElectron
|
||||
? resolveAppDir('./dist/electron')
|
||||
: resolveAppDir('./dist'),
|
||||
filename: '[name].js',
|
||||
// publicPath: appConfig.dev.assetsPublicPath,
|
||||
libraryTarget: isElectron ? 'commonjs2' : undefined
|
||||
},
|
||||
devtool: !isProduction ? 'cheap-module-eval-source-map' : '',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: file =>
|
||||
/node_modules/.test(file) && !/\.vue\.js/.test(file)
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: 'node-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['vue-style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'sass-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif)$/,
|
||||
use: ['file-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json', '.css', '.node'],
|
||||
alias: {
|
||||
vue$: 'vue/dist/vue.esm.js',
|
||||
deepmerge$: 'deepmerge/dist/umd.js',
|
||||
'@': appConfig.dev.srcDir ? resolveAppDir(appConfig.dev.srcDir) : null
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin(
|
||||
Object.assign(
|
||||
{
|
||||
'process.env': appConfig.dev.env,
|
||||
'process.env.NODE_ENV': isProduction
|
||||
? '"production"'
|
||||
: '"development"',
|
||||
'process.env.ELECTRON': JSON.stringify(process.env.ELECTRON)
|
||||
},
|
||||
!isProduction
|
||||
? {
|
||||
__static: `"${resolveAppDir(appConfig.staticPath).replace(
|
||||
/\\/g,
|
||||
'\\\\'
|
||||
)}"`
|
||||
}
|
||||
: {}
|
||||
)
|
||||
),
|
||||
new VueLoaderPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: resolveAppDir(appConfig.dev.entryHtml),
|
||||
nodeModules: !isProduction
|
||||
? isMonoRepo
|
||||
? resolveAppDir('../../node_modules')
|
||||
: resolveAppDir('./node_modules')
|
||||
: false
|
||||
}),
|
||||
new CaseSensitivePathsWebpackPlugin(),
|
||||
new webpack.NamedModulesPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new FriendlyErrorsWebpackPlugin({
|
||||
compilationSuccessInfo: {
|
||||
messages: [
|
||||
`FrappeJS server started at http://${
|
||||
appConfig.dev.devServerHost
|
||||
}:${appConfig.dev.devServerPort}`
|
||||
]
|
||||
}
|
||||
}),
|
||||
new webpack.ProgressPlugin(),
|
||||
isProduction
|
||||
? new CopyWebpackPlugin([
|
||||
{
|
||||
from: resolveAppDir(appConfig.staticPath),
|
||||
to: resolveAppDir('./dist/electron/static'),
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
: null
|
||||
// isProduction ? new BabiliWebpackPlugin() : null,
|
||||
// isProduction ? new webpack.LoaderOptionsPlugin({ minimize: true }) : null,
|
||||
].filter(Boolean),
|
||||
optimization: {
|
||||
noEmitOnErrors: false
|
||||
},
|
||||
devServer: {
|
||||
// contentBase: './dist', // dist path is directly configured in express
|
||||
hot: true,
|
||||
quiet: true
|
||||
},
|
||||
node: {
|
||||
// prevent webpack from injecting useless setImmediate polyfill because Vue
|
||||
// source contains it (although only uses it if it's native).
|
||||
setImmediate: false,
|
||||
// process is injected via DefinePlugin, although some 3rd party
|
||||
// libraries may require a mock to work properly (#934)
|
||||
process: 'mock',
|
||||
// prevent webpack from injecting mocks to Node native modules
|
||||
// that does not make sense for the client
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty'
|
||||
}
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
getElectronMainConfig = function getElectronMainConfig() {
|
||||
return {
|
||||
entry: {
|
||||
main: resolveAppDir(appConfig.electron.paths.main)
|
||||
},
|
||||
externals: externals,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: 'node-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
node: {
|
||||
__dirname: !isProduction,
|
||||
__filename: !isProduction
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
libraryTarget: 'commonjs2',
|
||||
path: resolveAppDir('./dist/electron')
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// isProduction && new BabiliWebpackPlugin(),
|
||||
isProduction &&
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
})
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.node']
|
||||
},
|
||||
target: 'electron-main'
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getConfig;
|
||||
makeConfig();
|
||||
|
||||
module.exports = {
|
||||
getConfig,
|
||||
getElectronMainConfig
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ const webpackHotMiddleware = require('webpack-hot-middleware');
|
||||
|
||||
const logger = require('./logger');
|
||||
const { getAppConfig, resolveAppDir } = require('./utils');
|
||||
const getWebpackConfig = require('./config');
|
||||
const { getConfig: getWebpackConfig } = require('./config');
|
||||
|
||||
const log = logger('serve');
|
||||
const warn = logger('serve', 'red');
|
||||
@ -52,7 +52,8 @@ function startWebpackDevServer() {
|
||||
|
||||
function addWebpackEntryPoints(webpackConfig, forDevServer) {
|
||||
const devServerEntryPoints = [
|
||||
resolveAppDir('node_modules/webpack-dev-server/client/index.js') + '?http://localhost',
|
||||
// resolveAppDir('node_modules/webpack-dev-server/client/index.js') + '?http://localhost',
|
||||
'webpack-dev-server/client/index.js?http://localhost',
|
||||
'webpack/hot/dev-server'
|
||||
];
|
||||
const middlewareEntryPoints = [
|
||||
|
@ -4,22 +4,22 @@ const { getAppConfig, resolveAppDir } = require('./utils');
|
||||
const appConfig = getAppConfig();
|
||||
|
||||
module.exports = function start(mode) {
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
if (mode === 'electron') {
|
||||
const electron = require('electron');
|
||||
const electronPaths = appConfig.electron.paths;
|
||||
if (mode === 'electron') {
|
||||
const electron = require('electron');
|
||||
const electronPaths = appConfig.electron.paths;
|
||||
|
||||
startWebpackDevServer()
|
||||
.then((devServer) => {
|
||||
const p = spawn(electron, [resolveAppDir(electronPaths.mainDev)], { stdio: 'inherit' })
|
||||
p.on('close', () => {
|
||||
devServer.close();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const nodePaths = appConfig.node.paths;
|
||||
startWebpackDevServer()
|
||||
.then((devServer) => {
|
||||
const p = spawn(electron, [resolveAppDir(electronPaths.mainDev)], { stdio: 'inherit' })
|
||||
p.on('close', () => {
|
||||
devServer.close();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const nodePaths = appConfig.node.paths;
|
||||
|
||||
spawn('node', [resolveAppDir(nodePaths.main)], { stdio: 'inherit' })
|
||||
}
|
||||
spawn('node', [resolveAppDir(nodePaths.main)], { stdio: 'inherit' })
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user