Merge pull request #15538 from noahjacob/phone_field_control
feat: Phone Control Type
This commit is contained in:
commit
ccc82e2c4c
22 changed files with 1135 additions and 252 deletions
47
cypress/fixtures/doctype_with_phone.js
Normal file
47
cypress/fixtures/doctype_with_phone.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
export default {
|
||||
name: "Doctype With Phone",
|
||||
actions: [],
|
||||
custom: 1,
|
||||
is_submittable: 1,
|
||||
autoname: "field:title",
|
||||
creation: '2022-03-30 06:29:07.215072',
|
||||
doctype: 'DocType',
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
|
||||
{
|
||||
fieldname: 'title',
|
||||
fieldtype: 'Data',
|
||||
label: 'title',
|
||||
unique: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'phone',
|
||||
fieldtype: 'Phone',
|
||||
label: 'Phone'
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
modified: '2019-03-30 14:40:53.127615',
|
||||
modified_by: 'Administrator',
|
||||
naming_rule: "By fieldname",
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1,
|
||||
submit: 1,
|
||||
cancel: 1
|
||||
}
|
||||
],
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
90
cypress/integration/control_phone.js
Normal file
90
cypress/integration/control_phone.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import doctype_with_phone from '../fixtures/doctype_with_phone';
|
||||
|
||||
context("Control Phone", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
function get_dialog_with_phone() {
|
||||
return cy.dialog({
|
||||
title: "Phone",
|
||||
fields: [{
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Phone",
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it("should set flag and data", () => {
|
||||
get_dialog_with_phone().as("dialog");
|
||||
cy.get(".selected-phone").click();
|
||||
cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click();
|
||||
cy.get(".selected-phone").click();
|
||||
cy.get(".phone-picker .phone-wrapper[id='india']").click();
|
||||
cy.get(".selected-phone .country").should("have.text", "+91");
|
||||
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg");
|
||||
|
||||
let phone_number = "9312672712";
|
||||
cy.get(".selected-phone > img").click().first();
|
||||
cy.get_field("phone")
|
||||
.first()
|
||||
.click({multiple: true});
|
||||
cy.get(".frappe-control[data-fieldname=phone]")
|
||||
.findByRole("textbox")
|
||||
.first()
|
||||
.type(phone_number, {force: true});
|
||||
|
||||
cy.get_field("phone").first().should("have.value", phone_number);
|
||||
cy.get_field("phone").first().blur({force: true});
|
||||
cy.wait(100);
|
||||
cy.get("@dialog").then(dialog => {
|
||||
let value = dialog.get_value("phone");
|
||||
expect(value).to.equal("+91-" + phone_number);
|
||||
});
|
||||
});
|
||||
|
||||
it("case insensitive search for country and clear search", () => {
|
||||
let search_text = "india";
|
||||
cy.get(".selected-phone").click().first();
|
||||
cy.get(".phone-picker").findByRole("searchbox").click().type(search_text);
|
||||
cy.get(".phone-section .phone-wrapper:not(.hidden)").then(i => {
|
||||
cy.get(`.phone-section .phone-wrapper[id*="${search_text.toLowerCase()}"]`).then(countries => {
|
||||
expect(i.length).to.equal(countries.length);
|
||||
});
|
||||
});
|
||||
|
||||
cy.get(".phone-picker").findByRole("searchbox").clear().blur();
|
||||
cy.get(".phone-section .phone-wrapper").should("not.have.class", "hidden");
|
||||
});
|
||||
|
||||
it("existing document should render phone field with data", () => {
|
||||
cy.visit("/app/doctype");
|
||||
cy.insert_doc("DocType", doctype_with_phone, true);
|
||||
cy.clear_cache();
|
||||
|
||||
// Creating custom doctype
|
||||
cy.insert_doc("DocType", doctype_with_phone, true);
|
||||
cy.visit("/app/doctype-with-phone");
|
||||
cy.click_listview_primary_button("Add Doctype With Phone");
|
||||
|
||||
// create a record
|
||||
cy.fill_field("title", "Test Phone 1");
|
||||
cy.fill_field("phone", "+91-9823341234");
|
||||
cy.get_field("phone").should("have.value", "9823341234");
|
||||
cy.click_doc_primary_button("Save");
|
||||
cy.get_doc("Doctype With Phone", "Test Phone 1").then((doc) => {
|
||||
let value = doc.data.phone;
|
||||
expect(value).to.equal("+91-9823341234");
|
||||
});
|
||||
|
||||
// open the doc from list view
|
||||
cy.go_to_list("Doctype With Phone");
|
||||
cy.clear_cache();
|
||||
cy.click_listview_row_item(0);
|
||||
cy.title().should("eq", "Test Phone 1");
|
||||
cy.get(".selected-phone .country").should("have.text", "+91");
|
||||
cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg");
|
||||
cy.get_field("phone").should("have.value", "9823341234");
|
||||
});
|
||||
});
|
||||
|
|
@ -367,6 +367,10 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
|
|||
cy.get('.primary-action').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_doc_primary_button', (btn_name) => {
|
||||
cy.get('.primary-action').contains(btn_name).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
|
||||
cy.get('.timeline-message-box .actions .action-btn').contains(btn_name).click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, ge
|
|||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
|
||||
from frappe.desk.form.load import get_meta_bundle
|
||||
from frappe.email.inbox import get_email_accounts
|
||||
from frappe.geo.country_info import get_all
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Count
|
||||
|
|
@ -67,6 +68,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()
|
||||
get_country_codes(bootinfo)
|
||||
set_time_zone(bootinfo)
|
||||
|
||||
# ipinfo
|
||||
|
|
@ -384,6 +386,11 @@ def get_notification_settings():
|
|||
return frappe.get_cached_doc("Notification Settings", frappe.session.user)
|
||||
|
||||
|
||||
def get_country_codes(bootinfo):
|
||||
country_codes = get_all()
|
||||
bootinfo.country_codes = frappe._dict(country_codes)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_link_title_doctypes():
|
||||
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -557,4 +557,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -477,4 +477,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class MariaDBDatabase(Database):
|
|||
"Geolocation": ("longtext", ""),
|
||||
"Duration": ("decimal", "21,9"),
|
||||
"Icon": ("varchar", self.VARCHAR_LEN),
|
||||
"Phone": ("varchar", self.VARCHAR_LEN),
|
||||
"Autocomplete": ("varchar", self.VARCHAR_LEN),
|
||||
"JSON": ("json", ""),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class PostgresDatabase(Database):
|
|||
"Geolocation": ("text", ""),
|
||||
"Duration": ("decimal", "21,9"),
|
||||
"Icon": ("varchar", self.VARCHAR_LEN),
|
||||
"Phone": ("varchar", self.VARCHAR_LEN),
|
||||
"Autocomplete": ("varchar", self.VARCHAR_LEN),
|
||||
"JSON": ("json", ""),
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -36,6 +36,7 @@ data_fieldtypes = (
|
|||
"Geolocation",
|
||||
"Duration",
|
||||
"Icon",
|
||||
"Phone",
|
||||
"Autocomplete",
|
||||
"JSON",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -768,6 +768,10 @@ class BaseDocument(object):
|
|||
|
||||
def _validate_data_fields(self):
|
||||
# data_field options defined in frappe.model.data_field_options
|
||||
for phone_field in self.meta.get_phone_fields():
|
||||
phone = self.get(phone_field.fieldname)
|
||||
frappe.utils.validate_phone_number_with_country_code(phone, phone_field.fieldname)
|
||||
|
||||
for data_field in self.meta.get_data_fields():
|
||||
data = self.get(data_field.fieldname)
|
||||
data_field_options = data_field.get("options")
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ class Meta(Document):
|
|||
def get_data_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Data"})
|
||||
|
||||
def get_phone_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Phone"})
|
||||
|
||||
def get_dynamic_link_fields(self):
|
||||
if not hasattr(self, "_dynamic_link_fields"):
|
||||
self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"})
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import './multiselect_list';
|
|||
import './rating';
|
||||
import './duration';
|
||||
import './icon';
|
||||
import './phone';
|
||||
import './json';
|
||||
|
||||
frappe.ui.form.make_control = function (opts) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlDat
|
|||
get_all_icons() {
|
||||
frappe.symbols = [];
|
||||
$("#frappe-symbols > symbol[id]").each(function() {
|
||||
frappe.symbols.push(this.id.replace('icon-', ''));
|
||||
this.id.includes('icon-') && frappe.symbols.push(this.id.replace('icon-', ''));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
197
frappe/public/js/frappe/form/controls/phone.js
Normal file
197
frappe/public/js/frappe/form/controls/phone.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
|
||||
import PhonePicker from '../../phone_picker/phone_picker';
|
||||
|
||||
frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlData {
|
||||
|
||||
make_input() {
|
||||
super.make_input();
|
||||
this.setup_country_code_picker();
|
||||
this.input_events();
|
||||
}
|
||||
|
||||
input_events() {
|
||||
this.$input.keydown((e) => {
|
||||
const key_code = e.keyCode;
|
||||
if ([frappe.ui.keyCode.BACKSPACE].includes(key_code)) {
|
||||
if (this.$input.val().length == 0) {
|
||||
this.country_code_picker.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Replaces code when selected and removes previously selected.
|
||||
this.country_code_picker.on_change = (country) => {
|
||||
if (!country) {
|
||||
return this.reset_inputx();
|
||||
}
|
||||
const country_code = frappe.boot.country_codes[country].code;
|
||||
const country_isd = frappe.boot.country_codes[country].isd;
|
||||
this.set_flag(country_code);
|
||||
this.$icon = this.selected_icon.find('svg');
|
||||
this.$flag = this.selected_icon.find('img');
|
||||
|
||||
if (!this.$icon.hasClass('hide')) {
|
||||
this.$icon.toggleClass('hide');
|
||||
}
|
||||
if (!this.$flag.length) {
|
||||
this.selected_icon.prepend(this.get_country_flag(country));
|
||||
}
|
||||
if (!this.$isd.length) {
|
||||
this.selected_icon.append($(`<span class= "country"> ${country_isd}</span>`));
|
||||
} else {
|
||||
this.$isd.text(country_isd);
|
||||
}
|
||||
if (this.$input.val()) {
|
||||
this.set_value(this.get_country(country) +'-'+ this.$input.val());
|
||||
}
|
||||
this.update_padding();
|
||||
// hide popover and focus input
|
||||
this.$wrapper.popover('hide');
|
||||
this.$input.focus();
|
||||
};
|
||||
|
||||
this.$wrapper.find('.selected-phone').on('click', (e) => {
|
||||
this.$wrapper.popover('toggle');
|
||||
e.stopPropagation();
|
||||
|
||||
$('body').on('click.phone-popover', (ev) => {
|
||||
if (!$(ev.target).parents().is('.popover')) {
|
||||
this.$wrapper.popover('hide');
|
||||
}
|
||||
});
|
||||
$(window).on('hashchange.phone-popover', () => {
|
||||
this.$wrapper.popover('hide');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup_country_code_picker() {
|
||||
let picker_wrapper = $('<div>');
|
||||
this.country_code_picker = new PhonePicker({
|
||||
parent: picker_wrapper,
|
||||
countries: frappe.boot.country_codes
|
||||
});
|
||||
|
||||
this.$wrapper.popover({
|
||||
trigger: 'manual',
|
||||
offset: `${-this.$wrapper.width() / 4.5}, 5`,
|
||||
boundary: 'viewport',
|
||||
placement: 'bottom',
|
||||
template: `
|
||||
<div class="popover phone-picker-popover">
|
||||
<div class="picker-arrow arrow"></div>
|
||||
<div class="popover-body popover-content"></div>
|
||||
</div>
|
||||
`,
|
||||
content: () => picker_wrapper,
|
||||
html: true
|
||||
}).on('show.bs.popover', () => {
|
||||
setTimeout(() => {
|
||||
this.country_code_picker.refresh();
|
||||
this.country_code_picker.search_input.focus();
|
||||
}, 10);
|
||||
}).on('hidden.bs.popover', () => {
|
||||
$('body').off('click.phone-popover');
|
||||
$(window).off('hashchange.phone-popover');
|
||||
});
|
||||
|
||||
// Default icon when nothing is selected.
|
||||
this.selected_icon = this.$wrapper.find('.selected-phone');
|
||||
let input_value = this.get_input_value();
|
||||
if (!this.selected_icon.length) {
|
||||
this.selected_icon = $(`<div class="selected-phone">${frappe.utils.icon("down", "sm")}</div>`);
|
||||
this.selected_icon.insertAfter(this.$input);
|
||||
this.selected_icon.append($(`<span class= "country"></span>`));
|
||||
this.$isd = this.selected_icon.find('.country');
|
||||
if (input_value && input_value.split("-").length == 2) {
|
||||
this.$isd.text(this.value.split("-")[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
super.refresh();
|
||||
// Previously opened doc values showing up on a new doc
|
||||
|
||||
if (this.frm && this.frm.doc.__islocal && !this.get_value()) {
|
||||
this.reset_input();
|
||||
}
|
||||
}
|
||||
|
||||
reset_input() {
|
||||
this.$input.val("");
|
||||
this.$wrapper.find('.country').text("");
|
||||
if (this.selected_icon.find('svg').hasClass('hide')) {
|
||||
this.selected_icon.find('svg').toggleClass('hide');
|
||||
this.selected_icon.find('img').addClass('hide');
|
||||
}
|
||||
this.$input.css("padding-left", 30);
|
||||
}
|
||||
|
||||
set_formatted_input(value) {
|
||||
if (value && value.includes('-') && value.split('-').length == 2) {
|
||||
let isd = this.value.split("-")[0];
|
||||
this.get_country_code_and_change_flag(isd);
|
||||
this.country_code_picker.set_country(isd);
|
||||
this.country_code_picker.refresh();
|
||||
if (this.country_code_picker.country && this.country_code_picker.country !== this.$isd.text()) {
|
||||
this.$isd.length && this.$isd.text(isd);
|
||||
}
|
||||
this.update_padding();
|
||||
this.$input.val(value.split('-').pop());
|
||||
|
||||
} else if (this.$isd.text().trim() && this.value) {
|
||||
let code_number = this.$isd.text() + '-' + value;
|
||||
this.set_value(code_number);
|
||||
}
|
||||
}
|
||||
|
||||
get_value() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set_flag(country_code) {
|
||||
this.selected_icon.find('img').attr('src', `https://flagcdn.com/${country_code}.svg`);
|
||||
this.$icon = this.selected_icon.find('img');
|
||||
this.$icon.hasClass('hide') && this.$icon.toggleClass('hide');
|
||||
}
|
||||
|
||||
// country_code for India is 'in'
|
||||
get_country_code_and_change_flag(isd) {
|
||||
let country_data = frappe.boot.country_codes;
|
||||
let flag = this.selected_icon.find('img');
|
||||
for (const country in country_data) {
|
||||
if (Object.values(country_data[country]).includes(isd)) {
|
||||
let code = country_data[country].code;
|
||||
flag = this.selected_icon.find('img');
|
||||
if (!flag.length) {
|
||||
this.selected_icon.prepend(this.get_country_flag(country));
|
||||
this.selected_icon.find('svg').addClass('hide');
|
||||
} else {
|
||||
this.set_flag(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_country(country) {
|
||||
const country_codes = frappe.boot.country_codes;
|
||||
return country_codes[country].isd;
|
||||
}
|
||||
|
||||
get_country_flag(country) {
|
||||
const country_codes = frappe.boot.country_codes;
|
||||
let code = country_codes[country].code;
|
||||
return frappe.utils.flag(code);
|
||||
}
|
||||
|
||||
update_padding() {
|
||||
let len = this.$isd.text().length;
|
||||
let diff = len - 2;
|
||||
if (len > 2) {
|
||||
this.$input.css("padding-left", 60 + (diff * 7));
|
||||
} else {
|
||||
this.$input.css("padding-left", 60);
|
||||
}
|
||||
}
|
||||
};
|
||||
103
frappe/public/js/frappe/phone_picker/phone_picker.js
Normal file
103
frappe/public/js/frappe/phone_picker/phone_picker.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
class PhonePicker {
|
||||
constructor(opts) {
|
||||
this.parent = opts.parent;
|
||||
this.width = opts.width;
|
||||
this.height = opts.height;
|
||||
this.country = opts.country;
|
||||
opts.country && this.set_country(opts.country);
|
||||
this.countries = opts.countries;
|
||||
this.setup_picker();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.update_icon_selected(true);
|
||||
}
|
||||
|
||||
setup_picker() {
|
||||
this.phone_picker_wrapper = $(`
|
||||
<div class="phone-picker">
|
||||
<div class="search-phones">
|
||||
<input type="search" placeholder="${__('Search for countries...')}" class="form-control">
|
||||
<span class="search-phone">${frappe.utils.icon('search', "sm")}</span>
|
||||
</div>
|
||||
<div class="phone-section">
|
||||
<div class="phones"></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
this.parent.append(this.phone_picker_wrapper);
|
||||
this.phone_wrapper = this.phone_picker_wrapper.find('.phones');
|
||||
this.search_input = this.phone_picker_wrapper.find('.search-phones > input');
|
||||
this.refresh();
|
||||
this.setup_countries();
|
||||
}
|
||||
|
||||
setup_countries() {
|
||||
Object.entries(this.countries).forEach(([country, info]) => {
|
||||
if (!info.isd) {
|
||||
return;
|
||||
}
|
||||
let $country = $(`
|
||||
<div id="${country.toLowerCase()}" class="phone-wrapper">
|
||||
${frappe.utils.flag(info.code)}
|
||||
<span class="country">${country} (${info.isd})</span>
|
||||
</div>
|
||||
`);
|
||||
this.phone_wrapper.append($country);
|
||||
const set_values = () => {
|
||||
this.set_country(country);
|
||||
this.update_icon_selected();
|
||||
};
|
||||
$country.on('click', () => {
|
||||
set_values();
|
||||
});
|
||||
$country.hover(() => {
|
||||
$country.toggleClass("bg-gray-100");
|
||||
});
|
||||
this.search_input.keydown((e) => {
|
||||
const key_code = e.keyCode;
|
||||
if ([13].includes(key_code)) {
|
||||
e.preventDefault();
|
||||
set_values();
|
||||
}
|
||||
});
|
||||
this.search_input.keyup((e) => {
|
||||
e.preventDefault();
|
||||
this.filter_icons();
|
||||
});
|
||||
|
||||
this.search_input.on('search', () => {
|
||||
this.filter_icons();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
filter_icons() {
|
||||
let value = this.search_input.val();
|
||||
if (!value) {
|
||||
this.phone_wrapper.find(".phone-wrapper").removeClass('hidden');
|
||||
} else {
|
||||
this.phone_wrapper.find(".phone-wrapper").addClass('hidden');
|
||||
this.phone_wrapper.find(`.phone-wrapper[id*='${value.toLowerCase()}']`).removeClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
update_icon_selected(silent) {
|
||||
!silent && this.on_change && this.on_change(this.get_country());
|
||||
}
|
||||
|
||||
set_country(country) {
|
||||
this.country = country || '';
|
||||
}
|
||||
|
||||
get_country() {
|
||||
return this.country;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.set_country();
|
||||
this.update_icon_selected();
|
||||
}
|
||||
}
|
||||
|
||||
export default PhonePicker;
|
||||
|
|
@ -1192,6 +1192,12 @@ Object.assign(frappe.utils, {
|
|||
</svg>`;
|
||||
},
|
||||
|
||||
flag(country_code) {
|
||||
return `<img
|
||||
src="https://flagcdn.com/${country_code}.svg"
|
||||
width="20" height="15">`;
|
||||
},
|
||||
|
||||
make_chart(wrapper, custom_options={}) {
|
||||
let chart_args = {
|
||||
type: 'bar',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
@import "color_picker";
|
||||
@import "icon_picker";
|
||||
@import "datepicker";
|
||||
@import "phone_picker";
|
||||
|
||||
// password
|
||||
.form-control[data-fieldtype="Password"] {
|
||||
|
|
|
|||
144
frappe/public/scss/common/phone_picker.scss
Normal file
144
frappe/public/scss/common/phone_picker.scss
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
.phone-picker {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
--phone-picker-width: 290px;
|
||||
width: var(--phone-picker-width);
|
||||
.phones {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
max-height: 210px;
|
||||
cursor: pointer;
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.phone-wrapper {
|
||||
display: flex;
|
||||
width: 290px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
img {
|
||||
height: 15px;
|
||||
}
|
||||
.country {
|
||||
display: flex;
|
||||
margin-left: 0.6rem;
|
||||
flex-grow: 1;
|
||||
width: 290px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-phones {
|
||||
position: relative;
|
||||
|
||||
input[type='search'] {
|
||||
height: inherit;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.search-phone {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.phone-picker-popover {
|
||||
max-width: 325px;
|
||||
left: 29px !important;
|
||||
.picker-arrow {
|
||||
left: 15px !important;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
max-width: 325px;
|
||||
left: 48px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.frappe-control[data-fieldtype='Phone']
|
||||
{
|
||||
input {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.selected-phone {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
top: calc(50% + 2px);
|
||||
left: 8px;
|
||||
content: ' ';
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
|
||||
.country {
|
||||
display: flex;
|
||||
margin-left: 0.6rem;
|
||||
align-items: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
}
|
||||
.like-disabled-input {
|
||||
.phone-value {
|
||||
padding-left: 25px;
|
||||
}
|
||||
.selected-phone {
|
||||
top: 20%;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.frappe-control[data-fieldtype='Phone']
|
||||
{
|
||||
.selected-phone {
|
||||
top: calc(50% - 0.5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-row.row {
|
||||
.selected-phone {
|
||||
top: calc(50% - 10.1px);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(244,245,246,var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dt-cell__content {
|
||||
.selected-phone {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-cell__edit, .filter-field {
|
||||
.selected-phone {
|
||||
top: 5.5px !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,33 @@ def extract_email_id(email):
|
|||
return email_id
|
||||
|
||||
|
||||
def validate_phone_number_with_country_code(phone_number, fieldname):
|
||||
from phonenumbers import NumberParseException, is_valid_number, parse
|
||||
|
||||
from frappe import _
|
||||
|
||||
if not phone_number:
|
||||
return
|
||||
|
||||
valid_number = False
|
||||
error_message = _("Phone Number {0} set in field {1} is not valid.")
|
||||
error_title = _("Invalid Phone Number")
|
||||
try:
|
||||
if valid_number := is_valid_number(parse(phone_number)):
|
||||
return True
|
||||
except NumberParseException as e:
|
||||
if e.error_type == NumberParseException.INVALID_COUNTRY_CODE:
|
||||
error_message = _("Please select a country code for field {1}.")
|
||||
error_title = _("Country Code Required")
|
||||
finally:
|
||||
if not valid_number:
|
||||
frappe.throw(
|
||||
error_message.format(frappe.bold(phone_number), frappe.bold(fieldname)),
|
||||
title=error_title,
|
||||
exc=frappe.InvalidPhoneNumberError,
|
||||
)
|
||||
|
||||
|
||||
def validate_phone_number(phone_number, throw=False):
|
||||
"""Returns True if valid phone number"""
|
||||
if not phone_number:
|
||||
|
|
|
|||
|
|
@ -72,3 +72,5 @@ zxcvbn-python~=4.4.24
|
|||
tenacity~=8.0.1
|
||||
cairocffi==1.2.0
|
||||
WeasyPrint==52.5
|
||||
phonenumbers==8.12.40
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue