Quellcode durchsuchen

Merge pull request #15538 from noahjacob/phone_field_control

feat: Phone Control Type
version-14
Suraj Shetty vor 3 Jahren
committed by GitHub
Ursprung
Commit
ccc82e2c4c
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden GPG-Schlüssel-ID: 4AEE18F83AFDEB23
22 geänderte Dateien mit 1135 neuen und 252 gelöschten Zeilen
  1. +47
    -0
      cypress/fixtures/doctype_with_phone.js
  2. +90
    -0
      cypress/integration/control_phone.js
  3. +4
    -0
      cypress/support/commands.js
  4. +7
    -0
      frappe/boot.py
  5. +2
    -2
      frappe/core/doctype/docfield/docfield.json
  6. +1
    -1
      frappe/custom/doctype/custom_field/custom_field.json
  7. +2
    -2
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  8. +1
    -0
      frappe/database/mariadb/database.py
  9. +1
    -0
      frappe/database/postgres/database.py
  10. +490
    -246
      frappe/geo/country_info.json
  11. +1
    -0
      frappe/model/__init__.py
  12. +4
    -0
      frappe/model/base_document.py
  13. +3
    -0
      frappe/model/meta.py
  14. +1
    -0
      frappe/public/js/frappe/form/controls/control.js
  15. +1
    -1
      frappe/public/js/frappe/form/controls/icon.js
  16. +197
    -0
      frappe/public/js/frappe/form/controls/phone.js
  17. +103
    -0
      frappe/public/js/frappe/phone_picker/phone_picker.js
  18. +6
    -0
      frappe/public/js/frappe/utils/utils.js
  19. +1
    -0
      frappe/public/scss/common/controls.scss
  20. +144
    -0
      frappe/public/scss/common/phone_picker.scss
  21. +27
    -0
      frappe/utils/__init__.py
  22. +2
    -0
      requirements.txt

+ 47
- 0
cypress/fixtures/doctype_with_phone.js Datei anzeigen

@@ -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
- 0
cypress/integration/control_phone.js Datei anzeigen

@@ -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");
});
});

+ 4
- 0
cypress/support/commands.js Datei anzeigen

@@ -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();
});


+ 7
- 0
frappe/boot.py Datei anzeigen

@@ -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})


+ 2
- 2
frappe/core/doctype/docfield/docfield.json Datei anzeigen

@@ -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": []
}
}

+ 1
- 1
frappe/custom/doctype/custom_field/custom_field.json Datei anzeigen

@@ -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
},
{


+ 2
- 2
frappe/custom/doctype/customize_form_field/customize_form_field.json Datei anzeigen

@@ -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": []
}
}

+ 1
- 0
frappe/database/mariadb/database.py Datei anzeigen

@@ -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", ""),
}


+ 1
- 0
frappe/database/postgres/database.py Datei anzeigen

@@ -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", ""),
}


+ 490
- 246
frappe/geo/country_info.json
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
frappe/model/__init__.py Datei anzeigen

@@ -36,6 +36,7 @@ data_fieldtypes = (
"Geolocation",
"Duration",
"Icon",
"Phone",
"Autocomplete",
"JSON",
)


+ 4
- 0
frappe/model/base_document.py Datei anzeigen

@@ -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")


+ 3
- 0
frappe/model/meta.py Datei anzeigen

@@ -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"})


+ 1
- 0
frappe/public/js/frappe/form/controls/control.js Datei anzeigen

@@ -39,6 +39,7 @@ import './multiselect_list';
import './rating';
import './duration';
import './icon';
import './phone';
import './json';

frappe.ui.form.make_control = function (opts) {


+ 1
- 1
frappe/public/js/frappe/form/controls/icon.js Datei anzeigen

@@ -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
- 0
frappe/public/js/frappe/form/controls/phone.js Datei anzeigen

@@ -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
- 0
frappe/public/js/frappe/phone_picker/phone_picker.js Datei anzeigen

@@ -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;

+ 6
- 0
frappe/public/js/frappe/utils/utils.js Datei anzeigen

@@ -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',


+ 1
- 0
frappe/public/scss/common/controls.scss Datei anzeigen

@@ -2,6 +2,7 @@
@import "color_picker";
@import "icon_picker";
@import "datepicker";
@import "phone_picker";

// password
.form-control[data-fieldtype="Password"] {


+ 144
- 0
frappe/public/scss/common/phone_picker.scss Datei anzeigen

@@ -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;
}
}

+ 27
- 0
frappe/utils/__init__.py Datei anzeigen

@@ -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:


+ 2
- 0
requirements.txt Datei anzeigen

@@ -72,3 +72,5 @@ zxcvbn-python~=4.4.24
tenacity~=8.0.1
cairocffi==1.2.0
WeasyPrint==52.5
phonenumbers==8.12.40


Laden…
Abbrechen
Speichern