@@ -17,6 +17,7 @@ if [ "$TYPE" == "server" ]; then | |||||
fi | fi | ||||
if [ "$DB" == "mariadb" ];then | if [ "$DB" == "mariadb" ];then | ||||
sudo apt install mariadb-client-10.3 | |||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; | ||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; | ||||
@@ -58,4 +59,4 @@ cd ../.. | |||||
bench start & | bench start & | ||||
bench --site test_site reinstall --yes | bench --site test_site reinstall --yes | ||||
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi | if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi | ||||
bench build --app frappe | |||||
bench build --app frappe |
@@ -9,7 +9,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
name: Patch Test | name: Patch Test | ||||
@@ -13,7 +13,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
strategy: | strategy: | ||||
fail-fast: false | fail-fast: false | ||||
@@ -121,9 +121,10 @@ jobs: | |||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io | ORCHESTRATOR_URL: http://test-orchestrator.frappe.io | ||||
- name: Upload coverage data | - name: Upload coverage data | ||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }} | |||||
uses: codecov/codecov-action@v2 | uses: codecov/codecov-action@v2 | ||||
with: | with: | ||||
name: MariaDB | name: MariaDB | ||||
fail_ci_if_error: true | fail_ci_if_error: true | ||||
files: /home/runner/frappe-bench/sites/coverage.xml | files: /home/runner/frappe-bench/sites/coverage.xml | ||||
verbose: true | |||||
verbose: true |
@@ -12,7 +12,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
strategy: | strategy: | ||||
fail-fast: false | fail-fast: false | ||||
@@ -124,6 +124,7 @@ jobs: | |||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io | ORCHESTRATOR_URL: http://test-orchestrator.frappe.io | ||||
- name: Upload coverage data | - name: Upload coverage data | ||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }} | |||||
uses: codecov/codecov-action@v2 | uses: codecov/codecov-action@v2 | ||||
with: | with: | ||||
name: Postgres | name: Postgres | ||||
@@ -1,22 +0,0 @@ | |||||
name: Frappe Linter | |||||
on: | |||||
pull_request: | |||||
branches: | |||||
- develop | |||||
- version-12-hotfix | |||||
- version-11-hotfix | |||||
jobs: | |||||
check_translation: | |||||
name: Translation Syntax Check | |||||
runs-on: ubuntu-18.04 | |||||
steps: | |||||
- uses: actions/checkout@v2 | |||||
- name: Setup python3 | |||||
uses: actions/setup-python@v1 | |||||
with: | |||||
python-version: 3.6 | |||||
- name: Validating Translation Syntax | |||||
run: | | |||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q | |||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) | |||||
python $GITHUB_WORKSPACE/.github/helper/translation.py $files |
@@ -12,7 +12,7 @@ concurrency: | |||||
jobs: | jobs: | ||||
test: | test: | ||||
runs-on: ubuntu-18.04 | |||||
runs-on: ubuntu-latest | |||||
strategy: | strategy: | ||||
fail-fast: false | fail-fast: false | ||||
@@ -1,9 +1,13 @@ | |||||
codecov: | codecov: | ||||
require_ci_to_pass: yes | require_ci_to_pass: yes | ||||
coverage: | |||||
status: | status: | ||||
project: | project: | ||||
default: | default: | ||||
target: auto | |||||
threshold: 0.5% | threshold: 0.5% | ||||
comment: | comment: | ||||
layout: "diff, flags, files" | |||||
layout: "diff" | |||||
require_changes: true | require_changes: true |
@@ -31,8 +31,13 @@ context('API Resources', () => { | |||||
}); | }); | ||||
it('Removes the Comments', () => { | it('Removes the Comments', () => { | ||||
cy.get_list('Comment').then(body => body.data.forEach(comment => { | |||||
cy.remove_doc('Comment', comment.name); | |||||
})); | |||||
cy.get_list('Comment').then(body => { | |||||
let comment_names = []; | |||||
body.data.map(comment => comment_names.push(comment.name)); | |||||
comment_names = [...new Set(comment_names)]; // remove duplicates | |||||
comment_names.forEach((comment_name) => { | |||||
cy.remove_doc('Comment', comment_name); | |||||
}); | |||||
}); | |||||
}); | }); | ||||
}); | }); |
@@ -0,0 +1,93 @@ | |||||
context("Control Float", () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit("/app/website"); | |||||
}); | |||||
function get_dialog_with_float() { | |||||
return cy.dialog({ | |||||
title: "Float Check", | |||||
fields: [ | |||||
{ | |||||
fieldname: "float_number", | |||||
fieldtype: "Float", | |||||
Label: "Float" | |||||
} | |||||
] | |||||
}); | |||||
} | |||||
it("check value changes", () => { | |||||
get_dialog_with_float().as("dialog"); | |||||
let data = get_data(); | |||||
data.forEach(x => { | |||||
cy.window() | |||||
.its("frappe") | |||||
.then(frappe => { | |||||
frappe.boot.sysdefaults.number_format = x.number_format; | |||||
}); | |||||
x.values.forEach(d => { | |||||
cy.get_field("float_number", "Float").clear(); | |||||
cy.fill_field("float_number", d.input, "Float").blur(); | |||||
cy.get_field("float_number", "Float").should( | |||||
"have.value", | |||||
d.blur_expected | |||||
); | |||||
cy.get_field("float_number", "Float").focus(); | |||||
cy.get_field("float_number", "Float").blur(); | |||||
cy.get_field("float_number", "Float").focus(); | |||||
cy.get_field("float_number", "Float").should( | |||||
"have.value", | |||||
d.focus_expected | |||||
); | |||||
}); | |||||
}); | |||||
}); | |||||
function get_data() { | |||||
return [ | |||||
{ | |||||
number_format: "#.###,##", | |||||
values: [ | |||||
{ | |||||
input: "364.87,334", | |||||
blur_expected: "36.487,334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "36487,334", | |||||
blur_expected: "36.487,334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "100", | |||||
blur_expected: "100,000", | |||||
focus_expected: "100" | |||||
} | |||||
] | |||||
}, | |||||
{ | |||||
number_format: "#,###.##", | |||||
values: [ | |||||
{ | |||||
input: "364,87.334", | |||||
blur_expected: "36,487.334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "36487.334", | |||||
blur_expected: "36,487.334", | |||||
focus_expected: "36487.334" | |||||
}, | |||||
{ | |||||
input: "100", | |||||
blur_expected: "100.000", | |||||
focus_expected: "100" | |||||
} | |||||
] | |||||
} | |||||
]; | |||||
} | |||||
}); |
@@ -1,19 +1,19 @@ | |||||
context('Datetime Field Validation', () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit('/app/communication'); | |||||
cy.window().its('frappe').then(frappe => { | |||||
frappe.call("frappe.tests.ui_test_helpers.create_communication_records"); | |||||
}); | |||||
}); | |||||
// TODO: Enable this again | |||||
// currently this is flaky possibly because of different timezone in CI | |||||
// validating datetime field value when value is set from backend and get validated on form load. | |||||
it('datetime field form validation', () => { | |||||
cy.visit('/app/communication'); | |||||
cy.get('a[title="Test Form Communication 1"]').invoke('attr', 'data-name') | |||||
.then((name) => { | |||||
cy.visit(`/app/communication/${name}`); | |||||
cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red'); | |||||
}); | |||||
}); | |||||
}); | |||||
// context('Datetime Field Validation', () => { | |||||
// before(() => { | |||||
// cy.login(); | |||||
// cy.visit('/app/communication'); | |||||
// }); | |||||
// it('datetime field form validation', () => { | |||||
// // validating datetime field value when value is set from backend and get validated on form load. | |||||
// cy.window().its('frappe').then(frappe => { | |||||
// return frappe.xcall("frappe.tests.ui_test_helpers.create_communication_record"); | |||||
// }).then(doc => { | |||||
// cy.visit(`/app/communication/${doc.name}`); | |||||
// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red'); | |||||
// }); | |||||
// }); | |||||
// }); |
@@ -7,11 +7,11 @@ context('List View', () => { | |||||
}); | }); | ||||
}); | }); | ||||
it('enables "Actions" button', () => { | it('enables "Actions" button', () => { | ||||
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | |||||
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | |||||
cy.go_to_list('ToDo'); | cy.go_to_list('ToDo'); | ||||
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true }); | cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true }); | ||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | ||||
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => { | |||||
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 9).each((el, index) => { | |||||
cy.wrap(el).contains(actions[index]); | cy.wrap(el).contains(actions[index]); | ||||
}).then((elements) => { | }).then((elements) => { | ||||
cy.intercept({ | cy.intercept({ | ||||
@@ -0,0 +1,58 @@ | |||||
context('MultiSelectDialog', () => { | |||||
before(() => { | |||||
cy.login(); | |||||
cy.visit('/app'); | |||||
}); | |||||
function open_multi_select_dialog() { | |||||
cy.window().its('frappe').then(frappe => { | |||||
new frappe.ui.form.MultiSelectDialog({ | |||||
doctype: "Assignment Rule", | |||||
target: {}, | |||||
setters: { | |||||
document_type: null, | |||||
priority: null | |||||
}, | |||||
add_filters_group: 1, | |||||
allow_child_item_selection: 1, | |||||
child_fieldname: "assignment_days", | |||||
child_columns: ["day"] | |||||
}); | |||||
}); | |||||
} | |||||
it('multi select dialog api works', () => { | |||||
open_multi_select_dialog(); | |||||
cy.get_open_dialog().should('contain', 'Select Assignment Rules'); | |||||
}); | |||||
it('checks for filters', () => { | |||||
['search_term', 'document_type', 'priority'].forEach(fieldname => { | |||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist'); | |||||
}); | |||||
// add_filters_group: 1 should add a filter group | |||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist'); | |||||
}); | |||||
it('checks for child item selection', () => { | |||||
cy.get_open_dialog() | |||||
.get(`.dt-row-header`).should('not.exist'); | |||||
cy.get_open_dialog() | |||||
.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`) | |||||
.should('exist') | |||||
.click(); | |||||
cy.get_open_dialog() | |||||
.get(`.frappe-control[data-fieldname="child_selection_area"]`) | |||||
.should('exist'); | |||||
cy.get_open_dialog() | |||||
.get(`.dt-row-header`).should('contain', 'Assignment Rule'); | |||||
cy.get_open_dialog() | |||||
.get(`.dt-row-header`).should('contain', 'Day'); | |||||
}); | |||||
}); |
@@ -6,12 +6,12 @@ context('Sidebar', () => { | |||||
}); | }); | ||||
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { | it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { | ||||
cy.click_sidebar_button(0); | |||||
cy.click_sidebar_button("Assigned To"); | |||||
//To check if no filter is available in "Assigned To" dropdown | //To check if no filter is available in "Assigned To" dropdown | ||||
cy.get('.empty-state').should('contain', 'No filters found'); | cy.get('.empty-state').should('contain', 'No filters found'); | ||||
cy.click_sidebar_button(1); | |||||
cy.click_sidebar_button("Created By"); | |||||
//To check if "Created By" dropdown contains filter | //To check if "Created By" dropdown contains filter | ||||
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me'); | cy.get('.group-by-item > .dropdown-item').should('contain', 'Me'); | ||||
@@ -22,7 +22,7 @@ context('Sidebar', () => { | |||||
cy.get_field('assign_to_me', 'Check').click(); | cy.get_field('assign_to_me', 'Check').click(); | ||||
cy.get('.modal-footer > .standard-actions > .btn-primary').click(); | cy.get('.modal-footer > .standard-actions > .btn-primary').click(); | ||||
cy.visit('/app/doctype'); | cy.visit('/app/doctype'); | ||||
cy.click_sidebar_button(0); | |||||
cy.click_sidebar_button("Assigned To"); | |||||
//To check if filter is added in "Assigned To" dropdown after assignment | //To check if filter is added in "Assigned To" dropdown after assignment | ||||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1'); | cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1'); | ||||
@@ -38,20 +38,19 @@ context('Sidebar', () => { | |||||
cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To'); | cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To'); | ||||
cy.get('.condition').should('have.value', 'like'); | cy.get('.condition').should('have.value', 'like'); | ||||
cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%'); | cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%'); | ||||
cy.click_filter_button(); | |||||
//To remove the applied filter | //To remove the applied filter | ||||
cy.get('.filter-action-buttons > div > .btn-secondary').contains('Clear Filters').click(); | |||||
cy.click_filter_button(); | |||||
cy.get('.filter-selector > .btn').should('contain', 'Filter'); | |||||
cy.clear_filters(); | |||||
//To remove the assignment | //To remove the assignment | ||||
cy.visit('/app/doctype'); | cy.visit('/app/doctype'); | ||||
cy.click_listview_row_item(0); | cy.click_listview_row_item(0); | ||||
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click(); | cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click(); | ||||
cy.get('.remove-btn').click({force: true}); | cy.get('.remove-btn').click({force: true}); | ||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close').click(); | |||||
cy.hide_dialog(); | |||||
cy.visit('/app/doctype'); | cy.visit('/app/doctype'); | ||||
cy.click_sidebar_button(0); | |||||
cy.click_sidebar_button("Assigned To"); | |||||
cy.get('.empty-state').should('contain', 'No filters found'); | cy.get('.empty-state').should('contain', 'No filters found'); | ||||
}); | }); | ||||
}); | }); |
@@ -4,11 +4,11 @@ context('Timeline', () => { | |||||
before(() => { | before(() => { | ||||
cy.visit('/login'); | cy.visit('/login'); | ||||
cy.login(); | cy.login(); | ||||
cy.visit('/app/todo'); | |||||
}); | }); | ||||
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => { | it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => { | ||||
//Adding new ToDo | //Adding new ToDo | ||||
cy.visit('/app/todo'); | |||||
cy.click_listview_primary_button('Add ToDo'); | cy.click_listview_primary_button('Add ToDo'); | ||||
cy.findByRole('button', {name: 'Edit in full page'}).click(); | cy.findByRole('button', {name: 'Edit in full page'}).click(); | ||||
cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true}); | cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true}); | ||||
@@ -28,15 +28,15 @@ context('Timeline', () => { | |||||
cy.get('.timeline-content').should('contain', 'Testing Timeline'); | cy.get('.timeline-content').should('contain', 'Testing Timeline'); | ||||
//Editing comment | //Editing comment | ||||
cy.click_timeline_action_btn(0); | |||||
cy.click_timeline_action_btn("Edit"); | |||||
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123'); | cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123'); | ||||
cy.click_timeline_action_btn(0); | |||||
cy.click_timeline_action_btn("Save"); | |||||
//To check if the edited comment text is visible in timeline content | //To check if the edited comment text is visible in timeline content | ||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123'); | cy.get('.timeline-content').should('contain', 'Testing Timeline 123'); | ||||
//Discarding comment | //Discarding comment | ||||
cy.click_timeline_action_btn(0); | |||||
cy.click_timeline_action_btn("Edit"); | |||||
cy.findByRole('button', {name: 'Dismiss'}).click(); | cy.findByRole('button', {name: 'Dismiss'}).click(); | ||||
//To check if after discarding the timeline content is same as previous | //To check if after discarding the timeline content is same as previous | ||||
@@ -81,7 +81,7 @@ context('Timeline', () => { | |||||
cy.visit('/app/custom-submittable-doctype'); | cy.visit('/app/custom-submittable-doctype'); | ||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click(); | cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click(); | ||||
cy.findByRole('button', {name: 'Actions'}).click(); | cy.findByRole('button', {name: 'Actions'}).click(); | ||||
cy.get('.actions-btn-group > .dropdown-menu > li > .grey-link').eq(7).click(); | |||||
cy.get('.actions-btn-group > .dropdown-menu > li > .dropdown-item').contains("Delete").click(); | |||||
cy.click_modal_primary_button('Yes', {force: true, delay: 700}); | cy.click_modal_primary_button('Yes', {force: true, delay: 700}); | ||||
//Deleting the custom doctype | //Deleting the custom doctype | ||||
@@ -187,7 +187,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { | |||||
if (fieldtype === 'Select') { | if (fieldtype === 'Select') { | ||||
cy.get('@input').select(value); | cy.get('@input').select(value); | ||||
} else { | } else { | ||||
cy.get('@input').type(value, {waitForAnimations: false, force: true}); | |||||
cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100}); | |||||
} | } | ||||
return cy.get('@input'); | return cy.get('@input'); | ||||
}); | }); | ||||
@@ -252,7 +252,8 @@ Cypress.Commands.add('new_form', doctype => { | |||||
}); | }); | ||||
Cypress.Commands.add('go_to_list', doctype => { | Cypress.Commands.add('go_to_list', doctype => { | ||||
cy.visit(`/app/list/${doctype}/list`); | |||||
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); | |||||
cy.visit(`/app/${dt_in_route}`); | |||||
}); | }); | ||||
Cypress.Commands.add('clear_cache', () => { | Cypress.Commands.add('clear_cache', () => { | ||||
@@ -316,7 +317,11 @@ Cypress.Commands.add('add_filter', () => { | |||||
}); | }); | ||||
Cypress.Commands.add('clear_filters', () => { | Cypress.Commands.add('clear_filters', () => { | ||||
cy.get('.filter-section .filter-button').click(); | |||||
cy.intercept({ | |||||
method: 'POST', | |||||
url: 'api/method/frappe.model.utils.user_settings.save' | |||||
}).as('filter-saved'); | |||||
cy.get('.filter-section .filter-button').click({force: true}); | |||||
cy.wait(300); | cy.wait(300); | ||||
cy.get('.filter-popover').should('exist'); | cy.get('.filter-popover').should('exist'); | ||||
cy.get('.filter-popover').find('.clear-filters').click(); | cy.get('.filter-popover').find('.clear-filters').click(); | ||||
@@ -324,16 +329,15 @@ Cypress.Commands.add('clear_filters', () => { | |||||
cy.window().its('cur_list').then(cur_list => { | cy.window().its('cur_list').then(cur_list => { | ||||
cur_list && cur_list.filter_area && cur_list.filter_area.clear(); | cur_list && cur_list.filter_area && cur_list.filter_area.clear(); | ||||
}); | }); | ||||
cy.wait('@filter-saved'); | |||||
}); | }); | ||||
Cypress.Commands.add('click_modal_primary_button', (btn_name) => { | Cypress.Commands.add('click_modal_primary_button', (btn_name) => { | ||||
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true}); | cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true}); | ||||
}); | }); | ||||
Cypress.Commands.add('click_sidebar_button', (btn_no) => { | |||||
cy.get('.list-group-by-fields > .group-by-field > .btn').eq(btn_no).click(); | |||||
Cypress.Commands.add('click_sidebar_button', (btn_name) => { | |||||
cy.get('.list-group-by-fields .list-link > a').contains(btn_name).click({force: true}); | |||||
}); | }); | ||||
Cypress.Commands.add('click_listview_row_item', (row_no) => { | Cypress.Commands.add('click_listview_row_item', (row_no) => { | ||||
@@ -348,6 +352,6 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => { | |||||
cy.get('.primary-action').contains(btn_name).click({force: true}); | cy.get('.primary-action').contains(btn_name).click({force: true}); | ||||
}); | }); | ||||
Cypress.Commands.add('click_timeline_action_btn', (btn_no) => { | |||||
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').eq(btn_no).first().click(); | |||||
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => { | |||||
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click(); | |||||
}); | }); |
@@ -618,8 +618,6 @@ def read_only(): | |||||
try: | try: | ||||
retval = fn(*args, **get_newargs(fn, kwargs)) | retval = fn(*args, **get_newargs(fn, kwargs)) | ||||
except: | |||||
raise | |||||
finally: | finally: | ||||
if local and hasattr(local, 'primary_db'): | if local and hasattr(local, 'primary_db'): | ||||
local.db.close() | local.db.close() | ||||
@@ -629,6 +627,29 @@ def read_only(): | |||||
return wrapper_fn | return wrapper_fn | ||||
return innfn | return innfn | ||||
def write_only(): | |||||
# if replica connection exists, we have to replace it momentarily with the primary connection | |||||
def innfn(fn): | |||||
def wrapper_fn(*args, **kwargs): | |||||
primary_db = getattr(local, "primary_db", None) | |||||
replica_db = getattr(local, "replica_db", None) | |||||
in_read_only = getattr(local, "db", None) != primary_db | |||||
# switch to primary connection | |||||
if in_read_only and primary_db: | |||||
local.db = local.primary_db | |||||
try: | |||||
retval = fn(*args, **get_newargs(fn, kwargs)) | |||||
finally: | |||||
# switch back to replica connection | |||||
if in_read_only and replica_db: | |||||
local.db = replica_db | |||||
return retval | |||||
return wrapper_fn | |||||
return innfn | |||||
def only_for(roles, message=False): | def only_for(roles, message=False): | ||||
"""Raise `frappe.PermissionError` if the user does not have any of the given **Roles**. | """Raise `frappe.PermissionError` if the user does not have any of the given **Roles**. | ||||
@@ -65,7 +65,7 @@ class Address(Document): | |||||
def has_link(self, doctype, name): | def has_link(self, doctype, name): | ||||
for link in self.links: | for link in self.links: | ||||
if link.link_doctype==doctype and link.link_name== name: | |||||
if link.link_doctype == doctype and link.link_name == name: | |||||
return True | return True | ||||
def has_common_link(self, doc): | def has_common_link(self, doc): | ||||
@@ -47,14 +47,14 @@ class Contact(Document): | |||||
def get_link_for(self, link_doctype): | def get_link_for(self, link_doctype): | ||||
'''Return the link name, if exists for the given link DocType''' | '''Return the link name, if exists for the given link DocType''' | ||||
for link in self.links: | for link in self.links: | ||||
if link.link_doctype==link_doctype: | |||||
if link.link_doctype == link_doctype: | |||||
return link.link_name | return link.link_name | ||||
return None | return None | ||||
def has_link(self, doctype, name): | def has_link(self, doctype, name): | ||||
for link in self.links: | for link in self.links: | ||||
if link.link_doctype==doctype and link.link_name== name: | |||||
if link.link_doctype == doctype and link.link_name == name: | |||||
return True | return True | ||||
def has_common_link(self, doc): | def has_common_link(self, doc): | ||||
@@ -9,6 +9,7 @@ class AccessLog(Document): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@frappe.write_only() | |||||
def make_access_log(doctype=None, document=None, method=None, file_type=None, | def make_access_log(doctype=None, document=None, method=None, file_type=None, | ||||
report_name=None, filters=None, page=None, columns=None): | report_name=None, filters=None, page=None, columns=None): | ||||
@@ -36,8 +36,11 @@ class UserType(Document): | |||||
if not self.user_doctypes: | if not self.user_doctypes: | ||||
return | return | ||||
modules = frappe.get_all('DocType', fields=['distinct module as module'], | |||||
filters={'name': ('in', [d.document_type for d in self.user_doctypes])}) | |||||
modules = frappe.get_all("DocType", | |||||
fields=["module"], | |||||
filters={"name": ("in", [d.document_type for d in self.user_doctypes])}, | |||||
distinct=True, | |||||
) | |||||
self.set('user_type_modules', []) | self.set('user_type_modules', []) | ||||
for row in modules: | for row in modules: | ||||
@@ -1,21 +0,0 @@ | |||||
.version-info { | |||||
overflow: auto; | |||||
} | |||||
.version-info pre { | |||||
border: 0px; | |||||
margin: 0px; | |||||
background-color: inherit; | |||||
} | |||||
.version-info .table { | |||||
background-color: inherit; | |||||
} | |||||
.version-info .success { | |||||
background-color: #dff0d8 !important; | |||||
} | |||||
.version-info .danger { | |||||
background-color: #f2dede !important; | |||||
} |
@@ -1,45 +0,0 @@ | |||||
{ | |||||
"db_name": "testdb", | |||||
"db_password": "password", | |||||
"mute_emails": true, | |||||
"limits": { | |||||
"emails": 1500, | |||||
"space": 0.157, | |||||
"expiry": "2016-07-25", | |||||
"users": 1 | |||||
}, | |||||
"developer_mode": 1, | |||||
"auto_cache_clear": true, | |||||
"disable_website_cache": true, | |||||
"max_file_size": 1000000, | |||||
"mail_server": "localhost", | |||||
"mail_login": null, | |||||
"mail_password": null, | |||||
"mail_port": 25, | |||||
"use_ssl": 0, | |||||
"auto_email_id": "hello@example.com", | |||||
"google_analytics_id": "google_analytics_id", | |||||
"google_analytics_anonymize_ip": 1, | |||||
"google_login": { | |||||
"client_id": "google_client_id", | |||||
"client_secret": "google_client_secret" | |||||
}, | |||||
"github_login": { | |||||
"client_id": "github_client_id", | |||||
"client_secret": "github_client_secret" | |||||
}, | |||||
"facebook_login": { | |||||
"client_id": "facebook_client_id", | |||||
"client_secret": "facebook_client_secret" | |||||
}, | |||||
"celery_broker": "redis://localhost", | |||||
"celery_result_backend": null, | |||||
"scheduler_interval": 300, | |||||
"celery_queue_per_site": true | |||||
} |
@@ -256,11 +256,11 @@ class MariaDBDatabase(Database): | |||||
index_name=index_name | index_name=index_name | ||||
)) | )) | ||||
def add_index(self, doctype, fields, index_name=None): | |||||
def add_index(self, doctype: str, fields: List, index_name: str = None): | |||||
"""Creates an index with given fields if not already created. | """Creates an index with given fields if not already created. | ||||
Index name will be `fieldname1_fieldname2_index`""" | Index name will be `fieldname1_fieldname2_index`""" | ||||
index_name = index_name or self.get_index_name(fields) | index_name = index_name or self.get_index_name(fields) | ||||
table_name = 'tab' + doctype | |||||
table_name = get_table_name(doctype) | |||||
if not self.has_index(table_name, index_name): | if not self.has_index(table_name, index_name): | ||||
self.commit() | self.commit() | ||||
self.sql("""ALTER TABLE `%s` | self.sql("""ALTER TABLE `%s` | ||||
@@ -258,14 +258,14 @@ class PostgresDatabase(Database): | |||||
return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' | return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' | ||||
and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name)) | and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name)) | ||||
def add_index(self, doctype, fields, index_name=None): | |||||
def add_index(self, doctype: str, fields: List, index_name: str = None): | |||||
"""Creates an index with given fields if not already created. | """Creates an index with given fields if not already created. | ||||
Index name will be `fieldname1_fieldname2_index`""" | Index name will be `fieldname1_fieldname2_index`""" | ||||
table_name = get_table_name(doctype) | |||||
index_name = index_name or self.get_index_name(fields) | index_name = index_name or self.get_index_name(fields) | ||||
table_name = 'tab' + doctype | |||||
fields_str = '", "'.join(re.sub(r"\(.*\)", "", field) for field in fields) | |||||
self.commit() | |||||
self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields))) | |||||
self.sql_ddl(f'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}` ("{fields_str}")') | |||||
def add_unique(self, doctype, fields, constraint_name=None): | def add_unique(self, doctype, fields, constraint_name=None): | ||||
if isinstance(fields, str): | if isinstance(fields, str): | ||||
@@ -20,5 +20,46 @@ frappe.ui.form.on('System Console', { | |||||
$btn.text(__('Execute')); | $btn.text(__('Execute')); | ||||
}); | }); | ||||
}); | }); | ||||
}, | |||||
show_processlist: function(frm) { | |||||
if (frm.doc.show_processlist) { | |||||
// keep refreshing every 5 seconds | |||||
frm.events.refresh_processlist(frm); | |||||
frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000); | |||||
} else { | |||||
if (frm.processlist_interval) { | |||||
// end it | |||||
clearInterval(frm.processlist_interval); | |||||
} | |||||
} | |||||
}, | |||||
refresh_processlist: function(frm) { | |||||
let timestamp = new Date(); | |||||
frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => { | |||||
let rows = ''; | |||||
for (let row of r.message) { | |||||
rows += `<tr> | |||||
<td>${row.Id}</td> | |||||
<td>${row.Time}</td> | |||||
<td>${row.State}</td> | |||||
<td>${row.Info}</td> | |||||
<td>${row.Progress}</td> | |||||
</tr>` | |||||
} | |||||
frm.get_field('processlist').html(` | |||||
<p class='text-muted'>Requested on: ${timestamp}</p> | |||||
<table class='table-bordered' style='width: 100%'> | |||||
<thead><tr> | |||||
<th width='10%'>Id</ht> | |||||
<th width='10%'>Time</ht> | |||||
<th width='10%'>State</ht> | |||||
<th width='60%'>Info</ht> | |||||
<th width='10%'>Progress</ht> | |||||
</tr></thead> | |||||
<tbody>${rows}</thead>`); | |||||
}); | |||||
} | } | ||||
}); | }); |
@@ -17,9 +17,13 @@ | |||||
"editable_grid": 1, | "editable_grid": 1, | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | "field_order": [ | ||||
"execute_section", | |||||
"console", | "console", | ||||
"commit", | "commit", | ||||
"output" | |||||
"output", | |||||
"database_processes_section", | |||||
"show_processlist", | |||||
"processlist" | |||||
], | ], | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
@@ -40,13 +44,34 @@ | |||||
"fieldname": "commit", | "fieldname": "commit", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Commit" | "label": "Commit" | ||||
}, | |||||
{ | |||||
"fieldname": "execute_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Execute" | |||||
}, | |||||
{ | |||||
"fieldname": "database_processes_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Database Processes" | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "show_processlist", | |||||
"fieldtype": "Check", | |||||
"label": "Show Processlist" | |||||
}, | |||||
{ | |||||
"fieldname": "processlist", | |||||
"fieldtype": "HTML", | |||||
"label": "processlist" | |||||
} | } | ||||
], | ], | ||||
"hide_toolbar": 1, | "hide_toolbar": 1, | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"issingle": 1, | "issingle": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-08-21 14:44:35.296877", | |||||
"modified": "2021-09-09 13:10:14.237113", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Desk", | "module": "Desk", | ||||
"name": "System Console", | "name": "System Console", | ||||
@@ -65,4 +90,4 @@ | |||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -33,4 +33,9 @@ class SystemConsole(Document): | |||||
def execute_code(doc): | def execute_code(doc): | ||||
console = frappe.get_doc(json.loads(doc)) | console = frappe.get_doc(json.loads(doc)) | ||||
console.run() | console.run() | ||||
return console.as_dict() | |||||
return console.as_dict() | |||||
@frappe.whitelist() | |||||
def show_processlist(): | |||||
frappe.only_for('System Manager') | |||||
return frappe.db.sql('show full processlist', as_dict=1) |
@@ -1,6 +1,7 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
from typing import Dict, List, Union | |||||
import frappe, json | import frappe, json | ||||
import frappe.utils | import frappe.utils | ||||
import frappe.share | import frappe.share | ||||
@@ -105,9 +106,10 @@ def get_docinfo(doc=None, doctype=None, name=None): | |||||
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'), | "assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'), | ||||
"permissions": get_doc_permissions(doc), | "permissions": get_doc_permissions(doc), | ||||
"shared": frappe.share.get_users(doc.doctype, doc.name), | "shared": frappe.share.get_users(doc.doctype, doc.name), | ||||
"info_logs": get_comments(doc.doctype, doc.name, 'Info'), | |||||
"info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']), | |||||
"share_logs": get_comments(doc.doctype, doc.name, 'share'), | "share_logs": get_comments(doc.doctype, doc.name, 'share'), | ||||
"like_logs": get_comments(doc.doctype, doc.name, 'Like'), | "like_logs": get_comments(doc.doctype, doc.name, 'Like'), | ||||
"workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"), | |||||
"views": get_view_logs(doc.doctype, doc.name), | "views": get_view_logs(doc.doctype, doc.name), | ||||
"energy_point_logs": get_point_logs(doc.doctype, doc.name), | "energy_point_logs": get_point_logs(doc.doctype, doc.name), | ||||
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), | "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), | ||||
@@ -138,10 +140,11 @@ def get_communications(doctype, name, start=0, limit=20): | |||||
return _get_communications(doctype, name, start, limit) | return _get_communications(doctype, name, start, limit) | ||||
def get_comments(doctype, name, comment_type='Comment'): | |||||
comment_types = [comment_type] | |||||
def get_comments(doctype: str, name: str, comment_type : Union[str, List[str]] = "Comment") -> List[frappe._dict]: | |||||
if isinstance(comment_type, list): | |||||
comment_types = comment_type | |||||
if comment_type == 'share': | |||||
elif comment_type == 'share': | |||||
comment_types = ['Shared', 'Unshared'] | comment_types = ['Shared', 'Unshared'] | ||||
elif comment_type == 'assignment': | elif comment_type == 'assignment': | ||||
@@ -150,15 +153,21 @@ def get_comments(doctype, name, comment_type='Comment'): | |||||
elif comment_type == 'attachment': | elif comment_type == 'attachment': | ||||
comment_types = ['Attachment', 'Attachment Removed'] | comment_types = ['Attachment', 'Attachment Removed'] | ||||
comments = frappe.get_all('Comment', fields = ['name', 'creation', 'content', 'owner', 'comment_type'], filters=dict( | |||||
reference_doctype = doctype, | |||||
reference_name = name, | |||||
comment_type = ['in', comment_types] | |||||
)) | |||||
else: | |||||
comment_types = [comment_type] | |||||
comments = frappe.get_all("Comment", | |||||
fields=["name", "creation", "content", "owner", "comment_type"], | |||||
filters={ | |||||
"reference_doctype": doctype, | |||||
"reference_name": name, | |||||
"comment_type": ['in', comment_types], | |||||
} | |||||
) | |||||
# convert to markdown (legacy ?) | # convert to markdown (legacy ?) | ||||
if comment_type == 'Comment': | |||||
for c in comments: | |||||
for c in comments: | |||||
if c.comment_type == "Comment": | |||||
c.content = frappe.utils.markdown(c.content) | c.content = frappe.utils.markdown(c.content) | ||||
return comments | return comments | ||||
@@ -69,13 +69,11 @@ def make_tree_args(**kwarg): | |||||
doctype = kwarg['doctype'] | doctype = kwarg['doctype'] | ||||
parent_field = 'parent_' + doctype.lower().replace(' ', '_') | parent_field = 'parent_' + doctype.lower().replace(' ', '_') | ||||
name_field = kwarg.get('name_field', doctype.lower().replace(' ', '_') + '_name') | |||||
if kwarg['is_root'] == 'false': kwarg['is_root'] = False | if kwarg['is_root'] == 'false': kwarg['is_root'] = False | ||||
if kwarg['is_root'] == 'true': kwarg['is_root'] = True | if kwarg['is_root'] == 'true': kwarg['is_root'] = True | ||||
kwarg.update({ | kwarg.update({ | ||||
name_field: kwarg[name_field], | |||||
parent_field: kwarg.get("parent") or kwarg.get(parent_field) | parent_field: kwarg.get("parent") or kwarg.get(parent_field) | ||||
}) | }) | ||||
@@ -164,7 +164,8 @@ doc_events = { | |||||
"after_rename": "frappe.desk.notifications.clear_doctype_notifications", | "after_rename": "frappe.desk.notifications.clear_doctype_notifications", | ||||
"on_cancel": [ | "on_cancel": [ | ||||
"frappe.desk.notifications.clear_doctype_notifications", | "frappe.desk.notifications.clear_doctype_notifications", | ||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" | |||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", | |||||
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers" | |||||
], | ], | ||||
"on_trash": [ | "on_trash": [ | ||||
"frappe.desk.notifications.clear_doctype_notifications", | "frappe.desk.notifications.clear_doctype_notifications", | ||||
@@ -2,6 +2,7 @@ | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
"""build query for doclistview and return results""" | """build query for doclistview and return results""" | ||||
from typing import List | |||||
import frappe.defaults | import frappe.defaults | ||||
import frappe.share | import frappe.share | ||||
from frappe import _ | from frappe import _ | ||||
@@ -33,7 +34,7 @@ class DatabaseQuery(object): | |||||
join='left join', distinct=False, start=None, page_length=None, limit=None, | join='left join', distinct=False, start=None, page_length=None, limit=None, | ||||
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, | ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, | ||||
update=None, add_total_row=None, user_settings=None, reference_doctype=None, | update=None, add_total_row=None, user_settings=None, reference_doctype=None, | ||||
return_query=False, strict=True, pluck=None, ignore_ddl=False): | |||||
return_query=False, strict=True, pluck=None, ignore_ddl=False) -> List: | |||||
if not ignore_permissions and \ | if not ignore_permissions and \ | ||||
not frappe.has_permission(self.doctype, "select", user=user) and \ | not frappe.has_permission(self.doctype, "select", user=user) and \ | ||||
not frappe.has_permission(self.doctype, "read", user=user): | not frappe.has_permission(self.doctype, "read", user=user): | ||||
@@ -36,21 +36,23 @@ frappe.ui.form.on("Print Format", { | |||||
else if (frm.doc.custom_format && !frm.doc.raw_printing) { | else if (frm.doc.custom_format && !frm.doc.raw_printing) { | ||||
frm.set_df_property("html", "reqd", 1); | frm.set_df_property("html", "reqd", 1); | ||||
} | } | ||||
frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => { | |||||
if (r.default_print_format != frm.doc.name) { | |||||
frm.add_custom_button(__("Set as Default"), function () { | |||||
frappe.call({ | |||||
method: "frappe.printing.doctype.print_format.print_format.make_default", | |||||
args: { | |||||
name: frm.doc.name | |||||
}, | |||||
callback: function() { | |||||
frm.refresh(); | |||||
} | |||||
if (frappe.perm.has_perm('DocType', 0, 'read', frm.doc.doc_type)) { | |||||
frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => { | |||||
if (r.default_print_format != frm.doc.name) { | |||||
frm.add_custom_button(__("Set as Default"), function () { | |||||
frappe.call({ | |||||
method: "frappe.printing.doctype.print_format.print_format.make_default", | |||||
args: { | |||||
name: frm.doc.name | |||||
}, | |||||
callback: function() { | |||||
frm.refresh(); | |||||
} | |||||
}); | |||||
}); | }); | ||||
}); | |||||
} | |||||
}); | |||||
} | |||||
}); | |||||
} | |||||
} | } | ||||
}, | }, | ||||
custom_format: function (frm) { | custom_format: function (frm) { | ||||
@@ -174,7 +174,7 @@ frappe.ui.form.PrintView = class { | |||||
}); | }); | ||||
} | } | ||||
if (frappe.user.has_role('System Manager')) { | |||||
if (frappe.perm.has_perm('Print Format', 0, 'create')) { | |||||
this.page.add_menu_item(__('Customize'), () => | this.page.add_menu_item(__('Customize'), () => | ||||
this.edit_print_format() | this.edit_print_format() | ||||
); | ); | ||||
@@ -35,10 +35,13 @@ | |||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.717 7c2.29 0 4.211 1.859 4.494 4.266 1.272.152 2.289 1.31 2.289 2.742 0 1.523-1.13 2.742-2.543 2.742H8.043c-1.413 0-2.543-1.219-2.543-2.742 0-1.188.707-2.224 1.724-2.59C7.422 8.92 9.372 7 11.717 7zm.148 2.37a.499.499 0 0 0-.363.156l-1.556 1.555a.5.5 0 1 0 .708.707l.71-.711v3.097a.5.5 0 0 0 1 0v-3.098l.713.712a.5.5 0 1 0 .707-.707l-1.565-1.565a.498.498 0 0 0-.354-.146z" | <path fill-rule="evenodd" clip-rule="evenodd" d="M11.717 7c2.29 0 4.211 1.859 4.494 4.266 1.272.152 2.289 1.31 2.289 2.742 0 1.523-1.13 2.742-2.543 2.742H8.043c-1.413 0-2.543-1.219-2.543-2.742 0-1.188.707-2.224 1.724-2.59C7.422 8.92 9.372 7 11.717 7zm.148 2.37a.499.499 0 0 0-.363.156l-1.556 1.555a.5.5 0 1 0 .708.707l.71-.711v3.097a.5.5 0 0 0 1 0v-3.098l.713.712a.5.5 0 1 0 .707-.707l-1.565-1.565a.498.498 0 0 0-.354-.146z" | ||||
fill="#fff"></path> | fill="#fff"></path> | ||||
</symbol> | </symbol> | ||||
<symbol viewBox="0 0 20 20" id="icon-upload" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |||||
<symbol viewBox="0 0 20 20" id="icon-upload" fill="var(--icon-stroke)" stroke-width="0.2" xmlns="http://www.w3.org/2000/svg"> | |||||
<path d="M10.596 2.046a.5.5 0 0 0-.707 0L6.937 5a.5.5 0 0 0 .707.707l2.099-2.099v8.126a.5.5 0 1 0 1 0V3.607l2.098 2.099a.5.5 0 0 0 .708-.707l-2.953-2.953z"/> | <path d="M10.596 2.046a.5.5 0 0 0-.707 0L6.937 5a.5.5 0 0 0 .707.707l2.099-2.099v8.126a.5.5 0 1 0 1 0V3.607l2.098 2.099a.5.5 0 0 0 .708-.707l-2.953-2.953z"/> | ||||
<path d="M6.552 8.305v1H4.6V15.9a1 1 0 0 0 1 1h9.286a1 1 0 0 0 1-1V9.305h-1.953v-1h2.953V15.9a2 2 0 0 1-2 2H5.6a2 2 0 0 1-2-2V8.305h2.952z"/> | <path d="M6.552 8.305v1H4.6V15.9a1 1 0 0 0 1 1h9.286a1 1 0 0 0 1-1V9.305h-1.953v-1h2.953V15.9a2 2 0 0 1-2 2H5.6a2 2 0 0 1-2-2V8.305h2.952z"/> | ||||
</symbol> | </symbol> | ||||
<symbol viewBox="0 0 24 24" id="icon-milestone" fill="var(--icon-stroke)" fill-rule="evenodd" stroke-width="0.1" xmlns="http://www.w3.org/2000/svg"> | |||||
<path d="M3.5 3.75a.25.25 0 01.25-.25h13.5a.25.25 0 01.25.25v10a.75.75 0 001.5 0v-10A1.75 1.75 0 0017.25 2H3.75A1.75 1.75 0 002 3.75v16.5c0 .966.784 1.75 1.75 1.75h7a.75.75 0 000-1.5h-7a.25.25 0 01-.25-.25V3.75z"></path><path d="M6.25 7a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm-.75 4.75a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5h-4.5a.75.75 0 01-.75-.75zm16.28 4.53a.75.75 0 10-1.06-1.06l-4.97 4.97-1.97-1.97a.75.75 0 10-1.06 1.06l2.5 2.5a.75.75 0 001.06 0l5.5-5.5z"/> | |||||
</symbol> | |||||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-tag"> | <symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-tag"> | ||||
<path d="M12.6401 10.2571L10.107 12.7901C9.52125 13.3759 8.5718 13.3756 7.98601 12.7898L2.49654 7.30037C2.40278 7.2066 2.3501 7.07942 2.3501 6.94682L2.3501 3C2.3501 2.72386 2.57396 2.5 2.8501 2.5L6.79691 2.5C6.92952 2.5 7.0567 2.55268 7.15047 2.64645L12.6399 8.13591C13.2257 8.7217 13.2259 9.67131 12.6401 10.2571Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M12.6401 10.2571L10.107 12.7901C9.52125 13.3759 8.5718 13.3756 7.98601 12.7898L2.49654 7.30037C2.40278 7.2066 2.3501 7.07942 2.3501 6.94682L2.3501 3C2.3501 2.72386 2.57396 2.5 2.8501 2.5L6.79691 2.5C6.92952 2.5 7.0567 2.55268 7.15047 2.64645L12.6399 8.13591C13.2257 8.7217 13.2259 9.67131 12.6401 10.2571Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/> | ||||
<path d="M6.08001 5.46157C6.08001 5.88642 5.7356 6.23082 5.31076 6.23082C4.88591 6.23082 4.5415 5.88642 4.5415 5.46157C4.5415 5.03673 4.88591 4.69232 5.31076 4.69232C5.7356 4.69232 6.08001 5.03673 6.08001 5.46157Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M6.08001 5.46157C6.08001 5.88642 5.7356 6.23082 5.31076 6.23082C4.88591 6.23082 4.5415 5.88642 4.5415 5.46157C4.5415 5.03673 4.88591 4.69232 5.31076 4.69232C5.7356 4.69232 6.08001 5.03673 6.08001 5.46157Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/> | ||||
@@ -680,7 +683,7 @@ | |||||
<path d="M2 11.4167L5 14.25L13 4.75" stroke="#2D95F0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M2 11.4167L5 14.25L13 4.75" stroke="#2D95F0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | ||||
<path d="M9 13.4167L10 14.25L18 4.75" stroke="#2D95F0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M9 13.4167L10 14.25L18 4.75" stroke="#2D95F0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | ||||
</symbol> | </symbol> | ||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-branch"> | |||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-branch" stroke-width="1.2"> | |||||
<path d="M6.04541 6.59082V13.4089" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M6.04541 6.59082V13.4089" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | ||||
<path d="M14.2272 6.59082V7.95444C14.2272 8.67776 13.9398 9.37144 13.4284 9.8829C12.9169 10.3944 12.2232 10.6817 11.4999 10.6817H8.77266C8.04935 10.6817 7.35566 10.969 6.8442 11.4805C6.33274 11.9919 6.04541 12.6856 6.04541 13.4089" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M14.2272 6.59082V7.95444C14.2272 8.67776 13.9398 9.37144 13.4284 9.8829C12.9169 10.3944 12.2232 10.6817 11.4999 10.6817H8.77266C8.04935 10.6817 7.35566 10.969 6.8442 11.4805C6.33274 11.9919 6.04541 12.6856 6.04541 13.4089" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | ||||
<path d="M6.04544 6.59087C7.1751 6.59087 8.09087 5.6751 8.09087 4.54544C8.09087 3.41577 7.1751 2.5 6.04544 2.5C4.91577 2.5 4 3.41577 4 4.54544C4 5.6751 4.91577 6.59087 6.04544 6.59087Z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M6.04544 6.59087C7.1751 6.59087 8.09087 5.6751 8.09087 4.54544C8.09087 3.41577 7.1751 2.5 6.04544 2.5C4.91577 2.5 4 3.41577 4 4.54544C4 5.6751 4.91577 6.59087 6.04544 6.59087Z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | ||||
@@ -1,4 +1,17 @@ | |||||
frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { | frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { | ||||
make_input() { | |||||
super.make_input(); | |||||
const change_handler = e => { | |||||
if (this.change) this.change(e); | |||||
else { | |||||
let value = this.get_input_value(); | |||||
this.parse_validate_and_set_in_model(value, e); | |||||
} | |||||
}; | |||||
// convert to number format on focusout since focus converts it to flt. | |||||
this.$input.on("focusout", change_handler); | |||||
} | |||||
parse(value) { | parse(value) { | ||||
value = this.eval_expression(value); | value = this.eval_expression(value); | ||||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); | return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); | ||||
@@ -86,7 +86,7 @@ class BaseTimeline { | |||||
}); | }); | ||||
if (item.icon) { | if (item.icon) { | ||||
timeline_item.append(` | timeline_item.append(` | ||||
<div class="timeline-badge"> | |||||
<div class="timeline-badge" title='${item.title || frappe.utils.to_title_case(item.icon)}'> | |||||
${frappe.utils.icon(item.icon, item.icon_size || 'md')} | ${frappe.utils.icon(item.icon, item.icon_size || 'md')} | ||||
</div> | </div> | ||||
`); | `); | ||||
@@ -136,6 +136,7 @@ class FormTimeline extends BaseTimeline { | |||||
this.timeline_items.push(...this.get_energy_point_timeline_contents()); | this.timeline_items.push(...this.get_energy_point_timeline_contents()); | ||||
this.timeline_items.push(...this.get_version_timeline_contents()); | this.timeline_items.push(...this.get_version_timeline_contents()); | ||||
this.timeline_items.push(...this.get_share_timeline_contents()); | this.timeline_items.push(...this.get_share_timeline_contents()); | ||||
this.timeline_items.push(...this.get_workflow_timeline_contents()); | |||||
this.timeline_items.push(...this.get_like_timeline_contents()); | this.timeline_items.push(...this.get_like_timeline_contents()); | ||||
this.timeline_items.push(...this.get_custom_timeline_contents()); | this.timeline_items.push(...this.get_custom_timeline_contents()); | ||||
this.timeline_items.push(...this.get_assignment_timeline_contents()); | this.timeline_items.push(...this.get_assignment_timeline_contents()); | ||||
@@ -146,7 +147,9 @@ class FormTimeline extends BaseTimeline { | |||||
} | } | ||||
get_user_link(user) { | get_user_link(user) { | ||||
const user_display_text = (frappe.user_info(user).fullname || '').bold(); | |||||
const user_display_text = ( | |||||
(frappe.session.user == user ? __("You") : frappe.user_info(user).fullname) || '' | |||||
).bold(); | |||||
return frappe.utils.get_form_link('User', user, true, user_display_text); | return frappe.utils.get_form_link('User', user, true, user_display_text); | ||||
} | } | ||||
@@ -339,11 +342,26 @@ class FormTimeline extends BaseTimeline { | |||||
icon_size: 'sm', | icon_size: 'sm', | ||||
creation: like_log.creation, | creation: like_log.creation, | ||||
content: __('{0} Liked', [this.get_user_link(like_log.owner)]), | content: __('{0} Liked', [this.get_user_link(like_log.owner)]), | ||||
title: "Like", | |||||
}); | }); | ||||
}); | }); | ||||
return like_timeline_contents; | return like_timeline_contents; | ||||
} | } | ||||
get_workflow_timeline_contents() { | |||||
let workflow_timeline_contents = []; | |||||
(this.doc_info.workflow_logs || []).forEach(workflow_log => { | |||||
workflow_timeline_contents.push({ | |||||
icon: 'branch', | |||||
icon_size: 'sm', | |||||
creation: workflow_log.creation, | |||||
content: `${this.get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`, | |||||
title: "Workflow", | |||||
}); | |||||
}); | |||||
return workflow_timeline_contents; | |||||
} | |||||
get_custom_timeline_contents() { | get_custom_timeline_contents() { | ||||
let custom_timeline_contents = []; | let custom_timeline_contents = []; | ||||
(this.doc_info.additional_timeline_content || []).forEach(custom_item => { | (this.doc_info.additional_timeline_content || []).forEach(custom_item => { | ||||
@@ -2,86 +2,191 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
constructor(opts) { | constructor(opts) { | ||||
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */ | /* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */ | ||||
Object.assign(this, opts); | Object.assign(this, opts); | ||||
var me = this; | |||||
if (this.doctype != "[Select]") { | |||||
frappe.model.with_doctype(this.doctype, function () { | |||||
me.make(); | |||||
}); | |||||
this.for_select = this.doctype == "[Select]"; | |||||
if (!this.for_select) { | |||||
frappe.model.with_doctype(this.doctype, () => this.init()); | |||||
} else { | } else { | ||||
this.make(); | |||||
this.init(); | |||||
} | } | ||||
} | } | ||||
make() { | |||||
let me = this; | |||||
init() { | |||||
this.page_length = 20; | this.page_length = 20; | ||||
this.start = 0; | this.start = 0; | ||||
let fields = this.get_primary_filters(); | |||||
this.fields = this.get_fields(); | |||||
this.make(); | |||||
} | |||||
get_fields() { | |||||
const primary_fields = this.get_primary_filters(); | |||||
const result_fields = this.get_result_fields(); | |||||
const data_fields = this.get_data_fields(); | |||||
const child_selection_fields = this.get_child_selection_fields(); | |||||
// Make results area | |||||
fields = fields.concat([ | |||||
{ fieldtype: "HTML", fieldname: "results_area" }, | |||||
return [...primary_fields, ...result_fields, ...data_fields, ...child_selection_fields]; | |||||
} | |||||
get_result_fields() { | |||||
const show_next_page = () => { | |||||
this.start += 20; | |||||
this.get_results(); | |||||
}; | |||||
return [ | |||||
{ | { | ||||
fieldtype: "Button", fieldname: "more_btn", label: __("More"), | |||||
click: () => { | |||||
this.start += 20; | |||||
this.get_results(); | |||||
} | |||||
fieldtype: "HTML", fieldname: "results_area" | |||||
}, | |||||
{ | |||||
fieldtype: "Button", fieldname: "more_btn", | |||||
label: __("More"), click: show_next_page.bind(this) | |||||
} | } | ||||
]); | |||||
]; | |||||
} | |||||
// Custom Data Fields | |||||
if (this.data_fields) { | |||||
fields.push({ fieldtype: "Section Break" }); | |||||
fields = fields.concat(this.data_fields); | |||||
get_data_fields() { | |||||
if (this.data_fields && this.data_fields.length) { | |||||
// Custom Data Fields | |||||
return [ | |||||
{ fieldtype: "Section Break" }, | |||||
...this.data_fields | |||||
]; | |||||
} else { | |||||
return []; | |||||
} | } | ||||
} | |||||
get_child_selection_fields() { | |||||
const fields = []; | |||||
if (this.allow_child_item_selection && this.child_fieldname) { | |||||
fields.push({ fieldtype: "HTML", fieldname: "child_selection_area" }); | |||||
} | |||||
return fields; | |||||
} | |||||
make() { | |||||
let doctype_plural = this.doctype.plural(); | let doctype_plural = this.doctype.plural(); | ||||
let title = __("Select {0}", [this.for_select ? __("value") : __(doctype_plural)]); | |||||
this.dialog = new frappe.ui.Dialog({ | this.dialog = new frappe.ui.Dialog({ | ||||
title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]), | |||||
fields: fields, | |||||
title: title, | |||||
fields: this.fields, | |||||
primary_action_label: this.primary_action_label || __("Get Items"), | primary_action_label: this.primary_action_label || __("Get Items"), | ||||
secondary_action_label: __("Make {0}", [__(me.doctype)]), | |||||
primary_action: function () { | |||||
let filters_data = me.get_custom_filters(); | |||||
me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data); | |||||
secondary_action_label: __("Make {0}", [__(this.doctype)]), | |||||
primary_action: () => { | |||||
let filters_data = this.get_custom_filters(); | |||||
const data_values = cur_dialog.get_values(); // to pass values of data fields | |||||
const filtered_children = this.get_selected_child_names(); | |||||
const selected_documents = [...this.get_checked_values(), ...this.get_parent_name_of_selected_children()]; | |||||
this.action(selected_documents, { | |||||
...this.args, | |||||
...data_values, | |||||
...filters_data, | |||||
filtered_children | |||||
}); | |||||
}, | }, | ||||
secondary_action: function (e) { | |||||
// If user wants to close the modal | |||||
if (e) { | |||||
frappe.route_options = {}; | |||||
if (Array.isArray(me.setters)) { | |||||
for (let df of me.setters) { | |||||
frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; | |||||
} | |||||
} else { | |||||
Object.keys(me.setters).forEach(function (setter) { | |||||
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; | |||||
}); | |||||
} | |||||
frappe.new_doc(me.doctype, true); | |||||
} | |||||
} | |||||
secondary_action: this.make_new_document.bind(this) | |||||
}); | }); | ||||
if (this.add_filters_group) { | if (this.add_filters_group) { | ||||
this.make_filter_area(); | this.make_filter_area(); | ||||
} | } | ||||
this.args = {}; | |||||
this.setup_results(); | |||||
this.bind_events(); | |||||
this.get_results(); | |||||
this.dialog.show(); | |||||
} | |||||
make_new_document(e) { | |||||
// If user wants to close the modal | |||||
if (e) { | |||||
this.set_route_options(); | |||||
frappe.new_doc(this.doctype, true); | |||||
} | |||||
} | |||||
set_route_options() { | |||||
// set route options to get pre-filled form fields | |||||
frappe.route_options = {}; | |||||
if (Array.isArray(this.setters)) { | |||||
for (let df of this.setters) { | |||||
frappe.route_options[df.fieldname] = this.dialog.fields_dict[df.fieldname].get_value() || undefined; | |||||
} | |||||
} else { | |||||
Object.keys(this.setters).forEach(setter => { | |||||
frappe.route_options[setter] = this.dialog.fields_dict[setter].get_value() || undefined; | |||||
}); | |||||
} | |||||
} | |||||
setup_results() { | |||||
this.$parent = $(this.dialog.body); | this.$parent = $(this.dialog.body); | ||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results" | |||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results mt-3" | |||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`); | style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`); | ||||
this.$results = this.$wrapper.find('.results'); | this.$results = this.$wrapper.find('.results'); | ||||
this.$results.append(this.make_list_row()); | this.$results.append(this.make_list_row()); | ||||
} | |||||
this.args = {}; | |||||
toggle_child_selection() { | |||||
if (this.dialog.fields_dict['allow_child_item_selection'].get_value()) { | |||||
this.get_child_result().then(r => { | |||||
this.child_results = r.message || []; | |||||
this.render_child_datatable(); | |||||
this.$wrapper.addClass('hidden'); | |||||
this.$child_wrapper.removeClass('hidden'); | |||||
this.dialog.fields_dict.more_btn.$wrapper.hide(); | |||||
}); | |||||
} else { | |||||
this.child_results = []; | |||||
this.get_results(); | |||||
this.$wrapper.removeClass('hidden'); | |||||
this.$child_wrapper.addClass('hidden'); | |||||
} | |||||
} | |||||
this.bind_events(); | |||||
this.get_results(); | |||||
this.dialog.show(); | |||||
render_child_datatable() { | |||||
if (!this.child_datatable) { | |||||
this.setup_child_datatable(); | |||||
} else { | |||||
setTimeout(() => { | |||||
this.child_datatable.rowmanager.checkMap = []; | |||||
this.child_datatable.refresh(this.get_child_datatable_rows()); | |||||
this.$child_wrapper.find('.dt-scrollable').css('height', '300px'); | |||||
}, 500); | |||||
} | |||||
} | |||||
get_child_datatable_columns() { | |||||
const parent = this.doctype; | |||||
return [parent, ...this.child_columns].map(d => ({ name: frappe.unscrub(d), editable: false })); | |||||
} | |||||
get_child_datatable_rows() { | |||||
return this.child_results.map(d => Object.values(d).slice(1)); // slice name field | |||||
} | |||||
setup_child_datatable() { | |||||
const header_columns = this.get_child_datatable_columns(); | |||||
const rows = this.get_child_datatable_rows(); | |||||
this.$child_wrapper = this.dialog.fields_dict.child_selection_area.$wrapper; | |||||
this.$child_wrapper.addClass('mt-3'); | |||||
this.child_datatable = new frappe.DataTable(this.$child_wrapper.get(0), { | |||||
columns: header_columns, | |||||
data: rows, | |||||
layout: 'fluid', | |||||
inlineFilters: true, | |||||
serialNoColumn: false, | |||||
checkboxColumn: true, | |||||
cellHeight: 35, | |||||
noDataMessage: __('No Data'), | |||||
disableReorderColumn: true | |||||
}); | |||||
this.$child_wrapper.find('.dt-scrollable').css('height', '300px'); | |||||
} | } | ||||
get_primary_filters() { | get_primary_filters() { | ||||
@@ -94,7 +199,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
columns[0] = [ | columns[0] = [ | ||||
{ | { | ||||
fieldtype: "Data", | fieldtype: "Data", | ||||
label: __("Search"), | |||||
label: __("Name"), | |||||
fieldname: "search_term" | fieldname: "search_term" | ||||
} | } | ||||
]; | ]; | ||||
@@ -127,6 +232,16 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
// now a is a fixed-size array with mutable entries | // now a is a fixed-size array with mutable entries | ||||
} | } | ||||
if (this.allow_child_item_selection) { | |||||
this.child_doctype = frappe.meta.get_docfield(this.doctype, this.child_fieldname).options; | |||||
columns[0].push({ | |||||
fieldtype: "Check", | |||||
label: __("Select {0}", [this.child_doctype]), | |||||
fieldname: "allow_child_item_selection", | |||||
onchange: this.toggle_child_selection.bind(this) | |||||
}); | |||||
} | |||||
fields = [ | fields = [ | ||||
...columns[0], | ...columns[0], | ||||
{ fieldtype: "Column Break" }, | { fieldtype: "Column Break" }, | ||||
@@ -156,6 +271,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
this.get_results(); | this.get_results(); | ||||
} | } | ||||
}); | }); | ||||
// 'Apply Filter' breaks since the filers are not in a popover | |||||
// Hence keeping it hidden | |||||
this.filter_group.wrapper.find('.apply-filters').hide(); | |||||
} | } | ||||
get_custom_filters() { | get_custom_filters() { | ||||
@@ -166,7 +284,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
}); | }); | ||||
}, {}); | }, {}); | ||||
} else { | } else { | ||||
return []; | |||||
return {}; | |||||
} | } | ||||
} | } | ||||
@@ -200,6 +318,34 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
}); | }); | ||||
} | } | ||||
get_parent_name_of_selected_children() { | |||||
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return []; | |||||
let parent_names = this.child_datatable.rowmanager.checkMap.reduce((parent_names, checked, index) => { | |||||
if (checked == 1) { | |||||
const parent_name = this.child_results[index].parent; | |||||
parent_names.push(parent_name); | |||||
} | |||||
return parent_names; | |||||
}, []); | |||||
return parent_names; | |||||
} | |||||
get_selected_child_names() { | |||||
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return []; | |||||
let checked_names = this.child_datatable.rowmanager.checkMap.reduce((checked_names, checked, index) => { | |||||
if (checked == 1) { | |||||
const child_row_name = this.child_results[index].name; | |||||
checked_names.push(child_row_name); | |||||
} | |||||
return checked_names; | |||||
}, []); | |||||
return checked_names; | |||||
} | |||||
get_checked_values() { | get_checked_values() { | ||||
// Return name of checked value. | // Return name of checked value. | ||||
return this.$results.find('.list-item-container').map(function () { | return this.$results.find('.list-item-container').map(function () { | ||||
@@ -276,6 +422,8 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
me.$results.append(me.make_list_row(result)); | me.$results.append(me.make_list_row(result)); | ||||
}); | }); | ||||
this.$results.find(".list-item--head").css("z-index", 0); | |||||
if (frappe.flags.auto_scroll) { | if (frappe.flags.auto_scroll) { | ||||
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500); | this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500); | ||||
} | } | ||||
@@ -297,7 +445,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
this.render_result_list(checked, 0, false); | this.render_result_list(checked, 0, false); | ||||
} | } | ||||
get_results() { | |||||
get_filters_from_setters() { | |||||
let me = this; | let me = this; | ||||
let filters = this.get_query ? this.get_query().filters : {} || {}; | let filters = this.get_query ? this.get_query().filters : {} || {}; | ||||
let filter_fields = []; | let filter_fields = []; | ||||
@@ -321,12 +469,18 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
}); | }); | ||||
} | } | ||||
let filter_group = this.get_custom_filters(); | |||||
Object.assign(filters, filter_group); | |||||
return [filters, filter_fields]; | |||||
} | |||||
get_args_for_search() { | |||||
let [filters, filter_fields] = this.get_filters_from_setters(); | |||||
let custom_filters = this.get_custom_filters(); | |||||
Object.assign(filters, custom_filters); | |||||
let args = { | |||||
doctype: me.doctype, | |||||
txt: me.dialog.fields_dict["search_term"].get_value(), | |||||
return { | |||||
doctype: this.doctype, | |||||
txt: this.dialog.fields_dict["search_term"].get_value(), | |||||
filters: filters, | filters: filters, | ||||
filter_fields: filter_fields, | filter_fields: filter_fields, | ||||
start: this.start, | start: this.start, | ||||
@@ -334,25 +488,81 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { | |||||
query: this.get_query ? this.get_query().query : '', | query: this.get_query ? this.get_query().query : '', | ||||
as_dict: 1 | as_dict: 1 | ||||
}; | }; | ||||
frappe.call({ | |||||
} | |||||
async perform_search(args) { | |||||
const res = await frappe.call({ | |||||
type: "GET", | type: "GET", | ||||
method: 'frappe.desk.search.search_widget', | method: 'frappe.desk.search.search_widget', | ||||
no_spinner: true, | no_spinner: true, | ||||
args: args, | args: args, | ||||
callback: function (r) { | |||||
let more = 0; | |||||
me.results = []; | |||||
if (r.values.length) { | |||||
if (r.values.length > me.page_length) { | |||||
r.values.pop(); | |||||
more = 1; | |||||
} | |||||
r.values.forEach(function (result) { | |||||
result.checked = 0; | |||||
me.results.push(result); | |||||
}); | |||||
}); | |||||
const more = res.values.length && res.values.length > this.page_length ? 1 : 0; | |||||
if (more) { | |||||
res.values.pop(); | |||||
} | |||||
return [res, more]; | |||||
} | |||||
async get_results() { | |||||
const args = this.get_args_for_search(); | |||||
const [res, more] = await this.perform_search(args); | |||||
this.results = []; | |||||
if (res.values.length) { | |||||
res.values.forEach(result => { | |||||
result.checked = 0; | |||||
this.results.push(result); | |||||
}); | |||||
} | |||||
this.render_result_list(this.results, more); | |||||
} | |||||
async get_filtered_parents_for_child_search() { | |||||
const parent_search_args = this.get_args_for_search(); | |||||
parent_search_args.filter_fields = ['name']; | |||||
// eslint-disable-next-line no-unused-vars | |||||
const [response, _] = await this.perform_search(parent_search_args); | |||||
let parent_names = []; | |||||
if (response.values.length) { | |||||
parent_names = response.values.map(v => v.name); | |||||
} | |||||
return parent_names; | |||||
} | |||||
async add_parent_filters(filters) { | |||||
const parent_names = await this.get_filtered_parents_for_child_search(); | |||||
if (parent_names.length) { | |||||
filters.push([ "parent", "in", parent_names ]); | |||||
} | |||||
} | |||||
add_custom_child_filters(filters) { | |||||
if (this.add_filters_group && this.filter_group) { | |||||
this.filter_group.get_filters().forEach(filter => { | |||||
if (filter[0] == this.child_doctype) { | |||||
filters.push([filter[1], filter[2], filter[3]]); | |||||
} | } | ||||
me.render_result_list(me.results, more); | |||||
}); | |||||
} | |||||
} | |||||
async get_child_result() { | |||||
let filters = [["parentfield", "=", this.child_fieldname]]; | |||||
await this.add_parent_filters(filters); | |||||
this.add_custom_child_filters(filters); | |||||
return frappe.call({ | |||||
method: "frappe.client.get_list", | |||||
args: { | |||||
doctype: this.child_doctype, | |||||
filters: filters, | |||||
fields: ['name', 'parent', ...this.child_columns], | |||||
parent: this.doctype, | |||||
order_by: 'parent' | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
@@ -3,9 +3,15 @@ frappe.provide("frappe.views"); | |||||
frappe.views.BaseList = class BaseList { | frappe.views.BaseList = class BaseList { | ||||
constructor(opts) { | constructor(opts) { | ||||
Object.assign(this, opts); | Object.assign(this, opts); | ||||
this.init_page() | |||||
} | } | ||||
show() { | show() { | ||||
this.meta = frappe.get_meta(this.doctype); | |||||
this.set_title(); | |||||
// in loading state? | |||||
if (!this.meta) return; | |||||
frappe.run_serially([ | frappe.run_serially([ | ||||
() => this.init(), | () => this.init(), | ||||
() => this.before_refresh(), | () => this.before_refresh(), | ||||
@@ -34,8 +40,6 @@ frappe.views.BaseList = class BaseList { | |||||
setup_defaults() { | setup_defaults() { | ||||
this.page_name = frappe.get_route_str(); | this.page_name = frappe.get_route_str(); | ||||
this.page_title = this.page_title || frappe.router.doctype_layout || __(this.doctype); | |||||
this.meta = frappe.get_meta(this.doctype); | |||||
this.settings = frappe.listview_settings[this.doctype] || {}; | this.settings = frappe.listview_settings[this.doctype] || {}; | ||||
this.user_settings = frappe.get_user_settings(this.doctype); | this.user_settings = frappe.get_user_settings(this.doctype); | ||||
@@ -150,13 +154,21 @@ frappe.views.BaseList = class BaseList { | |||||
} | } | ||||
} | } | ||||
setup_page() { | |||||
init_page() { | |||||
this.page = this.parent.page; | this.page = this.parent.page; | ||||
this.make_skeleton(); | |||||
this.$page = $(this.parent); | this.$page = $(this.parent); | ||||
!this.hide_card_layout && this.page.main.addClass('frappe-card'); | !this.hide_card_layout && this.page.main.addClass('frappe-card'); | ||||
this.page.page_form.removeClass("row").addClass("flex"); | this.page.page_form.removeClass("row").addClass("flex"); | ||||
this.hide_page_form && this.page.page_form.hide(); | this.hide_page_form && this.page.page_form.hide(); | ||||
this.hide_sidebar && this.$page.addClass('no-list-sidebar'); | this.hide_sidebar && this.$page.addClass('no-list-sidebar'); | ||||
} | |||||
make_skeleton() { | |||||
this.skeleton = $(`<div class='skeleton-bg' style='min-height: 400px'></div>`).prependTo(this.page.main.parent()); | |||||
} | |||||
setup_page() { | |||||
this.setup_page_head(); | this.setup_page_head(); | ||||
} | } | ||||
@@ -167,6 +179,7 @@ frappe.views.BaseList = class BaseList { | |||||
} | } | ||||
set_title() { | set_title() { | ||||
this.page_title = this.page_title || frappe.router.doctype_layout || __(this.doctype); | |||||
this.page.set_title(this.page_title); | this.page.set_title(this.page_title); | ||||
} | } | ||||
@@ -280,6 +293,7 @@ frappe.views.BaseList = class BaseList { | |||||
} | } | ||||
setup_list_wrapper() { | setup_list_wrapper() { | ||||
this.skeleton.remove(); // clear skeleton | |||||
this.$frappe_list = $('<div class="frappe-list">').appendTo( | this.$frappe_list = $('<div class="frappe-list">').appendTo( | ||||
this.page.main | this.page.main | ||||
); | ); | ||||
@@ -4,7 +4,7 @@ export default class BulkOperations { | |||||
this.doctype = doctype; | this.doctype = doctype; | ||||
} | } | ||||
print(docs) { | |||||
print (docs) { | |||||
const print_settings = frappe.model.get_doc(':Print Settings', 'Print Settings'); | const print_settings = frappe.model.get_doc(':Print Settings', 'Print Settings'); | ||||
const allow_print_for_draft = cint(print_settings.allow_print_for_draft); | const allow_print_for_draft = cint(print_settings.allow_print_for_draft); | ||||
const is_submittable = frappe.model.is_submittable(this.doctype); | const is_submittable = frappe.model.is_submittable(this.doctype); | ||||
@@ -27,31 +27,38 @@ export default class BulkOperations { | |||||
if (valid_docs.length > 0) { | if (valid_docs.length > 0) { | ||||
const dialog = new frappe.ui.Dialog({ | const dialog = new frappe.ui.Dialog({ | ||||
title: __('Print Documents'), | title: __('Print Documents'), | ||||
fields: [{ | |||||
'fieldtype': 'Check', | |||||
'label': __('With Letterhead'), | |||||
'fieldname': 'with_letterhead' | |||||
}, | |||||
{ | |||||
'fieldtype': 'Select', | |||||
'label': __('Print Format'), | |||||
'fieldname': 'print_sel', | |||||
options: frappe.meta.get_print_formats(this.doctype) | |||||
}] | |||||
fields: [ | |||||
{ | |||||
'fieldtype': 'Select', | |||||
'label': __('Letter Head'), | |||||
'fieldname': 'letter_sel', | |||||
'default': __('No Letterhead'), | |||||
options: this.get_letterhead_options() | |||||
}, | |||||
{ | |||||
'fieldtype': 'Select', | |||||
'label': __('Print Format'), | |||||
'fieldname': 'print_sel', | |||||
options: frappe.meta.get_print_formats(this.doctype) | |||||
} | |||||
] | |||||
}); | }); | ||||
dialog.set_primary_action(__('Print'), args => { | dialog.set_primary_action(__('Print'), args => { | ||||
if (!args) return; | if (!args) return; | ||||
const default_print_format = frappe.get_meta(this.doctype).default_print_format; | const default_print_format = frappe.get_meta(this.doctype).default_print_format; | ||||
const with_letterhead = args.with_letterhead ? 1 : 0; | |||||
const with_letterhead = args.letter_sel == __("No Letterhead") ? 0 : 1; | |||||
const print_format = args.print_sel ? args.print_sel : default_print_format; | const print_format = args.print_sel ? args.print_sel : default_print_format; | ||||
const json_string = JSON.stringify(valid_docs); | const json_string = JSON.stringify(valid_docs); | ||||
const letterhead = args.letter_sel; | |||||
const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' + | const w = window.open('/api/method/frappe.utils.print_format.download_multi_pdf?' + | ||||
'doctype=' + encodeURIComponent(this.doctype) + | 'doctype=' + encodeURIComponent(this.doctype) + | ||||
'&name=' + encodeURIComponent(json_string) + | '&name=' + encodeURIComponent(json_string) + | ||||
'&format=' + encodeURIComponent(print_format) + | '&format=' + encodeURIComponent(print_format) + | ||||
'&no_letterhead=' + (with_letterhead ? '0' : '1')); | |||||
'&no_letterhead=' + (with_letterhead ? '0' : '1') + | |||||
'&letterhead=' + encodeURIComponent(letterhead) | |||||
); | |||||
if (!w) { | if (!w) { | ||||
frappe.msgprint(__('Please enable pop-ups')); | frappe.msgprint(__('Please enable pop-ups')); | ||||
return; | return; | ||||
@@ -64,7 +71,28 @@ export default class BulkOperations { | |||||
} | } | ||||
} | } | ||||
delete(docnames, done = null) { | |||||
get_letterhead_options () { | |||||
const letterhead_options = [__("No Letterhead")]; | |||||
frappe.call({ | |||||
method: "frappe.client.get_list", | |||||
args: { | |||||
doctype: 'Letter Head', | |||||
fields: ['name', 'is_default'], | |||||
limit: 0 | |||||
}, | |||||
async: false, | |||||
callback (r) { | |||||
if (r.message) { | |||||
r.message.forEach(letterhead => { | |||||
letterhead_options.push(letterhead.name); | |||||
}); | |||||
} | |||||
} | |||||
}); | |||||
return letterhead_options; | |||||
} | |||||
delete (docnames, done = null) { | |||||
frappe | frappe | ||||
.call({ | .call({ | ||||
method: 'frappe.desk.reportview.delete_items', | method: 'frappe.desk.reportview.delete_items', | ||||
@@ -88,7 +116,7 @@ export default class BulkOperations { | |||||
}); | }); | ||||
} | } | ||||
assign(docnames, done) { | |||||
assign (docnames, done) { | |||||
if (docnames.length > 0) { | if (docnames.length > 0) { | ||||
const assign_to = new frappe.ui.form.AssignToDialog({ | const assign_to = new frappe.ui.form.AssignToDialog({ | ||||
obj: this, | obj: this, | ||||
@@ -106,7 +134,7 @@ export default class BulkOperations { | |||||
} | } | ||||
} | } | ||||
apply_assignment_rule(docnames, done) { | |||||
apply_assignment_rule (docnames, done) { | |||||
if (docnames.length > 0) { | if (docnames.length > 0) { | ||||
frappe.call('frappe.automation.doctype.assignment_rule.assignment_rule.bulk_apply', { | frappe.call('frappe.automation.doctype.assignment_rule.assignment_rule.bulk_apply', { | ||||
doctype: this.doctype, | doctype: this.doctype, | ||||
@@ -115,7 +143,7 @@ export default class BulkOperations { | |||||
} | } | ||||
} | } | ||||
submit_or_cancel(docnames, action='submit', done=null) { | |||||
submit_or_cancel (docnames, action = 'submit', done = null) { | |||||
action = action.toLowerCase(); | action = action.toLowerCase(); | ||||
frappe | frappe | ||||
.call({ | .call({ | ||||
@@ -140,7 +168,7 @@ export default class BulkOperations { | |||||
}); | }); | ||||
} | } | ||||
edit(docnames, field_mappings, done) { | |||||
edit (docnames, field_mappings, done) { | |||||
let field_options = Object.keys(field_mappings).sort(); | let field_options = Object.keys(field_mappings).sort(); | ||||
const status_regex = /status/i; | const status_regex = /status/i; | ||||
@@ -198,16 +226,16 @@ export default class BulkOperations { | |||||
if (default_field) set_value_field(dialog); // to set `Value` df based on default `Field` | if (default_field) set_value_field(dialog); // to set `Value` df based on default `Field` | ||||
function set_value_field(dialogObj) { | |||||
function set_value_field (dialogObj) { | |||||
const new_df = Object.assign({}, | const new_df = Object.assign({}, | ||||
field_mappings[dialogObj.get_value('field')]); | field_mappings[dialogObj.get_value('field')]); | ||||
/* if the field label has status in it and | /* if the field label has status in it and | ||||
if it has select fieldtype with no default value then | if it has select fieldtype with no default value then | ||||
set a default value from the available option. */ | set a default value from the available option. */ | ||||
if(new_df.label.match(status_regex) && | |||||
if (new_df.label.match(status_regex) && | |||||
new_df.fieldtype === 'Select' && !new_df.default) { | new_df.fieldtype === 'Select' && !new_df.default) { | ||||
let options = []; | let options = []; | ||||
if(typeof new_df.options==="string") { | |||||
if (typeof new_df.options === "string") { | |||||
options = new_df.options.split("\n"); | options = new_df.options.split("\n"); | ||||
} | } | ||||
//set second option as default if first option is an empty string | //set second option as default if first option is an empty string | ||||
@@ -223,8 +251,7 @@ export default class BulkOperations { | |||||
dialog.show(); | dialog.show(); | ||||
} | } | ||||
add_tags(docnames, done) { | |||||
add_tags (docnames, done) { | |||||
const dialog = new frappe.ui.Dialog({ | const dialog = new frappe.ui.Dialog({ | ||||
title: __('Add Tags'), | title: __('Add Tags'), | ||||
fields: [ | fields: [ | ||||
@@ -233,7 +260,7 @@ export default class BulkOperations { | |||||
fieldname: 'tags', | fieldname: 'tags', | ||||
label: __("Tags"), | label: __("Tags"), | ||||
reqd: true, | reqd: true, | ||||
get_data: function(txt) { | |||||
get_data: function (txt) { | |||||
return frappe.db.get_link_options("Tag", txt); | return frappe.db.get_link_options("Tag", txt); | ||||
} | } | ||||
}, | }, | ||||
@@ -262,7 +289,7 @@ export default class BulkOperations { | |||||
dialog.show(); | dialog.show(); | ||||
} | } | ||||
export(doctype, docnames) { | |||||
export (doctype, docnames) { | |||||
frappe.require('data_import_tools.bundle.js', () => { | frappe.require('data_import_tools.bundle.js', () => { | ||||
const data_exporter = new frappe.data_import.DataExporter(doctype, 'Insert New Records'); | const data_exporter = new frappe.data_import.DataExporter(doctype, 'Insert New Records'); | ||||
data_exporter.dialog.set_value('export_records', 'by_filter'); | data_exporter.dialog.set_value('export_records', 'by_filter'); | ||||
@@ -8,38 +8,50 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory { | |||||
make (route) { | make (route) { | ||||
var me = this; | var me = this; | ||||
var doctype = route[1]; | var doctype = route[1]; | ||||
const meta_loaded = frappe.get_meta(doctype) ? true : false; | |||||
const page_name = frappe.get_route_str(); | |||||
let view_class = this.get_view_class(route, doctype); | |||||
this.make_list_view_page(page_name, doctype, view_class); | |||||
if (view_class && view_class.load_last_view && view_class.load_last_view()) { | |||||
// view can have custom routing logic | |||||
return; | |||||
} | |||||
frappe.model.with_doctype(doctype, function () { | frappe.model.with_doctype(doctype, function () { | ||||
if (!meta_loaded) { | |||||
frappe.views.list_view[page_name].show(); | |||||
} | |||||
if (locals['DocType'][doctype].issingle) { | if (locals['DocType'][doctype].issingle) { | ||||
frappe.set_re_route('Form', doctype); | frappe.set_re_route('Form', doctype); | ||||
} else { | } else { | ||||
// List / Gantt / Kanban / etc | |||||
// File is a special view | |||||
const view_name = doctype !== 'File' ? frappe.utils.to_title_case(route[2] || 'List') : 'File'; | |||||
let view_class = frappe.views[view_name + 'View']; | |||||
if (!view_class) view_class = frappe.views.ListView; | |||||
if (view_class && view_class.load_last_view && view_class.load_last_view()) { | |||||
// view can have custom routing logic | |||||
return; | |||||
} | |||||
frappe.provide('frappe.views.list_view.' + doctype); | |||||
const page_name = frappe.get_route_str(); | |||||
if (!frappe.views.list_view[page_name]) { | |||||
frappe.views.list_view[page_name] = new view_class({ | |||||
doctype: doctype, | |||||
parent: me.make_page(true, page_name) | |||||
}); | |||||
} else { | |||||
frappe.container.change_to(page_name); | |||||
} | |||||
frappe.container.change_to(page_name); | |||||
me.set_cur_list(); | me.set_cur_list(); | ||||
} | } | ||||
}); | }); | ||||
} | } | ||||
get_view_class(route, doctype) { | |||||
// List / Gantt / Kanban / etc | |||||
// File is a special view | |||||
const view_name = doctype !== 'File' ? frappe.utils.to_title_case(route[2] || 'List') : 'File'; | |||||
let view_class = frappe.views[view_name + 'View']; | |||||
if (!view_class) view_class = frappe.views.ListView; | |||||
return view_class; | |||||
} | |||||
make_list_view_page(page_name, doctype, view_class) { | |||||
frappe.provide('frappe.views.list_view.' + doctype); | |||||
if (!frappe.views.list_view[page_name]) { | |||||
frappe.views.list_view[page_name] = new view_class({ | |||||
doctype: doctype, | |||||
parent: this.make_page(true, page_name) | |||||
}); | |||||
} | |||||
} | |||||
show() { | show() { | ||||
if (this.re_route_to_view()) { | if (this.re_route_to_view()) { | ||||
return; | return; | ||||
@@ -153,7 +153,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { | |||||
set_secondary_action(click) { | set_secondary_action(click) { | ||||
this.footer.removeClass('hide'); | this.footer.removeClass('hide'); | ||||
this.get_secondary_btn().removeClass('hide').on('click', click); | |||||
this.get_secondary_btn().removeClass('hide').off('click').on('click', click); | |||||
} | } | ||||
set_secondary_action_label(label) { | set_secondary_action_label(label) { | ||||
@@ -140,7 +140,7 @@ frappe.msgprint = function(msg, title, is_minimizable) { | |||||
return; | return; | ||||
} | } | ||||
if(data.alert) { | |||||
if(data.alert || data.toast) { | |||||
frappe.show_alert(data); | frappe.show_alert(data); | ||||
return; | return; | ||||
} | } | ||||
@@ -361,7 +361,7 @@ frappe.hide_progress = function() { | |||||
} | } | ||||
// Floating Message | // Floating Message | ||||
frappe.show_alert = function(message, seconds=7, actions={}) { | |||||
frappe.show_alert = frappe.toast = function(message, seconds=7, actions={}) { | |||||
let indicator_icon_map = { | let indicator_icon_map = { | ||||
'orange': "solid-warning", | 'orange': "solid-warning", | ||||
'yellow': "solid-warning", | 'yellow': "solid-warning", | ||||
@@ -8,7 +8,12 @@ if (!window.frappe) window.frappe = {}; | |||||
function flt(v, decimals, number_format) { | function flt(v, decimals, number_format) { | ||||
if (v == null || v == '') return 0; | if (v == null || v == '') return 0; | ||||
if (typeof v !== "number") { | |||||
if (!(typeof v === "number" || String(parseFloat(v)) == v)) { | |||||
// cases in which this block should not run | |||||
// 1. 'v' is already a number | |||||
// 2. v is already parsed but in string form | |||||
// if (typeof v !== "number") { | |||||
v = v + ""; | v = v + ""; | ||||
// strip currency symbol if exists | // strip currency symbol if exists | ||||
@@ -25,6 +30,7 @@ function flt(v, decimals, number_format) { | |||||
v = 0; | v = 0; | ||||
} | } | ||||
v = parseFloat(v); | |||||
if (decimals != null) | if (decimals != null) | ||||
return _round(v, decimals); | return _round(v, decimals); | ||||
return v; | return v; | ||||
@@ -148,4 +148,4 @@ frappe.show_message_page = function(opts) { | |||||
); | ); | ||||
frappe.container.change_to(opts.page_name); | frappe.container.change_to(opts.page_name); | ||||
}; | |||||
}; |
@@ -832,6 +832,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { | |||||
if (this.raw_data.add_total_row) { | if (this.raw_data.add_total_row) { | ||||
data = data.slice(); | data = data.slice(); | ||||
data.splice(-1, 1); | data.splice(-1, 1); | ||||
this.$page.find('.layout-main-section').css('--report-total-height', '310px'); | |||||
} | } | ||||
this.$report.show(); | this.$report.show(); | ||||
@@ -854,10 +855,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { | |||||
} | } | ||||
}; | }; | ||||
if (this.raw_data.add_total_row) { | |||||
this.$page.find('.layout-main-section').css('--report-total-height', '310px'); | |||||
} | |||||
if (this.report_settings.get_datatable_options) { | if (this.report_settings.get_datatable_options) { | ||||
datatable_options = this.report_settings.get_datatable_options(datatable_options); | datatable_options = this.report_settings.get_datatable_options(datatable_options); | ||||
} | } | ||||
@@ -739,6 +739,10 @@ body { | |||||
animation-duration: 400ms; | animation-duration: 400ms; | ||||
} | } | ||||
.skeleton-bg { | |||||
background-color: var(--skeleton-bg); | |||||
} | |||||
.workspace-skeleton { | .workspace-skeleton { | ||||
transition: ease; | transition: ease; | ||||
.widget-group-title { | .widget-group-title { | ||||
@@ -327,10 +327,6 @@ select.input-xs { | |||||
} | } | ||||
} | } | ||||
// .frappe-card { | |||||
// @include card(); | |||||
// } | |||||
.head-title { | .head-title { | ||||
font-size: var(--text-lg); | font-size: var(--text-lg); | ||||
font-weight: 700; | font-weight: 700; | ||||
@@ -591,4 +587,4 @@ details > summary:focus { | |||||
.chart-container { | .chart-container { | ||||
direction: ltr; | direction: ltr; | ||||
} | } | ||||
*/ | |||||
*/ |
@@ -47,3 +47,4 @@ | |||||
@import "link_preview"; | @import "link_preview"; | ||||
@import "../common/quill"; | @import "../common/quill"; | ||||
@import "plyr"; | @import "plyr"; | ||||
@import "version"; |
@@ -0,0 +1,33 @@ | |||||
.version-info { | |||||
overflow: auto; | |||||
pre { | |||||
border: 0px; | |||||
margin: 0px; | |||||
background-color: inherit; | |||||
} | |||||
.table { | |||||
background-color: inherit; | |||||
} | |||||
.success { | |||||
background-color: var(--green-100) !important; | |||||
} | |||||
.danger { | |||||
background-color: var(--red-100) !important; | |||||
} | |||||
} | |||||
[data-theme="dark"] { | |||||
.version-info { | |||||
.danger, .success { | |||||
color: var(--gray-900); | |||||
td { | |||||
color: var(--gray-900); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -8,6 +8,7 @@ | |||||
@import "../common/flex"; | @import "../common/flex"; | ||||
@import "../common/buttons"; | @import "../common/buttons"; | ||||
@import "../common/modal"; | @import "../common/modal"; | ||||
@import "../desk/toast"; | |||||
@import "../common/indicator"; | @import "../common/indicator"; | ||||
@import "../common/controls"; | @import "../common/controls"; | ||||
@import "../common/awesomeplete"; | @import "../common/awesomeplete"; | ||||
@@ -135,11 +135,13 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" | |||||
{% elif df.fieldtype=="Check" and doc[df.fieldname] %} | {% elif df.fieldtype=="Check" and doc[df.fieldname] %} | ||||
<!-- <i class="{{ 'fa fa-check' }}"></i> --> | <!-- <i class="{{ 'fa fa-check' }}"></i> --> | ||||
<svg viewBox="0 0 16 16" | <svg viewBox="0 0 16 16" | ||||
fill="transparent" stroke="var(--icon-stroke)" stroke-width="2" | |||||
fill="transparent" stroke="#1F272E" stroke-width="2" | |||||
xmlns="http://www.w3.org/2000/svg" id="icon-tick" | xmlns="http://www.w3.org/2000/svg" id="icon-tick" | ||||
style="width: 12px; height: 12px; margin-top: 5px;"> | style="width: 12px; height: 12px; margin-top: 5px;"> | ||||
<path d="M2 9.66667L5.33333 13L14 3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | <path d="M2 9.66667L5.33333 13L14 3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | ||||
</svg> | </svg> | ||||
{% elif df.fieldtype=="Check" and not doc[df.fieldname] %} | |||||
<!-- empty --> | |||||
{% elif df.fieldtype in ("Image", "Attach Image") and frappe.utils.is_image(doc[doc.meta.get_field(df.fieldname).options]) %} | {% elif df.fieldtype in ("Image", "Attach Image") and frappe.utils.is_image(doc[doc.meta.get_field(df.fieldname).options]) %} | ||||
<img src="{{ doc[doc.meta.get_field(df.fieldname).options] }}" | <img src="{{ doc[doc.meta.get_field(df.fieldname).options] }}" | ||||
class="img-responsive" | class="img-responsive" | ||||
@@ -178,7 +180,8 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" | |||||
{% macro get_align_class(df, no_of_cols=2) %} | {% macro get_align_class(df, no_of_cols=2) %} | ||||
{% if no_of_cols >= 3 %}{{ "" }} | {% if no_of_cols >= 3 %}{{ "" }} | ||||
{%- elif df.align -%}{{ "text-" + df.align }} | {%- elif df.align -%}{{ "text-" + df.align }} | ||||
{%- elif df.fieldtype in ("Int", "Float", "Currency", "Check", "Percent") -%}{{ "text-right" }} | |||||
{%- elif df.fieldtype in ("Int", "Float", "Currency", "Percent") -%}{{ "text-right" }} | |||||
{%- elif df.fieldtype in ("Check") -%}{{ "text-center" }} | |||||
{%- else -%}{{ "" }} | {%- else -%}{{ "" }} | ||||
{%- endif -%} | {%- endif -%} | ||||
{% endmacro %} | {% endmacro %} | ||||
@@ -63,15 +63,15 @@ def clean(value): | |||||
return value | return value | ||||
def exists_in_backup(doctypes, file): | |||||
"""Checks if the list of doctypes exist in the database.sql.gz file supplied | |||||
def missing_in_backup(doctypes, file): | |||||
"""Returns list of missing doctypes in the backup. | |||||
Args: | Args: | ||||
doctypes (list): List of DocTypes to be checked | doctypes (list): List of DocTypes to be checked | ||||
file (str): Path of the database file | file (str): Path of the database file | ||||
Returns: | Returns: | ||||
bool: True if all tables exist | |||||
doctypes(list): doctypes that are missing in backup | |||||
""" | """ | ||||
predicate = ( | predicate = ( | ||||
'COPY public."tab{}"' | 'COPY public."tab{}"' | ||||
@@ -79,8 +79,24 @@ def exists_in_backup(doctypes, file): | |||||
else "CREATE TABLE `tab{}`" | else "CREATE TABLE `tab{}`" | ||||
) | ) | ||||
with gzip.open(file, "rb") as f: | with gzip.open(file, "rb") as f: | ||||
content = f.read().decode("utf8") | |||||
return all(predicate.format(doctype).lower() in content.lower() for doctype in doctypes) | |||||
content = f.read().decode("utf8").lower() | |||||
return [doctype for doctype in doctypes | |||||
if predicate.format(doctype).lower() not in content] | |||||
def exists_in_backup(doctypes, file): | |||||
"""Checks if the list of doctypes exist in the database.sql.gz file supplied | |||||
Args: | |||||
doctypes (list): List of DocTypes to be checked | |||||
file (str): Path of the database file | |||||
Returns: | |||||
bool: True if all tables exist | |||||
""" | |||||
missing_doctypes = missing_in_backup(doctypes, file) | |||||
return len(missing_doctypes) == 0 | |||||
class BaseTestCommands(unittest.TestCase): | class BaseTestCommands(unittest.TestCase): | ||||
@@ -222,7 +238,7 @@ class TestCommands(BaseTestCommands): | |||||
self.execute("bench --site {site} backup --verbose") | self.execute("bench --site {site} backup --verbose") | ||||
self.assertEqual(self.returncode, 0) | self.assertEqual(self.returncode, 0) | ||||
database = fetch_latest_backups(partial=True)["database"] | database = fetch_latest_backups(partial=True)["database"] | ||||
self.assertTrue(exists_in_backup(backup["includes"]["includes"], database)) | |||||
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database)) | |||||
# test 8: take a backup with frappe.conf.backup.excludes | # test 8: take a backup with frappe.conf.backup.excludes | ||||
self.execute( | self.execute( | ||||
@@ -233,7 +249,7 @@ class TestCommands(BaseTestCommands): | |||||
self.assertEqual(self.returncode, 0) | self.assertEqual(self.returncode, 0) | ||||
database = fetch_latest_backups(partial=True)["database"] | database = fetch_latest_backups(partial=True)["database"] | ||||
self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database)) | self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database)) | ||||
self.assertTrue(exists_in_backup(backup["includes"]["includes"], database)) | |||||
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database)) | |||||
# test 9: take a backup with --include (with frappe.conf.excludes still set) | # test 9: take a backup with --include (with frappe.conf.excludes still set) | ||||
self.execute( | self.execute( | ||||
@@ -242,7 +258,7 @@ class TestCommands(BaseTestCommands): | |||||
) | ) | ||||
self.assertEqual(self.returncode, 0) | self.assertEqual(self.returncode, 0) | ||||
database = fetch_latest_backups(partial=True)["database"] | database = fetch_latest_backups(partial=True)["database"] | ||||
self.assertTrue(exists_in_backup(backup["includes"]["includes"], database)) | |||||
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database)) | |||||
# test 10: take a backup with --exclude | # test 10: take a backup with --exclude | ||||
self.execute( | self.execute( | ||||
@@ -257,7 +273,7 @@ class TestCommands(BaseTestCommands): | |||||
self.execute("bench --site {site} backup --ignore-backup-conf") | self.execute("bench --site {site} backup --ignore-backup-conf") | ||||
self.assertEqual(self.returncode, 0) | self.assertEqual(self.returncode, 0) | ||||
database = fetch_latest_backups()["database"] | database = fetch_latest_backups()["database"] | ||||
self.assertTrue(exists_in_backup(backup["excludes"]["excludes"], database)) | |||||
self.assertEqual([], missing_in_backup(backup["excludes"]["excludes"], database)) | |||||
def test_restore(self): | def test_restore(self): | ||||
# step 0: create a site to run the test on | # step 0: create a site to run the test on | ||||
@@ -197,6 +197,7 @@ class TestDB(unittest.TestCase): | |||||
frappe.delete_doc(test_doctype, doc) | frappe.delete_doc(test_doctype, doc) | ||||
clear_custom_fields(test_doctype) | clear_custom_fields(test_doctype) | ||||
@run_only_if(db_type_is.MARIADB) | @run_only_if(db_type_is.MARIADB) | ||||
class TestDDLCommandsMaria(unittest.TestCase): | class TestDDLCommandsMaria(unittest.TestCase): | ||||
test_table_name = "TestNotes" | test_table_name = "TestNotes" | ||||
@@ -205,7 +206,7 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||||
frappe.db.commit() | frappe.db.commit() | ||||
frappe.db.sql( | frappe.db.sql( | ||||
f""" | f""" | ||||
CREATE TABLE `tab{self.test_table_name}` (`id` INT NULL,PRIMARY KEY (`id`)); | |||||
CREATE TABLE `tab{self.test_table_name}` (`id` INT NULL, content TEXT, PRIMARY KEY (`id`)); | |||||
""" | """ | ||||
) | ) | ||||
@@ -230,7 +231,10 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||||
def test_describe(self) -> None: | def test_describe(self) -> None: | ||||
self.assertEqual( | self.assertEqual( | ||||
(("id", "int(11)", "NO", "PRI", None, ""),), | |||||
( | |||||
("id", "int(11)", "NO", "PRI", None, ""), | |||||
("content", "text", "YES", "", None, ""), | |||||
), | |||||
frappe.db.describe(self.test_table_name), | frappe.db.describe(self.test_table_name), | ||||
) | ) | ||||
@@ -240,6 +244,17 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||||
self.assertGreater(len(test_table_description), 0) | self.assertGreater(len(test_table_description), 0) | ||||
self.assertIn("varchar(255)", test_table_description[0]) | self.assertIn("varchar(255)", test_table_description[0]) | ||||
def test_add_index(self) -> None: | |||||
index_name = "test_index" | |||||
frappe.db.add_index(self.test_table_name, ["id", "content(50)"], index_name) | |||||
indexs_in_table = frappe.db.sql( | |||||
f""" | |||||
SHOW INDEX FROM tab{self.test_table_name} | |||||
WHERE Key_name = '{index_name}'; | |||||
""" | |||||
) | |||||
self.assertEquals(len(indexs_in_table), 2) | |||||
@run_only_if(db_type_is.POSTGRES) | @run_only_if(db_type_is.POSTGRES) | ||||
class TestDDLCommandsPost(unittest.TestCase): | class TestDDLCommandsPost(unittest.TestCase): | ||||
@@ -248,7 +263,7 @@ class TestDDLCommandsPost(unittest.TestCase): | |||||
def setUp(self) -> None: | def setUp(self) -> None: | ||||
frappe.db.sql( | frappe.db.sql( | ||||
f""" | f""" | ||||
CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL,PRIMARY KEY ("id")) | |||||
CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL, content text, PRIMARY KEY ("id")) | |||||
""" | """ | ||||
) | ) | ||||
@@ -273,7 +288,9 @@ class TestDDLCommandsPost(unittest.TestCase): | |||||
self.test_table_name = new_table_name | self.test_table_name = new_table_name | ||||
def test_describe(self) -> None: | def test_describe(self) -> None: | ||||
self.assertEqual([("id",)], frappe.db.describe(self.test_table_name)) | |||||
self.assertEqual( | |||||
[("id",), ("content",)], frappe.db.describe(self.test_table_name) | |||||
) | |||||
def test_change_type(self) -> None: | def test_change_type(self) -> None: | ||||
frappe.db.change_column_type(self.test_table_name, "id", "varchar(255)") | frappe.db.change_column_type(self.test_table_name, "id", "varchar(255)") | ||||
@@ -292,3 +309,15 @@ class TestDDLCommandsPost(unittest.TestCase): | |||||
self.assertGreater(len(check_change), 0) | self.assertGreater(len(check_change), 0) | ||||
self.assertIn("character varying", check_change[0]) | self.assertIn("character varying", check_change[0]) | ||||
def test_add_index(self) -> None: | |||||
index_name = "test_index" | |||||
frappe.db.add_index(self.test_table_name, ["id", "content(50)"], index_name) | |||||
indexs_in_table = frappe.db.sql( | |||||
f""" | |||||
SELECT indexname | |||||
FROM pg_indexes | |||||
WHERE tablename = 'tab{self.test_table_name}' | |||||
AND indexname = '{index_name}' ; | |||||
""", | |||||
) | |||||
self.assertEquals(len(indexs_in_table), 1) |
@@ -63,11 +63,12 @@ class TestTranslate(unittest.TestCase): | |||||
Case 2: frappe.form_dict._lang is not set, but preferred_language cookie is | Case 2: frappe.form_dict._lang is not set, but preferred_language cookie is | ||||
""" | """ | ||||
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang): | |||||
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)]) | |||||
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value='fr'): | |||||
set_request(method="POST", path="/", headers=[("Accept-Language", 'hr')]) | |||||
return_val = get_language() | return_val = get_language() | ||||
self.assertNotIn(return_val, [second_lang, get_parent_language(second_lang)]) | |||||
# system default language | |||||
self.assertEqual(return_val, 'en') | |||||
self.assertNotIn(return_val, [second_lang, get_parent_language(second_lang)]) | |||||
def test_guest_request_language_resolution_with_cookie(self): | def test_guest_request_language_resolution_with_cookie(self): | ||||
"""Test for frappe.translate.get_language | """Test for frappe.translate.get_language | ||||
@@ -0,0 +1,67 @@ | |||||
import unittest | |||||
import frappe | |||||
from frappe.www.list import get_list_context | |||||
class TestWebsite(unittest.TestCase): | |||||
def test_get_context_hook_of_webform(self): | |||||
create_custom_doctype() | |||||
create_webform() | |||||
# check context for apps without any hook | |||||
context_list = get_list_context("", "Custom Doctype", "test-webform") | |||||
self.assertFalse(context_list) | |||||
# create a hook to get webform_context | |||||
set_webform_hook( | |||||
"webform_list_context", | |||||
"frappe.www._test._test_webform.webform_list_context", | |||||
) | |||||
# check context for apps with hook | |||||
context_list = get_list_context("", "Custom Doctype", "test-webform") | |||||
self.assertTrue(context_list) | |||||
def create_custom_doctype(): | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"name": "Custom Doctype", | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [{"label": "Title", "fieldname": "title", "fieldtype": "Data"}], | |||||
} | |||||
).insert(ignore_if_duplicate=True) | |||||
def create_webform(): | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "Web Form", | |||||
"module": "Core", | |||||
"title": "Test Webform", | |||||
"route": "test-webform", | |||||
"doc_type": "Custom Doctype", | |||||
"web_form_fields": [ | |||||
{ | |||||
"doctype": "Web Form Field", | |||||
"fieldname": "title", | |||||
"fieldtype": "Data", | |||||
"label": "Title", | |||||
} | |||||
], | |||||
} | |||||
).insert(ignore_if_duplicate=True) | |||||
def set_webform_hook(key, value): | |||||
from frappe import hooks | |||||
# reset hooks | |||||
for hook in "webform_list_context": | |||||
if hasattr(hooks, hook): | |||||
delattr(hooks, hook) | |||||
setattr(hooks, key, value) | |||||
frappe.cache().delete_key("app_hooks") |
@@ -62,16 +62,15 @@ def create_todo_records(): | |||||
}).insert() | }).insert() | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def create_communication_records(): | |||||
if frappe.db.get_all('Communication', {'subject': 'Test Form Communication 1'}): | |||||
return | |||||
frappe.get_doc({ | |||||
def create_communication_record(): | |||||
doc = frappe.get_doc({ | |||||
"doctype": "Communication", | "doctype": "Communication", | ||||
"recipients": "test@gmail.com", | "recipients": "test@gmail.com", | ||||
"subject": "Test Form Communication 1", | "subject": "Test Form Communication 1", | ||||
"communication_date": frappe.utils.now_datetime(), | "communication_date": frappe.utils.now_datetime(), | ||||
}).insert() | |||||
}) | |||||
doc.insert() | |||||
return doc | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def setup_workflow(): | def setup_workflow(): | ||||
@@ -3262,7 +3262,7 @@ Drop,drop, | |||||
Drop Here,Drop hier, | Drop Here,Drop hier, | ||||
Drop files here,Laat lêers hier neer, | Drop files here,Laat lêers hier neer, | ||||
Dynamic Template,Dinamiese sjabloon, | Dynamic Template,Dinamiese sjabloon, | ||||
ERPNext Role,ERPVolgende rol, | |||||
ERPNext Role,ERPNext rol, | |||||
Email / Notifications,E-pos / kennisgewings, | Email / Notifications,E-pos / kennisgewings, | ||||
Email Account setup please enter your password for: {0},Voer u wagwoord in vir die e-posrekening vir: {0}, | Email Account setup please enter your password for: {0},Voer u wagwoord in vir die e-posrekening vir: {0}, | ||||
Email Address whose Google Contacts are to be synced.,E-posadres waarvan die Google-kontakte gesinkroniseer moet word., | Email Address whose Google Contacts are to be synced.,E-posadres waarvan die Google-kontakte gesinkroniseer moet word., | ||||
@@ -3262,7 +3262,7 @@ Drop,ጣል ያድርጉ።, | |||||
Drop Here,እዚህ ጣል ያድርጉ።, | Drop Here,እዚህ ጣል ያድርጉ።, | ||||
Drop files here,ፋይሎችን እዚህ ይጣሉ።, | Drop files here,ፋይሎችን እዚህ ይጣሉ።, | ||||
Dynamic Template,ተለዋዋጭ አብነት, | Dynamic Template,ተለዋዋጭ አብነት, | ||||
ERPNext Role,የኢአርኤክስ ቀጣይ ሚና።, | |||||
ERPNext Role,ERPNext ሚና, | |||||
Email / Notifications,ኢሜይል / ማስታወቂያዎች, | Email / Notifications,ኢሜይል / ማስታወቂያዎች, | ||||
Email Account setup please enter your password for: {0},የኢሜል አካውንት ማዋቀር እባክዎን ለሚከተለው ይለፍ ቃልዎን ያስገቡ ፦ {0}, | Email Account setup please enter your password for: {0},የኢሜል አካውንት ማዋቀር እባክዎን ለሚከተለው ይለፍ ቃልዎን ያስገቡ ፦ {0}, | ||||
Email Address whose Google Contacts are to be synced.,የጉግል አድራሻዎች የሚመሳሰሉበት የኢሜል አድራሻ ፡፡, | Email Address whose Google Contacts are to be synced.,የጉግል አድራሻዎች የሚመሳሰሉበት የኢሜል አድራሻ ፡፡, | ||||
@@ -3262,7 +3262,7 @@ Drop,Dråbe, | |||||
Drop Here,Drop Here, | Drop Here,Drop Here, | ||||
Drop files here,Slip filer her, | Drop files here,Slip filer her, | ||||
Dynamic Template,Dynamisk skabelon, | Dynamic Template,Dynamisk skabelon, | ||||
ERPNext Role,ERPNæste rolle, | |||||
ERPNext Role,ERPNext rolle, | |||||
Email / Notifications,E-mail / underretninger, | Email / Notifications,E-mail / underretninger, | ||||
Email Account setup please enter your password for: {0},Opsætning af e-mail-konto: indtast venligst din adgangskode til: {0}, | Email Account setup please enter your password for: {0},Opsætning af e-mail-konto: indtast venligst din adgangskode til: {0}, | ||||
Email Address whose Google Contacts are to be synced.,"E-mail-adresse, hvis Google-kontakter skal synkroniseres.", | Email Address whose Google Contacts are to be synced.,"E-mail-adresse, hvis Google-kontakter skal synkroniseres.", | ||||
@@ -3262,7 +3262,7 @@ Drop,Penurunan, | |||||
Drop Here,Jatuhkan Di Sini, | Drop Here,Jatuhkan Di Sini, | ||||
Drop files here,Letakkan file di sini, | Drop files here,Letakkan file di sini, | ||||
Dynamic Template,Template Dinamis, | Dynamic Template,Template Dinamis, | ||||
ERPNext Role,Peran ERPN, | |||||
ERPNext Role,Peran ERPNext, | |||||
Email / Notifications,Notifikasi email, | Email / Notifications,Notifikasi email, | ||||
Email Account setup please enter your password for: {0},"Pengaturan Akun Email, harap masukkan kata sandi Anda untuk: {0}", | Email Account setup please enter your password for: {0},"Pengaturan Akun Email, harap masukkan kata sandi Anda untuk: {0}", | ||||
Email Address whose Google Contacts are to be synced.,Alamat Email yang Kontak Google-nya harus disinkronkan., | Email Address whose Google Contacts are to be synced.,Alamat Email yang Kontak Google-nya harus disinkronkan., | ||||
@@ -3262,7 +3262,7 @@ Drop,Dropi, | |||||
Drop Here,Sendu hér, | Drop Here,Sendu hér, | ||||
Drop files here,Sendu skrár hér, | Drop files here,Sendu skrár hér, | ||||
Dynamic Template,Dynamískt sniðmát, | Dynamic Template,Dynamískt sniðmát, | ||||
ERPNext Role,ERPNæsta hlutverk, | |||||
ERPNext Role,ERPNext hlutverk, | |||||
Email / Notifications,Netfang / tilkynningar, | Email / Notifications,Netfang / tilkynningar, | ||||
Email Account setup please enter your password for: {0},Uppsetning tölvupóstreikninga vinsamlegast sláðu inn lykilorðið þitt fyrir: {0}, | Email Account setup please enter your password for: {0},Uppsetning tölvupóstreikninga vinsamlegast sláðu inn lykilorðið þitt fyrir: {0}, | ||||
Email Address whose Google Contacts are to be synced.,Netfang þar sem samstillt er Google tengiliði, | Email Address whose Google Contacts are to be synced.,Netfang þar sem samstillt er Google tengiliði, | ||||
@@ -3262,7 +3262,7 @@ Drop,Far cadere, | |||||
Drop Here,Drop Here, | Drop Here,Drop Here, | ||||
Drop files here,Trascina i file qui, | Drop files here,Trascina i file qui, | ||||
Dynamic Template,Modello dinamico, | Dynamic Template,Modello dinamico, | ||||
ERPNext Role,ERPSuccessivo ruolo, | |||||
ERPNext Role,ruolo ERPNext, | |||||
Email / Notifications,Notifiche di posta elettronica, | Email / Notifications,Notifiche di posta elettronica, | ||||
Email Account setup please enter your password for: {0},"Impostazione dell'account e-mail, inserire la password per: {0}", | Email Account setup please enter your password for: {0},"Impostazione dell'account e-mail, inserire la password per: {0}", | ||||
Email Address whose Google Contacts are to be synced.,Indirizzo email i cui contatti Google devono essere sincronizzati., | Email Address whose Google Contacts are to be synced.,Indirizzo email i cui contatti Google devono essere sincronizzati., | ||||
@@ -3262,7 +3262,7 @@ Drop,ドロップ, | |||||
Drop Here,ここにドロップ, | Drop Here,ここにドロップ, | ||||
Drop files here,ここにファイルをドロップします, | Drop files here,ここにファイルをドロップします, | ||||
Dynamic Template,動的テンプレート, | Dynamic Template,動的テンプレート, | ||||
ERPNext Role,ERP次のロール, | |||||
ERPNext Role,ERPNext の役割, | |||||
Email / Notifications,メール/通知, | Email / Notifications,メール/通知, | ||||
Email Account setup please enter your password for: {0},メールアカウントのセットアップ:{0}のパスワードを入力してください, | Email Account setup please enter your password for: {0},メールアカウントのセットアップ:{0}のパスワードを入力してください, | ||||
Email Address whose Google Contacts are to be synced.,Googleの連絡先を同期するメールアドレス。, | Email Address whose Google Contacts are to be synced.,Googleの連絡先を同期するメールアドレス。, | ||||
@@ -3262,7 +3262,7 @@ Drop,ទម្លាក់។, | |||||
Drop Here,ទម្លាក់នៅទីនេះ។, | Drop Here,ទម្លាក់នៅទីនេះ។, | ||||
Drop files here,ទម្លាក់ឯកសារនៅទីនេះ។, | Drop files here,ទម្លាក់ឯកសារនៅទីនេះ។, | ||||
Dynamic Template,គំរូឌីណាមិក, | Dynamic Template,គំរូឌីណាមិក, | ||||
ERPNext Role,តួនាទី ERP បន្ទាប់។, | |||||
ERPNext Role,ERPNext តួនាទី។, | |||||
Email / Notifications,អ៊ីមែល / ការជូនដំណឹង, | Email / Notifications,អ៊ីមែល / ការជូនដំណឹង, | ||||
Email Account setup please enter your password for: {0},រៀបចំគណនីអ៊ីមែលសូមបញ្ចូលពាក្យសម្ងាត់របស់អ្នកសម្រាប់៖ {0}, | Email Account setup please enter your password for: {0},រៀបចំគណនីអ៊ីមែលសូមបញ្ចូលពាក្យសម្ងាត់របស់អ្នកសម្រាប់៖ {0}, | ||||
Email Address whose Google Contacts are to be synced.,អាសយដ្ឋានអ៊ីមែលដែលទំនាក់ទំនងរបស់ Google នឹងត្រូវធ្វើសមកាលកម្ម។, | Email Address whose Google Contacts are to be synced.,អាសយដ្ឋានអ៊ីមែលដែលទំនាក់ទំនងរបស់ Google នឹងត្រូវធ្វើសមកាលកម្ម។, | ||||
@@ -3262,7 +3262,7 @@ Drop,थेंब, | |||||
Drop Here,येथे ड्रॉप करा, | Drop Here,येथे ड्रॉप करा, | ||||
Drop files here,फायली येथे सोडा, | Drop files here,फायली येथे सोडा, | ||||
Dynamic Template,डायनॅमिक टेम्पलेट, | Dynamic Template,डायनॅमिक टेम्पलेट, | ||||
ERPNext Role,ईआरपीनेक्स्ट रोल, | |||||
ERPNext Role,ERPNext रोल, | |||||
Email / Notifications,ईमेल / सूचना, | Email / Notifications,ईमेल / सूचना, | ||||
Email Account setup please enter your password for: {0},ईमेल खाते सेटअप यासाठी आपला संकेतशब्द प्रविष्ट करा: {0}, | Email Account setup please enter your password for: {0},ईमेल खाते सेटअप यासाठी आपला संकेतशब्द प्रविष्ट करा: {0}, | ||||
Email Address whose Google Contacts are to be synced.,ज्यांचे Google संपर्क समक्रमित केले जातील असा ईमेल पत्ता., | Email Address whose Google Contacts are to be synced.,ज्यांचे Google संपर्क समक्रमित केले जातील असा ईमेल पत्ता., | ||||
@@ -3262,7 +3262,7 @@ Drop,Tera, | |||||
Drop Here,Tera Hano, | Drop Here,Tera Hano, | ||||
Drop files here,Tera dosiye hano, | Drop files here,Tera dosiye hano, | ||||
Dynamic Template,Icyitegererezo, | Dynamic Template,Icyitegererezo, | ||||
ERPNext Role,Uruhare rwa ERPN, | |||||
ERPNext Role,uruhare rwa ERPNext, | |||||
Email / Notifications,Imeri / Amatangazo, | Email / Notifications,Imeri / Amatangazo, | ||||
Email Account setup please enter your password for: {0},Imeri ya konte ya imeri nyamuneka andika ijambo ryibanga rya: {0}, | Email Account setup please enter your password for: {0},Imeri ya konte ya imeri nyamuneka andika ijambo ryibanga rya: {0}, | ||||
Email Address whose Google Contacts are to be synced.,Aderesi ya imeri abo Google igomba guhuza., | Email Address whose Google Contacts are to be synced.,Aderesi ya imeri abo Google igomba guhuza., | ||||
@@ -3262,7 +3262,7 @@ Drop,Pokles, | |||||
Drop Here,Drop sem, | Drop Here,Drop sem, | ||||
Drop files here,Sem presuňte súbory, | Drop files here,Sem presuňte súbory, | ||||
Dynamic Template,Dynamická šablóna, | Dynamic Template,Dynamická šablóna, | ||||
ERPNext Role,ERPĎalšia rola, | |||||
ERPNext Role,ERPNext rola, | |||||
Email / Notifications,E-mail / Upozornenia, | Email / Notifications,E-mail / Upozornenia, | ||||
Email Account setup please enter your password for: {0},"Nastavenie e-mailového účtu, zadajte heslo pre: {0}", | Email Account setup please enter your password for: {0},"Nastavenie e-mailového účtu, zadajte heslo pre: {0}", | ||||
Email Address whose Google Contacts are to be synced.,"E-mailová adresa, ktorej kontakty Google sa majú synchronizovať.", | Email Address whose Google Contacts are to be synced.,"E-mailová adresa, ktorej kontakty Google sa majú synchronizovať.", | ||||
@@ -3262,7 +3262,7 @@ Drop,Drop, | |||||
Drop Here,Hidh këtu, | Drop Here,Hidh këtu, | ||||
Drop files here,Hidh skedarët këtu, | Drop files here,Hidh skedarët këtu, | ||||
Dynamic Template,Modeli Dinamik, | Dynamic Template,Modeli Dinamik, | ||||
ERPNext Role,Roli ERPN, | |||||
ERPNext Role,Roli i ERPNext, | |||||
Email / Notifications,Email / njoftime, | Email / Notifications,Email / njoftime, | ||||
Email Account setup please enter your password for: {0},Konfigurimi i llogarisë email ju lutemi shkruani fjalëkalimin tuaj për: {0, | Email Account setup please enter your password for: {0},Konfigurimi i llogarisë email ju lutemi shkruani fjalëkalimin tuaj për: {0, | ||||
Email Address whose Google Contacts are to be synced.,Adresa e Email-it Kontaktet e të cilit Google duhet të sinkronizohen., | Email Address whose Google Contacts are to be synced.,Adresa e Email-it Kontaktet e të cilit Google duhet të sinkronizohen., | ||||
@@ -3262,7 +3262,7 @@ Drop,Släppa, | |||||
Drop Here,Släpp här, | Drop Here,Släpp här, | ||||
Drop files here,Släpp filer här, | Drop files here,Släpp filer här, | ||||
Dynamic Template,Dynamisk mall, | Dynamic Template,Dynamisk mall, | ||||
ERPNext Role,ERPNästa roll, | |||||
ERPNext Role,ERPNext roll, | |||||
Email / Notifications,E-post / aviseringar, | Email / Notifications,E-post / aviseringar, | ||||
Email Account setup please enter your password for: {0},"Ange e-postkonto, ange ditt lösenord för: {0}", | Email Account setup please enter your password for: {0},"Ange e-postkonto, ange ditt lösenord för: {0}", | ||||
Email Address whose Google Contacts are to be synced.,E-postadress vars Google-kontakter ska synkroniseras., | Email Address whose Google Contacts are to be synced.,E-postadress vars Google-kontakter ska synkroniseras., | ||||
@@ -47,7 +47,7 @@ frappe.ui.form.on("Web Form", { | |||||
frm.add_custom_button(__('Get Fields'), () => { | frm.add_custom_button(__('Get Fields'), () => { | ||||
let webform_fieldtypes = frappe.meta.get_field('Web Form Field', 'fieldtype').options.split('\n'); | let webform_fieldtypes = frappe.meta.get_field('Web Form Field', 'fieldtype').options.split('\n'); | ||||
let fieldnames = (frm.doc.fields || []).map(d => d.fieldname); | |||||
let fieldnames = (frm.doc.web_form_fields || []).map(d => d.fieldname); | |||||
frappe.model.with_doctype(frm.doc.doc_type, () => { | frappe.model.with_doctype(frm.doc.doc_type, () => { | ||||
let meta = frappe.get_meta(frm.doc.doc_type); | let meta = frappe.get_meta(frm.doc.doc_type); | ||||
for (let field of meta.fields) { | for (let field of meta.fields) { | ||||
@@ -0,0 +1,6 @@ | |||||
def webform_list_context(module): | |||||
return {"get_list": get_webform_context_list} | |||||
def get_webform_context_list(): | |||||
pass |
@@ -161,6 +161,14 @@ def get_list_context(context, doctype, web_form_name=None): | |||||
module = load_doctype_module(doctype) | module = load_doctype_module(doctype) | ||||
list_context = update_context_from_module(module, list_context) | list_context = update_context_from_module(module, list_context) | ||||
# get context for custom webform | |||||
if meta.custom and web_form_name: | |||||
webform_list_contexts = frappe.get_hooks('webform_list_context') | |||||
if webform_list_contexts: | |||||
out = frappe._dict(frappe.get_attr(webform_list_contexts[0])(meta.module) or {}) | |||||
if out: | |||||
list_context = out | |||||
# get context from web form module | # get context from web form module | ||||
if web_form_name: | if web_form_name: | ||||
web_form = frappe.get_doc('Web Form', web_form_name) | web_form = frappe.get_doc('Web Form', web_form_name) | ||||
@@ -265,9 +265,11 @@ function get_chat_room(socket, room) { | |||||
} | } | ||||
function get_site_name(socket) { | function get_site_name(socket) { | ||||
var hostname_from_host = get_hostname(socket.request.headers.host); | |||||
if (socket.request.headers['x-frappe-site-name']) { | if (socket.request.headers['x-frappe-site-name']) { | ||||
return get_hostname(socket.request.headers['x-frappe-site-name']); | return get_hostname(socket.request.headers['x-frappe-site-name']); | ||||
} else if (['localhost', '127.0.0.1'].indexOf(socket.request.headers.host) !== -1 && | |||||
} else if (['localhost', '127.0.0.1'].indexOf(hostname_from_host) !== -1 && | |||||
conf.default_site) { | conf.default_site) { | ||||
// from currentsite.txt since host is localhost | // from currentsite.txt since host is localhost | ||||
return conf.default_site; | return conf.default_site; | ||||