@@ -14,8 +14,10 @@ coverage: | |||||
patch: | patch: | ||||
default: false | default: false | ||||
server: | server: | ||||
target: auto | |||||
threshold: 85% | |||||
target: 85% | |||||
threshold: 0% | |||||
only_pulls: true | |||||
if_ci_failed: ignore | |||||
flags: | flags: | ||||
- server | - server | ||||
@@ -92,15 +92,15 @@ context('Control Date, Time and DateTime', () => { | |||||
date_format: 'dd.mm.yyyy', | date_format: 'dd.mm.yyyy', | ||||
time_format: 'HH:mm:ss', | time_format: 'HH:mm:ss', | ||||
value: ' 02.12.2019 11:00:12', | 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', | date_format: 'mm-dd-yyyy', | ||||
time_format: 'HH:mm', | time_format: 'HH:mm', | ||||
value: ' 12-02-2019 11:00:00', | 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 => { | 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.go_to_list('ToDo'); | ||||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true }); | 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'); | 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.model.base_document import get_controller | ||||
from frappe.social.doctype.post.post import frequently_visited_links | 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.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo | ||||
from frappe.utils import get_time_zone | |||||
def get_bootinfo(): | def get_bootinfo(): | ||||
"""build and return boot info""" | """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.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) | ||||
bootinfo.navbar_settings = get_navbar_settings() | bootinfo.navbar_settings = get_navbar_settings() | ||||
bootinfo.notification_settings = get_notification_settings() | bootinfo.notification_settings = get_notification_settings() | ||||
set_time_zone(bootinfo) | |||||
# ipinfo | # ipinfo | ||||
if frappe.session.data.get('ipinfo'): | if frappe.session.data.get('ipinfo'): | ||||
@@ -220,8 +222,8 @@ def load_translations(bootinfo): | |||||
bootinfo["__messages"] = messages | bootinfo["__messages"] = messages | ||||
def get_user_info(): | 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)) | filters=dict(enabled=1)) | ||||
user_info_map = {d.name: d for d in user_info} | user_info_map = {d.name: d for d in user_info} | ||||
@@ -324,3 +326,9 @@ def get_desk_settings(): | |||||
def get_notification_settings(): | def get_notification_settings(): | ||||
return frappe.get_cached_doc('Notification Settings', frappe.session.user) | 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); | 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", | "fieldname": "time_zone", | ||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
"label": "Time Zone", | "label": "Time Zone", | ||||
"read_only": 1, | |||||
"reqd": 1 | "reqd": 1 | ||||
}, | }, | ||||
{ | { | ||||
@@ -77,7 +77,12 @@ frappe.ui.form.on('User', { | |||||
} | } | ||||
}, | }, | ||||
refresh: function(frm) { | 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) | if (in_list(['System User', 'Website User'], frm.doc.user_type) | ||||
&& !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) { | && !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) { | ||||
frm.reload_doc(); | 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 | import frappe.permissions | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime, | 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 import throw, msgprint, _ | ||||
from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit | from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit | ||||
from frappe.desk.notifications import clear_notifications | from frappe.desk.notifications import clear_notifications | ||||
@@ -74,6 +74,7 @@ class User(Document): | |||||
self.validate_roles() | self.validate_roles() | ||||
self.validate_allowed_modules() | self.validate_allowed_modules() | ||||
self.validate_user_image() | self.validate_user_image() | ||||
self.set_time_zone() | |||||
if self.language == "Loading...": | if self.language == "Loading...": | ||||
self.language = None | self.language = None | ||||
@@ -227,11 +228,11 @@ class User(Document): | |||||
def validate_share(self, docshare): | def validate_share(self, docshare): | ||||
pass | pass | ||||
# if docshare.user == self.name: | # 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): | def send_password_notification(self, new_password): | ||||
try: | try: | ||||
@@ -596,6 +597,10 @@ class User(Document): | |||||
return user | return user | ||||
def set_time_zone(self): | |||||
if not self.time_zone: | |||||
self.time_zone = get_time_zone() | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_timezones(): | def get_timezones(): | ||||
import pytz | import pytz | ||||
@@ -133,7 +133,7 @@ class SubmittableDocumentTree: | |||||
"""Returns list of submittable doctypes. | """Returns list of submittable doctypes. | ||||
""" | """ | ||||
if not self._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 | 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.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_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_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', 'doctype', force=True) #2017-09-22 | ||||
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 | execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 | ||||
frappe.patches.v11_0.drop_column_apply_user_permissions | 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.$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; | const me = this; | ||||
this.$scan_btn.on('click', 'a', () => { | 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() { | 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 | let date_format = sysdefaults && sysdefaults.date_format | ||||
? sysdefaults.date_format : 'yyyy-mm-dd'; | ? sysdefaults.date_format : 'yyyy-mm-dd'; | ||||
let now_date = new Date(); | |||||
this.today_text = __("Today"); | this.today_text = __("Today"); | ||||
this.date_format = frappe.defaultDateFormat; | this.date_format = frappe.defaultDateFormat; | ||||
this.datepicker_options = { | this.datepicker_options = { | ||||
@@ -62,7 +60,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||||
autoClose: true, | autoClose: true, | ||||
todayButton: true, | todayButton: true, | ||||
dateFormat: date_format, | dateFormat: date_format, | ||||
startDate: now_date, | |||||
startDate: this.get_start_date(), | |||||
keyboardNav: false, | keyboardNav: false, | ||||
onSelect: () => { | onSelect: () => { | ||||
this.$input.trigger('change'); | this.$input.trigger('change'); | ||||
@@ -77,6 +75,11 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||||
...(this.get_df_options()) | ...(this.get_df_options()) | ||||
}; | }; | ||||
} | } | ||||
get_start_date() { | |||||
return new Date(this.get_now_date()); | |||||
} | |||||
set_datepicker() { | set_datepicker() { | ||||
this.$input.datepicker(this.datepicker_options); | this.$input.datepicker(this.datepicker_options); | ||||
this.datepicker = this.$input.data('datepicker'); | 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); | this.datepicker.update('position', position); | ||||
} | } | ||||
get_now_date() { | 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() { | set_t_for_today() { | ||||
var me = this; | var me = this; | ||||
@@ -1,4 +1,22 @@ | |||||
frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.ControlDate { | 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() { | set_date_options() { | ||||
super.set_date_options(); | super.set_date_options(); | ||||
this.today_text = __("Now"); | this.today_text = __("Now"); | ||||
@@ -14,10 +32,31 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co | |||||
get_now_date() { | get_now_date() { | ||||
return frappe.datetime.now_datetime(true); | 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() { | 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) { | if (!description) { | ||||
this.df.description = time_zone; | this.df.description = time_zone; | ||||
} else if (!description.includes(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(); | super.set_description(); | ||||
} | } | ||||
get_user_time_zone() { | |||||
return frappe.boot.time_zone ? frappe.boot.time_zone.user : frappe.sys_defaults.time_zone; | |||||
} | |||||
set_datepicker() { | set_datepicker() { | ||||
super.set_datepicker(); | super.set_datepicker(); | ||||
if (this.datepicker.opts.timeFormat.indexOf('s') == -1) { | 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, | docname: value, | ||||
fields: columns_to_fetch, | fields: columns_to_fetch, | ||||
}).then((response) => { | }).then((response) => { | ||||
if (!response || !response.name) return ""; | |||||
if (!response || !response.name) return null; | |||||
if (!docname || !columns_to_fetch.length) return response.name; | if (!docname || !columns_to_fetch.length) return response.name; | ||||
for (const [target_field, source_field] of Object.entries(fetch_map)) { | 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() { | set_description() { | ||||
const { description } = this.df; | const { description } = this.df; | ||||
const { time_zone } = frappe.sys_defaults; | const { time_zone } = frappe.sys_defaults; | ||||
if (!frappe.datetime.is_timezone_same()) { | |||||
if (!frappe.datetime.is_system_time_zone()) { | |||||
if (!description) { | if (!description) { | ||||
this.df.description = time_zone; | this.df.description = time_zone; | ||||
} else if (!description.includes(time_zone)) { | } else if (!description.includes(time_zone)) { | ||||
@@ -116,7 +116,7 @@ function get_version_timeline_content(version_doc, frm) { | |||||
frm.perm); | frm.perm); | ||||
if (field_display_status === 'Read' || field_display_status === 'Write') { | 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 | // navigate records keyboard shortcuts | ||||
this.add_nav_keyboard_shortcuts(); | |||||
this.add_form_keyboard_shortcuts(); | |||||
// 2 column layout | // 2 column layout | ||||
this.setup_std_layout(); | this.setup_std_layout(); | ||||
@@ -116,7 +116,8 @@ frappe.ui.form.Form = class FrappeForm { | |||||
this.setup_done = true; | this.setup_done = true; | ||||
} | } | ||||
add_nav_keyboard_shortcuts() { | |||||
add_form_keyboard_shortcuts() { | |||||
// Navigate to next record | |||||
frappe.ui.keys.add_shortcut({ | frappe.ui.keys.add_shortcut({ | ||||
shortcut: 'shift+ctrl+>', | shortcut: 'shift+ctrl+>', | ||||
action: () => this.navigate_records(0), | action: () => this.navigate_records(0), | ||||
@@ -126,6 +127,7 @@ frappe.ui.form.Form = class FrappeForm { | |||||
condition: () => !this.is_new() | condition: () => !this.is_new() | ||||
}); | }); | ||||
// Navigate to previous record | |||||
frappe.ui.keys.add_shortcut({ | frappe.ui.keys.add_shortcut({ | ||||
shortcut: 'shift+ctrl+<', | shortcut: 'shift+ctrl+<', | ||||
action: () => this.navigate_records(1), | action: () => this.navigate_records(1), | ||||
@@ -134,6 +136,56 @@ frappe.ui.form.Form = class FrappeForm { | |||||
ignore_inputs: true, | ignore_inputs: true, | ||||
condition: () => !this.is_new() | 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() { | setup_std_layout() { | ||||
@@ -167,12 +167,8 @@ frappe.form.formatters = { | |||||
}, | }, | ||||
Datetime: function(value) { | Datetime: function(value) { | ||||
if(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 { | } else { | ||||
return ""; | 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 (this.is_editable()) { | ||||
if (go_to_last_page) { | if (go_to_last_page) { | ||||
this.grid_pagination.go_to_last_page_to_add_row(); | 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) { | if (this.frm) { | ||||
var d = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname, idx); | var d = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname, idx); | ||||
if (copy_doc) { | if (copy_doc) { | ||||
@@ -684,7 +687,7 @@ export default class Grid { | |||||
} | } | ||||
set_focus_on_row(idx) { | set_focus_on_row(idx) { | ||||
if (!idx) { | |||||
if (!idx && idx !== 0) { | |||||
idx = this.grid_rows.length - 1; | idx = this.grid_rows.length - 1; | ||||
} | } | ||||
@@ -723,6 +723,7 @@ export default class GridRow { | |||||
set_arrow_keys(field) { | set_arrow_keys(field) { | ||||
var me = this; | var me = this; | ||||
let ignore_fieldtypes = ['Text', 'Small Text', 'Code', 'Text Editor', 'HTML Editor']; | |||||
if (field.$input) { | if (field.$input) { | ||||
field.$input.on('keydown', function(e) { | field.$input.on('keydown', function(e) { | ||||
var { TAB, UP: UP_ARROW, DOWN: DOWN_ARROW } = frappe.ui.keyCode; | 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 fieldname = $(this).attr('data-fieldname'); | ||||
var fieldtype = $(this).attr('data-fieldtype'); | 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) { | 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; | return false; | ||||
} | } | ||||
if (field.autocomplete_open) { | 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() { | get_open_form() { | ||||
return frappe.ui.form.get_open_grid_form(); | return frappe.ui.form.get_open_grid_form(); | ||||
} | } | ||||
@@ -392,9 +392,9 @@ frappe.views.BaseList = class BaseList { | |||||
this.start = 0; | this.start = 0; | ||||
this.page_length = $this.data().value; | this.page_length = $this.data().value; | ||||
this.refresh(); | |||||
} else if ($this.is(".btn-more")) { | } else if ($this.is(".btn-more")) { | ||||
this.start = this.start + this.page_length; | this.start = this.start + this.page_length; | ||||
this.page_length = 20; | |||||
} | } | ||||
this.refresh(); | this.refresh(); | ||||
}); | }); | ||||
@@ -475,6 +475,7 @@ frappe.views.BaseList = class BaseList { | |||||
this.render(); | this.render(); | ||||
this.after_render(); | this.after_render(); | ||||
this.freeze(false); | this.freeze(false); | ||||
this.reset_defaults(); | |||||
if (this.settings.refresh) { | if (this.settings.refresh) { | ||||
this.settings.refresh(this); | this.settings.refresh(this); | ||||
} | } | ||||
@@ -492,6 +493,13 @@ frappe.views.BaseList = class BaseList { | |||||
} else { | } else { | ||||
this.data = this.data.concat(data); | 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() { | freeze() { | ||||
@@ -1317,7 +1317,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
return; | return; | ||||
} | } | ||||
frappe.realtime.on("list_update", (data) => { | frappe.realtime.on("list_update", (data) => { | ||||
if (this.filter_area.is_being_edited()) { | |||||
if (this.avoid_realtime_update()) { | |||||
return; | 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() { | set_rows_as_checked() { | ||||
$.each(this.$checks, (i, el) => { | $.each(this.$checks, (i, el) => { | ||||
let docname = $(el).attr("data-name"); | 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)); | 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) { | save_view_user_settings(obj) { | ||||
return frappe.model.user_settings.save( | return frappe.model.user_settings.save( | ||||
this.doctype, | this.doctype, | ||||
@@ -1655,11 +1673,17 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
const bulk_assignment = () => { | const bulk_assignment = () => { | ||||
return { | return { | ||||
label: __("Assign To"), | label: __("Assign To"), | ||||
action: () => | |||||
action: () => { | |||||
this.disable_list_update = true; | |||||
bulk_operations.assign( | bulk_operations.assign( | ||||
this.get_checked_items(true), | this.get_checked_items(true), | ||||
this.refresh | |||||
), | |||||
() => { | |||||
this.disable_list_update = false; | |||||
this.clear_checked_items(); | |||||
this.refresh(); | |||||
} | |||||
); | |||||
}, | |||||
standard: true, | standard: true, | ||||
}; | }; | ||||
}; | }; | ||||
@@ -1667,11 +1691,17 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
const bulk_assignment_rule = () => { | const bulk_assignment_rule = () => { | ||||
return { | return { | ||||
label: __("Apply Assignment Rule"), | label: __("Apply Assignment Rule"), | ||||
action: () => | |||||
action: () => { | |||||
this.disable_list_update = true; | |||||
bulk_operations.apply_assignment_rule( | bulk_operations.apply_assignment_rule( | ||||
this.get_checked_items(true), | this.get_checked_items(true), | ||||
this.refresh | |||||
), | |||||
() => { | |||||
this.disable_list_update = false; | |||||
this.clear_checked_items(); | |||||
this.refresh(); | |||||
} | |||||
); | |||||
}, | |||||
standard: true, | standard: true, | ||||
}; | }; | ||||
}; | }; | ||||
@@ -1679,11 +1709,17 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
const bulk_add_tags = () => { | const bulk_add_tags = () => { | ||||
return { | return { | ||||
label: __("Add Tags"), | label: __("Add Tags"), | ||||
action: () => | |||||
action: () => { | |||||
this.disable_list_update = true; | |||||
bulk_operations.add_tags( | bulk_operations.add_tags( | ||||
this.get_checked_items(true), | this.get_checked_items(true), | ||||
this.refresh | |||||
), | |||||
() => { | |||||
this.disable_list_update = false; | |||||
this.clear_checked_items(); | |||||
this.refresh(); | |||||
} | |||||
); | |||||
}, | |||||
standard: true, | standard: true, | ||||
}; | }; | ||||
}; | }; | ||||
@@ -1705,7 +1741,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
); | ); | ||||
frappe.confirm( | frappe.confirm( | ||||
__("Delete {0} items permanently?", [docnames.length]), | __("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, | standard: true, | ||||
@@ -1720,13 +1763,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
if (docnames.length > 0) { | if (docnames.length > 0) { | ||||
frappe.confirm( | frappe.confirm( | ||||
__("Cancel {0} documents?", [docnames.length]), | __("Cancel {0} documents?", [docnames.length]), | ||||
() => | |||||
() => { | |||||
this.disable_list_update = true; | |||||
bulk_operations.submit_or_cancel( | bulk_operations.submit_or_cancel( | ||||
docnames, | docnames, | ||||
"cancel", | "cancel", | ||||
this.refresh | |||||
) | |||||
); | |||||
() => { | |||||
this.disable_list_update = false; | |||||
this.clear_checked_items(); | |||||
this.refresh(); | |||||
} | |||||
); | |||||
}); | |||||
} | } | ||||
}, | }, | ||||
standard: true, | standard: true, | ||||
@@ -1741,12 +1789,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
if (docnames.length > 0) { | if (docnames.length > 0) { | ||||
frappe.confirm( | frappe.confirm( | ||||
__("Submit {0} documents?", [docnames.length]), | __("Submit {0} documents?", [docnames.length]), | ||||
() => | |||||
() => { | |||||
this.disable_list_update = true; | |||||
bulk_operations.submit_or_cancel( | bulk_operations.submit_or_cancel( | ||||
docnames, | docnames, | ||||
"submit", | "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( | bulk_operations.edit( | ||||
docnames, | |||||
this.get_checked_items(true), | |||||
field_mappings, | field_mappings, | ||||
this.refresh | |||||
() => { | |||||
this.disable_list_update = false; | |||||
this.clear_checked_items(); | |||||
this.refresh(); | |||||
} | |||||
); | ); | ||||
}, | }, | ||||
standard: true, | standard: true, | ||||
@@ -15,7 +15,7 @@ Object.assign(window, { | |||||
}); | }); | ||||
$.extend(frappe.perm, { | $.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"], | "report", "import", "export", "print", "email", "share", "set_user_permissions"], | ||||
doctype_perm: {}, | doctype_perm: {}, | ||||
@@ -72,6 +72,9 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => { | |||||
let current_page_shortcuts = standard_shortcuts.filter( | let current_page_shortcuts = standard_shortcuts.filter( | ||||
shortcut => shortcut.page && shortcut.page === window.cur_page.page.page); | 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) { | function generate_shortcuts_html(shortcuts, heading) { | ||||
if (!shortcuts.length) { | if (!shortcuts.length) { | ||||
return ''; | return ''; | ||||
@@ -100,6 +103,7 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => { | |||||
let global_shortcuts_html = generate_shortcuts_html(global_shortcuts, __('Global Shortcuts')); | 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 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({ | let dialog = new frappe.ui.Dialog({ | ||||
title: __('Keyboard Shortcuts'), | title: __('Keyboard Shortcuts'), | ||||
@@ -110,6 +114,7 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => { | |||||
dialog.$body.append(global_shortcuts_html); | dialog.$body.append(global_shortcuts_html); | ||||
dialog.$body.append(current_page_shortcuts_html); | dialog.$body.append(current_page_shortcuts_html); | ||||
dialog.$body.append(grid_shortcuts_html); | |||||
dialog.$body.append(` | dialog.$body.append(` | ||||
<div class="text-muted"> | <div class="text-muted"> | ||||
${__('Press Alt Key to trigger additional shortcuts in Menu and Sidebar')} | ${__('Press Alt Key to trigger additional shortcuts in Menu and Sidebar')} | ||||
@@ -13,33 +13,48 @@ frappe.provide("frappe.datetime"); | |||||
$.extend(frappe.datetime, { | $.extend(frappe.datetime, { | ||||
convert_to_user_tz: function(date, format) { | convert_to_user_tz: function(date, format) { | ||||
// format defaults to true | // 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 { | } 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) { | convert_to_system_tz: function(date, format) { | ||||
// format defaults to true | // 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 { | } 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) { | str_to_obj: function(d) { | ||||
@@ -98,11 +113,11 @@ $.extend(frappe.datetime, { | |||||
return moment().endOf("quarter").format(); | return moment().endOf("quarter").format(); | ||||
}, | }, | ||||
year_start: function(){ | |||||
year_start: function() { | |||||
return moment().startOf("year").format(); | return moment().startOf("year").format(); | ||||
}, | }, | ||||
year_end: function(){ | |||||
year_end: function() { | |||||
return moment().endOf("year").format(); | return moment().endOf("year").format(); | ||||
}, | }, | ||||
@@ -119,19 +134,25 @@ $.extend(frappe.datetime, { | |||||
}, | }, | ||||
str_to_user: function(val, only_time = false) { | 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 { | } 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) { | _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_obj = new Date(); | ||||
const date_array = moment.toArray(); | |||||
const date_array = moment_obj.toArray(); | |||||
date_obj.setFullYear(date_array[0]); | date_obj.setFullYear(date_array[0]); | ||||
date_obj.setMonth(date_array[1]); | date_obj.setMonth(date_array[1]); | ||||
date_obj.setDate(date_array[2]); | 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]*/, "")); | 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); | let day_diff = Math.floor(diff / 86400); | ||||
if (isNaN(day_diff) || day_diff < 0) return ''; | if (isNaN(day_diff) || day_diff < 0) return ''; | ||||
@@ -39,8 +39,7 @@ | |||||
.ql-snow { | .ql-snow { | ||||
.ql-editor { | .ql-editor { | ||||
min-height: 400px; | |||||
max-height: 600px; | |||||
height: 300px; | |||||
border-bottom-left-radius: var(--border-radius); | border-bottom-left-radius: var(--border-radius); | ||||
border-bottom-right-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)) | original_image = Image.open(io.BytesIO(content)) | ||||
output = io.BytesIO() | 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 = Image.new(original_image.mode, original_image.size) | ||||
new_image.putdata(list(original_image.getdata())) | new_image.putdata(list(original_image.getdata())) | ||||