@@ -14,8 +14,10 @@ coverage: | |||
patch: | |||
default: false | |||
server: | |||
target: auto | |||
threshold: 85% | |||
target: 85% | |||
threshold: 0% | |||
only_pulls: true | |||
if_ci_failed: ignore | |||
flags: | |||
- server | |||
@@ -92,15 +92,15 @@ context('Control Date, Time and DateTime', () => { | |||
date_format: 'dd.mm.yyyy', | |||
time_format: 'HH:mm:ss', | |||
value: ' 02.12.2019 11:00:12', | |||
doc_value: '2019-12-02 11:00:12', | |||
input_value: '02.12.2019 11:00:12' | |||
doc_value: '2019-12-02 00:30:12', // system timezone (America/New_York) | |||
input_value: '02.12.2019 11:00:12' // admin timezone (Asia/Kolkata) | |||
}, | |||
{ | |||
date_format: 'mm-dd-yyyy', | |||
time_format: 'HH:mm', | |||
value: ' 12-02-2019 11:00:00', | |||
doc_value: '2019-12-02 11:00:00', | |||
input_value: '12-02-2019 11:00' | |||
doc_value: '2019-12-02 00:30:00', // system timezone (America/New_York) | |||
input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata) | |||
} | |||
]; | |||
datetime_formats.forEach(d => { | |||
@@ -0,0 +1,49 @@ | |||
context('Grid Keyboard Shortcut', () => { | |||
let total_count = 0; | |||
beforeEach(() => { | |||
cy.login(); | |||
cy.visit('/app/doctype/User'); | |||
}); | |||
before(() => { | |||
cy.login(); | |||
cy.visit('/app/doctype/User'); | |||
return cy.window().its('frappe').then(frappe => { | |||
frappe.db.count('DocField', { | |||
filters: { | |||
'parent': 'User', 'parentfield': 'fields', 'parenttype': 'DocType' | |||
} | |||
}).then((r) => { | |||
total_count = r; | |||
}); | |||
}); | |||
}); | |||
it('Insert new row at the end', () => { | |||
cy.add_new_row_in_grid('{ctrl}{shift}{downarrow}', (cy, total_count) => { | |||
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', `${total_count+1}`); | |||
}, total_count); | |||
}); | |||
it('Insert new row at the top', () => { | |||
cy.add_new_row_in_grid('{ctrl}{shift}{uparrow}', (cy) => { | |||
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1'); | |||
}); | |||
}); | |||
it('Insert new row below', () => { | |||
cy.add_new_row_in_grid('{ctrl}{downarrow}', (cy) => { | |||
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '2'); | |||
}); | |||
}); | |||
it('Insert new row above', () => { | |||
cy.add_new_row_in_grid('{ctrl}{uparrow}', (cy) => { | |||
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1'); | |||
}); | |||
}); | |||
}); | |||
Cypress.Commands.add('add_new_row_in_grid', (shortcut_keys, callbackFn, total_count) => { | |||
cy.get('.frappe-control[data-fieldname="fields"]').as('table'); | |||
cy.get('@table').find('.grid-body .col-xs-2').first().click(); | |||
cy.get('@table').find('.grid-body .col-xs-2') | |||
.first().type(shortcut_keys); | |||
callbackFn(cy, total_count); | |||
}); |
@@ -7,18 +7,13 @@ context('List View', () => { | |||
}); | |||
}); | |||
it('Keep checkbox checked after Bulk Update', () => { | |||
it('Keep checkbox checked after Refresh', () => { | |||
cy.go_to_list('ToDo'); | |||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true }); | |||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | |||
cy.get('.dropdown-menu li:visible .dropdown-item .menu-item-label[data-label="Edit"]').click(); | |||
cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Priority').wait(200); | |||
cy.get('.modal-footer .standard-actions .btn-primary').click(); | |||
cy.wait(500); | |||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | |||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible'); | |||
cy.intercept('/api/method/frappe.desk.reportview.get').as('list-refresh'); | |||
cy.get('button[data-original-title="Refresh"]').click(); | |||
cy.wait('@list-refresh'); | |||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible'); | |||
}); | |||
@@ -17,6 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p | |||
from frappe.model.base_document import get_controller | |||
from frappe.social.doctype.post.post import frequently_visited_links | |||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo | |||
from frappe.utils import get_time_zone | |||
def get_bootinfo(): | |||
"""build and return boot info""" | |||
@@ -58,6 +59,7 @@ def get_bootinfo(): | |||
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) | |||
bootinfo.navbar_settings = get_navbar_settings() | |||
bootinfo.notification_settings = get_notification_settings() | |||
set_time_zone(bootinfo) | |||
# ipinfo | |||
if frappe.session.data.get('ipinfo'): | |||
@@ -220,8 +222,8 @@ def load_translations(bootinfo): | |||
bootinfo["__messages"] = messages | |||
def get_user_info(): | |||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', | |||
'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'], | |||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender', | |||
'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'], | |||
filters=dict(enabled=1)) | |||
user_info_map = {d.name: d for d in user_info} | |||
@@ -324,3 +326,9 @@ def get_desk_settings(): | |||
def get_notification_settings(): | |||
return frappe.get_cached_doc('Notification Settings', frappe.session.user) | |||
def set_time_zone(bootinfo): | |||
bootinfo.time_zone = { | |||
"system": get_time_zone(), | |||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone() | |||
} |
@@ -32,5 +32,11 @@ frappe.ui.form.on("System Settings", { | |||
frm.set_value('prepared_report_expiry_period', 7); | |||
} | |||
} | |||
}, | |||
on_update: function(frm) { | |||
if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) { | |||
// Clear cache after saving to refresh the values of boot. | |||
frappe.ui.toolbar.clear_cache(); | |||
} | |||
} | |||
}); |
@@ -97,6 +97,7 @@ | |||
"fieldname": "time_zone", | |||
"fieldtype": "Select", | |||
"label": "Time Zone", | |||
"read_only": 1, | |||
"reqd": 1 | |||
}, | |||
{ | |||
@@ -77,7 +77,12 @@ frappe.ui.form.on('User', { | |||
} | |||
}, | |||
refresh: function(frm) { | |||
var doc = frm.doc; | |||
let doc = frm.doc; | |||
if (frm.is_new()) { | |||
frm.set_value("time_zone", frappe.sys_defaults.time_zone); | |||
} | |||
if (in_list(['System User', 'Website User'], frm.doc.user_type) | |||
&& !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) { | |||
frm.reload_doc(); | |||
@@ -267,6 +272,12 @@ frappe.ui.form.on('User', { | |||
} | |||
} | |||
}); | |||
}, | |||
on_update: function(frm) { | |||
if (frappe.boot.time_zone && frappe.boot.time_zone.user !== frm.doc.time_zone) { | |||
// Clear cache after saving to refresh the values of boot. | |||
frappe.ui.toolbar.clear_cache(); | |||
} | |||
} | |||
}); | |||
@@ -7,7 +7,7 @@ import frappe.defaults | |||
import frappe.permissions | |||
from frappe.model.document import Document | |||
from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime, | |||
now_datetime, get_formatted_email, today) | |||
now_datetime, get_formatted_email, today, get_time_zone) | |||
from frappe import throw, msgprint, _ | |||
from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit | |||
from frappe.desk.notifications import clear_notifications | |||
@@ -74,6 +74,7 @@ class User(Document): | |||
self.validate_roles() | |||
self.validate_allowed_modules() | |||
self.validate_user_image() | |||
self.set_time_zone() | |||
if self.language == "Loading...": | |||
self.language = None | |||
@@ -227,11 +228,11 @@ class User(Document): | |||
def validate_share(self, docshare): | |||
pass | |||
# if docshare.user == self.name: | |||
# if self.user_type=="System User": | |||
# if docshare.share != 1: | |||
# frappe.throw(_("Sorry! User should have complete access to their own record.")) | |||
# else: | |||
# frappe.throw(_("Sorry! Sharing with Website User is prohibited.")) | |||
# if self.user_type=="System User": | |||
# if docshare.share != 1: | |||
# frappe.throw(_("Sorry! User should have complete access to their own record.")) | |||
# else: | |||
# frappe.throw(_("Sorry! Sharing with Website User is prohibited.")) | |||
def send_password_notification(self, new_password): | |||
try: | |||
@@ -596,6 +597,10 @@ class User(Document): | |||
return user | |||
def set_time_zone(self): | |||
if not self.time_zone: | |||
self.time_zone = get_time_zone() | |||
@frappe.whitelist() | |||
def get_timezones(): | |||
import pytz | |||
@@ -133,7 +133,7 @@ class SubmittableDocumentTree: | |||
"""Returns list of submittable doctypes. | |||
""" | |||
if not self._submittable_doctypes: | |||
self._submittable_doctypes = frappe.db.get_list('DocType', {'is_submittable': 1}, pluck='name') | |||
self._submittable_doctypes = frappe.db.get_all('DocType', {'is_submittable': 1}, pluck='name') | |||
return self._submittable_doctypes | |||
@@ -2,6 +2,7 @@ frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3 | |||
execute:frappe.utils.global_search.setup_global_search_table() | |||
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 | |||
execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2020-10-17 | |||
execute:frappe.reload_doc('core', 'doctype', 'doctype_state', force=True) #2021-12-15 | |||
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 | |||
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 | |||
frappe.patches.v11_0.drop_column_apply_user_permissions | |||
@@ -122,12 +122,7 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp | |||
); | |||
this.$scan_btn = this.$wrapper.find('.link-btn'); | |||
this.$input.on("focus", () => { | |||
setTimeout(() => { | |||
this.$scan_btn.toggle(true); | |||
}, 500); | |||
}); | |||
this.$scan_btn.toggle(true); | |||
const me = this; | |||
this.$scan_btn.on('click', 'a', () => { | |||
@@ -141,12 +136,6 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp | |||
} | |||
}); | |||
}); | |||
this.$input.on("blur", () => { | |||
setTimeout(() => { | |||
this.$scan_btn.toggle(false); | |||
}, 500); | |||
}); | |||
} | |||
bind_change_event() { | |||
@@ -53,8 +53,6 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||
let date_format = sysdefaults && sysdefaults.date_format | |||
? sysdefaults.date_format : 'yyyy-mm-dd'; | |||
let now_date = new Date(); | |||
this.today_text = __("Today"); | |||
this.date_format = frappe.defaultDateFormat; | |||
this.datepicker_options = { | |||
@@ -62,7 +60,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||
autoClose: true, | |||
todayButton: true, | |||
dateFormat: date_format, | |||
startDate: now_date, | |||
startDate: this.get_start_date(), | |||
keyboardNav: false, | |||
onSelect: () => { | |||
this.$input.trigger('change'); | |||
@@ -77,6 +75,11 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||
...(this.get_df_options()) | |||
}; | |||
} | |||
get_start_date() { | |||
return new Date(this.get_now_date()); | |||
} | |||
set_datepicker() { | |||
this.$input.datepicker(this.datepicker_options); | |||
this.datepicker = this.$input.data('datepicker'); | |||
@@ -113,7 +116,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||
this.datepicker.update('position', position); | |||
} | |||
get_now_date() { | |||
return frappe.datetime.now_date(true); | |||
return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true)); | |||
} | |||
set_t_for_today() { | |||
var me = this; | |||
@@ -1,4 +1,22 @@ | |||
frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.ControlDate { | |||
set_formatted_input(value) { | |||
if (this.timepicker_only) return; | |||
if (!this.datepicker) return; | |||
if (!value) { | |||
this.datepicker.clear(); | |||
return; | |||
} else if (value === "Today") { | |||
value = this.get_now_date(); | |||
} | |||
value = this.format_for_input(value); | |||
this.$input && this.$input.val(value); | |||
this.datepicker.selectDate(frappe.datetime.user_to_obj(value)); | |||
} | |||
get_start_date() { | |||
let value = frappe.datetime.convert_to_user_tz(this.value); | |||
return frappe.datetime.str_to_obj(value); | |||
} | |||
set_date_options() { | |||
super.set_date_options(); | |||
this.today_text = __("Now"); | |||
@@ -14,10 +32,31 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co | |||
get_now_date() { | |||
return frappe.datetime.now_datetime(true); | |||
} | |||
parse(value) { | |||
if (value) { | |||
value = frappe.datetime.user_to_str(value, false); | |||
if (!frappe.datetime.is_system_time_zone()) { | |||
value = frappe.datetime.convert_to_system_tz(value, true); | |||
} | |||
return value; | |||
} | |||
} | |||
format_for_input(value) { | |||
if (!value) return ""; | |||
return frappe.datetime.str_to_user(value, false); | |||
} | |||
set_description() { | |||
const { description } = this.df; | |||
const { time_zone } = frappe.sys_defaults; | |||
if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) { | |||
const description = this.df.description; | |||
const time_zone = this.get_user_time_zone(); | |||
if (!this.df.hide_timezone) { | |||
// Always show the timezone when rendering the Datetime field since the datetime value will | |||
// always be in system_time_zone rather then local time. | |||
if (!description) { | |||
this.df.description = time_zone; | |||
} else if (!description.includes(time_zone)) { | |||
@@ -26,6 +65,9 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co | |||
} | |||
super.set_description(); | |||
} | |||
get_user_time_zone() { | |||
return frappe.boot.time_zone ? frappe.boot.time_zone.user : frappe.sys_defaults.time_zone; | |||
} | |||
set_datepicker() { | |||
super.set_datepicker(); | |||
if (this.datepicker.opts.timeFormat.indexOf('s') == -1) { | |||
@@ -471,7 +471,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat | |||
docname: value, | |||
fields: columns_to_fetch, | |||
}).then((response) => { | |||
if (!response || !response.name) return ""; | |||
if (!response || !response.name) return null; | |||
if (!docname || !columns_to_fetch.length) return response.name; | |||
for (const [target_field, source_field] of Object.entries(fetch_map)) { | |||
@@ -71,7 +71,7 @@ frappe.ui.form.ControlTime = class ControlTime extends frappe.ui.form.ControlDat | |||
set_description() { | |||
const { description } = this.df; | |||
const { time_zone } = frappe.sys_defaults; | |||
if (!frappe.datetime.is_timezone_same()) { | |||
if (!frappe.datetime.is_system_time_zone()) { | |||
if (!description) { | |||
this.df.description = time_zone; | |||
} else if (!description.includes(time_zone)) { | |||
@@ -116,7 +116,7 @@ function get_version_timeline_content(version_doc, frm) { | |||
frm.perm); | |||
if (field_display_status === 'Read' || field_display_status === 'Write') { | |||
return frappe.meta.get_label(frm.doctype, p[0]); | |||
return __(frappe.meta.get_label(frm.doctype, p[0])); | |||
} | |||
} | |||
}); | |||
@@ -85,7 +85,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
}); | |||
// navigate records keyboard shortcuts | |||
this.add_nav_keyboard_shortcuts(); | |||
this.add_form_keyboard_shortcuts(); | |||
// 2 column layout | |||
this.setup_std_layout(); | |||
@@ -116,7 +116,8 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.setup_done = true; | |||
} | |||
add_nav_keyboard_shortcuts() { | |||
add_form_keyboard_shortcuts() { | |||
// Navigate to next record | |||
frappe.ui.keys.add_shortcut({ | |||
shortcut: 'shift+ctrl+>', | |||
action: () => this.navigate_records(0), | |||
@@ -126,6 +127,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
condition: () => !this.is_new() | |||
}); | |||
// Navigate to previous record | |||
frappe.ui.keys.add_shortcut({ | |||
shortcut: 'shift+ctrl+<', | |||
action: () => this.navigate_records(1), | |||
@@ -134,6 +136,56 @@ frappe.ui.form.Form = class FrappeForm { | |||
ignore_inputs: true, | |||
condition: () => !this.is_new() | |||
}); | |||
let grid_shortcut_keys = [ | |||
{ | |||
'shortcut': 'Up Arrow', | |||
'description': __('Move cursor to above row') | |||
}, | |||
{ | |||
'shortcut': 'Down Arrow', | |||
'description': __('Move cursor to below row') | |||
}, | |||
{ | |||
'shortcut': 'tab', | |||
'description': __('Move cursor to next column') | |||
}, | |||
{ | |||
'shortcut': 'shift+tab', | |||
'description': __('Move cursor to previous column') | |||
}, | |||
{ | |||
'shortcut': 'Ctrl+up', | |||
'description': __('Add a row above the current row') | |||
}, | |||
{ | |||
'shortcut': 'Ctrl+down', | |||
'description': __('Add a row below the current row') | |||
}, | |||
{ | |||
'shortcut': 'Ctrl+shift+up', | |||
'description': __('Add a row at the top') | |||
}, | |||
{ | |||
'shortcut': 'Ctrl+shift+down', | |||
'description': __('Add a row at the bottom') | |||
}, | |||
{ | |||
'shortcut': 'shift+alt+down', | |||
'description': __('To duplcate current row') | |||
} | |||
]; | |||
grid_shortcut_keys.forEach(row => { | |||
frappe.ui.keys.add_shortcut({ | |||
shortcut: row.shortcut, | |||
page: this, | |||
description: __(row.description), | |||
ignore_inputs: true, | |||
condition: () => !this.is_new() | |||
}); | |||
}); | |||
} | |||
setup_std_layout() { | |||
@@ -167,12 +167,8 @@ frappe.form.formatters = { | |||
}, | |||
Datetime: function(value) { | |||
if(value) { | |||
var m = moment(frappe.datetime.convert_to_user_tz(value)); | |||
if(frappe.boot.sysdefaults.time_zone) { | |||
m = m.tz(frappe.boot.sysdefaults.time_zone); | |||
} | |||
return m.format(frappe.boot.sysdefaults.date_format.toUpperCase() | |||
+ ' ' + (frappe.boot.sysdefaults.time_format || 'HH:mm:ss')); | |||
return moment(frappe.datetime.convert_to_user_tz(value)) | |||
.format(frappe.boot.sysdefaults.date_format.toUpperCase() + ' ' + frappe.boot.sysdefaults.time_format || 'HH:mm:ss'); | |||
} else { | |||
return ""; | |||
} | |||
@@ -616,11 +616,14 @@ export default class Grid { | |||
}); | |||
} | |||
add_new_row(idx, callback, show, copy_doc, go_to_last_page = false) { | |||
add_new_row(idx, callback, show, copy_doc, go_to_last_page = false, go_to_first_page = false) { | |||
if (this.is_editable()) { | |||
if (go_to_last_page) { | |||
this.grid_pagination.go_to_last_page_to_add_row(); | |||
} else if (go_to_first_page) { | |||
this.grid_pagination.go_to_page(1); | |||
} | |||
if (this.frm) { | |||
var d = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname, idx); | |||
if (copy_doc) { | |||
@@ -684,7 +687,7 @@ export default class Grid { | |||
} | |||
set_focus_on_row(idx) { | |||
if (!idx) { | |||
if (!idx && idx !== 0) { | |||
idx = this.grid_rows.length - 1; | |||
} | |||
@@ -723,6 +723,7 @@ export default class GridRow { | |||
set_arrow_keys(field) { | |||
var me = this; | |||
let ignore_fieldtypes = ['Text', 'Small Text', 'Code', 'Text Editor', 'HTML Editor']; | |||
if (field.$input) { | |||
field.$input.on('keydown', function(e) { | |||
var { TAB, UP: UP_ARROW, DOWN: DOWN_ARROW } = frappe.ui.keyCode; | |||
@@ -734,8 +735,20 @@ export default class GridRow { | |||
var fieldname = $(this).attr('data-fieldname'); | |||
var fieldtype = $(this).attr('data-fieldtype'); | |||
let ctrl_key = e.metaKey || e.ctrlKey; | |||
if (!in_list(ignore_fieldtypes, fieldtype) | |||
&& ctrl_key && e.which !== TAB) { | |||
me.add_new_row_using_keys(e); | |||
return; | |||
} | |||
if (e.shiftKey && e.altKey && DOWN_ARROW === e.which) { | |||
me.duplicate_row_using_keys(); | |||
return; | |||
} | |||
var move_up_down = function(base) { | |||
if (in_list(['Text', 'Small Text', 'Code', 'Text Editor', 'HTML Editor'], fieldtype) && !e.altKey) { | |||
if (in_list(ignore_fieldtypes, fieldtype) && !e.altKey) { | |||
return false; | |||
} | |||
if (field.autocomplete_open) { | |||
@@ -790,6 +803,40 @@ export default class GridRow { | |||
} | |||
} | |||
duplicate_row_using_keys() { | |||
setTimeout(() => { | |||
this.insert(false, true, true); | |||
this.grid.grid_rows[this.doc.idx].toggle_editable_row(); | |||
this.grid.set_focus_on_row(this.doc.idx); | |||
}, 100); | |||
} | |||
add_new_row_using_keys(e) { | |||
let idx = ''; | |||
let ctrl_key = e.metaKey || e.ctrlKey; | |||
let is_down_arrow_key_press = (e.which === 40); | |||
// Add new row at the end or start of the table | |||
if (ctrl_key && e.shiftKey) { | |||
idx = is_down_arrow_key_press ? null : 1; | |||
this.grid.add_new_row(idx, null, is_down_arrow_key_press, | |||
false, is_down_arrow_key_press, !is_down_arrow_key_press); | |||
idx = is_down_arrow_key_press ? (cint(this.grid.grid_rows.length) - 1) : 0; | |||
} else if (ctrl_key) { | |||
idx = is_down_arrow_key_press ? this.doc.idx : (this.doc.idx - 1); | |||
this.insert(false, is_down_arrow_key_press); | |||
} | |||
if (idx !== '') { | |||
setTimeout(() => { | |||
this.grid.grid_rows[idx].toggle_editable_row(); | |||
this.grid.set_focus_on_row(idx); | |||
}, 100); | |||
} | |||
} | |||
get_open_form() { | |||
return frappe.ui.form.get_open_grid_form(); | |||
} | |||
@@ -392,9 +392,9 @@ frappe.views.BaseList = class BaseList { | |||
this.start = 0; | |||
this.page_length = $this.data().value; | |||
this.refresh(); | |||
} else if ($this.is(".btn-more")) { | |||
this.start = this.start + this.page_length; | |||
this.page_length = 20; | |||
} | |||
this.refresh(); | |||
}); | |||
@@ -475,6 +475,7 @@ frappe.views.BaseList = class BaseList { | |||
this.render(); | |||
this.after_render(); | |||
this.freeze(false); | |||
this.reset_defaults(); | |||
if (this.settings.refresh) { | |||
this.settings.refresh(this); | |||
} | |||
@@ -492,6 +493,13 @@ frappe.views.BaseList = class BaseList { | |||
} else { | |||
this.data = this.data.concat(data); | |||
} | |||
this.data = this.data.uniqBy((d) => d.name); | |||
} | |||
reset_defaults() { | |||
this.page_length = this.page_length + this.start; | |||
this.start = 0; | |||
} | |||
freeze() { | |||
@@ -1317,7 +1317,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
return; | |||
} | |||
frappe.realtime.on("list_update", (data) => { | |||
if (this.filter_area.is_being_edited()) { | |||
if (this.avoid_realtime_update()) { | |||
return; | |||
} | |||
@@ -1379,6 +1379,19 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
}); | |||
} | |||
avoid_realtime_update() { | |||
if (this.filter_area.is_being_edited()) { | |||
return true; | |||
} | |||
// this is set when a bulk operation is called from a list view which might update the list view | |||
// this is to avoid the list view from refreshing a lot of times | |||
// the list view is updated once after the bulk operation is complete | |||
if (this.disable_list_update) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
set_rows_as_checked() { | |||
$.each(this.$checks, (i, el) => { | |||
let docname = $(el).attr("data-name"); | |||
@@ -1433,6 +1446,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
return this.data.filter((d) => docnames.includes(d.name)); | |||
} | |||
clear_checked_items() { | |||
this.$checks && this.$checks.prop("checked", false); | |||
this.on_row_checked(); | |||
} | |||
save_view_user_settings(obj) { | |||
return frappe.model.user_settings.save( | |||
this.doctype, | |||
@@ -1655,11 +1673,17 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_assignment = () => { | |||
return { | |||
label: __("Assign To"), | |||
action: () => | |||
action: () => { | |||
this.disable_list_update = true; | |||
bulk_operations.assign( | |||
this.get_checked_items(true), | |||
this.refresh | |||
), | |||
() => { | |||
this.disable_list_update = false; | |||
this.clear_checked_items(); | |||
this.refresh(); | |||
} | |||
); | |||
}, | |||
standard: true, | |||
}; | |||
}; | |||
@@ -1667,11 +1691,17 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_assignment_rule = () => { | |||
return { | |||
label: __("Apply Assignment Rule"), | |||
action: () => | |||
action: () => { | |||
this.disable_list_update = true; | |||
bulk_operations.apply_assignment_rule( | |||
this.get_checked_items(true), | |||
this.refresh | |||
), | |||
() => { | |||
this.disable_list_update = false; | |||
this.clear_checked_items(); | |||
this.refresh(); | |||
} | |||
); | |||
}, | |||
standard: true, | |||
}; | |||
}; | |||
@@ -1679,11 +1709,17 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const bulk_add_tags = () => { | |||
return { | |||
label: __("Add Tags"), | |||
action: () => | |||
action: () => { | |||
this.disable_list_update = true; | |||
bulk_operations.add_tags( | |||
this.get_checked_items(true), | |||
this.refresh | |||
), | |||
() => { | |||
this.disable_list_update = false; | |||
this.clear_checked_items(); | |||
this.refresh(); | |||
} | |||
); | |||
}, | |||
standard: true, | |||
}; | |||
}; | |||
@@ -1705,7 +1741,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
); | |||
frappe.confirm( | |||
__("Delete {0} items permanently?", [docnames.length]), | |||
() => bulk_operations.delete(docnames, this.refresh) | |||
() => { | |||
this.disable_list_update = true; | |||
bulk_operations.delete(docnames, () => { | |||
this.disable_list_update = false; | |||
this.clear_checked_items(); | |||
this.refresh(); | |||
}); | |||
} | |||
); | |||
}, | |||
standard: true, | |||
@@ -1720,13 +1763,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (docnames.length > 0) { | |||
frappe.confirm( | |||
__("Cancel {0} documents?", [docnames.length]), | |||
() => | |||
() => { | |||
this.disable_list_update = true; | |||
bulk_operations.submit_or_cancel( | |||
docnames, | |||
"cancel", | |||
this.refresh | |||
) | |||
); | |||
() => { | |||
this.disable_list_update = false; | |||
this.clear_checked_items(); | |||
this.refresh(); | |||
} | |||
); | |||
}); | |||
} | |||
}, | |||
standard: true, | |||
@@ -1741,12 +1789,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if (docnames.length > 0) { | |||
frappe.confirm( | |||
__("Submit {0} documents?", [docnames.length]), | |||
() => | |||
() => { | |||
this.disable_list_update = true; | |||
bulk_operations.submit_or_cancel( | |||
docnames, | |||
"submit", | |||
this.refresh | |||
) | |||
() => { | |||
this.disable_list_update = false; | |||
this.clear_checked_items(); | |||
this.refresh(); | |||
} | |||
); | |||
} | |||
); | |||
} | |||
}, | |||
@@ -1769,12 +1823,15 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
} | |||
}); | |||
const docnames = this.get_checked_items(true); | |||
this.disable_list_update = true; | |||
bulk_operations.edit( | |||
docnames, | |||
this.get_checked_items(true), | |||
field_mappings, | |||
this.refresh | |||
() => { | |||
this.disable_list_update = false; | |||
this.clear_checked_items(); | |||
this.refresh(); | |||
} | |||
); | |||
}, | |||
standard: true, | |||
@@ -15,7 +15,7 @@ Object.assign(window, { | |||
}); | |||
$.extend(frappe.perm, { | |||
rights: ["read", "write", "create", "delete", "submit", "cancel", "amend", | |||
rights: ["select", "read", "write", "create", "delete", "submit", "cancel", "amend", | |||
"report", "import", "export", "print", "email", "share", "set_user_permissions"], | |||
doctype_perm: {}, | |||
@@ -72,6 +72,9 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => { | |||
let current_page_shortcuts = standard_shortcuts.filter( | |||
shortcut => shortcut.page && shortcut.page === window.cur_page.page.page); | |||
let grid_shortcuts = standard_shortcuts.filter( | |||
shortcut => shortcut.page && shortcut.page === window.cur_page.page.frm); | |||
function generate_shortcuts_html(shortcuts, heading) { | |||
if (!shortcuts.length) { | |||
return ''; | |||
@@ -100,6 +103,7 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => { | |||
let global_shortcuts_html = generate_shortcuts_html(global_shortcuts, __('Global Shortcuts')); | |||
let current_page_shortcuts_html = generate_shortcuts_html(current_page_shortcuts, __('Page Shortcuts')); | |||
let grid_shortcuts_html = generate_shortcuts_html(grid_shortcuts, __('Grid Shortcuts')); | |||
let dialog = new frappe.ui.Dialog({ | |||
title: __('Keyboard Shortcuts'), | |||
@@ -110,6 +114,7 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => { | |||
dialog.$body.append(global_shortcuts_html); | |||
dialog.$body.append(current_page_shortcuts_html); | |||
dialog.$body.append(grid_shortcuts_html); | |||
dialog.$body.append(` | |||
<div class="text-muted"> | |||
${__('Press Alt Key to trigger additional shortcuts in Menu and Sidebar')} | |||
@@ -13,33 +13,48 @@ frappe.provide("frappe.datetime"); | |||
$.extend(frappe.datetime, { | |||
convert_to_user_tz: function(date, format) { | |||
// format defaults to true | |||
if(frappe.sys_defaults.time_zone) { | |||
var date_obj = moment.tz(date, frappe.sys_defaults.time_zone).local(); | |||
// Converts the datetime string to system time zone first since the database only stores datetime in | |||
// system time zone and then convert the string to user time zone(from User doctype). | |||
let date_obj = null; | |||
if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) { | |||
date_obj = moment.tz(date, frappe.boot.time_zone.system) | |||
.clone() | |||
.tz(frappe.boot.time_zone.user); | |||
} else { | |||
var date_obj = moment(date); | |||
date_obj = moment(date); | |||
} | |||
return (format===false) ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); | |||
return format === false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); | |||
}, | |||
convert_to_system_tz: function(date, format) { | |||
// format defaults to true | |||
if(frappe.sys_defaults.time_zone) { | |||
var date_obj = moment(date).tz(frappe.sys_defaults.time_zone); | |||
// Converts the datetime string to user time zone (from User doctype) first since this fn is called in datetime which accepts datetime | |||
// in user time zone then convert the string to user time zone. | |||
// This is done so that only one timezone is present in database and we do not end up storing local timezone since it changes | |||
// as per the location of user. | |||
let date_obj = null; | |||
if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) { | |||
date_obj = moment.tz(date, frappe.boot.time_zone.user) | |||
.clone() | |||
.tz(frappe.boot.time_zone.system); | |||
} else { | |||
var date_obj = moment(date); | |||
date_obj = moment(date); | |||
} | |||
return (format===false) ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); | |||
return format===false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat); | |||
}, | |||
is_timezone_same: function() { | |||
if(frappe.sys_defaults.time_zone) { | |||
return moment().tz(frappe.sys_defaults.time_zone).utcOffset() === moment().utcOffset(); | |||
} else { | |||
return true; | |||
is_system_time_zone: function() { | |||
if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) { | |||
return moment().tz(frappe.boot.time_zone.system).utcOffset() === moment().tz(frappe.boot.time_zone.user).utcOffset(); | |||
} | |||
return true; | |||
}, | |||
is_timezone_same: function() { | |||
return frappe.datetime.is_system_time_zone(); | |||
}, | |||
str_to_obj: function(d) { | |||
@@ -98,11 +113,11 @@ $.extend(frappe.datetime, { | |||
return moment().endOf("quarter").format(); | |||
}, | |||
year_start: function(){ | |||
year_start: function() { | |||
return moment().startOf("year").format(); | |||
}, | |||
year_end: function(){ | |||
year_end: function() { | |||
return moment().endOf("year").format(); | |||
}, | |||
@@ -119,19 +134,25 @@ $.extend(frappe.datetime, { | |||
}, | |||
str_to_user: function(val, only_time = false) { | |||
if(!val) return ""; | |||
var user_time_fmt = frappe.datetime.get_user_time_fmt(); | |||
if(only_time) { | |||
return moment(val, frappe.defaultTimeFormat) | |||
.format(user_time_fmt); | |||
} | |||
var user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase(); | |||
if(typeof val !== "string" || val.indexOf(" ")===-1) { | |||
return moment(val).format(user_date_fmt); | |||
if (!val) return ""; | |||
const user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase(); | |||
const user_time_fmt = frappe.datetime.get_user_time_fmt(); | |||
let user_format = user_time_fmt; | |||
if (only_time) { | |||
let date_obj = moment(val, frappe.defaultTimeFormat); | |||
return date_obj.format(user_format); | |||
} else { | |||
return moment(val, "YYYY-MM-DD HH:mm:ss").format(user_date_fmt + " " + user_time_fmt); | |||
let date_obj = moment.tz(val, frappe.boot.time_zone.system); | |||
if (typeof val !== "string" || val.indexOf(" ") === -1) { | |||
user_format = user_date_fmt; | |||
} else { | |||
user_format = user_date_fmt + " " + user_time_fmt; | |||
} | |||
return date_obj | |||
.clone() | |||
.tz(frappe.boot.time_zone.user) | |||
.format(user_format); | |||
} | |||
}, | |||
@@ -186,23 +207,22 @@ $.extend(frappe.datetime, { | |||
}, | |||
_date: function(format, as_obj = false) { | |||
const time_zone = frappe.sys_defaults && frappe.sys_defaults.time_zone; | |||
let date; | |||
if (time_zone) { | |||
date = moment.tz(time_zone); | |||
} else { | |||
date = moment(); | |||
} | |||
if (as_obj) { | |||
return frappe.datetime.moment_to_date_obj(date); | |||
} else { | |||
return date.format(format); | |||
} | |||
/** | |||
* Whenever we are getting now_date/datetime, always make sure dates are fetched using user time zone. | |||
* This is to make sure that time is as per user time zone set in User doctype, If a user had to change the timezone, | |||
* we will end up having multiple timezone by not honouring timezone in User doctype. | |||
* This will make sure that at any point we know which timezone the user if following and not have random timezone | |||
* when the timezone of the local machine changes. | |||
*/ | |||
let time_zone = frappe.boot.time_zone ? frappe.boot.time_zone.user || frappe.boot.time_zone.system : frappe.sys_defaults.time_zone; | |||
let date = moment.tz(time_zone); | |||
return as_obj ? frappe.datetime.moment_to_date_obj(date) : date.format(format); | |||
}, | |||
moment_to_date_obj: function(moment) { | |||
moment_to_date_obj: function(moment_obj) { | |||
const date_obj = new Date(); | |||
const date_array = moment.toArray(); | |||
const date_array = moment_obj.toArray(); | |||
date_obj.setFullYear(date_array[0]); | |||
date_obj.setMonth(date_array[1]); | |||
date_obj.setDate(date_array[2]); | |||
@@ -6,7 +6,7 @@ function prettyDate(date, mini) { | |||
date = new Date((date || "").replace(/-/g, "/").replace(/[TZ]/g, " ").replace(/\.[0-9]*/, "")); | |||
} | |||
let diff = (((new Date()).getTime() - date.getTime()) / 1000); | |||
let diff = (((new Date(frappe.datetime.now_datetime())).getTime() - date.getTime()) / 1000); | |||
let day_diff = Math.floor(diff / 86400); | |||
if (isNaN(day_diff) || day_diff < 0) return ''; | |||
@@ -39,8 +39,7 @@ | |||
.ql-snow { | |||
.ql-editor { | |||
min-height: 400px; | |||
max-height: 600px; | |||
height: 300px; | |||
border-bottom-left-radius: var(--border-radius); | |||
border-bottom-right-radius: var(--border-radius); | |||
} | |||
@@ -30,6 +30,9 @@ def strip_exif_data(content, content_type): | |||
original_image = Image.open(io.BytesIO(content)) | |||
output = io.BytesIO() | |||
# ref: https://stackoverflow.com/a/48248432 | |||
if content_type == "image/jpeg" and original_image.mode in ("RGBA", "P"): | |||
original_image = original_image.convert("RGB") | |||
new_image = Image.new(original_image.mode, original_image.size) | |||
new_image.putdata(list(original_image.getdata())) | |||