2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 22:58:28 +00:00

multi-delete

This commit is contained in:
Rushabh Mehta 2018-02-08 15:08:47 +05:30
parent ed4dbb496c
commit 449df1dbca
24 changed files with 284 additions and 306 deletions

View File

@ -18,6 +18,7 @@
"window": false,
"describe": true,
"before": true,
"after": true,
"it": true
},

View File

@ -189,6 +189,12 @@ module.exports = class Database {
return values;
}
async deleteMany(doctype, names) {
for (const name of names) {
await this.delete(doctype, name);
}
}
async delete(doctype, name) {
await this.deleteOne(doctype, name);
@ -208,10 +214,10 @@ module.exports = class Database {
}
async exists(doctype, name) {
return (await this.get_value(doctype, name)) ? true : false;
return (await this.getValue(doctype, name)) ? true : false;
}
async get_value(doctype, filters, fieldname = 'name') {
async getValue(doctype, filters, fieldname = 'name') {
if (typeof filters === 'string') {
filters = { name: filters };
}

View File

@ -2,16 +2,11 @@ const frappe = require('frappejs');
const path = require('path');
module.exports = class RESTClient {
constructor({server, protocol='http'}) {
constructor({ server, protocol = 'http' }) {
this.server = server;
this.protocol = protocol;
this.initTypeMap();
this.json_headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}
connect() {
@ -20,29 +15,25 @@ module.exports = class RESTClient {
async insert(doctype, doc) {
doc.doctype = doctype;
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
let response = await frappe.fetch(url, {
let url = this.getURL('/api/resource', frappe.slug(doctype));
return await this.fetch(url, {
method: 'POST',
headers: this.json_headers,
body: JSON.stringify(doc)
});
return await response.json();
})
}
async get(doctype, name) {
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
let response = await frappe.fetch(url, {
let url = this.getURL('/api/resource', frappe.slug(doctype), name);
return await this.fetch(url, {
method: 'GET',
headers: this.json_headers
});
return await response.json();
headers: this.getHeaders()
})
}
async getAll({doctype, fields, filters, start, limit, sort_by, order}) {
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
async getAll({ doctype, fields, filters, start, limit, sort_by, order }) {
let url = this.getURL('/api/resource', frappe.slug(doctype));
url = url + "?" + this.get_query_string({
url = url + "?" + this.getQueryString({
fields: JSON.stringify(fields),
filters: JSON.stringify(filters),
start: start,
@ -51,80 +42,101 @@ module.exports = class RESTClient {
order: order
});
let response = await frappe.fetch(url, {
return await this.fetch(url, {
method: 'GET',
headers: this.json_headers
});
return await response.json();
}
async update(doctype, doc) {
doc.doctype = doctype;
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${doc.name}`);
let response = await frappe.fetch(url, {
let url = this.getURL('/api/resource', frappe.slug(doctype), doc.name);
return await this.fetch(url, {
method: 'PUT',
headers: this.json_headers,
body: JSON.stringify(doc)
});
return await response.json();
}
async delete(doctype, name) {
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
let url = this.getURL('/api/resource', frappe.slug(doctype), name);
let response = await frappe.fetch(url, {
return await this.fetch(url, {
method: 'DELETE',
headers: this.json_headers
});
}
async deleteMany(doctype, names) {
let url = this.getURL('/api/resource', frappe.slug(doctype));
return await this.fetch(url, {
method: 'DELETE',
body: JSON.stringify(names)
});
}
async exists(doctype, name) {
return (await this.getValue(doctype, name, 'name')) ? true : false;
}
async getValue(doctype, name, fieldname) {
let url = this.getURL('/api/resource', frappe.slug(doctype), name, fieldname);
return (await this.fetch(url, {
method: 'GET',
})).value;
}
async fetch(url, args) {
args.headers = this.getHeaders();
let response = await frappe.fetch(url, args);
return await response.json();
}
get_query_string(params) {
getURL(...parts) {
return this.protocol + '://' + path.join(this.server, ...parts);
}
getQueryString(params) {
return Object.keys(params)
.map(k => params[k] != null ? encodeURIComponent(k) + '=' + encodeURIComponent(params[k]) : null)
.filter(v => v)
.join('&');
}
async get_value(doctype, name, fieldname) {
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}/${fieldname}`);
let response = await frappe.fetch(url, {
method: 'GET',
headers: this.json_headers
});
return (await response.json()).value;
getHeaders() {
return {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}
initTypeMap() {
this.type_map = {
'Currency': true
,'Int': true
,'Float': true
,'Percent': true
,'Check': true
,'Small Text': true
,'Long Text': true
,'Code': true
,'Text Editor': true
,'Date': true
,'Datetime': true
,'Time': true
,'Text': true
,'Data': true
,'Link': true
,'Dynamic Link':true
,'Password': true
,'Select': true
,'Read Only': true
,'Attach': true
,'Attach Image':true
,'Signature': true
,'Color': true
,'Barcode': true
,'Geolocation': true
'Currency': true
, 'Int': true
, 'Float': true
, 'Percent': true
, 'Check': true
, 'Small Text': true
, 'Long Text': true
, 'Code': true
, 'Text Editor': true
, 'Date': true
, 'Datetime': true
, 'Time': true
, 'Text': true
, 'Data': true
, 'Link': true
, 'Dynamic Link': true
, 'Password': true
, 'Select': true
, 'Read Only': true
, 'Attach': true
, 'Attach Image': true
, 'Signature': true
, 'Color': true
, 'Barcode': true
, 'Geolocation': true
}
}

View File

@ -1,6 +1,6 @@
const Page = require('frappejs/client/view/page');
const view = require('frappejs/client/view');
const frappejs = require('frappejs');
const frappe = require('frappejs');
module.exports = class FormPage extends Page {
constructor(doctype) {
@ -10,7 +10,8 @@ module.exports = class FormPage extends Page {
this.form = new (view.get_form_class(doctype))({
doctype: doctype,
parent: this.body
parent: this.body,
page: this
});
this.on('show', async (params) => {
@ -21,13 +22,13 @@ module.exports = class FormPage extends Page {
this.form.on('submit', async (params) => {
let route = frappe.router.get_route();
if (this.form.doc.name && !(route && route[2] === this.form.doc.name)) {
await frappe.router.set_route('edit', this.form.doc.doctype, this.form.doc.name);
this.form.show_alert('Added', 'success');
await frappe.router.setRoute('edit', this.form.doc.doctype, this.form.doc.name);
this.form.showAlert('Added', 'success');
}
});
this.form.on('delete', async (params) => {
await frappe.router.set_route('list', this.form.doctype);
await frappe.router.setRoute('list', this.form.doctype);
});
}
@ -36,7 +37,7 @@ module.exports = class FormPage extends Page {
this.doc = await frappe.get_doc(doctype, name);
this.form.use(this.doc);
} catch (e) {
this.render_error(e.status_code, e.message);
this.renderError(e.status_code, e.message);
}
}
}

View File

@ -1,5 +1,5 @@
const frappe = require('frappejs');
const Search = require('./search');
// const Search = require('./search');
const Router = require('frappejs/common/router');
const Page = require('frappejs/client/view/page');
const FormPage = require('frappejs/client/desk/formpage');
@ -16,7 +16,7 @@ module.exports = class Desk {
this.container = frappe.ui.add('div', 'container-fluid', body);
this.container_row = frappe.ui.add('div', 'row', this.container)
this.sidebar = frappe.ui.add('div', 'col-md-2 p-3 sidebar', this.container_row);
this.sidebar = frappe.ui.add('div', 'col-md-2 p-3 sidebar d-none d-md-block', this.container_row);
this.body = frappe.ui.add('div', 'col-md-10 p-3 main', this.container_row);
this.sidebar_items = [];
@ -35,11 +35,11 @@ module.exports = class Desk {
this.not_found_page = new Page('Not Found');
}
await this.not_found_page.show();
this.not_found_page.render_error('Not Found', params ? params.route : '');
this.not_found_page.renderError('Not Found', params ? params.route : '');
})
frappe.router.add('list/:doctype', async (params) => {
let page = this.get_list_page(params.doctype);
let page = this.getList_page(params.doctype);
await page.show(params);
});
@ -51,13 +51,13 @@ module.exports = class Desk {
frappe.router.add('new/:doctype', async (params) => {
let doc = await frappe.get_new_doc(params.doctype);
// unset the name, its local
await frappe.router.set_route('edit', doc.doctype, doc.name);
await frappe.router.setRoute('edit', doc.doctype, doc.name);
await doc.set('name', '');
});
}
get_list_page(doctype) {
getList_page(doctype) {
if (!this.pages.lists[doctype]) {
this.pages.lists[doctype] = new ListPage(doctype);
}

View File

@ -1,13 +1,15 @@
const frappe = require('frappejs');
const Page = require('frappejs/client/view/page');
const view = require('frappejs/client/view');
module.exports = class FormPage extends Page {
module.exports = class ListPage extends Page {
constructor(doctype) {
let meta = frappe.getMeta(doctype);
super(`List ${meta.name}`);
this.list = new (view.get_list_class(doctype))({
super(frappe._("List: {0}", meta.name));
this.list = new (view.getList_class(doctype))({
doctype: doctype,
parent: this.body
parent: this.body,
page: this
});
this.on('show', async () => {
await this.list.run();

View File

@ -1,97 +0,0 @@
@import "node_modules/bootstrap/scss/bootstrap";
@import "node_modules/awesomplete/awesomplete";
$spacer-1: 0.25rem;
$spacer-2: 0.5rem;
$spacer-3: 1rem;
$spacer-4: 2rem;
html {
font-size: 14px;
}
.hide {
display: none !important;
}
.page-error {
text-align: center;
padding: 200px 0px;
}
.form-body {
max-width: 600px;
.form-toolbar {
height: $spacer-4;
margin-bottom: $spacer-3;
}
.alert {
margin-top: $spacer-3;
}
}
.dropdown-menu-right {
right: 0;
left: auto;
}
.awesomplete {
display: block;
}
.awesomplete > ul > li {
padding: .75rem .375rem;
}
.awesomplete > ul > li:hover {
background: $gray-300;
color: $body-color;
}
.awesomplete > ul > li[aria-selected="true"] {
background: $gray-300;
color: $body-color;
}
.awesomplete > ul > li[aria-selected="true"]:hover {
background: $gray-300;
color: $body-color;
}
mark {
padding: none;
background: inherit;
}
.awesomplete li[aria-selected="true"] mark, .awesomplete li[aria-selected="false"] mark {
background: inherit;
color: inherit;
padding: 0px;
}
.table-wrapper {
margin-bottom: $spacer-2;
}
.table-toolbar {
margin-top: $spacer-2;
}
.data-table-col .edit-cell {
padding: 0px !important;
input, textarea {
border-radius: none;
margin: none;
padding: $spacer-1;
}
.awesomplete > ul {
position: fixed;
left: auto;
width: auto;
min-width: 120px;
}
}

View File

@ -70,7 +70,7 @@ class Dropdown {
if (typeof action === 'string') {
item.src = action;
item.addEventListener('click', async () => {
await frappe.router.set_route(action);
await frappe.router.setRoute(action);
this.toggle();
});
} else {

View File

@ -19,17 +19,17 @@ class BaseControl {
bind(doc) {
this.doc = doc;
this.set_doc_value();
this.setDocValue();
}
refresh() {
this.make();
this.set_doc_value();
this.setDocValue();
}
set_doc_value() {
setDocValue() {
if (this.doc) {
this.set_input_value(this.doc.get(this.fieldname));
this.setInputValue(this.doc.get(this.fieldname));
}
}
@ -77,7 +77,7 @@ class BaseControl {
}
}
set_input_value(value) {
setInputValue(value) {
this.input.value = this.format(value);
}
@ -92,7 +92,7 @@ class BaseControl {
return await this.parse(this.input.value);
}
get_input_value() {
getInputValue() {
return this.input.value;
}
@ -109,14 +109,14 @@ class BaseControl {
}
async handle_change(e) {
let value = await this.parse(this.get_input_value());
let value = await this.parse(this.getInputValue());
value = await this.validate(value);
if (this.doc[this.fieldname] !== value) {
if (this.doc.set) {
await this.doc.set(this.fieldname, value);
}
if (this.parent_control) {
await this.parent_control.doc.set(this.fieldname, this.parent_control.get_input_value());
await this.parent_control.doc.set(this.fieldname, this.parent_control.getInputValue());
}
}
}

View File

@ -10,11 +10,11 @@ const control_classes = {
}
module.exports = {
get_control_class(fieldtype) {
getControlClass(fieldtype) {
return control_classes[fieldtype];
},
make_control({field, form, parent}) {
const control_class = this.get_control_class(field.fieldtype);
makeControl({field, form, parent}) {
const control_class = this.getControlClass(field.fieldtype);
let control = new control_class({field:field, form:form, parent:parent});
control.make();
return control;

View File

@ -14,19 +14,19 @@ class LinkControl extends BaseControl {
// rebuild the list on input
this.input.addEventListener('input', async (event) => {
this.awesomplete.list = await this.get_list(this.input.value);
this.awesomplete.list = await this.getList(this.input.value);
});
}
async get_list(query) {
async getList(query) {
return (await frappe.db.getAll({
doctype: this.target,
filters: this.get_filters(query),
filters: this.getFilters(query),
limit: 50
})).map(d => d.name);
}
get_filters(query) {
getFilters(query) {
return { keywords: ["like", query] }
}
};

View File

@ -40,11 +40,11 @@ class TableControl extends BaseControl {
}
}
get_input_value() {
getInputValue() {
return this.doc[this.fieldname];
}
set_input_value(value) {
setInputValue(value) {
this.datatable.refresh(this.get_table_data(value));
}
@ -64,20 +64,20 @@ class TableControl extends BaseControl {
get_control(field, parent) {
field.only_input = true;
const control = controls.make_control({field: field, parent: parent});
const control = controls.makeControl({field: field, parent: parent});
return {
initValue: (value, rowIndex, column) => {
control.parent_control = this;
control.doc = this.doc[this.fieldname][rowIndex];
control.set_focus();
return control.set_input_value(value);
return control.setInputValue(value);
},
setValue: (value, rowIndex, column) => {
return control.set_input_value(value);
return control.setInputValue(value);
},
getValue: () => {
return control.get_input_value();
return control.getInputValue();
}
}

View File

@ -3,14 +3,11 @@ const controls = require('./controls');
const Observable = require('frappejs/utils/observable');
module.exports = class BaseForm extends Observable {
constructor({doctype, parent, submit_label='Submit'}) {
constructor({doctype, parent, submit_label='Submit', page}) {
super();
this.parent = parent;
this.doctype = doctype;
this.submit_label = submit_label;
Object.assign(this, arguments[0]);
this.controls = {};
this.controls_list = [];
this.controlList = [];
this.meta = frappe.getMeta(this.doctype);
if (this.setup) {
@ -25,35 +22,26 @@ module.exports = class BaseForm extends Observable {
}
this.body = frappe.ui.add('div', 'form-body', this.parent);
this.make_toolbar();
this.makeToolbar();
this.form = frappe.ui.add('div', 'form-container', this.body);
for(let field of this.meta.fields) {
if (controls.get_control_class(field.fieldtype)) {
let control = controls.make_control({field: field, form: this});
this.controls_list.push(control);
if (controls.getControlClass(field.fieldtype)) {
let control = controls.makeControl({field: field, form: this});
this.controlList.push(control);
this.controls[field.fieldname] = control;
}
}
}
make_toolbar() {
this.toolbar = frappe.ui.add('div', 'form-toolbar text-right', this.body);
this.toolbar.innerHTML = `
<button class="btn btn-outline-secondary btn-delete">Delete</button>
<button class="btn btn-primary btn-submit">Save</button>
`
this.btn_submit = this.toolbar.querySelector('.btn-submit');;
this.btn_submit.addEventListener('click', async (event) => {
this.submit();
event.preventDefault();
makeToolbar() {
this.btnSubmit = this.page.addButton(frappe._("Save"), 'btn-primary', async () => {
await this.submit();
})
this.btn_delete = this.toolbar.querySelector('.btn-delete');
this.btn_delete.addEventListener('click', async () => {
this.btnDelete = this.page.addButton(frappe._("Delete"), 'btn-outline-secondary', async () => {
await this.doc.delete();
this.show_alert('Deleted', 'success');
this.showAlert('Deleted', 'success');
this.trigger('delete');
});
}
@ -61,20 +49,20 @@ module.exports = class BaseForm extends Observable {
async use(doc, is_new = false) {
if (this.doc) {
// clear handlers of outgoing doc
this.doc.clear_handlers();
this.doc.clearHandlers();
}
this.clear_alert();
this.clearAlert();
this.doc = doc;
this.is_new = is_new;
for (let control of this.controls_list) {
for (let control of this.controlList) {
control.bind(this.doc);
}
// refresh value in control
this.doc.add_handler('change', (params) => {
this.doc.addHandler('change', (params) => {
let control = this.controls[params.fieldname];
if (control && control.get_input_value() !== control.format(params.fieldname)) {
control.set_doc_value();
if (control && control.getInputValue() !== control.format(params.fieldname)) {
control.setDocValue();
}
});
@ -89,29 +77,29 @@ module.exports = class BaseForm extends Observable {
await this.doc.update();
}
await this.refresh();
this.show_alert('Saved', 'success');
this.showAlert('Saved', 'success');
} catch (e) {
this.show_alert('Failed', 'danger');
this.showAlert('Failed', 'danger');
}
await this.trigger('submit');
}
refresh() {
for(let control of this.controls_list) {
for(let control of this.controlList) {
control.refresh();
}
}
show_alert(message, type, clear_after = 5) {
this.clear_alert();
showAlert(message, type, clear_after = 5) {
this.clearAlert();
this.alert = frappe.ui.add('div', `alert alert-${type}`, this.body);
this.alert.textContent = message;
setTimeout(() => {
this.clear_alert();
this.clearAlert();
}, clear_after * 1000);
}
clear_alert() {
clearAlert() {
if (this.alert) {
frappe.ui.remove(this.alert);
this.alert = null;

View File

@ -5,7 +5,7 @@ module.exports = {
get_form_class(doctype) {
return this.get_view_class(doctype, 'Form', BaseForm);
},
get_list_class(doctype) {
getList_class(doctype) {
return this.get_view_class(doctype, 'List', BaseList);
},
get_view_class(doctype, class_name, default_class) {

View File

@ -1,28 +1,30 @@
const frappe = require('frappejs');
module.exports = class BaseList {
constructor({doctype, parent, fields}) {
this.doctype = doctype;
this.parent = parent;
this.fields = fields;
constructor({doctype, parent, fields, page}) {
Object.assign(this, arguments[0]);
this.meta = frappe.getMeta(this.doctype);
this.start = 0;
this.page_length = 20;
this.pageLength = 20;
this.body = null;
this.rows = [];
this.data = [];
}
async refresh() {
return await this.run();
}
async run() {
this.make_body();
this.makeBody();
let data = await this.get_data();
let data = await this.getData();
for (let i=0; i< Math.min(this.page_length, data.length); i++) {
this.render_row(this.start + i, data[i]);
for (let i=0; i< Math.min(this.pageLength, data.length); i++) {
this.renderRow(this.start + i, data[i]);
}
if (this.start > 0) {
@ -31,47 +33,62 @@ module.exports = class BaseList {
this.data = data;
}
this.clear_empty_rows();
this.update_more(data.length > this.page_length);
this.clearEmptyRows();
this.updateMore(data.length > this.pageLength);
}
async get_data() {
async getData() {
return await frappe.db.getAll({
doctype: this.doctype,
fields: this.get_fields(),
filters: this.get_filters(),
fields: this.getFields(),
filters: this.getFilters(),
start: this.start,
limit: this.page_length + 1
limit: this.pageLength + 1
});
}
get_fields() {
getFields() {
return ['name'];
}
async append() {
this.start += this.page_length;
this.start += this.pageLength;
await this.run();
}
get_filters() {
getFilters() {
let filters = {};
if (this.search_input.value) {
filters.keywords = ['like', '%' + this.search_input.value + '%'];
if (this.searchInput.value) {
filters.keywords = ['like', '%' + this.searchInput.value + '%'];
}
return filters;
}
make_body() {
makeBody() {
if (!this.body) {
this.make_toolbar();
//this.make_new();
this.makeToolbar();
this.body = frappe.ui.add('div', 'list-body', this.parent);
this.make_more_btn();
this.makeMoreBtn();
}
}
make_toolbar() {
makeToolbar() {
this.makeSearch();
this.btnNew = this.page.addButton(frappe._('New'), 'btn-outline-primary', async () => {
await frappe.router.setRoute('new', frappe.slug(this.doctype));
})
this.btnDelete = this.page.addButton(frappe._('Delete'), 'btn-outline-secondary hide', async () => {
await frappe.db.deleteMany(this.doctype, this.getCheckedRowNames());
await this.refresh();
});
this.page.body.addEventListener('click', (event) => {
if(event.target.classList.contains('checkbox')) {
this.btnDelete.classList.toggle('hide', this.getCheckedRowNames().length===0);
}
})
}
makeSearch() {
this.toolbar = frappe.ui.add('div', 'list-toolbar', this.parent);
this.toolbar.innerHTML = `
<div class="row">
@ -83,67 +100,70 @@ module.exports = class BaseList {
</div>
</div>
</div>
<div class="col-md-3 col-3">
<a href="#new/${frappe.slug(this.doctype)}" class="btn btn-outline-primary">
New
</a>
</div>
</div>
`;
this.search_input = this.toolbar.querySelector('input');
this.search_input.addEventListener('keypress', (event) => {
this.searchInput = this.toolbar.querySelector('input');
this.searchInput.addEventListener('keypress', (event) => {
if (event.keyCode===13) {
this.run();
this.refresh();
}
});
this.search_button = this.toolbar.querySelector('.btn-search');
this.search_button.addEventListener('click', (event) => {
this.run();
this.btnSearch = this.toolbar.querySelector('.btn-search');
this.btnSearch.addEventListener('click', (event) => {
this.refresh();
});
}
make_more_btn() {
this.more_btn = frappe.ui.add('button', 'btn btn-secondary hide', this.parent);
this.more_btn.textContent = 'More';
this.more_btn.addEventListener('click', () => {
makeMoreBtn() {
this.btnMore = frappe.ui.add('button', 'btn btn-secondary hide', this.parent);
this.btnMore.textContent = 'More';
this.btnMore.addEventListener('click', () => {
this.append();
})
}
render_row(i, data) {
let row = this.get_row(i);
row.innerHTML = this.get_row_html(data);
renderRow(i, data) {
let row = this.getRow(i);
row.innerHTML = this.getRowBodyHTML(data);
row.style.display = 'block';
}
get_row_html(data) {
getRowBodyHTML(data) {
return `<input class="checkbox" type="checkbox" data-name="${data.name}"> ` + this.getRowHTML(data);
}
getRowHTML(data) {
return `<a href="#edit/${this.doctype}/${data.name}">${data.name}</a>`;
}
get_row(i) {
getRow(i) {
if (!this.rows[i]) {
this.rows[i] = frappe.ui.add('div', 'list-row py-2', this.body);
}
return this.rows[i];
}
clear_empty_rows() {
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.get_row(i);
let row = this.getRow(i);
row.innerHTML = '';
row.style.display = 'none';
}
}
}
update_more(show) {
updateMore(show) {
if (show) {
this.more_btn.classList.remove('hide');
this.btnMore.classList.remove('hide');
} else {
this.more_btn.classList.add('hide');
this.btnMore.classList.add('hide');
}
}

View File

@ -10,7 +10,10 @@ module.exports = class Page extends Observable {
make() {
this.wrapper = frappe.ui.add('div', 'page hide', frappe.desk.body);
this.body = frappe.ui.add('div', 'page-body', this.wrapper);
this.wrapper.innerHTML = `<div class="page-head hide"></div>
<div class="page-body"></div>`
this.head = this.wrapper.querySelector('.page-head');
this.body = this.wrapper.querySelector('.page-body');
}
hide() {
@ -18,6 +21,14 @@ module.exports = class Page extends Observable {
this.trigger('hide');
}
addButton(label, cssClass, action) {
this.head.classList.remove('hide');
this.button = frappe.ui.add('button', 'btn btn-sm ' + cssClass, this.head);
this.button.innerHTML = label;
this.button.addEventListener('click', action);
return this.button;
}
async show(params) {
if (frappe.router.current_page) {
frappe.router.current_page.hide();
@ -35,7 +46,7 @@ module.exports = class Page extends Observable {
await this.trigger('show', params);
}
render_error(title, message) {
renderError(title, message) {
if (!this.page_error) {
this.page_error = frappe.ui.add('div', 'page-error', this.wrapper);
}

View File

@ -70,7 +70,7 @@ module.exports = class Router {
}
}
async set_route(...parts) {
async setRoute(...parts) {
const route = parts.join('/');
// setting this first, does not trigger show via hashchange,

View File

@ -1,5 +1,5 @@
module.exports = {
input: './node_modules/frappejs/client/style/index.scss',
input: './node_modules/frappejs/client/style/style.scss',
output: {
file: './dist/css/style.css',
format: 'cjs'

View File

@ -11,11 +11,11 @@ module.exports = class BaseDocument {
// add handlers
}
clear_handlers() {
clearHandlers() {
this.handlers = {};
}
add_handler(key, method) {
addHandler(key, method) {
if (!this.handlers[key]) {
this.handlers[key] = [];
}

View File

@ -9,7 +9,7 @@ class ToDoMeta extends BaseMeta {
class ToDo extends BaseDocument {
setup() {
this.add_handler('validate');
this.addHandler('validate');
}
validate() {
if (!this.status) {

View File

@ -1,10 +1,10 @@
const BaseList = require('frappejs/client/view/list');
class ToDoList extends BaseList {
get_fields() {
getFields() {
return ['name', 'subject', 'status'];
}
get_row_html(data) {
getRowHTML(data) {
let symbol = data.status=="Closed" ? "✔" : "";
return `<a href="#edit/todo/${data.name}">${symbol} ${data.subject}</a>`;
}

View File

@ -25,7 +25,7 @@ module.exports = {
// create
app.post('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
data = request.body;
let data = request.body;
data.doctype = request.params.doctype;
let doc = frappe.new_doc(data);
await doc.insert();
@ -35,7 +35,7 @@ module.exports = {
// update
app.put('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
data = request.body;
let data = request.body;
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
Object.assign(doc, data);
await doc.update();
@ -52,7 +52,7 @@ module.exports = {
// get value
app.get('/api/resource/:doctype/:name/:fieldname', frappe.async_handler(async function(request, response) {
let value = await frappe.db.get_value(request.params.doctype, request.params.name, request.params.fieldname);
let value = await frappe.db.getValue(request.params.doctype, request.params.name, request.params.fieldname);
return response.json({value: value});
}));
@ -62,5 +62,16 @@ module.exports = {
await doc.delete();
return response.json({});
}));
// delete many
app.delete('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
let names = request.body;
for (let name of names) {
let doc = await frappe.get_doc(request.params.doctype, name);
await doc.delete();
}
return response.json({});
}));
}
};

View File

@ -27,12 +27,12 @@ describe('Document', () => {
let doc = test_doc();
await doc.insert();
assert.notEqual(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
assert.notEqual(await frappe.db.getValue(doc.doctype, doc.name, 'subject'), 'subject 2');
doc.subject = 'subject 2'
await doc.update();
assert.equal(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
assert.equal(await frappe.db.getValue(doc.doctype, doc.name, 'subject'), 'subject 2');
})
it('should get a value', async () => {
@ -60,11 +60,11 @@ describe('Document', () => {
let doc = test_doc();
await doc.insert();
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), doc.name);
assert.equal(await frappe.db.getValue(doc.doctype, doc.name), doc.name);
await doc.delete();
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), null);
assert.equal(await frappe.db.getValue(doc.doctype, doc.name), null);
});
it('should add, fetch and delete documents with children', async() => {

View File

@ -62,5 +62,28 @@ describe('REST', () => {
assert.ok(subjects.includes('all test 2'));
});
it('should delete a document', async () => {
let doc = frappe.new_doc({doctype:'ToDo', subject:'test rest insert 1'});
await doc.insert();
assert.equal(await frappe.db.exists(doc.doctype, doc.name), true);
await doc.delete();
assert.equal(await frappe.db.exists(doc.doctype, doc.name), false);
});
it('should delete multiple documents', async () => {
let doc1 = frappe.new_doc({doctype:'ToDo', subject:'test rest insert 5'});
let doc2 = frappe.new_doc({doctype:'ToDo', subject:'test rest insert 6'});
await doc1.insert();
await doc2.insert();
assert.equal(await frappe.db.exists(doc1.doctype, doc1.name), true);
assert.equal(await frappe.db.exists(doc2.doctype, doc2.name), true);
await frappe.db.deleteMany(doc1.doctype, [doc1.name, doc2.name]);
assert.equal(await frappe.db.exists(doc1.doctype, doc1.name), false);
assert.equal(await frappe.db.exists(doc2.doctype, doc2.name), false);
});
});