2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

Upstream merge

This commit is contained in:
thefalconx33 2019-07-12 16:23:19 +05:30
commit e1892c9f54
81 changed files with 665 additions and 3945 deletions

View File

@ -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) {

View File

@ -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) {

View File

@ -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
View File

@ -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')

View File

@ -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;

View File

@ -1,11 +0,0 @@
<!-- styles -->
<style>
:host {
display: block;
}
</style>
<!-- template -->
<div class="tree">
<slot></slot>
</div>

View File

@ -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
}

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
});
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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 || {}));
}
}

View File

@ -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();
}
}
})
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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');
}
};

View File

@ -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;
}
};

View File

@ -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
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,9 +0,0 @@
@import "./variables.scss";
.tree-body {
padding: $spacer-3 $spacer-4;
}
f-tree-node {
--tree-node-hover: $gray-100;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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'
},
}

View File

@ -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">&times;</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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,9 +0,0 @@
const LinkControl = require('./link');
class DynamicLinkControl extends LinkControl {
getTarget() {
return this.doc[this.references];
}
};
module.exports = DynamicLinkControl;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -1,10 +0,0 @@
const BaseControl = require('./base');
class PasswordControl extends BaseControl {
make() {
super.make();
this.input.setAttribute('type', 'password');
}
};
module.exports = PasswordControl;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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'});
}
}
}

View File

@ -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]);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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');
}
};

View File

@ -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;
}
}

View File

@ -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;
}
};

View File

@ -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);
}));
}
},

View File

@ -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;
}
};

View File

@ -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",

View File

@ -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() {

Binary file not shown.

View File

@ -40,7 +40,7 @@ export default {
</script>
<style>
.feather-icon {
display: inline-block;
display: inline-flex;
}
</style>

View File

@ -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();

View File

@ -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));
}

View File

@ -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) {

View File

@ -8,7 +8,7 @@
class="form-control shadow-none w-100"
:placeholder="_('Search...')">
</form>
<div class="navbar-text">.</div>
<div class="navbar-text">&nbsp;</div>
</nav>
</template>
<script>

View File

@ -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
}
});
}
}
}

View File

@ -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
};

View File

@ -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>

View File

@ -70,7 +70,14 @@ export default {
return Boolean(disabled);
}
}
},
provide() {
return {
dynamicLinkTarget: reference => {
return this.doc[reference];
}
};
},
};
</script>
<style scoped>

View File

@ -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({

View File

@ -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] || '&nbsp;' }}
</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>

View File

@ -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;

View File

@ -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: {

View File

@ -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: {

View File

@ -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;

View File

@ -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
View 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
}));
}
});
});
}

View File

@ -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
};

View File

@ -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 = [

View File

@ -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' })
}
}