feat: Phone Field Control Type
This commit is contained in:
parent
95dc3875bb
commit
d08a332a85
15 changed files with 960 additions and 549 deletions
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ data_fieldtypes = (
|
|||
'Barcode',
|
||||
'Geolocation',
|
||||
'Duration',
|
||||
'Icon'
|
||||
'Icon',
|
||||
'Phone'
|
||||
)
|
||||
|
||||
no_value_fields = (
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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, "");
|
||||
|
|
|
|||
159
frappe/public/js/frappe/form/controls/phone.js
Normal file
159
frappe/public/js/frappe/form/controls/phone.js
Normal 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")
|
||||
}
|
||||
};
|
||||
92
frappe/public/js/frappe/phone_picker/phone_picker.js
Normal file
92
frappe/public/js/frappe/phone_picker/phone_picker.js
Normal 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;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
@import "color_picker";
|
||||
@import "icon_picker";
|
||||
@import "datepicker";
|
||||
@import "phone_picker";
|
||||
|
||||
// password
|
||||
.form-control[data-fieldtype="Password"] {
|
||||
|
|
|
|||
119
frappe/public/scss/common/phone_picker.scss
Normal file
119
frappe/public/scss/common/phone_picker.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue