[ui-tests] python is back! (#3565)
* [ui-tests] python is back! * [minor] remove old test * [test] dont test test_runner * [tests] try firefox * [tests] try chrome * [tests] try chrome * [tests] try chrome * [tests] try chrome 1 * [tests] try chrome 2 * [tests] try chrome 3 * [tests] try phantomJS * [tests] try chrome * [tests] try chrome * [tests] try chrome * [tests] try chrome * [tests] try chrome * [tests] try chrome * [tests] try chrome * [tests] try chrome * [tests] login click button * [tests] login click button * [tests] show log * [test] test with start_maximized * [test] test only login * [travis] test another port for selenium * [travis] try running ui tests after unittests are done * [travis] pring body_div if fails * [tests] complete setup wizard for frappe * [minor] move ui tests to frappe/ui/tests * [tests] ui tests in public and codacy fixes * [fix] tests + eslint * [minor] move tests to tests/ui folder and print console after print * [fix] linting * [tests] added documentation and better integration testing * [promise] form triggering is now promise based * [test] * [test] * [test] * [test] * [test] print output * [minor] default empty in select and print console * [cleanup] more minor fixes * [enhance] first-cut done! * [minor] frappe.run_serially to pass arguments while chaining
This commit is contained in:
parent
b72dded0f7
commit
f409fd7358
52 changed files with 6886 additions and 5727 deletions
|
|
@ -117,6 +117,7 @@
|
|||
"set_field_options": true,
|
||||
"getCookie": true,
|
||||
"getCookies": true,
|
||||
"get_url_arg": true
|
||||
"get_url_arg": true,
|
||||
"QUnit": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
.travis.yml
26
.travis.yml
|
|
@ -2,23 +2,22 @@ language: python
|
|||
dist: trusty
|
||||
group: deprecated-2017Q2
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- google-chrome
|
||||
packages:
|
||||
- google-chrome-stable
|
||||
# sauce_connect:
|
||||
# username: "rmehta1"
|
||||
# access_key: "a80640ec-24c8-44ad-9398-1b6f123ae4a1"
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
|
||||
services:
|
||||
- mysql
|
||||
|
||||
before_install:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
|
||||
install:
|
||||
- sudo apt-get purge -y mysql-common mysql-server mysql-client
|
||||
- nvm install v7.10.0
|
||||
|
|
@ -31,6 +30,15 @@ install:
|
|||
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
|
||||
|
||||
before_script:
|
||||
- wget http://chromedriver.storage.googleapis.com/2.27/chromedriver_linux64.zip
|
||||
- unzip chromedriver_linux64.zip
|
||||
- sudo apt-get install libnss3
|
||||
- sudo apt-get --only-upgrade install google-chrome-stable
|
||||
- sudo cp chromedriver /usr/local/bin/.
|
||||
- sudo chmod +x /usr/local/bin/chromedriver
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
- sleep 3
|
||||
- mysql -u root -ptravis -e 'create database test_frappe'
|
||||
- echo "USE mysql;\nCREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe';\nFLUSH PRIVILEGES;\n" | mysql -u root -ptravis
|
||||
- echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis
|
||||
|
|
@ -44,5 +52,5 @@ before_script:
|
|||
script:
|
||||
- set -e
|
||||
- bench --verbose run-tests
|
||||
- bench reinstall --yes
|
||||
- bench run-ui-tests --ci
|
||||
- sleep 5
|
||||
- bench --verbose run-tests --ui-tests
|
||||
|
|
|
|||
|
|
@ -298,11 +298,13 @@ def console(context):
|
|||
@click.option('--doctype', help="For DocType")
|
||||
@click.option('--test', multiple=True, help="Specific test")
|
||||
@click.option('--driver', help="For Travis")
|
||||
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
|
||||
@click.option('--module', help="Run tests in a module")
|
||||
@click.option('--profile', is_flag=True, default=False)
|
||||
@click.option('--junit-xml-output', help="Destination file path for junit xml report")
|
||||
@pass_context
|
||||
def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False, junit_xml_output=False):
|
||||
def run_tests(context, app=None, module=None, doctype=None, test=(),
|
||||
driver=None, profile=False, junit_xml_output=False, ui_tests = False):
|
||||
"Run tests"
|
||||
import frappe.test_runner
|
||||
tests = test
|
||||
|
|
@ -311,7 +313,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None
|
|||
frappe.init(site=site)
|
||||
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
force=context.force, profile=profile, junit_xml_output=junit_xml_output)
|
||||
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
|
||||
ui_tests = ui_tests)
|
||||
if len(ret.failures) == 0 and len(ret.errors) == 0:
|
||||
ret = 0
|
||||
|
||||
|
|
|
|||
0
frappe/core/doctype/test_runner/__init__.py
Normal file
0
frappe/core/doctype/test_runner/__init__.py
Normal file
63
frappe/core/doctype/test_runner/test_runner.js
Normal file
63
frappe/core/doctype/test_runner/test_runner.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Test Runner', {
|
||||
refresh: (frm) => {
|
||||
frm.disable_save();
|
||||
frm.page.set_primary_action(__("Run Tests"), () => {
|
||||
return new Promise(resolve => {
|
||||
let wrapper = $(frm.fields_dict.output.wrapper).empty();
|
||||
$("<p>Loading...</p>").appendTo(wrapper);
|
||||
|
||||
// all tests
|
||||
frappe.call({
|
||||
method: 'frappe.core.doctype.test_runner.test_runner.get_all_tests'
|
||||
}).always((data) => {
|
||||
$("<div id='qunit'></div>").appendTo(wrapper.empty());
|
||||
frm.events.run_tests(frm, data.message);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
run_tests: function(frm, files) {
|
||||
let require_list = [
|
||||
"assets/frappe/js/lib/jquery/qunit.js",
|
||||
"assets/frappe/js/lib/jquery/qunit.css"
|
||||
].concat();
|
||||
|
||||
frappe.require(require_list, () => {
|
||||
files.forEach((f) => {
|
||||
frappe.dom.eval(f.script);
|
||||
});
|
||||
|
||||
// if(frm.doc.module_name) {
|
||||
// QUnit.module.only(frm.doc.module_name);
|
||||
// }
|
||||
|
||||
QUnit.testDone(function(details) {
|
||||
var result = {
|
||||
"Module name": details.module,
|
||||
"Test name": details.name,
|
||||
"Assertions": {
|
||||
"Total": details.total,
|
||||
"Passed": details.passed,
|
||||
"Failed": details.failed
|
||||
},
|
||||
"Skipped": details.skipped,
|
||||
"Todo": details.todo,
|
||||
"Runtime": details.runtime
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
});
|
||||
QUnit.load();
|
||||
QUnit.done(() => {
|
||||
frappe.set_route('Form', 'Test Runner', 'Test Runner');
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
122
frappe/core/doctype/test_runner/test_runner.json
Normal file
122
frappe/core/doctype/test_runner/test_runner.json
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2017-06-26 10:57:19.976624",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "module_path",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Module Path",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "output",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Output",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-06-26 10:57:19.976624",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Test Runner",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
27
frappe/core/doctype/test_runner/test_runner.py
Normal file
27
frappe/core/doctype/test_runner/test_runner.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, os
|
||||
from frappe.model.document import Document
|
||||
|
||||
class TestRunner(Document):
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_tests():
|
||||
tests = []
|
||||
for app in frappe.get_installed_apps():
|
||||
tests_path = frappe.get_app_path(app, 'tests', 'ui')
|
||||
if os.path.exists(tests_path):
|
||||
for basepath, folders, files in os.walk(tests_path): # pylint: disable=unused-variable
|
||||
for fname in files:
|
||||
if fname.startswith('test') and fname.endswith('.js'):
|
||||
path = os.path.join(basepath, fname)
|
||||
with open(path, 'r') as fileobj:
|
||||
tests.append(dict(
|
||||
path = os.path.relpath(frappe.get_app_path(app), path),
|
||||
script = fileobj.read()
|
||||
))
|
||||
return tests
|
||||
BIN
frappe/docs/assets/img/app-development/test-runner.png
Normal file
BIN
frappe/docs/assets/img/app-development/test-runner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
7
frappe/docs/user/en/guides/automated-testing/index.md
Normal file
7
frappe/docs/user/en/guides/automated-testing/index.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Automated Testing
|
||||
|
||||
Frappé Provides you a test framework to write and execute tests that can be run directly on a Continuous Integration Tool like Travis
|
||||
|
||||
You can write server-side unit tests or UI tests
|
||||
|
||||
{index}
|
||||
3
frappe/docs/user/en/guides/automated-testing/index.txt
Normal file
3
frappe/docs/user/en/guides/automated-testing/index.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
unit-testing
|
||||
integration-testing
|
||||
qunit-testing
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# UI Integration Testing
|
||||
|
||||
You can write integration tests using the Selenium Driver. `frappe.utils.selenium_driver` gives you a friendly API to write selenium based tests
|
||||
|
||||
To write integration tests, create a standard test case by creating a python file starting with `test_`
|
||||
|
||||
All integration tests will be run at the end of the unittests.
|
||||
|
||||
### Example
|
||||
|
||||
Here is an example of an integration test to check insertion of a To Do
|
||||
|
||||
from __future__ import print_function
|
||||
from frappe.utils.selenium_testdriver import TestDriver
|
||||
import unittest
|
||||
import time
|
||||
|
||||
class TestToDo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.driver = TestDriver()
|
||||
|
||||
def test_todo(self):
|
||||
self.driver.login()
|
||||
|
||||
# list view
|
||||
self.driver.set_route('List', 'ToDo')
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# new
|
||||
self.driver.click_primary_action()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# set input
|
||||
self.driver.set_text_editor('description', 'hello')
|
||||
|
||||
# save
|
||||
self.driver.click_modal_primary_action()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
self.assertTrue(self.driver.get_visible_element('.result-list')
|
||||
.find_element_by_css_selector('.list-item')
|
||||
.find_element_by_css_selector('.list-id').text=='hello')
|
||||
|
||||
def tearDown(self):
|
||||
self.driver.close()
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# UI Testing with Frappe API
|
||||
|
||||
You can either write integration tests, or directly write tests in Javascript using [QUnit](http://api.qunitjs.com/)
|
||||
|
||||
QUnit helps you write UI tests using the UQuit framework and native frappe API. As you might have guessed, this is a much faster way of writing tests.
|
||||
|
||||
### Test Runner
|
||||
|
||||
To write QUnit based tests, add your tests in the `tests/ui` folder of your application. Your test files must begin with `test_` and end with `.js` extension.
|
||||
|
||||
To run your files, you can use the **Test Runner**. The **Test Runner** gives a user interface to load all your QUnit tests and run them in the browser.
|
||||
|
||||
In the CI, all QUnit tests are run by the **Test Runner** using `frappe/tests/test_test_runner.py`
|
||||
|
||||
<img src="{{docs_base_url}}/assets/img/app-development/test-runner.png" class="screenshot">
|
||||
|
||||
### Example QUnit Test
|
||||
|
||||
Here is the example of the To Do test in QUnit
|
||||
|
||||
QUnit.test("test quick entry", function(assert) {
|
||||
assert.expect(2);
|
||||
let done = assert.async();
|
||||
let random = frappe.utils.get_random(10);
|
||||
|
||||
frappe.set_route('List', 'ToDo')
|
||||
.then(() => {
|
||||
return frappe.new_doc('ToDo');
|
||||
})
|
||||
.then(() => {
|
||||
frappe.quick_entry.dialog.set_value('description', random);
|
||||
return frappe.quick_entry.insert();
|
||||
})
|
||||
.then((doc) => {
|
||||
assert.ok(doc && !doc.__islocal);
|
||||
return frappe.set_route('Form', 'ToDo', doc.name);
|
||||
})
|
||||
.then(() => {
|
||||
assert.ok(cur_frm.doc.description.includes(random));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
### Writing Test Friendly Code with Promises
|
||||
|
||||
Promises are a great way to write test-friendly code. If your method calls an aysnchronous call (ajax), then you should return an `Promise` object. While writing tests, if you encounter a function that does not return a `Promise` object, you should update the code to return a `Promise` object.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Writing Tests Guide
|
||||
# Unit Testing
|
||||
|
||||
## 1.Introduction
|
||||
|
||||
|
|
@ -197,9 +197,3 @@ It’s designed for the CI Jenkins, but will work for anything else that underst
|
|||
self.assertTrue("_Test Event 3" in subjects)
|
||||
self.assertFalse("_Test Event 2" in subjects)
|
||||
|
||||
|
||||
## 4. Client Side Testing (Using Selenium)
|
||||
|
||||
This feature is still under development.
|
||||
|
||||
For an example see, [https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py](https://github.com/frappe/erpnext/blob/develop/erpnext/tests/sel_tests.py)
|
||||
|
|
@ -180,8 +180,8 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""):
|
|||
try:
|
||||
if key not in doctype_python_modules:
|
||||
doctype_python_modules[key] = frappe.get_module(module_name)
|
||||
except ImportError:
|
||||
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name))
|
||||
except ImportError, e:
|
||||
raise ImportError('Module import failed for {0} ({1})'.format(doctype, module_name + ' Error: ' + str(e)))
|
||||
|
||||
return doctype_python_modules[key]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
var chromedriver = require('chromedriver');
|
||||
module.exports = {
|
||||
before: function (done) {
|
||||
chromedriver.start();
|
||||
done();
|
||||
},
|
||||
|
||||
after: function (done) {
|
||||
chromedriver.stop();
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
const fs = require('fs');
|
||||
|
||||
const ci_mode = get_cli_arg('env') === 'ci_server';
|
||||
const site_name = get_cli_arg('site');
|
||||
let app_name = get_cli_arg('app');
|
||||
|
||||
if(!app_name) {
|
||||
console.log('Please specify app to run tests');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ci_mode && !site_name) {
|
||||
console.log('Please specify site to run tests');
|
||||
return;
|
||||
}
|
||||
|
||||
// site url
|
||||
let site_url;
|
||||
if(site_name) {
|
||||
site_url = 'http://' + site_name + ':' + get_port();
|
||||
}
|
||||
|
||||
// multiple apps
|
||||
if(app_name.includes(',')) {
|
||||
app_name = app_name.split(',');
|
||||
} else {
|
||||
app_name = [app_name];
|
||||
}
|
||||
|
||||
let test_folders = [];
|
||||
let page_objects = [];
|
||||
for(const app of app_name) {
|
||||
const test_folder = `apps/${app}/${app}/tests/ui`;
|
||||
const page_object = test_folder + '/page_objects';
|
||||
|
||||
if(!fs.existsSync(test_folder)) {
|
||||
console.log(`No test folder found for "${app}"`);
|
||||
continue;
|
||||
}
|
||||
test_folders.push(test_folder);
|
||||
|
||||
if(fs.existsSync(page_object)) {
|
||||
page_objects.push();
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
"src_folders": test_folders,
|
||||
"globals_path" : 'apps/frappe/frappe/nightwatch.global.js',
|
||||
"page_objects_path": page_objects,
|
||||
"selenium": {
|
||||
"start_process": false
|
||||
},
|
||||
"test_settings": {
|
||||
"default": {
|
||||
"launch_url": site_url,
|
||||
"selenium_port": 9515,
|
||||
"selenium_host": "127.0.0.1",
|
||||
"default_path_prefix": "",
|
||||
"silent": true,
|
||||
// "screenshots": {
|
||||
// "enabled": true,
|
||||
// "path": SCREENSHOT_PATH
|
||||
// },
|
||||
"globals": {
|
||||
"waitForConditionTimeout": 15000
|
||||
},
|
||||
"desiredCapabilities": {
|
||||
"browserName": "chrome",
|
||||
"chromeOptions": {
|
||||
"args": ["--no-sandbox", "--start-maximized"]
|
||||
},
|
||||
"javascriptEnabled": true,
|
||||
"acceptSslCerts": true
|
||||
}
|
||||
},
|
||||
"ci_server": {
|
||||
"launch_url": 'http://localhost:8000'
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = config;
|
||||
|
||||
function get_cli_arg(key) {
|
||||
var args = process.argv;
|
||||
var i = args.indexOf('--' + key);
|
||||
if(i === -1) {
|
||||
return null;
|
||||
}
|
||||
return args[i + 1];
|
||||
}
|
||||
|
||||
function get_port() {
|
||||
var bench_config = JSON.parse(fs.readFileSync('sites/common_site_config.json'));
|
||||
return bench_config.webserver_port;
|
||||
}
|
||||
|
|
@ -101,6 +101,7 @@
|
|||
|
||||
"public/js/frappe/ui/page.html",
|
||||
"public/js/frappe/ui/page.js",
|
||||
"public/js/frappe/ui/find.js",
|
||||
"public/js/frappe/ui/iconbar.js",
|
||||
"public/js/frappe/form/layout.js",
|
||||
"public/js/frappe/ui/field_group.js",
|
||||
|
|
@ -194,6 +195,8 @@
|
|||
"public/js/frappe/form/save.js",
|
||||
"public/js/frappe/form/script_manager.js",
|
||||
"public/js/frappe/form/grid.js",
|
||||
"public/js/frappe/form/grid_row.js",
|
||||
"public/js/frappe/form/grid_row_form.js",
|
||||
"public/js/frappe/form/linked_with.js",
|
||||
"public/js/frappe/form/workflow.js",
|
||||
"public/js/frappe/form/print.js",
|
||||
|
|
|
|||
|
|
@ -195,6 +195,23 @@ frappe.ellipsis = function(text, max) {
|
|||
return text;
|
||||
};
|
||||
|
||||
frappe.run_serially = function(tasks) {
|
||||
var result = Promise.resolve();
|
||||
tasks.forEach(task => {
|
||||
if(task) {
|
||||
result = result.then ? result.then(task) : Promise.resolve();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
frappe.timeout = seconds => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), seconds * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
frappe.get_modal = function(title, content) {
|
||||
return $(frappe.render_template("modal", {title:title, content:content})).appendTo(document.body);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -98,29 +98,44 @@ frappe.ui.form.Control = Class.extend({
|
|||
}
|
||||
},
|
||||
set_value: function(value) {
|
||||
this.parse_validate_and_set_in_model(value);
|
||||
return this.parse_validate_and_set_in_model(value);
|
||||
},
|
||||
parse_validate_and_set_in_model: function(value, e) {
|
||||
var me = this;
|
||||
if(this.inside_change_event) return;
|
||||
this.inside_change_event = true;
|
||||
if(this.parse) value = this.parse(value);
|
||||
|
||||
var set = function(value) {
|
||||
me.set_model_value(value);
|
||||
me.inside_change_event = false;
|
||||
me.set_mandatory && me.set_mandatory(value);
|
||||
|
||||
if(me.df.change || me.df.onchange) {
|
||||
// onchange event specified in df
|
||||
(me.df.change || me.df.onchange).apply(me, [e]);
|
||||
return new Promise(resolve => {
|
||||
if(this.inside_change_event) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.inside_change_event = true;
|
||||
if(this.parse) {
|
||||
value = this.parse(value);
|
||||
}
|
||||
}
|
||||
|
||||
this.validate ? this.validate(value, set) : set(value);
|
||||
var set = function(value) {
|
||||
me.inside_change_event = false;
|
||||
me.set_model_value(value)
|
||||
.then(() => {
|
||||
me.set_mandatory && me.set_mandatory(value);
|
||||
|
||||
if(me.df.change || me.df.onchange) {
|
||||
// onchange event specified in df
|
||||
let _promise = (me.df.change || me.df.onchange).apply(me, [e]);
|
||||
if(_promise && _promise.then) {
|
||||
_promise.then(() => { resolve(); });
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.validate ? this.validate(value, set) : set(value);
|
||||
})
|
||||
},
|
||||
get_parsed_value: function() {
|
||||
var me = this;
|
||||
if(this.get_status()==='Write') {
|
||||
return this.get_value ?
|
||||
(this.parse ? this.parse(this.get_value()) : this.get_value()) :
|
||||
|
|
@ -132,17 +147,20 @@ frappe.ui.form.Control = Class.extend({
|
|||
}
|
||||
},
|
||||
set_model_value: function(value) {
|
||||
if(this.doctype && this.docname) {
|
||||
if(frappe.model.set_value(this.doctype, this.docname, this.df.fieldname,
|
||||
value, this.df.fieldtype)) {
|
||||
return new Promise(resolve => {
|
||||
if(this.doctype && this.docname) {
|
||||
frappe.model.set_value(this.doctype, this.docname, this.df.fieldname,
|
||||
value, this.df.fieldtype)
|
||||
.then(() => resolve());
|
||||
this.last_value = value;
|
||||
} else {
|
||||
if(this.doc) {
|
||||
this.doc[this.df.fieldname] = value;
|
||||
}
|
||||
this.set_input(value);
|
||||
resolve();
|
||||
}
|
||||
} else {
|
||||
if(this.doc) {
|
||||
this.doc[this.df.fieldname] = value;
|
||||
}
|
||||
this.set_input(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
set_focus: function() {
|
||||
if(this.$input) {
|
||||
|
|
@ -878,7 +896,7 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
|
|||
},
|
||||
onclick: function() {
|
||||
if(this.frm && this.frm.doc) {
|
||||
if(this.frm.script_manager.get_handlers(this.df.fieldname, this.doctype, this.docname).length) {
|
||||
if(this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) {
|
||||
this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
|
||||
} else {
|
||||
this.frm.runscript(this.df.options, this);
|
||||
|
|
@ -1290,14 +1308,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
|
|||
frappe._from_link = this;
|
||||
frappe._from_link_scrollY = $(document).scrollTop();
|
||||
|
||||
var trimmed_doctype = doctype.replace(/ /g, '');
|
||||
var controller_name = "QuickEntryForm";
|
||||
|
||||
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
|
||||
controller_name = trimmed_doctype + "QuickEntryForm";
|
||||
}
|
||||
|
||||
new frappe.ui.form[controller_name](doctype, function(doc) {
|
||||
frappe.ui.form.make_quick_entry(doctype, (doc) => {
|
||||
if(me.frm) {
|
||||
me.parse_validate_and_set_in_model(doc.name);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,11 @@ frappe.ui.form.Grid = Class.extend({
|
|||
select_row: function(name) {
|
||||
this.grid_rows_by_docname[name].select();
|
||||
},
|
||||
remove_all: function() {
|
||||
this.grid_rows.forEach(row => {
|
||||
row.remove();
|
||||
});
|
||||
},
|
||||
refresh_remove_rows_button: function() {
|
||||
this.remove_rows_button.toggleClass('hide',
|
||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
|
||||
|
|
@ -257,7 +262,7 @@ frappe.ui.form.Grid = Class.extend({
|
|||
if (this.frm && this.frm.docname) {
|
||||
// use doc specific docfield object
|
||||
this.df = frappe.meta.get_docfield(this.frm.doctype, this.df.fieldname,
|
||||
this.frm.docname);
|
||||
this.frm.docname);
|
||||
} else {
|
||||
// use non-doc specific docfield
|
||||
if(this.df.options) {
|
||||
|
|
@ -360,8 +365,19 @@ frappe.ui.form.Grid = Class.extend({
|
|||
get_docfield: function(fieldname) {
|
||||
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null);
|
||||
},
|
||||
get_grid_row: function(docname) {
|
||||
return this.grid_rows_by_docname[docname];
|
||||
get_row: function(key) {
|
||||
if(typeof key == 'number') {
|
||||
if(key < 0) {
|
||||
return this.grid_rows[this.grid_rows.length + key];
|
||||
} else {
|
||||
return this.grid_rows[key];
|
||||
}
|
||||
} else {
|
||||
return this.grid_rows_by_docname[key];
|
||||
}
|
||||
},
|
||||
get_grid_row: function(key) {
|
||||
return this.get_row(key);
|
||||
},
|
||||
get_field: function(fieldname) {
|
||||
// Note: workaround for get_query
|
||||
|
|
@ -435,21 +451,21 @@ frappe.ui.form.Grid = Class.extend({
|
|||
&& (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm)
|
||||
&& !in_list(frappe.model.layout_fields, df.fieldtype)) {
|
||||
|
||||
if(df.columns) {
|
||||
df.colsize=df.columns;
|
||||
}
|
||||
else {
|
||||
var colsize=2;
|
||||
switch(df.fieldtype){
|
||||
case"Text":
|
||||
case"Small Text":
|
||||
colsize=3;
|
||||
break;
|
||||
case"Check":
|
||||
colsize=1
|
||||
}
|
||||
df.colsize=colsize
|
||||
if(df.columns) {
|
||||
df.colsize=df.columns;
|
||||
}
|
||||
else {
|
||||
var colsize=2;
|
||||
switch(df.fieldtype) {
|
||||
case"Text":
|
||||
case"Small Text":
|
||||
colsize=3;
|
||||
break;
|
||||
case"Check":
|
||||
colsize=1
|
||||
}
|
||||
df.colsize=colsize;
|
||||
}
|
||||
|
||||
if(df.columns) {
|
||||
df.colsize=df.columns;
|
||||
|
|
@ -642,672 +658,3 @@ frappe.ui.form.Grid = Class.extend({
|
|||
this.grid_buttons.find('.btn-custom').addClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.GridRow = Class.extend({
|
||||
init: function(opts) {
|
||||
this.on_grid_fields_dict = {};
|
||||
this.on_grid_fields = [];
|
||||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
|
||||
this.columns = {};
|
||||
this.columns_list = [];
|
||||
$.extend(this, opts);
|
||||
this.make();
|
||||
},
|
||||
make: function() {
|
||||
var me = this;
|
||||
|
||||
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this);
|
||||
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper)
|
||||
.on("click", function(e) {
|
||||
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) {
|
||||
return;
|
||||
}
|
||||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
|
||||
// pass
|
||||
} else {
|
||||
me.toggle_view();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// no checkboxes if too small
|
||||
// if(this.is_too_small()) {
|
||||
// this.row_check_html = '';
|
||||
// }
|
||||
|
||||
if(this.grid.template && !this.grid.meta.editable_grid) {
|
||||
this.render_template();
|
||||
} else {
|
||||
this.render_row();
|
||||
}
|
||||
if(this.doc) {
|
||||
this.set_data();
|
||||
}
|
||||
},
|
||||
set_data: function() {
|
||||
this.wrapper.data({
|
||||
"doc": this.doc
|
||||
})
|
||||
},
|
||||
set_row_index: function() {
|
||||
if(this.doc) {
|
||||
this.wrapper
|
||||
.attr('data-name', this.doc.name)
|
||||
.attr("data-idx", this.doc.idx)
|
||||
.find(".row-index span, .grid-form-row-index").html(this.doc.idx)
|
||||
|
||||
}
|
||||
},
|
||||
select: function(checked) {
|
||||
this.doc.__checked = checked ? 1 : 0;
|
||||
},
|
||||
refresh_check: function() {
|
||||
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false);
|
||||
this.grid.refresh_remove_rows_button();
|
||||
},
|
||||
remove: function() {
|
||||
var me = this;
|
||||
if(this.grid.is_editable()) {
|
||||
if(this.frm) {
|
||||
if(this.get_open_form()) {
|
||||
this.hide_form();
|
||||
}
|
||||
|
||||
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove",
|
||||
this.doc.doctype, this.doc.name);
|
||||
|
||||
//this.wrapper.toggle(false);
|
||||
frappe.model.clear_doc(this.doc.doctype, this.doc.name);
|
||||
|
||||
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove",
|
||||
this.doc.doctype, this.doc.name);
|
||||
this.frm.dirty();
|
||||
} else {
|
||||
this.grid.df.data = this.grid.df.data.filter(function(d) {
|
||||
return d.name !== me.doc.name;
|
||||
})
|
||||
// remap idxs
|
||||
this.grid.df.data.forEach(function(d, i) {
|
||||
d.idx = i+1;
|
||||
});
|
||||
}
|
||||
this.grid.refresh();
|
||||
}
|
||||
},
|
||||
insert: function(show, below) {
|
||||
var idx = this.doc.idx;
|
||||
if(below) idx ++;
|
||||
this.toggle_view(false);
|
||||
this.grid.add_new_row(idx, null, show);
|
||||
},
|
||||
refresh: function() {
|
||||
if(this.frm && this.doc) {
|
||||
this.doc = locals[this.doc.doctype][this.doc.name];
|
||||
}
|
||||
// re write columns
|
||||
this.visible_columns = null;
|
||||
|
||||
if(this.grid.template && !this.grid.meta.editable_grid) {
|
||||
this.render_template();
|
||||
} else {
|
||||
this.render_row(true);
|
||||
}
|
||||
|
||||
// refersh form fields
|
||||
if(this.grid_form) {
|
||||
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
|
||||
}
|
||||
},
|
||||
render_template: function() {
|
||||
this.set_row_index();
|
||||
|
||||
if(this.row_display) {
|
||||
this.row_display.remove();
|
||||
}
|
||||
var index_html = '';
|
||||
|
||||
// row index
|
||||
if(this.doc) {
|
||||
if(!this.row_index) {
|
||||
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
|
||||
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
|
||||
}
|
||||
this.row_index.find('span').html(this.doc.idx);
|
||||
}
|
||||
|
||||
this.row_display = $('<div class="row-data sortable-handle template-row">'+
|
||||
+'</div>').appendTo(this.row)
|
||||
.html(frappe.render(this.grid.template, {
|
||||
doc: this.doc ? frappe.get_format_helper(this.doc) : null,
|
||||
frm: this.frm,
|
||||
row: this
|
||||
}));
|
||||
},
|
||||
render_row: function(refresh) {
|
||||
var me = this;
|
||||
this.set_row_index();
|
||||
|
||||
// index (1, 2, 3 etc)
|
||||
if(!this.row_index) {
|
||||
var txt = (this.doc ? this.doc.idx : " ");
|
||||
this.row_index = $(
|
||||
`<div class="row-index sortable-handle col col-xs-1">
|
||||
${this.row_check_html}
|
||||
<span>${txt}</span></div>`)
|
||||
.appendTo(this.row)
|
||||
.on('click', function(e) {
|
||||
if(!$(e.target).hasClass('grid-row-check')) {
|
||||
me.toggle_view();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.row_index.find('span').html(txt);
|
||||
}
|
||||
|
||||
this.setup_columns();
|
||||
this.add_open_form_button();
|
||||
this.refresh_check();
|
||||
|
||||
if(this.frm && this.doc) {
|
||||
$(this.frm.wrapper).trigger("grid-row-render", [this]);
|
||||
}
|
||||
},
|
||||
|
||||
make_editable: function() {
|
||||
this.row.toggleClass('editable-row', this.grid.is_editable());
|
||||
},
|
||||
|
||||
is_too_small: function() {
|
||||
return this.row.width() ? this.row.width() < 300 : false;
|
||||
},
|
||||
|
||||
add_open_form_button: function() {
|
||||
var me = this;
|
||||
if(this.doc && !this.grid.df.in_place_edit) {
|
||||
// remove row
|
||||
if(!this.open_form_button) {
|
||||
this.open_form_button = $('<a class="close btn-open-row">\
|
||||
<span class="octicon octicon-triangle-down"></span></a>')
|
||||
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row))
|
||||
.on('click', function() { me.toggle_view(); return false; });
|
||||
|
||||
if(this.is_too_small()) {
|
||||
// narrow
|
||||
this.open_form_button.css({'margin-right': '-2px'});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setup_columns: function() {
|
||||
var me = this;
|
||||
this.focus_set = false;
|
||||
this.grid.setup_visible_columns();
|
||||
|
||||
for(var ci in this.grid.visible_columns) {
|
||||
var df = this.grid.visible_columns[ci][0],
|
||||
colsize = this.grid.visible_columns[ci][1],
|
||||
txt = this.doc ?
|
||||
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
|
||||
__(df.label);
|
||||
|
||||
if(this.doc && df.fieldtype === "Select") {
|
||||
txt = __(txt);
|
||||
}
|
||||
|
||||
if(!this.columns[df.fieldname]) {
|
||||
var column = this.make_column(df, colsize, txt, ci);
|
||||
} else {
|
||||
var column = this.columns[df.fieldname];
|
||||
this.refresh_field(df.fieldname, txt);
|
||||
}
|
||||
|
||||
// background color for cellz
|
||||
if(this.doc) {
|
||||
if(df.reqd && !txt) {
|
||||
column.addClass('error');
|
||||
}
|
||||
if (df.reqd || df.bold) {
|
||||
column.addClass('bold');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
make_column: function(df, colsize, txt, ci) {
|
||||
var me = this;
|
||||
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ?
|
||||
" grid-overflow-no-ellipsis" : "");
|
||||
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ?
|
||||
" text-right": "";
|
||||
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ?
|
||||
" text-center": "";
|
||||
|
||||
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>')
|
||||
.attr("data-fieldname", df.fieldname)
|
||||
.attr("data-fieldtype", df.fieldtype)
|
||||
.data("df", df)
|
||||
.appendTo(this.row)
|
||||
.on('click', function() {
|
||||
if(frappe.ui.form.editable_row===me) {
|
||||
return;
|
||||
}
|
||||
var out = me.toggle_editable_row();
|
||||
var col = this;
|
||||
setTimeout(function() {
|
||||
$(col).find('input[type="Text"]:first').focus();
|
||||
}, 500);
|
||||
return out;
|
||||
});
|
||||
|
||||
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false);
|
||||
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt);
|
||||
$col.df = df;
|
||||
$col.column_index = ci;
|
||||
|
||||
this.columns[df.fieldname] = $col;
|
||||
this.columns_list.push($col);
|
||||
|
||||
return $col;
|
||||
},
|
||||
|
||||
toggle_editable_row: function(show) {
|
||||
var me = this;
|
||||
// show static for field based on
|
||||
// whether grid is editable
|
||||
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) {
|
||||
|
||||
// disable other editale row
|
||||
if(frappe.ui.form.editable_row
|
||||
&& frappe.ui.form.editable_row !== this) {
|
||||
frappe.ui.form.editable_row.toggle_editable_row(false);
|
||||
}
|
||||
|
||||
this.row.toggleClass('editable-row', true);
|
||||
|
||||
// setup controls
|
||||
this.columns_list.forEach(function(column) {
|
||||
me.make_control(column);
|
||||
column.static_area.toggle(false);
|
||||
column.field_area.toggle(true);
|
||||
});
|
||||
|
||||
frappe.ui.form.editable_row = this;
|
||||
return false;
|
||||
} else {
|
||||
this.row.toggleClass('editable-row', false);
|
||||
this.columns_list.forEach(function(column) {
|
||||
column.static_area.toggle(true);
|
||||
column.field_area && column.field_area.toggle(false);
|
||||
});
|
||||
frappe.ui.form.editable_row = null;
|
||||
}
|
||||
},
|
||||
|
||||
make_control: function(column) {
|
||||
if(column.field) return;
|
||||
|
||||
var me = this,
|
||||
parent = column.field_area,
|
||||
df = column.df;
|
||||
|
||||
// no text editor in grid
|
||||
if (df.fieldtype=='Text Editor') {
|
||||
df.fieldtype = 'Text';
|
||||
}
|
||||
|
||||
var field = frappe.ui.form.make_control({
|
||||
df: df,
|
||||
parent: parent,
|
||||
only_input: true,
|
||||
with_link_btn: true,
|
||||
doc: this.doc,
|
||||
doctype: this.doc.doctype,
|
||||
docname: this.doc.name,
|
||||
frm: this.grid.frm,
|
||||
grid: this.grid,
|
||||
grid_row: this,
|
||||
value: this.doc[df.fieldname]
|
||||
});
|
||||
|
||||
// sync get_query
|
||||
field.get_query = this.grid.get_field(df.fieldname).get_query;
|
||||
field.refresh();
|
||||
if(field.$input) {
|
||||
field.$input
|
||||
.addClass('input-sm')
|
||||
.attr('data-col-idx', column.column_index)
|
||||
.attr('placeholder', __(df.label));
|
||||
// flag list input
|
||||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
|
||||
field.$input.attr('data-last-input', 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.set_arrow_keys(field);
|
||||
column.field = field;
|
||||
this.on_grid_fields_dict[df.fieldname] = field;
|
||||
this.on_grid_fields.push(field);
|
||||
|
||||
},
|
||||
|
||||
set_arrow_keys: function(field) {
|
||||
var me = this;
|
||||
if(field.$input) {
|
||||
field.$input.on('keydown', function(e) {
|
||||
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode;
|
||||
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var values = me.grid.get_data();
|
||||
var fieldname = $(this).attr('data-fieldname');
|
||||
var fieldtype = $(this).attr('data-fieldtype');
|
||||
|
||||
var move_up_down = function(base) {
|
||||
if(in_list(['Text', 'Small Text'], fieldtype)) {
|
||||
return;
|
||||
}
|
||||
|
||||
base.toggle_editable_row();
|
||||
setTimeout(function() {
|
||||
var input = base.columns[fieldname].field.$input;
|
||||
if(input) {
|
||||
input.focus();
|
||||
}
|
||||
}, 400)
|
||||
|
||||
}
|
||||
|
||||
// TAB
|
||||
if(e.which==TAB && !e.shiftKey) {
|
||||
// last column
|
||||
if($(this).attr('data-last-input') ||
|
||||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) {
|
||||
setTimeout(function() {
|
||||
if(me.doc.idx === values.length) {
|
||||
// last row
|
||||
me.grid.add_new_row(null, null, true);
|
||||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row();
|
||||
me.grid.set_focus_on_row();
|
||||
} else {
|
||||
me.grid.grid_rows[me.doc.idx].toggle_editable_row();
|
||||
me.grid.set_focus_on_row(me.doc.idx+1);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
} else if(e.which==UP_ARROW) {
|
||||
if(me.doc.idx > 1) {
|
||||
var prev = me.grid.grid_rows[me.doc.idx-2];
|
||||
move_up_down(prev);
|
||||
}
|
||||
} else if(e.which==DOWN_ARROW) {
|
||||
if(me.doc.idx < values.length) {
|
||||
var next = me.grid.grid_rows[me.doc.idx];
|
||||
move_up_down(next);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
get_open_form: function() {
|
||||
return frappe.ui.form.get_open_grid_form();
|
||||
},
|
||||
|
||||
toggle_view: function(show, callback) {
|
||||
if(!this.doc) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if(this.frm) {
|
||||
// reload doc
|
||||
this.doc = locals[this.doc.doctype][this.doc.name];
|
||||
}
|
||||
|
||||
// hide other
|
||||
var open_row = this.get_open_form();
|
||||
|
||||
if (show===undefined) show = !!!open_row;
|
||||
|
||||
// call blur
|
||||
document.activeElement && document.activeElement.blur();
|
||||
|
||||
if(show && open_row) {
|
||||
if(open_row==this) {
|
||||
// already open, do nothing
|
||||
callback && callback();
|
||||
return;
|
||||
} else {
|
||||
// close other views
|
||||
open_row.toggle_view(false);
|
||||
}
|
||||
}
|
||||
|
||||
if(show) {
|
||||
this.show_form();
|
||||
} else {
|
||||
this.hide_form();
|
||||
}
|
||||
callback && callback();
|
||||
|
||||
return this;
|
||||
},
|
||||
show_form: function() {
|
||||
if(!this.grid_form) {
|
||||
this.grid_form = new frappe.ui.form.GridRowForm({
|
||||
row: this
|
||||
});
|
||||
}
|
||||
this.grid_form.render();
|
||||
this.row.toggle(false);
|
||||
// this.form_panel.toggle(true);
|
||||
frappe.dom.freeze("", "dark");
|
||||
cur_frm.cur_grid = this;
|
||||
this.wrapper.addClass("grid-row-open");
|
||||
if(!frappe.dom.is_element_in_viewport(this.wrapper)) {
|
||||
frappe.utils.scroll_to(this.wrapper, true, 15);
|
||||
}
|
||||
|
||||
if(this.frm) {
|
||||
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered");
|
||||
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name);
|
||||
}
|
||||
},
|
||||
hide_form: function() {
|
||||
frappe.dom.unfreeze();
|
||||
this.row.toggle(true);
|
||||
this.refresh();
|
||||
cur_frm.cur_grid = null;
|
||||
this.wrapper.removeClass("grid-row-open");
|
||||
},
|
||||
open_prev: function() {
|
||||
if(this.grid.grid_rows[this.doc.idx-2]) {
|
||||
this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
|
||||
}
|
||||
},
|
||||
open_next: function() {
|
||||
if(this.grid.grid_rows[this.doc.idx]) {
|
||||
this.grid.grid_rows[this.doc.idx].toggle_view(true);
|
||||
} else {
|
||||
this.grid.add_new_row(null, null, true);
|
||||
}
|
||||
},
|
||||
refresh_field: function(fieldname, txt) {
|
||||
var df = this.grid.get_docfield(fieldname) || undefined;
|
||||
|
||||
// format values if no frm
|
||||
if(!df) {
|
||||
df = this.grid.visible_columns.find((col) => {
|
||||
return col[0].fieldname === fieldname;
|
||||
});
|
||||
if(df && this.doc) {
|
||||
var txt = frappe.format(this.doc[fieldname], df[0],
|
||||
null, this.doc);
|
||||
}
|
||||
}
|
||||
|
||||
if(txt===undefined && this.frm) {
|
||||
var txt = frappe.format(this.doc[fieldname], df,
|
||||
null, this.frm.doc);
|
||||
}
|
||||
|
||||
// reset static value
|
||||
var column = this.columns[fieldname];
|
||||
if(column) {
|
||||
column.static_area.html(txt || "");
|
||||
if(df && df.reqd) {
|
||||
column.toggleClass('error', !!(txt===null || txt===''));
|
||||
}
|
||||
}
|
||||
|
||||
// reset field value
|
||||
var field = this.on_grid_fields_dict[fieldname];
|
||||
if(field) {
|
||||
field.docname = this.doc.name;
|
||||
field.refresh();
|
||||
}
|
||||
|
||||
// in form
|
||||
if(this.grid_form) {
|
||||
this.grid_form.refresh_field(fieldname);
|
||||
}
|
||||
},
|
||||
get_visible_columns: function(blacklist) {
|
||||
var me = this;
|
||||
var visible_columns = $.map(this.docfields, function(df) {
|
||||
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read")
|
||||
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname);
|
||||
|
||||
return visible ? df : null;
|
||||
});
|
||||
return visible_columns;
|
||||
},
|
||||
set_field_property: function(fieldname, property, value) {
|
||||
// set a field property for open form / grid form
|
||||
var me = this;
|
||||
|
||||
var set_property = function(field) {
|
||||
if(!field) return;
|
||||
field.df[property] = value;
|
||||
field.refresh();
|
||||
}
|
||||
|
||||
// set property in grid form
|
||||
if(this.grid_form) {
|
||||
set_property(this.grid_form.fields_dict[fieldname]);
|
||||
this.grid_form.layout && this.grid_form.layout.refresh_sections();
|
||||
}
|
||||
|
||||
// set property in on grid fields
|
||||
set_property(this.on_grid_fields_dict[fieldname]);
|
||||
},
|
||||
toggle_reqd: function(fieldname, reqd) {
|
||||
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0);
|
||||
},
|
||||
toggle_display: function(fieldname, show) {
|
||||
this.set_field_property(fieldname, 'hidden', show ? 0 : 1);
|
||||
},
|
||||
toggle_editable: function(fieldname, editable) {
|
||||
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.GridRowForm = Class.extend({
|
||||
init: function(opts) {
|
||||
$.extend(this, opts);
|
||||
this.wrapper = $('<div class="form-in-grid"></div>')
|
||||
.appendTo(this.row.wrapper);
|
||||
|
||||
},
|
||||
render: function() {
|
||||
var me = this;
|
||||
this.make_form();
|
||||
this.form_area.empty();
|
||||
|
||||
this.layout = new frappe.ui.form.Layout({
|
||||
fields: this.row.docfields,
|
||||
body: this.form_area,
|
||||
no_submit_on_enter: true,
|
||||
frm: this.row.frm,
|
||||
});
|
||||
this.layout.make();
|
||||
|
||||
this.fields = this.layout.fields;
|
||||
this.fields_dict = this.layout.fields_dict;
|
||||
|
||||
this.layout.refresh(this.row.doc);
|
||||
|
||||
// copy get_query to fields
|
||||
for(var fieldname in (this.row.grid.fieldinfo || {})) {
|
||||
var fi = this.row.grid.fieldinfo[fieldname];
|
||||
$.extend(me.fields_dict[fieldname], fi);
|
||||
}
|
||||
|
||||
this.toggle_add_delete_button_display(this.wrapper);
|
||||
|
||||
this.row.grid.open_grid_row = this;
|
||||
|
||||
this.set_focus();
|
||||
},
|
||||
make_form: function() {
|
||||
if(!this.form_area) {
|
||||
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper);
|
||||
this.form_area = this.wrapper.find(".form-area");
|
||||
this.row.set_row_index();
|
||||
this.set_form_events();
|
||||
}
|
||||
},
|
||||
set_form_events: function() {
|
||||
var me = this;
|
||||
this.wrapper.find(".grid-delete-row")
|
||||
.on('click', function() {
|
||||
me.row.remove(); return false;
|
||||
});
|
||||
this.wrapper.find(".grid-insert-row")
|
||||
.on('click', function() {
|
||||
me.row.insert(true); return false;
|
||||
});
|
||||
this.wrapper.find(".grid-insert-row-below")
|
||||
.on('click', function() {
|
||||
me.row.insert(true, true); return false;
|
||||
});
|
||||
this.wrapper.find(".grid-append-row")
|
||||
.on('click', function() {
|
||||
me.row.toggle_view(false);
|
||||
me.row.grid.add_new_row(me.row.doc.idx+1, null, true);
|
||||
return false;
|
||||
});
|
||||
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() {
|
||||
me.row.toggle_view();
|
||||
return false;
|
||||
});
|
||||
},
|
||||
toggle_add_delete_button_display: function($parent) {
|
||||
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn")
|
||||
.toggle(this.row.grid.is_editable());
|
||||
},
|
||||
refresh_field: function(fieldname) {
|
||||
if(this.fields_dict[fieldname]) {
|
||||
this.fields_dict[fieldname].refresh();
|
||||
this.layout && this.layout.refresh_dependency();
|
||||
}
|
||||
},
|
||||
set_focus: function() {
|
||||
// wait for animation and then focus on the first row
|
||||
var me = this;
|
||||
setTimeout(function() {
|
||||
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) {
|
||||
var first = me.form_area.find("input:first");
|
||||
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) {
|
||||
try {
|
||||
first.get(0).focus();
|
||||
} catch(e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
586
frappe/public/js/frappe/form/grid_row.js
Normal file
586
frappe/public/js/frappe/form/grid_row.js
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
frappe.ui.form.GridRow = Class.extend({
|
||||
init: function(opts) {
|
||||
this.on_grid_fields_dict = {};
|
||||
this.on_grid_fields = [];
|
||||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
|
||||
this.columns = {};
|
||||
this.columns_list = [];
|
||||
$.extend(this, opts);
|
||||
this.make();
|
||||
},
|
||||
make: function() {
|
||||
var me = this;
|
||||
|
||||
this.wrapper = $('<div class="grid-row"></div>').appendTo(this.parent).data("grid_row", this);
|
||||
this.row = $('<div class="data-row row"></div>').appendTo(this.wrapper)
|
||||
.on("click", function(e) {
|
||||
if($(e.target).hasClass('grid-row-check') || $(e.target).hasClass('row-index') || $(e.target).parent().hasClass('row-index')) {
|
||||
return;
|
||||
}
|
||||
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
|
||||
// pass
|
||||
} else {
|
||||
me.toggle_view();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// no checkboxes if too small
|
||||
// if(this.is_too_small()) {
|
||||
// this.row_check_html = '';
|
||||
// }
|
||||
|
||||
if(this.grid.template && !this.grid.meta.editable_grid) {
|
||||
this.render_template();
|
||||
} else {
|
||||
this.render_row();
|
||||
}
|
||||
if(this.doc) {
|
||||
this.set_data();
|
||||
}
|
||||
},
|
||||
set_data: function() {
|
||||
this.wrapper.data({
|
||||
"doc": this.doc
|
||||
})
|
||||
},
|
||||
set_row_index: function() {
|
||||
if(this.doc) {
|
||||
this.wrapper
|
||||
.attr('data-name', this.doc.name)
|
||||
.attr("data-idx", this.doc.idx)
|
||||
.find(".row-index span, .grid-form-row-index").html(this.doc.idx)
|
||||
|
||||
}
|
||||
},
|
||||
select: function(checked) {
|
||||
this.doc.__checked = checked ? 1 : 0;
|
||||
},
|
||||
refresh_check: function() {
|
||||
this.wrapper.find('.grid-row-check').prop('checked', this.doc ? !!this.doc.__checked : false);
|
||||
this.grid.refresh_remove_rows_button();
|
||||
},
|
||||
remove: function() {
|
||||
var me = this;
|
||||
if(this.grid.is_editable()) {
|
||||
if(this.frm) {
|
||||
if(this.get_open_form()) {
|
||||
this.hide_form();
|
||||
}
|
||||
|
||||
this.frm.script_manager.trigger("before_" + this.grid.df.fieldname + "_remove",
|
||||
this.doc.doctype, this.doc.name);
|
||||
|
||||
//this.wrapper.toggle(false);
|
||||
frappe.model.clear_doc(this.doc.doctype, this.doc.name);
|
||||
|
||||
this.frm.script_manager.trigger(this.grid.df.fieldname + "_remove",
|
||||
this.doc.doctype, this.doc.name);
|
||||
this.frm.dirty();
|
||||
} else {
|
||||
this.grid.df.data = this.grid.df.data.filter(function(d) {
|
||||
return d.name !== me.doc.name;
|
||||
})
|
||||
// remap idxs
|
||||
this.grid.df.data.forEach(function(d, i) {
|
||||
d.idx = i+1;
|
||||
});
|
||||
}
|
||||
this.grid.refresh();
|
||||
}
|
||||
},
|
||||
insert: function(show, below) {
|
||||
var idx = this.doc.idx;
|
||||
if(below) idx ++;
|
||||
this.toggle_view(false);
|
||||
this.grid.add_new_row(idx, null, show);
|
||||
},
|
||||
refresh: function() {
|
||||
if(this.frm && this.doc) {
|
||||
this.doc = locals[this.doc.doctype][this.doc.name];
|
||||
}
|
||||
// re write columns
|
||||
this.visible_columns = null;
|
||||
|
||||
if(this.grid.template && !this.grid.meta.editable_grid) {
|
||||
this.render_template();
|
||||
} else {
|
||||
this.render_row(true);
|
||||
}
|
||||
|
||||
// refersh form fields
|
||||
if(this.grid_form) {
|
||||
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
|
||||
}
|
||||
},
|
||||
render_template: function() {
|
||||
this.set_row_index();
|
||||
|
||||
if(this.row_display) {
|
||||
this.row_display.remove();
|
||||
}
|
||||
var index_html = '';
|
||||
|
||||
// row index
|
||||
if(this.doc) {
|
||||
if(!this.row_index) {
|
||||
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
|
||||
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
|
||||
}
|
||||
this.row_index.find('span').html(this.doc.idx);
|
||||
}
|
||||
|
||||
this.row_display = $('<div class="row-data sortable-handle template-row">'+
|
||||
+'</div>').appendTo(this.row)
|
||||
.html(frappe.render(this.grid.template, {
|
||||
doc: this.doc ? frappe.get_format_helper(this.doc) : null,
|
||||
frm: this.frm,
|
||||
row: this
|
||||
}));
|
||||
},
|
||||
render_row: function(refresh) {
|
||||
var me = this;
|
||||
this.set_row_index();
|
||||
|
||||
// index (1, 2, 3 etc)
|
||||
if(!this.row_index) {
|
||||
var txt = (this.doc ? this.doc.idx : " ");
|
||||
this.row_index = $(
|
||||
`<div class="row-index sortable-handle col col-xs-1">
|
||||
${this.row_check_html}
|
||||
<span>${txt}</span></div>`)
|
||||
.appendTo(this.row)
|
||||
.on('click', function(e) {
|
||||
if(!$(e.target).hasClass('grid-row-check')) {
|
||||
me.toggle_view();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.row_index.find('span').html(txt);
|
||||
}
|
||||
|
||||
this.setup_columns();
|
||||
this.add_open_form_button();
|
||||
this.refresh_check();
|
||||
|
||||
if(this.frm && this.doc) {
|
||||
$(this.frm.wrapper).trigger("grid-row-render", [this]);
|
||||
}
|
||||
},
|
||||
|
||||
make_editable: function() {
|
||||
this.row.toggleClass('editable-row', this.grid.is_editable());
|
||||
},
|
||||
|
||||
is_too_small: function() {
|
||||
return this.row.width() ? this.row.width() < 300 : false;
|
||||
},
|
||||
|
||||
add_open_form_button: function() {
|
||||
var me = this;
|
||||
if(this.doc && !this.grid.df.in_place_edit) {
|
||||
// remove row
|
||||
if(!this.open_form_button) {
|
||||
this.open_form_button = $('<a class="close btn-open-row">\
|
||||
<span class="octicon octicon-triangle-down"></span></a>')
|
||||
.appendTo($('<div class="col col-xs-1 sortable-handle"></div>').appendTo(this.row))
|
||||
.on('click', function() { me.toggle_view(); return false; });
|
||||
|
||||
if(this.is_too_small()) {
|
||||
// narrow
|
||||
this.open_form_button.css({'margin-right': '-2px'});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setup_columns: function() {
|
||||
var me = this;
|
||||
this.focus_set = false;
|
||||
this.grid.setup_visible_columns();
|
||||
|
||||
for(var ci in this.grid.visible_columns) {
|
||||
var df = this.grid.visible_columns[ci][0],
|
||||
colsize = this.grid.visible_columns[ci][1],
|
||||
txt = this.doc ?
|
||||
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
|
||||
__(df.label);
|
||||
|
||||
if(this.doc && df.fieldtype === "Select") {
|
||||
txt = __(txt);
|
||||
}
|
||||
|
||||
if(!this.columns[df.fieldname]) {
|
||||
var column = this.make_column(df, colsize, txt, ci);
|
||||
} else {
|
||||
var column = this.columns[df.fieldname];
|
||||
this.refresh_field(df.fieldname, txt);
|
||||
}
|
||||
|
||||
// background color for cellz
|
||||
if(this.doc) {
|
||||
if(df.reqd && !txt) {
|
||||
column.addClass('error');
|
||||
}
|
||||
if (df.reqd || df.bold) {
|
||||
column.addClass('bold');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
make_column: function(df, colsize, txt, ci) {
|
||||
var me = this;
|
||||
var add_class = ((["Text", "Small Text"].indexOf(df.fieldtype)!==-1) ?
|
||||
" grid-overflow-no-ellipsis" : "");
|
||||
add_class += (["Int", "Currency", "Float", "Percent"].indexOf(df.fieldtype)!==-1) ?
|
||||
" text-right": "";
|
||||
add_class += (["Check"].indexOf(df.fieldtype)!==-1) ?
|
||||
" text-center": "";
|
||||
|
||||
var $col = $('<div class="col grid-static-col col-xs-'+colsize+' '+add_class+'"></div>')
|
||||
.attr("data-fieldname", df.fieldname)
|
||||
.attr("data-fieldtype", df.fieldtype)
|
||||
.data("df", df)
|
||||
.appendTo(this.row)
|
||||
.on('click', function() {
|
||||
if(frappe.ui.form.editable_row===me) {
|
||||
return;
|
||||
}
|
||||
var out = me.toggle_editable_row();
|
||||
var col = this;
|
||||
setTimeout(function() {
|
||||
$(col).find('input[type="Text"]:first').focus();
|
||||
}, 500);
|
||||
return out;
|
||||
});
|
||||
|
||||
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false);
|
||||
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt);
|
||||
$col.df = df;
|
||||
$col.column_index = ci;
|
||||
|
||||
this.columns[df.fieldname] = $col;
|
||||
this.columns_list.push($col);
|
||||
|
||||
return $col;
|
||||
},
|
||||
|
||||
activate: function() {
|
||||
this.toggle_editable_row(true);
|
||||
return this;
|
||||
},
|
||||
|
||||
toggle_editable_row: function(show) {
|
||||
var me = this;
|
||||
// show static for field based on
|
||||
// whether grid is editable
|
||||
if(this.grid.allow_on_grid_editing() && this.grid.is_editable() && this.doc && show !== false) {
|
||||
|
||||
// disable other editale row
|
||||
if(frappe.ui.form.editable_row
|
||||
&& frappe.ui.form.editable_row !== this) {
|
||||
frappe.ui.form.editable_row.toggle_editable_row(false);
|
||||
}
|
||||
|
||||
this.row.toggleClass('editable-row', true);
|
||||
|
||||
// setup controls
|
||||
this.columns_list.forEach(function(column) {
|
||||
me.make_control(column);
|
||||
column.static_area.toggle(false);
|
||||
column.field_area.toggle(true);
|
||||
});
|
||||
|
||||
frappe.ui.form.editable_row = this;
|
||||
return false;
|
||||
} else {
|
||||
this.row.toggleClass('editable-row', false);
|
||||
this.columns_list.forEach(function(column) {
|
||||
column.static_area.toggle(true);
|
||||
column.field_area && column.field_area.toggle(false);
|
||||
});
|
||||
frappe.ui.form.editable_row = null;
|
||||
}
|
||||
},
|
||||
|
||||
make_control: function(column) {
|
||||
if(column.field) return;
|
||||
|
||||
var me = this,
|
||||
parent = column.field_area,
|
||||
df = column.df;
|
||||
|
||||
// no text editor in grid
|
||||
if (df.fieldtype=='Text Editor') {
|
||||
df.fieldtype = 'Text';
|
||||
}
|
||||
|
||||
var field = frappe.ui.form.make_control({
|
||||
df: df,
|
||||
parent: parent,
|
||||
only_input: true,
|
||||
with_link_btn: true,
|
||||
doc: this.doc,
|
||||
doctype: this.doc.doctype,
|
||||
docname: this.doc.name,
|
||||
frm: this.grid.frm,
|
||||
grid: this.grid,
|
||||
grid_row: this,
|
||||
value: this.doc[df.fieldname]
|
||||
});
|
||||
|
||||
// sync get_query
|
||||
field.get_query = this.grid.get_field(df.fieldname).get_query;
|
||||
field.refresh();
|
||||
if(field.$input) {
|
||||
field.$input
|
||||
.addClass('input-sm')
|
||||
.attr('data-col-idx', column.column_index)
|
||||
.attr('placeholder', __(df.label));
|
||||
// flag list input
|
||||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
|
||||
field.$input.attr('data-last-input', 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.set_arrow_keys(field);
|
||||
column.field = field;
|
||||
this.on_grid_fields_dict[df.fieldname] = field;
|
||||
this.on_grid_fields.push(field);
|
||||
|
||||
},
|
||||
|
||||
set_arrow_keys: function(field) {
|
||||
var me = this;
|
||||
if(field.$input) {
|
||||
field.$input.on('keydown', function(e) {
|
||||
var { TAB, UP_ARROW, DOWN_ARROW } = frappe.ui.keyCode;
|
||||
if(!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var values = me.grid.get_data();
|
||||
var fieldname = $(this).attr('data-fieldname');
|
||||
var fieldtype = $(this).attr('data-fieldtype');
|
||||
|
||||
var move_up_down = function(base) {
|
||||
if(in_list(['Text', 'Small Text'], fieldtype)) {
|
||||
return;
|
||||
}
|
||||
|
||||
base.toggle_editable_row();
|
||||
setTimeout(function() {
|
||||
var input = base.columns[fieldname].field.$input;
|
||||
if(input) {
|
||||
input.focus();
|
||||
}
|
||||
}, 400)
|
||||
|
||||
}
|
||||
|
||||
// TAB
|
||||
if(e.which==TAB && !e.shiftKey) {
|
||||
// last column
|
||||
if($(this).attr('data-last-input') ||
|
||||
me.grid.wrapper.find('.grid-row :input:enabled:last').get(0)===this) {
|
||||
setTimeout(function() {
|
||||
if(me.doc.idx === values.length) {
|
||||
// last row
|
||||
me.grid.add_new_row(null, null, true);
|
||||
me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row();
|
||||
me.grid.set_focus_on_row();
|
||||
} else {
|
||||
me.grid.grid_rows[me.doc.idx].toggle_editable_row();
|
||||
me.grid.set_focus_on_row(me.doc.idx+1);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
} else if(e.which==UP_ARROW) {
|
||||
if(me.doc.idx > 1) {
|
||||
var prev = me.grid.grid_rows[me.doc.idx-2];
|
||||
move_up_down(prev);
|
||||
}
|
||||
} else if(e.which==DOWN_ARROW) {
|
||||
if(me.doc.idx < values.length) {
|
||||
var next = me.grid.grid_rows[me.doc.idx];
|
||||
move_up_down(next);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
get_open_form: function() {
|
||||
return frappe.ui.form.get_open_grid_form();
|
||||
},
|
||||
|
||||
toggle_view: function(show, callback) {
|
||||
if(!this.doc) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if(this.frm) {
|
||||
// reload doc
|
||||
this.doc = locals[this.doc.doctype][this.doc.name];
|
||||
}
|
||||
|
||||
// hide other
|
||||
var open_row = this.get_open_form();
|
||||
|
||||
if (show===undefined) show = !!!open_row;
|
||||
|
||||
// call blur
|
||||
document.activeElement && document.activeElement.blur();
|
||||
|
||||
if(show && open_row) {
|
||||
if(open_row==this) {
|
||||
// already open, do nothing
|
||||
callback && callback();
|
||||
return;
|
||||
} else {
|
||||
// close other views
|
||||
open_row.toggle_view(false);
|
||||
}
|
||||
}
|
||||
|
||||
if(show) {
|
||||
this.show_form();
|
||||
} else {
|
||||
this.hide_form();
|
||||
}
|
||||
callback && callback();
|
||||
|
||||
return this;
|
||||
},
|
||||
show_form: function() {
|
||||
if(!this.grid_form) {
|
||||
this.grid_form = new frappe.ui.form.GridRowForm({
|
||||
row: this
|
||||
});
|
||||
}
|
||||
this.grid_form.render();
|
||||
this.row.toggle(false);
|
||||
// this.form_panel.toggle(true);
|
||||
frappe.dom.freeze("", "dark");
|
||||
cur_frm.cur_grid = this;
|
||||
this.wrapper.addClass("grid-row-open");
|
||||
if(!frappe.dom.is_element_in_viewport(this.wrapper)) {
|
||||
frappe.utils.scroll_to(this.wrapper, true, 15);
|
||||
}
|
||||
|
||||
if(this.frm) {
|
||||
this.frm.script_manager.trigger(this.doc.parentfield + "_on_form_rendered");
|
||||
this.frm.script_manager.trigger("form_render", this.doc.doctype, this.doc.name);
|
||||
}
|
||||
},
|
||||
hide_form: function() {
|
||||
frappe.dom.unfreeze();
|
||||
this.row.toggle(true);
|
||||
this.refresh();
|
||||
cur_frm.cur_grid = null;
|
||||
this.wrapper.removeClass("grid-row-open");
|
||||
},
|
||||
open_prev: function() {
|
||||
if(this.grid.grid_rows[this.doc.idx-2]) {
|
||||
this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
|
||||
}
|
||||
},
|
||||
open_next: function() {
|
||||
if(this.grid.grid_rows[this.doc.idx]) {
|
||||
this.grid.grid_rows[this.doc.idx].toggle_view(true);
|
||||
} else {
|
||||
this.grid.add_new_row(null, null, true);
|
||||
}
|
||||
},
|
||||
refresh_field: function(fieldname, txt) {
|
||||
var df = this.grid.get_docfield(fieldname) || undefined;
|
||||
|
||||
// format values if no frm
|
||||
if(!df) {
|
||||
df = this.grid.visible_columns.find((col) => {
|
||||
return col[0].fieldname === fieldname;
|
||||
});
|
||||
if(df && this.doc) {
|
||||
var txt = frappe.format(this.doc[fieldname], df[0],
|
||||
null, this.doc);
|
||||
}
|
||||
}
|
||||
|
||||
if(txt===undefined && this.frm) {
|
||||
var txt = frappe.format(this.doc[fieldname], df,
|
||||
null, this.frm.doc);
|
||||
}
|
||||
|
||||
// reset static value
|
||||
var column = this.columns[fieldname];
|
||||
if(column) {
|
||||
column.static_area.html(txt || "");
|
||||
if(df && df.reqd) {
|
||||
column.toggleClass('error', !!(txt===null || txt===''));
|
||||
}
|
||||
}
|
||||
|
||||
// reset field value
|
||||
var field = this.on_grid_fields_dict[fieldname];
|
||||
if(field) {
|
||||
field.docname = this.doc.name;
|
||||
field.refresh();
|
||||
}
|
||||
|
||||
// in form
|
||||
if(this.grid_form) {
|
||||
this.grid_form.refresh_field(fieldname);
|
||||
}
|
||||
},
|
||||
get_field: function(fieldname) {
|
||||
let field = this.on_grid_fields_dict[fieldname];
|
||||
if (field) {
|
||||
return field;
|
||||
} else if(this.grid_form) {
|
||||
return this.grid_form.fields_dict[fieldname];
|
||||
} else {
|
||||
throw `fieldname ${fieldname} not found`;
|
||||
}
|
||||
},
|
||||
|
||||
get_visible_columns: function(blacklist) {
|
||||
var me = this;
|
||||
var visible_columns = $.map(this.docfields, function(df) {
|
||||
var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read")
|
||||
&& !in_list(frappe.model.layout_fields, df.fieldtype) && !in_list(blacklist, df.fieldname);
|
||||
|
||||
return visible ? df : null;
|
||||
});
|
||||
return visible_columns;
|
||||
},
|
||||
set_field_property: function(fieldname, property, value) {
|
||||
// set a field property for open form / grid form
|
||||
var me = this;
|
||||
|
||||
var set_property = function(field) {
|
||||
if(!field) return;
|
||||
field.df[property] = value;
|
||||
field.refresh();
|
||||
}
|
||||
|
||||
// set property in grid form
|
||||
if(this.grid_form) {
|
||||
set_property(this.grid_form.fields_dict[fieldname]);
|
||||
this.grid_form.layout && this.grid_form.layout.refresh_sections();
|
||||
}
|
||||
|
||||
// set property in on grid fields
|
||||
set_property(this.on_grid_fields_dict[fieldname]);
|
||||
},
|
||||
toggle_reqd: function(fieldname, reqd) {
|
||||
this.set_field_property(fieldname, 'reqd', reqd ? 1 : 0);
|
||||
},
|
||||
toggle_display: function(fieldname, show) {
|
||||
this.set_field_property(fieldname, 'hidden', show ? 0 : 1);
|
||||
},
|
||||
toggle_editable: function(fieldname, editable) {
|
||||
this.set_field_property(fieldname, 'read_only', editable ? 0 : 1);
|
||||
},
|
||||
});
|
||||
97
frappe/public/js/frappe/form/grid_row_form.js
Normal file
97
frappe/public/js/frappe/form/grid_row_form.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
frappe.ui.form.GridRowForm = Class.extend({
|
||||
init: function(opts) {
|
||||
$.extend(this, opts);
|
||||
this.wrapper = $('<div class="form-in-grid"></div>')
|
||||
.appendTo(this.row.wrapper);
|
||||
|
||||
},
|
||||
render: function() {
|
||||
var me = this;
|
||||
this.make_form();
|
||||
this.form_area.empty();
|
||||
|
||||
this.layout = new frappe.ui.form.Layout({
|
||||
fields: this.row.docfields,
|
||||
body: this.form_area,
|
||||
no_submit_on_enter: true,
|
||||
frm: this.row.frm,
|
||||
});
|
||||
this.layout.make();
|
||||
|
||||
this.fields = this.layout.fields;
|
||||
this.fields_dict = this.layout.fields_dict;
|
||||
|
||||
this.layout.refresh(this.row.doc);
|
||||
|
||||
// copy get_query to fields
|
||||
for(var fieldname in (this.row.grid.fieldinfo || {})) {
|
||||
var fi = this.row.grid.fieldinfo[fieldname];
|
||||
$.extend(me.fields_dict[fieldname], fi);
|
||||
}
|
||||
|
||||
this.toggle_add_delete_button_display(this.wrapper);
|
||||
|
||||
this.row.grid.open_grid_row = this;
|
||||
|
||||
this.set_focus();
|
||||
},
|
||||
make_form: function() {
|
||||
if(!this.form_area) {
|
||||
$(frappe.render_template("grid_form", {grid:this})).appendTo(this.wrapper);
|
||||
this.form_area = this.wrapper.find(".form-area");
|
||||
this.row.set_row_index();
|
||||
this.set_form_events();
|
||||
}
|
||||
},
|
||||
set_form_events: function() {
|
||||
var me = this;
|
||||
this.wrapper.find(".grid-delete-row")
|
||||
.on('click', function() {
|
||||
me.row.remove(); return false;
|
||||
});
|
||||
this.wrapper.find(".grid-insert-row")
|
||||
.on('click', function() {
|
||||
me.row.insert(true); return false;
|
||||
});
|
||||
this.wrapper.find(".grid-insert-row-below")
|
||||
.on('click', function() {
|
||||
me.row.insert(true, true); return false;
|
||||
});
|
||||
this.wrapper.find(".grid-append-row")
|
||||
.on('click', function() {
|
||||
me.row.toggle_view(false);
|
||||
me.row.grid.add_new_row(me.row.doc.idx+1, null, true);
|
||||
return false;
|
||||
});
|
||||
this.wrapper.find(".grid-form-heading, .grid-footer-toolbar").on("click", function() {
|
||||
me.row.toggle_view();
|
||||
return false;
|
||||
});
|
||||
},
|
||||
toggle_add_delete_button_display: function($parent) {
|
||||
$parent.find(".grid-header-toolbar .btn, .grid-footer-toolbar .btn")
|
||||
.toggle(this.row.grid.is_editable());
|
||||
},
|
||||
refresh_field: function(fieldname) {
|
||||
if(this.fields_dict[fieldname]) {
|
||||
this.fields_dict[fieldname].refresh();
|
||||
this.layout && this.layout.refresh_dependency();
|
||||
}
|
||||
},
|
||||
set_focus: function() {
|
||||
// wait for animation and then focus on the first row
|
||||
var me = this;
|
||||
setTimeout(function() {
|
||||
if(me.row.frm && me.row.frm.doc.docstatus===0 || !me.row.frm) {
|
||||
var first = me.form_area.find("input:first");
|
||||
if(first.length && !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))) {
|
||||
try {
|
||||
first.get(0).focus();
|
||||
} catch(e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
});
|
||||
|
|
@ -1,20 +1,37 @@
|
|||
frappe.provide('frappe.ui.form');
|
||||
|
||||
frappe.ui.form.make_quick_entry = (doctype, after_insert) => {
|
||||
var trimmed_doctype = doctype.replace(/ /g, '');
|
||||
var controller_name = "QuickEntryForm";
|
||||
|
||||
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
|
||||
controller_name = trimmed_doctype + "QuickEntryForm";
|
||||
}
|
||||
|
||||
frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert);
|
||||
return frappe.quick_entry.setup();
|
||||
};
|
||||
|
||||
frappe.ui.form.QuickEntryForm = Class.extend({
|
||||
init: function(doctype, success_function){
|
||||
init: function(doctype, after_insert){
|
||||
this.doctype = doctype;
|
||||
this.success_function = success_function;
|
||||
this.setup();
|
||||
this.after_insert = after_insert;
|
||||
},
|
||||
|
||||
setup: function(){
|
||||
var me = this;
|
||||
frappe.model.with_doctype(this.doctype, function() {
|
||||
me.set_meta_and_mandatory_fields();
|
||||
var validate_flag = me.validate_quick_entry();
|
||||
if(!validate_flag){
|
||||
me.render_dialog();
|
||||
}
|
||||
setup: function() {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.model.with_doctype(this.doctype, function() {
|
||||
me.set_meta_and_mandatory_fields();
|
||||
if(me.is_quick_entry()) {
|
||||
me.render_dialog();
|
||||
resolve(me);
|
||||
} else {
|
||||
frappe.quick_entry = null;
|
||||
frappe.set_route('Form', me.doctype, me.doc.name)
|
||||
.then(() => resolve(me));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -25,34 +42,34 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
this.doc = frappe.model.get_new_doc(this.doctype, null, null, true);
|
||||
},
|
||||
|
||||
validate_quick_entry: function(){
|
||||
is_quick_entry: function(){
|
||||
if(this.meta.quick_entry != 1) {
|
||||
frappe.set_route('Form', this.doctype, this.doc.name);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
var mandatory_flag = this.validate_mandatory_length();
|
||||
var child_table_flag = this.validate_for_child_table();
|
||||
|
||||
if (mandatory_flag || child_table_flag){
|
||||
return true;
|
||||
if (this.too_many_mandatory_fields() || this.has_child_table()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.validate_for_prompt_autoname();
|
||||
return true;
|
||||
},
|
||||
|
||||
validate_mandatory_length: function(){
|
||||
too_many_mandatory_fields: function(){
|
||||
if(this.mandatory.length > 7) {
|
||||
// too many fields, show form
|
||||
frappe.set_route('Form', this.doctype, this.doc.name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
validate_for_child_table: function(){
|
||||
if($.map(this.mandatory, function(d) { return d.fieldtype==='Table' ? d : null; }).length) {
|
||||
has_child_table: function(){
|
||||
if($.map(this.mandatory, function(d) {
|
||||
return d.fieldtype==='Table' ? d : null; }).length) {
|
||||
// has mandatory table, quit!
|
||||
frappe.set_route('Form', this.doctype, this.doc.name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
validate_for_prompt_autoname: function(){
|
||||
|
|
@ -86,6 +103,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
}
|
||||
});
|
||||
|
||||
this.dialog.onhide = () => frappe.quick_entry = null;
|
||||
this.dialog.show();
|
||||
this.set_defaults();
|
||||
},
|
||||
|
|
@ -93,44 +111,62 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
register_primary_action: function(){
|
||||
var me = this;
|
||||
this.dialog.set_primary_action(__('Save'), function() {
|
||||
if(me.dialog.working) return;
|
||||
if(me.dialog.working) {
|
||||
return;
|
||||
}
|
||||
var data = me.dialog.get_values();
|
||||
|
||||
if(data) {
|
||||
me.dialog.working = true;
|
||||
var values = me.update_doc();
|
||||
me.insert_document(values);
|
||||
me.insert();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
insert_document: function(values){
|
||||
var me = this;
|
||||
frappe.call({
|
||||
method: "frappe.client.insert",
|
||||
args: {
|
||||
doc: values
|
||||
},
|
||||
callback: function(r) {
|
||||
me.dialog.hide();
|
||||
// delete the old doc
|
||||
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
|
||||
var doc = r.message;
|
||||
if(me.success_function) {
|
||||
me.success_function(doc);
|
||||
}
|
||||
frappe.ui.form.update_calling_link(doc.name);
|
||||
},
|
||||
error: function() {
|
||||
me.open_doc();
|
||||
},
|
||||
always: function() {
|
||||
me.dialog.working = false;
|
||||
},
|
||||
freeze: true
|
||||
insert: function() {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
me.update_doc();
|
||||
frappe.call({
|
||||
method: "frappe.client.insert",
|
||||
args: {
|
||||
doc: me.dialog.doc
|
||||
},
|
||||
callback: function(r) {
|
||||
me.dialog.hide();
|
||||
// delete the old doc
|
||||
frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
|
||||
me.dialog.doc = r.message;
|
||||
if(frappe._from_link) {
|
||||
frappe.ui.form.update_calling_link(me.dialog.doc.name);
|
||||
} else {
|
||||
if(me.after_insert) {
|
||||
me.after_insert(me.dialig.doc);
|
||||
} else {
|
||||
me.open_from_if_not_list();
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
me.open_doc();
|
||||
},
|
||||
always: function() {
|
||||
me.dialog.working = false;
|
||||
resolve(me.dialog.doc);
|
||||
},
|
||||
freeze: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
open_from_if_not_list: function() {
|
||||
let route = frappe.get_route();
|
||||
let doc = this.dialog.doc;
|
||||
if(route && !(route[0]==='List' && route[1]===doc.doctype)) {
|
||||
frappe.set_route('Form', doc.doctype, doc.name);
|
||||
}
|
||||
},
|
||||
|
||||
update_doc: function(){
|
||||
var me = this;
|
||||
var data = this.dialog.get_values(true);
|
||||
|
|
|
|||
|
|
@ -226,8 +226,11 @@ frappe.ui.form.update_calling_link = function (newdoc) {
|
|||
|
||||
// if from form, switch
|
||||
if (frappe._from_link.frm) {
|
||||
frappe.set_route("Form", frappe._from_link.frm.doctype, frappe._from_link.frm.docname);
|
||||
setTimeout(function () { frappe.utils.scroll_to(frappe._from_link_scrollY); }, 100);
|
||||
frappe.set_route("Form",
|
||||
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
|
||||
.then(() => {
|
||||
frappe.utils.scroll_to(frappe._from_link_scrollY);
|
||||
});
|
||||
}
|
||||
|
||||
frappe._from_link = null;
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ frappe.ui.form.off = function(doctype, fieldname, handler) {
|
|||
}
|
||||
|
||||
|
||||
frappe.ui.form.trigger = function(doctype, fieldname, callback) {
|
||||
cur_frm.script_manager.trigger(fieldname, doctype, null, callback);
|
||||
frappe.ui.form.trigger = function(doctype, fieldname) {
|
||||
cur_frm.script_manager.trigger(fieldname, doctype);
|
||||
}
|
||||
|
||||
frappe.ui.form.ScriptManager = Class.extend({
|
||||
|
|
@ -64,32 +64,76 @@ frappe.ui.form.ScriptManager = Class.extend({
|
|||
$.extend(this, opts);
|
||||
},
|
||||
make: function(ControllerClass) {
|
||||
this.frm.cscript = $.extend(this.frm.cscript, new ControllerClass({frm: this.frm}));
|
||||
this.frm.cscript = $.extend(this.frm.cscript,
|
||||
new ControllerClass({frm: this.frm}));
|
||||
},
|
||||
trigger: function(event_name, doctype, name, callback) {
|
||||
var me = this;
|
||||
doctype = doctype || this.frm.doctype;
|
||||
name = name || this.frm.docname;
|
||||
var handlers = this.get_handlers(event_name, doctype, name, callback);
|
||||
if(callback) handlers.push(callback);
|
||||
trigger: function(event_name, doctype, name) {
|
||||
// trigger all the form level events that
|
||||
// are bound to this event_name
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
doctype = doctype || this.frm.doctype;
|
||||
name = name || this.frm.docname;
|
||||
|
||||
this.frm.selected_doc = frappe.get_doc(doctype, name);
|
||||
let tasks = [];
|
||||
let handlers = this.get_handlers(event_name, doctype);
|
||||
|
||||
return $.when.apply($, $.map(handlers, function(fn) { return fn(); }));
|
||||
// helper for child table
|
||||
this.frm.selected_doc = frappe.get_doc(doctype, name);
|
||||
|
||||
let runner = (_function, is_old_style) => {
|
||||
let _promise = null;
|
||||
if(is_old_style) {
|
||||
// old style arguments (doc, cdt, cdn)
|
||||
_promise = me.frm.cscript[_function](me.frm.doc, doctype, name);
|
||||
} else {
|
||||
// new style (frm, doctype, name)
|
||||
_promise = _function(me.frm, doctype, name);
|
||||
}
|
||||
|
||||
// if the trigger returns a promise, return it,
|
||||
// or use the default promise frappe.after_ajax
|
||||
if (_promise && _promise.then) {
|
||||
return _promise;
|
||||
} else {
|
||||
return frappe.after_server_call();
|
||||
}
|
||||
};
|
||||
|
||||
// make list of functions to be run serially
|
||||
handlers.new_style.forEach((_function) => {
|
||||
tasks.push(() => runner(_function, false));
|
||||
});
|
||||
|
||||
handlers.old_style.forEach((_function) => {
|
||||
tasks.push(() => runner(_function, true));
|
||||
});
|
||||
|
||||
// run them serially
|
||||
frappe.run_serially(tasks).then(resolve());
|
||||
});
|
||||
},
|
||||
get_handlers: function(event_name, doctype, name, callback) {
|
||||
var handlers = [];
|
||||
var me = this;
|
||||
has_handlers: function(event_name, doctype) {
|
||||
let handlers = this.get_handlers(event_name, doctype);
|
||||
return handlers && (handlers.old_style.length || handlers.new_style.length);
|
||||
},
|
||||
get_handlers: function(event_name, doctype) {
|
||||
// returns list of all functions to be called (old style and new style)
|
||||
let me = this;
|
||||
let handlers = {
|
||||
old_style: [],
|
||||
new_style: []
|
||||
};
|
||||
if(frappe.ui.form.handlers[doctype] && frappe.ui.form.handlers[doctype][event_name]) {
|
||||
$.each(frappe.ui.form.handlers[doctype][event_name], function(i, fn) {
|
||||
handlers.push(function() { return fn(me.frm, doctype, name) });
|
||||
handlers.new_style.push(fn);
|
||||
});
|
||||
}
|
||||
if(this.frm.cscript[event_name]) {
|
||||
handlers.push(function() { return me.frm.cscript[event_name](me.frm.doc, doctype, name); });
|
||||
handlers.old_style.push(event_name);
|
||||
}
|
||||
if(this.frm.cscript["custom_" + event_name]) {
|
||||
handlers.push(function() { return me.frm.cscript["custom_" + event_name](me.frm.doc, doctype, name); });
|
||||
handlers.old_style.push("custom_" + event_name);
|
||||
}
|
||||
return handlers;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -313,32 +313,20 @@ $.extend(frappe.model, {
|
|||
|
||||
frappe.create_routes = {};
|
||||
frappe.new_doc = function (doctype, opts) {
|
||||
if(opts && $.isPlainObject(opts)) { frappe.route_options = opts; }
|
||||
frappe.model.with_doctype(doctype, function() {
|
||||
if(frappe.create_routes[doctype]) {
|
||||
frappe.set_route(frappe.create_routes[doctype]);
|
||||
} else {
|
||||
var trimmed_doctype = doctype.replace(/ /g, '');
|
||||
var controller_name = "QuickEntryForm";
|
||||
|
||||
if(frappe.ui.form[trimmed_doctype + "QuickEntryForm"]){
|
||||
controller_name = trimmed_doctype + "QuickEntryForm";
|
||||
}
|
||||
|
||||
new frappe.ui.form[controller_name](doctype, function(doc) {
|
||||
//frappe.set_route('List', doctype);
|
||||
var title = doc.name;
|
||||
var title_field = frappe.get_meta(doc.doctype).title_field;
|
||||
if (title_field) {
|
||||
title = doc[title_field];
|
||||
}
|
||||
|
||||
var route = frappe.get_route();
|
||||
if(route && !(route[0]==='List' && route[1]===doc.doctype)) {
|
||||
frappe.set_route('Form', doc.doctype, doc.name);
|
||||
}
|
||||
});
|
||||
return new Promise(resolve => {
|
||||
if(opts && $.isPlainObject(opts)) {
|
||||
frappe.route_options = opts;
|
||||
}
|
||||
frappe.model.with_doctype(doctype, function() {
|
||||
if(frappe.create_routes[doctype]) {
|
||||
frappe.set_route(frappe.create_routes[doctype])
|
||||
.then(() => resolve());
|
||||
} else {
|
||||
frappe.ui.form.make_quick_entry(doctype)
|
||||
.then(() => resolve());
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,8 @@ $.extend(frappe.meta, {
|
|||
});
|
||||
|
||||
if(!out) {
|
||||
frappe.msgprint(__('Warning: Unable to find {0} in any table related to {1}', [
|
||||
// eslint-disable-next-line
|
||||
console.log(__('Warning: Unable to find {0} in any table related to {1}', [
|
||||
key, __(doctype)]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,9 +327,11 @@ $.extend(frappe.model, {
|
|||
|
||||
set_value: function(doctype, docname, fieldname, value, fieldtype) {
|
||||
/* help: Set a value locally (if changed) and execute triggers */
|
||||
|
||||
var doc = locals[doctype] && locals[doctype][docname];
|
||||
|
||||
var to_update = fieldname;
|
||||
let tasks = [];
|
||||
if(!$.isPlainObject(to_update)) {
|
||||
to_update = {};
|
||||
to_update[fieldname] = value;
|
||||
|
|
@ -343,14 +345,16 @@ $.extend(frappe.model, {
|
|||
}
|
||||
|
||||
doc[key] = value;
|
||||
frappe.model.trigger(key, value, doc);
|
||||
tasks.push(() => frappe.model.trigger(key, value, doc));
|
||||
} else {
|
||||
// execute link triggers (want to reselect to execute triggers)
|
||||
if(fieldtype=="Link" && doc) {
|
||||
frappe.model.trigger(key, value, doc);
|
||||
tasks.push(() => frappe.model.trigger(key, value, doc));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return frappe.run_serially(tasks);
|
||||
},
|
||||
|
||||
on: function(doctype, fieldname, fn) {
|
||||
|
|
@ -371,21 +375,34 @@ $.extend(frappe.model, {
|
|||
},
|
||||
|
||||
trigger: function(fieldname, value, doc) {
|
||||
|
||||
var run = function(events, event_doc) {
|
||||
let tasks = [];
|
||||
var runner = function(events, event_doc) {
|
||||
$.each(events || [], function(i, fn) {
|
||||
fn && fn(fieldname, value, event_doc || doc);
|
||||
if(fn) {
|
||||
let _promise = fn(fieldname, value, event_doc || doc);
|
||||
|
||||
// if the trigger returns a promise, return it,
|
||||
// or use the default promise frappe.after_ajax
|
||||
if (_promise && _promise.then) {
|
||||
return _promise;
|
||||
} else {
|
||||
return frappe.after_server_call();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if(frappe.model.events[doc.doctype]) {
|
||||
tasks.push(() => {
|
||||
return runner(frappe.model.events[doc.doctype][fieldname]);
|
||||
});
|
||||
|
||||
// field-level
|
||||
run(frappe.model.events[doc.doctype][fieldname]);
|
||||
|
||||
// doctype-level
|
||||
run(frappe.model.events[doc.doctype]['*']);
|
||||
tasks.push(() => {
|
||||
runner(frappe.model.events[doc.doctype]['*']);
|
||||
});
|
||||
}
|
||||
|
||||
frappe.run_serially(tasks);
|
||||
},
|
||||
|
||||
get_doc: function(doctype, name) {
|
||||
|
|
|
|||
|
|
@ -26,4 +26,4 @@ frappe.provide("frappe.utils");
|
|||
frappe.provide("frappe.ui");
|
||||
frappe.provide("frappe.modules");
|
||||
frappe.provide("frappe.templates");
|
||||
|
||||
frappe.provide("frappe.test_data");
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ frappe.request.waiting_for_ajax = [];
|
|||
|
||||
// generic server call (call page, object)
|
||||
frappe.call = function(opts) {
|
||||
if(opts.quiet)
|
||||
if(opts.quiet) {
|
||||
opts.no_spinner = true;
|
||||
}
|
||||
var args = $.extend({}, opts.args);
|
||||
|
||||
// cmd
|
||||
|
|
@ -302,13 +303,31 @@ frappe.request.cleanup = function(opts, r) {
|
|||
}
|
||||
}
|
||||
|
||||
frappe.after_ajax = function(fn) {
|
||||
frappe.after_server_call = () => {
|
||||
if(frappe.request.ajax_count) {
|
||||
frappe.request.waiting_for_ajax.push(fn);
|
||||
return new Promise(resolve => {
|
||||
frappe.request.waiting_for_ajax.push(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
fn();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
frappe.after_ajax = function(fn) {
|
||||
return new Promise(resolve => {
|
||||
if(frappe.request.ajax_count) {
|
||||
frappe.request.waiting_for_ajax.push(() => {
|
||||
if(fn) fn();
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
if(fn) fn();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
frappe.request.report_error = function(xhr, request_opts) {
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
|
|
|
|||
|
|
@ -123,24 +123,31 @@ frappe.get_route_str = function(route) {
|
|||
}
|
||||
|
||||
frappe.set_route = function() {
|
||||
var params = arguments;
|
||||
if(params.length===1 && $.isArray(params[0])) {
|
||||
params = params[0];
|
||||
}
|
||||
var route = $.map(params, function(a) {
|
||||
if($.isPlainObject(a)) {
|
||||
frappe.route_options = a;
|
||||
return null;
|
||||
} else {
|
||||
return a;
|
||||
// return a ? encodeURIComponent(a) : null;
|
||||
return new Promise(resolve => {
|
||||
var params = arguments;
|
||||
if(params.length===1 && $.isArray(params[0])) {
|
||||
params = params[0];
|
||||
}
|
||||
}).join('/');
|
||||
var route = $.map(params, function(a) {
|
||||
if($.isPlainObject(a)) {
|
||||
frappe.route_options = a;
|
||||
return null;
|
||||
} else {
|
||||
return a;
|
||||
// return a ? encodeURIComponent(a) : null;
|
||||
}
|
||||
}).join('/');
|
||||
|
||||
window.location.hash = route;
|
||||
window.location.hash = route;
|
||||
|
||||
// Set favicon (app.js)
|
||||
frappe.app.set_favicon && frappe.app.set_favicon();
|
||||
// Set favicon (app.js)
|
||||
frappe.app.set_favicon && frappe.app.set_favicon();
|
||||
setTimeout(() => {
|
||||
frappe.after_ajax(() => {
|
||||
resolve();
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
frappe.set_re_route = function() {
|
||||
|
|
|
|||
|
|
@ -192,7 +192,6 @@ frappe.ui.BaseList = Class.extend({
|
|||
onchange: () => { me.refresh(true); }
|
||||
});
|
||||
|
||||
var has_standard_filters = false;
|
||||
this.meta.fields.forEach(function(df) {
|
||||
if(df.in_standard_filter) {
|
||||
if(df.fieldtype == "Select" && df.options) {
|
||||
|
|
@ -205,17 +204,13 @@ frappe.ui.BaseList = Class.extend({
|
|||
me.page.add_field({
|
||||
fieldtype: df.fieldtype,
|
||||
label: __(df.label),
|
||||
options: df.options,
|
||||
options: options,
|
||||
fieldname: df.fieldname,
|
||||
onchange: () => {me.refresh(true);}
|
||||
onchange: () => { me.refresh(true); }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.page.page_form.on('change', ':input', function() {
|
||||
me.refresh(true);
|
||||
});
|
||||
|
||||
this.standard_filters_added = true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -89,11 +89,18 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({
|
|||
return f && (f.get_parsed_value ? f.get_parsed_value() : null);
|
||||
},
|
||||
set_value: function(key, val){
|
||||
var f = this.fields_dict[key];
|
||||
if(f) {
|
||||
f.set_input(val);
|
||||
this.refresh_dependency();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
var f = this.fields_dict[key];
|
||||
if(f) {
|
||||
f.set_value(val).then(() => {
|
||||
f.set_input(val);
|
||||
this.refresh_dependency();
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
set_input: function(key, val) {
|
||||
return this.set_value(key, val);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@
|
|||
<div class="col-sm-6 col-xs-12">
|
||||
<div class="filter_field pull-left" style="width: calc(100% - 70px)"></div>
|
||||
<div class="filter-actions pull-left">
|
||||
<a class="set-filter-and-run btn btn-primary pull-left"><i class=" fa fa-check"></i></a>
|
||||
<a class="small grey remove-filter pull-left"><i class="octicon octicon-trashcan visible-xs"></i>
|
||||
<a class="set-filter-and-run btn btn-sm btn-primary pull-left">
|
||||
<i class=" fa fa-check visible-xs"></i>
|
||||
<span class="hidden-xs">{%= __("Apply") %}</span></a>
|
||||
<a class="small grey remove-filter pull-left">
|
||||
<i class="octicon octicon-trashcan visible-xs"></i>
|
||||
<span class="hidden-xs">{%= __("Remove") %}</span></a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
|
|||
16
frappe/public/js/frappe/ui/find.js
Normal file
16
frappe/public/js/frappe/ui/find.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
frappe.find = {
|
||||
page_primary_action: () => {
|
||||
return $('.page-actions:visible .btn-primary');
|
||||
},
|
||||
field: (fieldname, value) => {
|
||||
return new Promise(resolve => {
|
||||
let input = $(`[data-fieldname="${fieldname}"] :input`);
|
||||
if(value) {
|
||||
input.val(value).trigger('change');
|
||||
frappe.after_ajax(() => { resolve(input); });
|
||||
} else {
|
||||
resolve(input);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.standard_pages["test-runner"] = function() {
|
||||
var wrapper = frappe.container.add_page('test-runner');
|
||||
|
||||
frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
single_column: true,
|
||||
title: __("Test Runner")
|
||||
});
|
||||
|
||||
$("<div id='qunit'></div>").appendTo($(wrapper).find(".layout-main"));
|
||||
|
||||
var route = frappe.get_route();
|
||||
if(route.length < 2) {
|
||||
frappe.msgprint(__("To run a test add the module name in the route after '{0}'. For example, {1}", ['test-runner/', '#test-runner/lib/js/frappe/test_app.js']));
|
||||
return;
|
||||
}
|
||||
|
||||
var requires = ["assets/frappe/js/lib/jquery/qunit.js",
|
||||
"assets/frappe/js/lib/jquery/qunit.css"].concat(route.splice(1).join("/"));
|
||||
|
||||
frappe.require(requires, function() {
|
||||
QUnit.load();
|
||||
});
|
||||
}
|
||||
|
|
@ -368,13 +368,13 @@ _f.Frm.prototype.set_read_only = function() {
|
|||
}
|
||||
|
||||
_f.Frm.prototype.trigger = function(event) {
|
||||
this.script_manager.trigger(event);
|
||||
return this.script_manager.trigger(event);
|
||||
};
|
||||
|
||||
_f.Frm.prototype.get_formatted = function(fieldname) {
|
||||
return frappe.format(this.doc[fieldname],
|
||||
frappe.meta.get_docfield(this.doctype, fieldname, this.docname),
|
||||
{no_icon:true}, this.doc);
|
||||
frappe.meta.get_docfield(this.doctype, fieldname, this.docname),
|
||||
{no_icon:true}, this.doc);
|
||||
}
|
||||
|
||||
_f.Frm.prototype.open_grid_row = function() {
|
||||
|
|
|
|||
|
|
@ -606,18 +606,19 @@ _f.Frm.prototype.setnewdoc = function() {
|
|||
var me = this;
|
||||
|
||||
// hide any open grid
|
||||
this.script_manager.trigger("before_load", this.doctype, this.docname, function() {
|
||||
me.script_manager.trigger("onload");
|
||||
me.opendocs[me.docname] = true;
|
||||
me.render_form();
|
||||
this.script_manager.trigger("before_load", this.doctype, this.docname)
|
||||
.then(() => {
|
||||
me.script_manager.trigger("onload");
|
||||
me.opendocs[me.docname] = true;
|
||||
me.render_form();
|
||||
|
||||
frappe.after_ajax(function() {
|
||||
me.trigger_link_fields();
|
||||
frappe.after_ajax(function() {
|
||||
me.trigger_link_fields();
|
||||
});
|
||||
|
||||
frappe.breadcrumbs.add(me.meta.module, me.doctype)
|
||||
});
|
||||
|
||||
frappe.breadcrumbs.add(me.meta.module, me.doctype)
|
||||
});
|
||||
|
||||
// update seen
|
||||
if(this.meta.track_seen) {
|
||||
$('.list-id[data-name="'+ me.docname +'"]').addClass('seen');
|
||||
|
|
@ -705,17 +706,21 @@ Object.defineProperty(window, 'validated', {
|
|||
});
|
||||
|
||||
_f.Frm.prototype.save = function(save_action, callback, btn, on_error) {
|
||||
btn && $(btn).prop("disabled", true);
|
||||
$(document.activeElement).blur();
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
btn && $(btn).prop("disabled", true);
|
||||
$(document.activeElement).blur();
|
||||
|
||||
frappe.ui.form.close_grid_form();
|
||||
frappe.ui.form.close_grid_form();
|
||||
|
||||
// let any pending js process finish
|
||||
var me = this;
|
||||
setTimeout(function() { me._save(save_action, callback, btn, on_error) }, 100);
|
||||
// let any pending js process finish
|
||||
setTimeout(function() {
|
||||
me._save(save_action, callback, btn, on_error, resolve);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
_f.Frm.prototype._save = function(save_action, callback, btn, on_error) {
|
||||
_f.Frm.prototype._save = function(save_action, callback, btn, on_error, resolve) {
|
||||
var me = this;
|
||||
if(!save_action) save_action = "Save";
|
||||
this.validate_form_action(save_action);
|
||||
|
|
@ -736,26 +741,29 @@ _f.Frm.prototype._save = function(save_action, callback, btn, on_error) {
|
|||
on_error();
|
||||
}
|
||||
callback && callback(r);
|
||||
resolve();
|
||||
}
|
||||
|
||||
if(save_action != "Update") {
|
||||
// validate
|
||||
frappe.validated = true;
|
||||
$.when(this.script_manager.trigger("validate"), this.script_manager.trigger("before_save"))
|
||||
.done(function() {
|
||||
// done is called after all ajaxes in validate & before_save are completed :)
|
||||
Promise.all([
|
||||
this.script_manager.trigger("validate"),
|
||||
this.script_manager.trigger("before_save")
|
||||
]).then(() => {
|
||||
// done is called after all ajaxes in validate & before_save are completed :)
|
||||
|
||||
if(!frappe.validated) {
|
||||
btn && $(btn).prop("disabled", false);
|
||||
if(on_error) {
|
||||
on_error();
|
||||
}
|
||||
return;
|
||||
if(!frappe.validated) {
|
||||
btn && $(btn).prop("disabled", false);
|
||||
if(on_error) {
|
||||
on_error();
|
||||
}
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.ui.form.save(me, save_action, after_save, btn);
|
||||
});
|
||||
|
||||
frappe.ui.form.save(me, save_action, after_save, btn);
|
||||
});
|
||||
} else {
|
||||
frappe.ui.form.save(me, save_action, after_save, btn);
|
||||
}
|
||||
|
|
@ -767,7 +775,7 @@ _f.Frm.prototype.savesubmit = function(btn, callback, on_error) {
|
|||
this.validate_form_action("Submit");
|
||||
frappe.confirm(__("Permanently Submit {0}?", [this.docname]), function() {
|
||||
frappe.validated = true;
|
||||
me.script_manager.trigger("before_submit").done(function() {
|
||||
me.script_manager.trigger("before_submit").then(function() {
|
||||
if(!frappe.validated) {
|
||||
if(on_error)
|
||||
on_error();
|
||||
|
|
@ -964,10 +972,6 @@ _f.Frm.prototype.validate_form_action = function(action) {
|
|||
}
|
||||
};
|
||||
|
||||
_f.Frm.prototype.get_handlers = function(fieldname, doctype, docname) {
|
||||
return this.script_manager.get_handlers(fieldname, doctype || this.doctype, docname || this.docname)
|
||||
}
|
||||
|
||||
_f.Frm.prototype.has_perm = function(ptype) {
|
||||
return frappe.perm.has_perm(this.doctype, 0, ptype, this.doc);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
/*!
|
||||
* QUnit 2.0.0
|
||||
* QUnit 2.3.3
|
||||
* https://qunitjs.com/
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license
|
||||
* https://jquery.org/license
|
||||
*
|
||||
* Date: 2016-06-16T17:09Z
|
||||
* Date: 2017-06-02T14:07Z
|
||||
*/
|
||||
|
||||
/** Font Family and Sizes */
|
||||
|
|
@ -226,7 +226,8 @@
|
|||
#qunit-tests li.running,
|
||||
#qunit-tests li.pass,
|
||||
#qunit-tests li.fail,
|
||||
#qunit-tests li.skipped {
|
||||
#qunit-tests li.skipped,
|
||||
#qunit-tests li.aborted {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +236,7 @@
|
|||
}
|
||||
|
||||
#qunit-tests.hidepass li.running,
|
||||
#qunit-tests.hidepass li.pass {
|
||||
#qunit-tests.hidepass li.pass:not(.todo) {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
|
|
@ -374,12 +375,16 @@
|
|||
|
||||
#qunit-banner.qunit-fail { background-color: #EE5757; }
|
||||
|
||||
|
||||
/*** Aborted tests */
|
||||
#qunit-tests .aborted { color: #000; background-color: orange; }
|
||||
/*** Skipped tests */
|
||||
|
||||
#qunit-tests .skipped {
|
||||
background-color: #EBECE9;
|
||||
}
|
||||
|
||||
#qunit-tests .qunit-todo-label,
|
||||
#qunit-tests .qunit-skipped-label {
|
||||
background-color: #F4FF77;
|
||||
display: inline-block;
|
||||
|
|
@ -390,19 +395,35 @@
|
|||
margin: -0.4em 0.4em -0.4em 0;
|
||||
}
|
||||
|
||||
#qunit-tests .qunit-todo-label {
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
/** Result */
|
||||
|
||||
#qunit-testresult {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
|
||||
color: #2B81AF;
|
||||
background-color: #D2E0E6;
|
||||
|
||||
border-bottom: 1px solid #FFF;
|
||||
}
|
||||
#qunit-testresult .clearfix {
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
||||
#qunit-testresult .module-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
#qunit-testresult-display {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
width: 85%;
|
||||
float:left;
|
||||
}
|
||||
#qunit-testresult-controls {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
width: 10%;
|
||||
float:left;
|
||||
}
|
||||
|
||||
/** Fixture */
|
||||
|
||||
|
|
|
|||
9327
frappe/public/js/lib/jquery/qunit.js
vendored
9327
frappe/public/js/lib/jquery/qunit.js
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -22,7 +22,8 @@ def xmlrunner_wrapper(output):
|
|||
return xmlrunner.XMLTestRunner(*args, **kwargs)
|
||||
return _runner
|
||||
|
||||
def main(app=None, module=None, doctype=None, verbose=False, tests=(), force=False, profile=False, junit_xml_output=None):
|
||||
def main(app=None, module=None, doctype=None, verbose=False, tests=(),
|
||||
force=False, profile=False, junit_xml_output=None, ui_tests=False):
|
||||
global unittest_runner
|
||||
|
||||
xmloutput_fh = None
|
||||
|
|
@ -57,7 +58,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(), force=Fal
|
|||
elif module:
|
||||
ret = run_tests_for_module(module, verbose, tests, profile)
|
||||
else:
|
||||
ret = run_all_tests(app, verbose, profile)
|
||||
ret = run_all_tests(app, verbose, profile, ui_tests)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ def set_test_email_config():
|
|||
"admin_password": "admin"
|
||||
})
|
||||
|
||||
def run_all_tests(app=None, verbose=False, profile=False):
|
||||
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False):
|
||||
import os
|
||||
|
||||
apps = [app] if app else frappe.get_installed_apps()
|
||||
|
|
@ -95,9 +96,11 @@ def run_all_tests(app=None, verbose=False, profile=False):
|
|||
# print path
|
||||
for filename in files:
|
||||
filename = cstr(filename)
|
||||
if filename.startswith("test_") and filename.endswith(".py"):
|
||||
if filename.startswith("test_") and filename.endswith(".py")\
|
||||
and filename != 'test_runner.py':
|
||||
# print filename[:-3]
|
||||
_add_test(app, path, filename, verbose, test_suite=test_suite)
|
||||
_add_test(app, path, filename, verbose,
|
||||
test_suite, ui_tests)
|
||||
|
||||
if profile:
|
||||
pr = cProfile.Profile()
|
||||
|
|
@ -163,7 +166,7 @@ def _run_unittest(module, verbose=False, tests=(), profile=False):
|
|||
return out
|
||||
|
||||
|
||||
def _add_test(app, path, filename, verbose, test_suite=None):
|
||||
def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False):
|
||||
import os
|
||||
|
||||
if os.path.sep.join(["doctype", "doctype", "boilerplate"]) in path:
|
||||
|
|
@ -179,8 +182,9 @@ def _add_test(app, path, filename, verbose, test_suite=None):
|
|||
relative_path=relative_path.replace('/', '.'), module_name=filename[:-3])
|
||||
|
||||
module = frappe.get_module(module_name)
|
||||
is_ui_test = True if hasattr(module, 'TestDriver') else False
|
||||
|
||||
if getattr(module, "selenium_tests", False) and not frappe.conf.run_selenium_tests:
|
||||
if is_ui_test != ui_tests:
|
||||
return
|
||||
|
||||
if not test_suite:
|
||||
|
|
@ -325,3 +329,5 @@ def print_mandatory_fields(doctype):
|
|||
for d in meta.get("fields", {"reqd":1}):
|
||||
print(d.parent + ":" + d.fieldname + " | " + d.fieldtype + " | " + (d.options or ""))
|
||||
print()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe
|
||||
from frappe.utils import sel
|
||||
|
||||
selenium_tests = True
|
||||
|
||||
class TestLogin(unittest.TestCase):
|
||||
def setUp(self):
|
||||
return
|
||||
sel.login()
|
||||
|
||||
def test_login(self):
|
||||
return
|
||||
self.assertEquals(sel._driver.current_url, sel.get_localhost() + "/desk")
|
||||
|
||||
def test_to_do(self):
|
||||
return
|
||||
# too unpredictable in travis
|
||||
sel.go_to_module("ToDo")
|
||||
sel.primary_action()
|
||||
sel.wait_for_page("Form/ToDo")
|
||||
sel.set_field("description", "test description", "textarea")
|
||||
sel.primary_action()
|
||||
self.assertTrue(sel.wait_for_state("clean"))
|
||||
0
frappe/tests/ui/__init__.py
Normal file
0
frappe/tests/ui/__init__.py
Normal file
|
|
@ -1,19 +0,0 @@
|
|||
module.exports = {
|
||||
beforeEach: browser => {
|
||||
browser
|
||||
.url(browser.launch_url + '/login')
|
||||
.waitForElementVisible('body', 5000)
|
||||
},
|
||||
'Login': browser => {
|
||||
browser
|
||||
.assert.title('Login')
|
||||
.assert.visible('#login_email', 'Check if login box is visible')
|
||||
.setValue("#login_email", "Administrator")
|
||||
.setValue("#login_password", "admin")
|
||||
.click(".btn-login")
|
||||
.waitForElementVisible("#body_div", 15000);
|
||||
},
|
||||
after: browser => {
|
||||
browser.end();
|
||||
},
|
||||
};
|
||||
93
frappe/tests/ui/test_lib.js
Normal file
93
frappe/tests/ui/test_lib.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
frappe.tests = {
|
||||
data: {},
|
||||
get_fixture_names: (doctype) => {
|
||||
return Object.keys(frappe.test_data[doctype]);
|
||||
},
|
||||
make: function(doctype, data) {
|
||||
return frappe.run_serially([
|
||||
() => frappe.set_route('List', doctype),
|
||||
() => frappe.new_doc(doctype),
|
||||
() => {
|
||||
let frm = frappe.quick_entry ? frappe.quick_entry.dialog : cur_frm;
|
||||
return frappe.tests.set_form_values(frm, data);
|
||||
},
|
||||
() => frappe.timeout(1),
|
||||
() => (frappe.quick_entry ? frappe.quick_entry.insert() : cur_frm.save())
|
||||
]);
|
||||
},
|
||||
set_form_values: (frm, data) => {
|
||||
let tasks = [];
|
||||
|
||||
data.forEach(item => {
|
||||
for (let key in item) {
|
||||
let task = () => {
|
||||
let value = item[key];
|
||||
if ($.isArray(value)) {
|
||||
return frappe.tests.set_grid_values(frm, key, value);
|
||||
} else {
|
||||
// single value
|
||||
return frm.set_value(key, value);
|
||||
}
|
||||
};
|
||||
tasks.push(task);
|
||||
}
|
||||
});
|
||||
|
||||
// set values
|
||||
return frappe.run_serially(tasks);
|
||||
|
||||
},
|
||||
set_grid_values: (frm, key, value) => {
|
||||
// set value in grid
|
||||
let grid = frm.get_field(key).grid;
|
||||
grid.remove_all();
|
||||
|
||||
let grid_row_tasks = [];
|
||||
|
||||
// build tasks for each row
|
||||
value.forEach(d => {
|
||||
grid_row_tasks.push(() => {
|
||||
grid.add_new_row();
|
||||
let grid_row = grid.get_row(-1).toggle_view(true);
|
||||
let grid_value_tasks = [];
|
||||
|
||||
// build tasks to set each row value
|
||||
d.forEach(child_value => {
|
||||
for (let child_key in child_value) {
|
||||
grid_value_tasks.push(() => {
|
||||
return frappe.model.set_value(grid_row.doc.doctype,
|
||||
grid_row.doc.name, child_key, child_value[child_key]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return frappe.run_serially(grid_value_tasks);
|
||||
});
|
||||
});
|
||||
return frappe.run_serially(grid_row_tasks);
|
||||
},
|
||||
setup_doctype: (doctype) => {
|
||||
return frappe.set_route('List', doctype)
|
||||
.then(() => {
|
||||
frappe.tests.data[doctype] = [];
|
||||
let expected = frappe.tests.get_fixture_names(doctype);
|
||||
cur_list.data.forEach((d) => {
|
||||
frappe.tests.data[doctype].push(d.name);
|
||||
if(expected.indexOf(d.name) !== -1) {
|
||||
expected[expected.indexOf(d.name)] = null;
|
||||
}
|
||||
});
|
||||
|
||||
let tasks = [];
|
||||
|
||||
expected.forEach(function(d) {
|
||||
if(d) {
|
||||
tasks.push(() => frappe.tests.make(doctype,
|
||||
frappe.test_data[doctype][d]));
|
||||
}
|
||||
});
|
||||
|
||||
return frappe.run_serially(tasks);
|
||||
});
|
||||
}
|
||||
};
|
||||
33
frappe/tests/ui/test_list.js
Normal file
33
frappe/tests/ui/test_list.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
QUnit.module('views');
|
||||
|
||||
QUnit.test("test quick entry", function(assert) {
|
||||
assert.expect(2);
|
||||
let done = assert.async();
|
||||
let random = frappe.utils.get_random(10);
|
||||
|
||||
frappe.run_serially([
|
||||
() => frappe.set_route('List', 'ToDo'),
|
||||
() => frappe.new_doc('ToDo'),
|
||||
() => frappe.quick_entry.dialog.set_value('description', random),
|
||||
() => frappe.quick_entry.insert(),
|
||||
(doc) => {
|
||||
assert.ok(doc && !doc.__islocal);
|
||||
return frappe.set_route('Form', 'ToDo', doc.name);
|
||||
},
|
||||
() => {
|
||||
assert.ok(cur_frm.doc.description.includes(random));
|
||||
return done();
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("test list values", function(assert) {
|
||||
assert.expect(2);
|
||||
let done = assert.async();
|
||||
frappe.set_route('List', 'DocType')
|
||||
.then(() => {
|
||||
assert.deepEqual(['List', 'DocType', 'List'], frappe.get_route());
|
||||
assert.ok($('.list-item:visible').length > 10);
|
||||
done();
|
||||
});
|
||||
});
|
||||
17
frappe/tests/ui/test_test_runner.py
Normal file
17
frappe/tests/ui/test_test_runner.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from __future__ import print_function
|
||||
from frappe.utils.selenium_testdriver import TestDriver
|
||||
import unittest
|
||||
|
||||
class TestLogin(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.driver = TestDriver()
|
||||
|
||||
def test_test_runner(self):
|
||||
self.driver.login()
|
||||
self.driver.set_route('Form', 'Test Runner')
|
||||
self.driver.click_primary_action()
|
||||
self.driver.wait_for('#qunit-testresult-display', timeout=60)
|
||||
self.driver.print_console()
|
||||
|
||||
def tearDown(self):
|
||||
self.driver.close()
|
||||
50
frappe/tests/ui/test_todo.py
Normal file
50
frappe/tests/ui/test_todo.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from __future__ import print_function
|
||||
from frappe.utils.selenium_testdriver import TestDriver
|
||||
import unittest
|
||||
import time, os
|
||||
|
||||
class TestToDo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.driver = TestDriver()
|
||||
|
||||
def test_todo(self):
|
||||
self.driver.login()
|
||||
|
||||
# list view
|
||||
self.driver.set_route('List', 'ToDo')
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# new
|
||||
self.driver.click_primary_action()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# set input
|
||||
self.driver.set_text_editor('description', 'hello')
|
||||
|
||||
# save
|
||||
self.driver.click_modal_primary_action()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# refresh
|
||||
self.driver.click_secondary_action()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
result_list = self.driver.get_visible_element('.result-list')
|
||||
first_element_text = (result_list
|
||||
.find_element_by_css_selector('.list-item')
|
||||
.find_element_by_css_selector('.list-id').text)
|
||||
|
||||
# if os.environ.get('CI'):
|
||||
# # we don't run this test in Travis as it always fails
|
||||
# # reinforcing why we use Unit Testing instead of integration
|
||||
# # testing
|
||||
# return
|
||||
|
||||
self.assertTrue('hello' in first_element_text)
|
||||
|
||||
def tearDown(self):
|
||||
self.driver.close()
|
||||
|
|
@ -94,6 +94,22 @@ def before_tests():
|
|||
frappe.db.commit()
|
||||
frappe.clear_cache()
|
||||
|
||||
# complete setup if missing
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
||||
if not int(frappe.db.get_single_value('System Settings', 'setup_complete') or 0):
|
||||
setup_complete({
|
||||
"language" :"english",
|
||||
"email" :"test@erpnext.com",
|
||||
"full_name" :"Test User",
|
||||
"password" :"test",
|
||||
"country" :"United States",
|
||||
"timezone" :"America/New_York",
|
||||
"currency" :"USD"
|
||||
})
|
||||
|
||||
frappe.db.commit()
|
||||
frappe.clear_cache()
|
||||
|
||||
def import_country_and_currency():
|
||||
from frappe.geo.country_info import get_all
|
||||
from frappe.utils import update_progress_bar
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ class NestedSet(Document):
|
|||
self.validate_ledger()
|
||||
|
||||
def on_trash(self):
|
||||
if not self.nsm_parent_field:
|
||||
if not getattr(self, 'nsm_parent_field', None):
|
||||
self.nsm_parent_field = frappe.scrub(self.doctype) + "_parent"
|
||||
|
||||
parent = self.get(self.nsm_parent_field)
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support.select import Select
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
from urllib import unquote
|
||||
import time, subprocess
|
||||
import signal
|
||||
import sys
|
||||
|
||||
host = "http://localhost"
|
||||
pipe = None
|
||||
port = "8000"
|
||||
_driver = None
|
||||
_verbose = None
|
||||
logged_in = False
|
||||
cur_route = False
|
||||
input_wait = 0
|
||||
|
||||
def get_localhost():
|
||||
return "{host}:{port}".format(host=host, port=port)
|
||||
|
||||
def start(verbose=None, driver=None):
|
||||
global _driver, _verbose
|
||||
_verbose = verbose
|
||||
|
||||
_driver = getattr(webdriver, driver or "PhantomJS")()
|
||||
_driver.set_window_size(1080,800)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
close()
|
||||
sys.exit(0)
|
||||
|
||||
def start_test_server(verbose):
|
||||
global pipe
|
||||
pipe = subprocess.Popen(["bench", "serve", "--port", port], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
#time.sleep(5)
|
||||
while not pipe.stderr.readline():
|
||||
time.sleep(0.5)
|
||||
if verbose:
|
||||
print("Test server started")
|
||||
|
||||
def get(url):
|
||||
_driver.get(url)
|
||||
|
||||
def login(wait_for_id="#page-desktop"):
|
||||
global logged_in
|
||||
if logged_in:
|
||||
return
|
||||
get(get_localhost() + "/login")
|
||||
wait("#login_email")
|
||||
set_input("#login_email", "Administrator")
|
||||
set_input("#login_password", "admin", key=Keys.RETURN)
|
||||
wait(wait_for_id)
|
||||
logged_in = True
|
||||
|
||||
|
||||
def go_to_module(module_name, item=None):
|
||||
global cur_route
|
||||
|
||||
# desktop
|
||||
find(".navbar-home", True)[0].click()
|
||||
cur_route = None
|
||||
wait("#page-desktop")
|
||||
|
||||
page = "Module/" + module_name
|
||||
m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
|
||||
if not m:
|
||||
page = "List/" + module_name
|
||||
m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
|
||||
if not m:
|
||||
raise Exception("Module {0} not found".format(module_name))
|
||||
|
||||
m[0].click()
|
||||
wait_for_page(page)
|
||||
|
||||
if item:
|
||||
elem = find('[data-label="{0}"]'.format(item))[0]
|
||||
elem.click()
|
||||
page = elem.get_attribute("data-route")
|
||||
wait_for_page(page)
|
||||
|
||||
def new_doc(module, doctype):
|
||||
go_to_module(module, doctype)
|
||||
primary_action()
|
||||
wait_for_page("Form/" + doctype)
|
||||
|
||||
def add_child(fieldname):
|
||||
find('[data-fieldname="{0}"] .grid-add-row'.format(fieldname))[0].click()
|
||||
wait('[data-fieldname="{0}"] .form-grid'.format(fieldname))
|
||||
|
||||
def done_add_child(fieldname):
|
||||
selector = '[data-fieldname="{0}"] .grid-row-open .btn-success'.format(fieldname)
|
||||
scroll_to(selector)
|
||||
wait_till_clickable(selector).click()
|
||||
|
||||
def find(selector, everywhere=False):
|
||||
if cur_route and not everywhere:
|
||||
selector = cur_route + " " + selector
|
||||
return _driver.find_elements_by_css_selector(selector)
|
||||
|
||||
def set_field(fieldname, value, fieldtype="input"):
|
||||
_driver.switch_to.window(_driver.current_window_handle)
|
||||
selector = '{0}[data-fieldname="{1}"]'.format(fieldtype, fieldname)
|
||||
set_input(selector, value, key=Keys.TAB)
|
||||
wait_for_ajax()
|
||||
|
||||
def set_select(fieldname, value):
|
||||
select = Select(find('select[data-fieldname="{0}"]'.format(fieldname))[0])
|
||||
select.select_by_value(value)
|
||||
wait_for_ajax()
|
||||
|
||||
def primary_action():
|
||||
selector = ".page-actions .primary-action"
|
||||
scroll_to(selector)
|
||||
wait_till_clickable(selector).click()
|
||||
wait_for_ajax()
|
||||
|
||||
def wait_for_page(name):
|
||||
global cur_route
|
||||
cur_route = None
|
||||
route = '[data-page-route="{0}"]'.format(name)
|
||||
wait_for_ajax()
|
||||
elem = wait(route)
|
||||
wait_for_ajax()
|
||||
cur_route = route
|
||||
return elem
|
||||
|
||||
def wait_till_clickable(selector):
|
||||
if cur_route:
|
||||
selector = cur_route + " " + selector
|
||||
return get_wait().until(EC.element_to_be_clickable((By.CSS_SELECTOR, selector)))
|
||||
|
||||
def wait_till_visible(selector):
|
||||
if cur_route:
|
||||
selector = cur_route + " " + selector
|
||||
return get_wait().until(EC.visibility_of_element_located((By.CSS_SELECTOR, selector)))
|
||||
|
||||
def wait_for_ajax():
|
||||
wait('body[data-ajax-state="complete"]', True)
|
||||
|
||||
def wait_for_state(state):
|
||||
return wait(cur_route + '[data-state="{0}"]'.format(state), True)
|
||||
|
||||
def wait(selector, everywhere=False):
|
||||
if cur_route and not everywhere:
|
||||
selector = cur_route + " " + selector
|
||||
|
||||
time.sleep(0.5)
|
||||
elem = get_wait().until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
|
||||
return elem
|
||||
|
||||
def get_wait():
|
||||
return WebDriverWait(_driver, 20)
|
||||
|
||||
def set_input(selector, text, key=None):
|
||||
elem = find(selector)[0]
|
||||
elem.clear()
|
||||
elem.send_keys(text)
|
||||
if key:
|
||||
time.sleep(0.5)
|
||||
elem.send_keys(key)
|
||||
if input_wait:
|
||||
time.sleep(input_wait)
|
||||
|
||||
def scroll_to(selector):
|
||||
execute_script("frappe.ui.scroll('{0}')".format(selector))
|
||||
|
||||
def execute_script(js):
|
||||
_driver.execute_script(js)
|
||||
|
||||
def close():
|
||||
global _driver, pipe
|
||||
if _driver:
|
||||
_driver.quit()
|
||||
if pipe:
|
||||
pipe.kill()
|
||||
_driver = pipe = None
|
||||
260
frappe/utils/selenium_testdriver.py
Normal file
260
frappe/utils/selenium_testdriver.py
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
#from selenium.webdriver.support.select import Select
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
#from selenium.common.exceptions import TimeoutException
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
|
||||
import time
|
||||
import signal
|
||||
import os, sys
|
||||
import frappe
|
||||
from ast import literal_eval
|
||||
|
||||
class TestDriver(object):
|
||||
def __init__(self, port='8000'):
|
||||
self.port = port
|
||||
|
||||
chrome_options = Options()
|
||||
capabilities = DesiredCapabilities.CHROME
|
||||
|
||||
if os.environ.get('CI'):
|
||||
self.host = 'localhost'
|
||||
else:
|
||||
self.host = frappe.local.site
|
||||
|
||||
# enable browser logging
|
||||
capabilities['loggingPrefs'] = {'browser':'ALL'}
|
||||
|
||||
chrome_options.add_argument('--no-sandbox')
|
||||
chrome_options.add_argument('--start-maximized')
|
||||
self.driver = webdriver.Chrome(chrome_options=chrome_options,
|
||||
desired_capabilities=capabilities, port=9515)
|
||||
|
||||
self.driver.set_window_size(1080,800)
|
||||
self.cur_route = None
|
||||
self.logged_in = False
|
||||
|
||||
@property
|
||||
def localhost(self):
|
||||
return "http://{host}:{port}".format(host=self.host, port=self.port)
|
||||
|
||||
def get(self, url):
|
||||
return self.driver.get(os.path.join(self.localhost, url))
|
||||
|
||||
def start(self):
|
||||
def signal_handler(signal, frame):
|
||||
self.close()
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
def close(self):
|
||||
if self.driver:
|
||||
self.driver.quit()
|
||||
self.driver = None
|
||||
|
||||
def login(self, wait_for_id="#page-desktop"):
|
||||
if self.logged_in:
|
||||
return
|
||||
self.get('login')
|
||||
self.wait_for("#login_email")
|
||||
self.set_input("#login_email", "Administrator")
|
||||
self.set_input("#login_password", "admin")
|
||||
self.click('.btn-login')
|
||||
self.wait_for(wait_for_id)
|
||||
self.logged_in = True
|
||||
|
||||
def set_input(self, selector, text, key=None, xpath=None):
|
||||
elem = self.find(selector, xpath=xpath)[0]
|
||||
elem.clear()
|
||||
elem.send_keys(text)
|
||||
if key:
|
||||
time.sleep(0.5)
|
||||
elem.send_keys(key)
|
||||
time.sleep(0.2)
|
||||
|
||||
def set_field(self, fieldname, text):
|
||||
elem = self.find(xpath='//input[@data-fieldname="{0}"]'.format(fieldname))
|
||||
elem[0].send_keys(text)
|
||||
|
||||
def set_text_editor(self, fieldname, text):
|
||||
elem = self.find(xpath='//div[@data-fieldname="{0}"]//div[@contenteditable="true"]'.format(fieldname))
|
||||
elem[0].send_keys(text)
|
||||
|
||||
def find(self, selector=None, everywhere=False, xpath=None):
|
||||
if xpath:
|
||||
return self.driver.find_elements_by_xpath(xpath)
|
||||
else:
|
||||
if self.cur_route and not everywhere:
|
||||
selector = self.cur_route + " " + selector
|
||||
return self.driver.find_elements_by_css_selector(selector)
|
||||
|
||||
def wait_for(self, selector=None, everywhere=False, timeout=20, xpath=None):
|
||||
if self.cur_route and not everywhere:
|
||||
selector = self.cur_route + " " + selector
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
if selector:
|
||||
_by = By.CSS_SELECTOR
|
||||
if xpath:
|
||||
_by = By.XPATH
|
||||
selector = xpath
|
||||
|
||||
try:
|
||||
elem = self.get_wait(timeout).until(
|
||||
EC.presence_of_element_located((_by, selector)))
|
||||
return elem
|
||||
except Exception, e:
|
||||
# body = self.driver.find_element_by_id('body_div')
|
||||
# print(body.get_attribute('innerHTML'))
|
||||
self.print_console()
|
||||
raise e
|
||||
|
||||
def print_console(self):
|
||||
for entry in self.driver.get_log('browser'):
|
||||
source, line_no, message = entry.get('message').split(' ', 2)
|
||||
|
||||
if message[0] in ('"', "'"):
|
||||
# message is a quoted/escaped string
|
||||
message = literal_eval(message)
|
||||
|
||||
print(source + ' ' + line_no)
|
||||
print(message)
|
||||
print('-'*40)
|
||||
|
||||
def get_wait(self, timeout=20):
|
||||
return WebDriverWait(self.driver, timeout)
|
||||
|
||||
def scroll_to(self, selector):
|
||||
self.execute_script("frappe.ui.scroll('{0}')".format(selector))
|
||||
|
||||
def set_route(self, *args):
|
||||
self.execute_script('frappe.set_route({0})'\
|
||||
.format(', '.join(['"{0}"'.format(r) for r in args])))
|
||||
|
||||
self.wait_for(xpath='//div[@data-page-route="{0}"]'.format('/'.join(args)), timeout=4)
|
||||
|
||||
def click(self, css_selector, xpath=None):
|
||||
self.wait_till_clickable(css_selector, xpath).click()
|
||||
|
||||
def click_primary_action(self):
|
||||
selector = ".page-actions .primary-action"
|
||||
#self.scroll_to(selector)
|
||||
self.wait_till_clickable(selector).click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def click_secondary_action(self):
|
||||
selector = ".page-actions .btn-secondary"
|
||||
#self.scroll_to(selector)
|
||||
self.wait_till_clickable(selector).click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def click_modal_primary_action(self):
|
||||
self.get_visible_modal().find_element_by_css_selector('.btn-primary').click()
|
||||
|
||||
def get_visible_modal(self):
|
||||
return self.get_visible_element('.modal-content')
|
||||
|
||||
def get_visible_element(self, selector=None, xpath=None):
|
||||
for elem in self.find(selector=selector, xpath=xpath):
|
||||
if elem.is_displayed():
|
||||
return elem
|
||||
|
||||
def wait_till_clickable(self, selector=None, xpath=None):
|
||||
if self.cur_route:
|
||||
selector = self.cur_route + " " + selector
|
||||
|
||||
by = By.CSS_SELECTOR
|
||||
if xpath:
|
||||
by = By.XPATH
|
||||
selector = xpath
|
||||
|
||||
return self.get_wait().until(EC.element_to_be_clickable(
|
||||
(by, selector)))
|
||||
|
||||
def execute_script(self, js):
|
||||
self.driver.execute_script(js)
|
||||
|
||||
def wait_for_ajax(self):
|
||||
self.wait_for('body[data-ajax-state="complete"]', True)
|
||||
|
||||
# def go_to_module(module_name, item=None):
|
||||
# global cur_route
|
||||
#
|
||||
# # desktop
|
||||
# find(".navbar-home", True)[0].click()
|
||||
# cur_route = None
|
||||
# wait("#page-desktop")
|
||||
#
|
||||
# page = "Module/" + module_name
|
||||
# m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
|
||||
# if not m:
|
||||
# page = "List/" + module_name
|
||||
# m = find('#page-desktop [data-link="{0}"] .app-icon'.format(page))
|
||||
# if not m:
|
||||
# raise Exception("Module {0} not found".format(module_name))
|
||||
#
|
||||
# m[0].click()
|
||||
# wait_for_page(page)
|
||||
#
|
||||
# if item:
|
||||
# elem = find('[data-label="{0}"]'.format(item))[0]
|
||||
# elem.click()
|
||||
# page = elem.get_attribute("data-route")
|
||||
# wait_for_page(page)
|
||||
#
|
||||
# def new_doc(module, doctype):
|
||||
# go_to_module(module, doctype)
|
||||
# primary_action()
|
||||
# wait_for_page("Form/" + doctype)
|
||||
#
|
||||
# def add_child(fieldname):
|
||||
# find('[data-fieldname="{0}"] .grid-add-row'.format(fieldname))[0].click()
|
||||
# wait('[data-fieldname="{0}"] .form-grid'.format(fieldname))
|
||||
#
|
||||
# def done_add_child(fieldname):
|
||||
# selector = '[data-fieldname="{0}"] .grid-row-open .btn-success'.format(fieldname)
|
||||
# scroll_to(selector)
|
||||
# wait_till_clickable(selector).click()
|
||||
#
|
||||
# def set_field(fieldname, value, fieldtype="input"):
|
||||
# _driver.switch_to.window(_driver.current_window_handle)
|
||||
# selector = '{0}[data-fieldname="{1}"]'.format(fieldtype, fieldname)
|
||||
# set_input(selector, value, key=Keys.TAB)
|
||||
# wait_for_ajax()
|
||||
#
|
||||
# def set_select(fieldname, value):
|
||||
# select = Select(find('select[data-fieldname="{0}"]'.format(fieldname))[0])
|
||||
# select.select_by_value(value)
|
||||
# wait_for_ajax()
|
||||
#
|
||||
#
|
||||
# def wait_for_page(name):
|
||||
# global cur_route
|
||||
# cur_route = None
|
||||
# route = '[data-page-route="{0}"]'.format(name)
|
||||
# wait_for_ajax()
|
||||
# elem = wait(route)
|
||||
# wait_for_ajax()
|
||||
# cur_route = route
|
||||
# return elem
|
||||
#
|
||||
#
|
||||
# def wait_till_visible(selector):
|
||||
# if cur_route:
|
||||
# selector = cur_route + " " + selector
|
||||
# return get_wait().until(EC.visibility_of_element_located((By.CSS_SELECTOR, selector)))
|
||||
#
|
||||
#
|
||||
# def wait_for_state(state):
|
||||
# return wait(cur_route + '[data-state="{0}"]'.format(state), True)
|
||||
#
|
||||
#
|
||||
Loading…
Add table
Reference in a new issue