feat: Phone Field Control Type

This commit is contained in:
Noah Jacob 2021-12-30 14:50:33 +05:30
parent 95dc3875bb
commit d08a332a85
15 changed files with 960 additions and 549 deletions

View file

@ -58,6 +58,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)
# ipinfo
if frappe.session.data.get('ipinfo'):
@ -324,3 +325,9 @@ def get_desk_settings():
def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
def get_country_codes(bootinfo):
country_codes = {
"United States": {"isd":"+1","code":"us" },
"India": {"isd":"+91","code":"in" }}
bootinfo.country_codes = frappe._dict(country_codes)

File diff suppressed because it is too large Load diff

View file

@ -120,7 +120,7 @@
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\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\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"options": "Attach\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\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
@ -455,4 +455,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View file

@ -82,7 +82,7 @@
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\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\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break",
"options": "Attach\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\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break",
"reqd": 1,
"search_index": 1
},
@ -436,4 +436,4 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
}
}

View file

@ -52,7 +52,8 @@ class MariaDBDatabase(Database):
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''),
'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
'Icon': ('varchar', self.VARCHAR_LEN),
'Phone': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):

View file

@ -62,7 +62,8 @@ class PostgresDatabase(Database):
'Barcode': ('text', ''),
'Geolocation': ('text', ''),
'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
'Icon': ('varchar', self.VARCHAR_LEN),
'Phone': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):

View file

@ -35,7 +35,8 @@ data_fieldtypes = (
'Barcode',
'Geolocation',
'Duration',
'Icon'
'Icon',
'Phone'
)
no_value_fields = (

View file

@ -11,6 +11,7 @@ from frappe.model import display_fieldtypes
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils.html_utils import unescape_html
import phonenumbers as ph
max_positive_value = {
'smallint': 2 ** 15,
@ -652,6 +653,17 @@ class BaseDocument(object):
from frappe.core.doctype.user.user import STANDARD_USERS
# 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)
try:
phone = ph.parse(phone)
except Exception as e:
if e.error_type == 1:
frappe.throw(_("The entered value is not a phone number."), title="Invalid Number")
frappe.throw(_("Please select a country code."), title = _("Country Code Required"))
if not ph.is_valid_number(phone):
frappe.throw('This is not a valid phone number', title = "Invalid Number")
for data_field in self.meta.get_data_fields():
data = self.get(data_field.fieldname)
data_field_options = data_field.get("options")

View file

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

View file

@ -40,6 +40,7 @@ import './multiselect_list';
import './rating';
import './duration';
import './icon';
import './phone'
frappe.ui.form.make_control = function (opts) {
var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, "");

View file

@ -0,0 +1,159 @@
import Picker from '../../phone_picker/phone_picker';
frappe.ui.form.ControlPhone = class ControlPhone extends frappe.ui.form.ControlData {
make_input() {
super.make_input();
this.make_icon_input();
this.input_events();
}
input_events() {
// Replaces code when selected and removes previously selected.
this.picker.on_change = (country) => {
const country_code = frappe.boot.country_codes[country].code;
const country_isd = frappe.boot.country_codes[country].isd;
this.selected_icon.find('use').attr('href', '#'+country_code)
this.$icon = this.selected_icon.find('svg');
if (this.$icon.hasClass('icon-sm')) {
this.$icon.removeClass('icon-sm');
this.selected_icon.find('svg').addClass('flag-md')
}
if (!this.$isd.length) {
this.selected_icon.append($(`<span class= "country"> ${country_isd}</span>`))
} else {
this.$isd.text(country_isd)
}
// this.selected_icon.text('+' + this.get_country(country))
if(this.$input.val()) {
this.set_formatted_input(this.get_country(country) +'-'+ this.$input.val())
}
};
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');
});
});
}
make_icon_input() {
let picker_wrapper = $('<div>');
this.picker = new Picker({
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.picker.refresh();
}, 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 get fetched.
if(!this.value && this.frm.is_new()) {
this.$input.val("");
this.$wrapper.find('.country').text("")
this.selected_icon.find('use').attr('href', '#icon-down')
this.flag = this.selected_icon.find('svg');
let has_flag = this.flag.hasClass('flag-md');
if (has_flag) {
this.flag.toggleClass('flag-md');
this.flag.toggleClass('icon-sm');
}
}
if(this.value && this.value.split("-").length == 2) {
let isd = this.value.split("-")[0];
let country_data = frappe.boot.country_codes;
for (const country in country_data) {
if (Object.values(country_data[country]).includes(isd)) {
let code = country_data[country].code;
this.change_flag(code);
}
}
this.picker.set_country(isd);
this.picker.refresh();
if (this.picker.country && this.picker.country !== this.$isd.text()) {
this.$isd.length && this.$isd.text(isd)
}
}
}
set_formatted_input(value) {
if(value && value.includes('-')) {
this.set_model_value(value)
} else if(this.$isd.text().trim() && this.value) {
let code_number = this.$isd.text() + '-' + value;
this.set_model_value(code_number)
}
this.$input && value && this.$input.val(value.split("-").pop())
}
reset_icon() {
}
change_flag(country_code) {
this.selected_icon.find('use').attr('href', '#'+country_code)
this.$icon = this.selected_icon.find('svg');
if (this.$icon.hasClass('icon-sm')) {
this.$icon.removeClass('icon-sm');
this.selected_icon.find('svg').addClass('flag-md')
}
}
get_country(country=null) {
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, "md")
}
};

View file

@ -0,0 +1,92 @@
class Picker {
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.icon_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.icon_picker_wrapper);
this.icon_wrapper = this.icon_picker_wrapper.find('.phones');
this.search_input = this.icon_picker_wrapper.find('.search-phones > input');
this.refresh();
this.setup_icons();
}
setup_icons() {
Object.entries(this.countries).forEach(([country, info]) => {
let $country = $(`<div id="${country}" class="phone-wrapper">${frappe.utils.flag(info.code, "md")}<span class="country">${country}</span></div>`);
this.icon_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, 32].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.icon_wrapper.find(".phone-wrapper").removeClass('hidden');
} else {
this.icon_wrapper.find(".phone-wrapper").addClass('hidden');
this.icon_wrapper.find(`.phone-wrapper[id*='${value}']`).removeClass('hidden');
}
}
update_icon_selected(silent) {
!silent && this.on_change && this.on_change(this.get_country());
}
set_country(country) {
this.country = country || '';
}
get_country() {
if (!this.country) return frappe.utils.icon("down", "sm")
return this.country;
}
}
export default Picker;

View file

@ -1156,6 +1156,19 @@ Object.assign(frappe.utils, {
</svg>`;
},
flag(icon_name, size="sm", icon_class="", icon_style="", svg_class="") {
let size_class = "";
if (typeof size == "object") {
icon_style += ` width: ${size.width}; height: ${size.height}`;
} else {
size_class = `flag-${size}`;
}
return `<svg class="icon ${svg_class} ${size_class}" style="${icon_style}">
<use class="${icon_class}" href="#${icon_name}"></use>
</svg>`;
},
make_chart(wrapper, custom_options={}) {
let chart_args = {
type: 'bar',

View file

@ -2,6 +2,7 @@
@import "color_picker";
@import "icon_picker";
@import "datepicker";
@import "phone_picker";
// password
.form-control[data-fieldtype="Password"] {

View file

@ -0,0 +1,119 @@
.phone-picker {
font-size: var(--text-xs);
color: var(--text-muted);
--phone-picker-width: 210px;
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: 210px;
height: 30px;
text-align: center;
align-items: center;
border-radius: 0.375rem;
padding: 0.5rem;
.country {
display: flex;
margin-left: 0.6rem;
flex-grow: 1;
}
}
}
.search-phones {
position: relative;
input[type='search'] {
height: inherit;
padding-left: 30px;
}
.search-phone {
position: absolute;
top: 7px;
left: 7px;
}
}
}
.phone-picker-popover {
left: -20px !important;
.picker-arrow {
left: 15px !important;
}
}
.frappe-control[data-fieldtype='Phone']
{
input {
padding-left: 70px;
}
.selected-phone {
display: flex;
cursor: pointer;
width: 52px;
height: 18px;
border-radius: 5px;
position: absolute;
top: calc(50% + 2.6px);
left: 8px;
content: ' ';
.country {
display: flex;
margin-left: 0.6rem;
flex-grow: 1;
}
}
.like-disabled-input {
.phone-value {
padding-left: 25px;
}
.selected-phone {
top: 20%;
cursor: default;
}
}
}
.data-row.row {
.selected-phone {
top: calc(50% - 11px);
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: 5px !important;
}
}