* feat(control): Add Table MultiSelect control * fix: Use btn-group instead of span * fix: Remove functionality * fix: Add 'Table MultiSelect' to Field doctypes * fix: Replace usage of string 'Table' with array `table_fields` * fix: Use internal array to store values instead of building from HTML elements * fix(style): Add semicolon * fix: Read only mode and click to navigate to form * style: indent * fix: fallback to empty array * fix: Add formatters in js and py * style: missing semicolon * fix: Add docfield validationversion-14
@@ -70,7 +70,8 @@ def clear_doctype_cache(doctype=None): | |||
# clear all parent doctypes | |||
for dt in frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=doctype)): | |||
for dt in frappe.db.get_all('DocField', 'parent', | |||
dict(fieldtype=['in', frappe.model.table_fields], options=doctype)): | |||
clear_single(dt.parent) | |||
# clear all notifications | |||
@@ -10,7 +10,7 @@ import frappe | |||
from frappe import _ | |||
from frappe.utils import now, cint | |||
from frappe.model import no_value_fields, default_fields, data_fieldtypes | |||
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields | |||
from frappe.model.document import Document | |||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter | |||
from frappe.desk.notifications import delete_notification_count_for | |||
@@ -82,7 +82,7 @@ class DocType(Document): | |||
if not [d.fieldname for d in self.fields if d.in_list_view]: | |||
cnt = 0 | |||
for d in self.fields: | |||
if d.reqd and not d.hidden and not d.fieldtype == "Table": | |||
if d.reqd and not d.hidden and not d.fieldtype in table_fields: | |||
d.in_list_view = 1 | |||
cnt += 1 | |||
if cnt == 4: break | |||
@@ -171,7 +171,8 @@ class DocType(Document): | |||
"""Change the timestamp of parent DocType if the current one is a child to clear caches.""" | |||
if frappe.flags.in_import: | |||
return | |||
parent_list = frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=self.name)) | |||
parent_list = frappe.db.get_all('DocField', 'parent', | |||
dict(fieldtype=['in', frappe.model.table_fields], options=self.name)) | |||
for p in parent_list: | |||
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent)) | |||
@@ -511,11 +512,11 @@ def validate_fields(meta): | |||
validate_column_length(fieldname) | |||
def check_illegal_mandatory(d): | |||
if (d.fieldtype in no_value_fields) and d.fieldtype!="Table" and d.reqd: | |||
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd: | |||
frappe.throw(_("Field {0} of type {1} cannot be mandatory").format(d.label, d.fieldtype)) | |||
def check_link_table_options(d): | |||
if d.fieldtype in ("Link", "Table"): | |||
if d.fieldtype in ("Link",) + table_fields: | |||
if not d.options: | |||
frappe.throw(_("Options required for Link or Table type field {0} in row {1}").format(d.label, d.idx)) | |||
if d.options=="[Select]" or d.options==d.parent: | |||
@@ -692,6 +693,19 @@ def validate_fields(meta): | |||
re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on): | |||
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError) | |||
def check_table_multiselect_option(docfield): | |||
'''check if the doctype provided in Option has atleast 1 Link field''' | |||
if not docfield.fieldtype == 'Table MultiSelect': return | |||
doctype = docfield.options | |||
meta = frappe.get_meta(doctype) | |||
link_field = [df for df in meta.fields if df.fieldtype == 'Link'] | |||
if not link_field: | |||
frappe.throw(_('DocType <b>{0}</b> provided for the field <b>{1}</b> must have atleast one Link field') | |||
.format(doctype, docfield.fieldname), frappe.ValidationError) | |||
fields = meta.get("fields") | |||
fieldname_list = [d.fieldname for d in fields] | |||
@@ -702,7 +716,7 @@ def validate_fields(meta): | |||
for d in fields: | |||
if not d.permlevel: d.permlevel = 0 | |||
if d.fieldtype != "Table": d.allow_bulk_edit = 0 | |||
if d.fieldtype not in table_fields: d.allow_bulk_edit = 0 | |||
if d.fieldtype == "Barcode": d.ignore_xss_filter = 1 | |||
if not d.fieldname: | |||
d.fieldname = d.fieldname.lower() | |||
@@ -719,6 +733,7 @@ def validate_fields(meta): | |||
check_illegal_default(d) | |||
check_unique_and_text(d) | |||
check_illegal_depends_on_conditions(d) | |||
check_table_multiselect_option(d) | |||
check_fold(fields) | |||
check_search_fields(meta, fields) | |||
@@ -7,7 +7,7 @@ from __future__ import unicode_literals | |||
import frappe, json | |||
from frappe.model.document import Document | |||
from frappe.model import no_value_fields | |||
from frappe.model import no_value_fields, table_fields | |||
class Version(Document): | |||
def set_diff(self, old, new): | |||
@@ -42,12 +42,12 @@ def get_diff(old, new, for_child=False): | |||
}''' | |||
out = frappe._dict(changed = [], added = [], removed = [], row_changed = []) | |||
for df in new.meta.fields: | |||
if df.fieldtype in no_value_fields and df.fieldtype != 'Table': | |||
if df.fieldtype in no_value_fields and df.fieldtype not in table_fields: | |||
continue | |||
old_value, new_value = old.get(df.fieldname), new.get(df.fieldname) | |||
if df.fieldtype=='Table': | |||
if df.fieldtype in table_fields: | |||
# make maps | |||
old_row_by_name, new_row_by_name = {}, {} | |||
for d in old_value: | |||
@@ -234,7 +234,7 @@ | |||
"no_copy": 0, | |||
"oldfieldname": "fieldtype", | |||
"oldfieldtype": "Select", | |||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", | |||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature", | |||
"permlevel": 0, | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
@@ -1302,7 +1302,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2018-11-23 19:56:43.328280", | |||
"modified": "2018-12-19 18:34:46.031246", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Custom Field", | |||
@@ -69,7 +69,7 @@ docfield_properties = { | |||
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), | |||
('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'), ('Data', 'Select'), | |||
('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation')) | |||
('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation'), ('Table', 'Table MultiSelect')) | |||
allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select', 'Data') | |||
@@ -37,12 +37,16 @@ class PropertySetter(Document): | |||
and property = %(property)s""", self.get_valid_dict()) | |||
def get_property_list(self, dt): | |||
return frappe.db.sql("""select fieldname, label, fieldtype | |||
from tabDocField | |||
where parent=%s | |||
and fieldtype not in ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Table', 'Fold') | |||
and coalesce(fieldname, '') != '' | |||
order by label asc""", dt, as_dict=1) | |||
return frappe.db.get_all('DocField', | |||
fields=['fieldname', 'label', 'fieldtype'], | |||
filters={ | |||
'parent': dt, | |||
'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], | |||
'fieldname': ['!=', ''] | |||
}, | |||
order_by='label asc', | |||
as_dict=1 | |||
) | |||
def get_setup_data(self): | |||
return { | |||
@@ -42,9 +42,10 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None): | |||
link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt) | |||
linkmeta = link_meta_bundle[0] | |||
if not linkmeta.get("issingle"): | |||
fields = [d.fieldname for d in linkmeta.get("fields", {"in_list_view":1, | |||
"fieldtype": ["not in", ["Image", "HTML", "Button", "Table"]]})] \ | |||
+ ["name", "modified", "docstatus"] | |||
fields = [d.fieldname for d in linkmeta.get("fields", { | |||
"in_list_view": 1, | |||
"fieldtype": ["not in", ("Image", "HTML", "Button") + frappe.model.table_fields] | |||
})] + ["name", "modified", "docstatus"] | |||
if link.get("add_fields"): | |||
fields += link["add_fields"] | |||
@@ -116,7 +117,7 @@ def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False) | |||
ret.update(get_linked_fields(doctype, without_ignore_user_permissions_enabled)) | |||
ret.update(get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled)) | |||
filters=[['fieldtype','=','Table'], ['options', '=', doctype]] | |||
filters=[['fieldtype', 'in', frappe.model.table_fields], ['options', '=', doctype]] | |||
if without_ignore_user_permissions_enabled: filters.append(['ignore_user_permissions', '!=', 1]) | |||
# find links of parents | |||
links = frappe.get_all("DocField", fields=["parent as dt"], filters=filters) | |||
@@ -159,7 +160,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): | |||
for doctype_name in links_dict: | |||
ret[doctype_name] = { "fieldname": links_dict.get(doctype_name) } | |||
table_doctypes = frappe.get_all("DocType", filters=[["istable", "=", "1"], ["name", "in", tuple(links_dict)]]) | |||
child_filters = [['fieldtype','=', 'Table'], ['options', 'in', tuple(doctype.name for doctype in table_doctypes)]] | |||
child_filters = [['fieldtype','in', frappe.model.table_fields], ['options', 'in', tuple(doctype.name for doctype in table_doctypes)]] | |||
if without_ignore_user_permissions_enabled: child_filters.append(['ignore_user_permissions', '!=', 1]) | |||
# find out if linked in a child table | |||
@@ -80,7 +80,7 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None): | |||
def get_meta_bundle(doctype): | |||
bundle = [frappe.desk.form.meta.get_meta(doctype)] | |||
for df in bundle[0].fields: | |||
if df.fieldtype=="Table": | |||
if df.fieldtype in frappe.model.table_fields: | |||
bundle.append(frappe.desk.form.meta.get_meta(df.options, not frappe.conf.developer_mode)) | |||
return bundle | |||
@@ -8,7 +8,7 @@ frappe.webhook = { | |||
frappe.model.with_doctype(doc.webhook_doctype, function() { | |||
var fields = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { | |||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || | |||
d.fieldtype === 'Table') { | |||
frappe.model.table_fields.includes(d.fieldtype)) { | |||
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; | |||
} | |||
else if (d.fieldtype === 'Currency' || d.fieldtype === 'Float') { | |||
@@ -37,12 +37,13 @@ data_fieldtypes = ( | |||
'Geolocation' | |||
) | |||
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Button', 'Image', | |||
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect', 'Button', 'Image', | |||
'Fold', 'Heading') | |||
display_fieldtypes = ('Section Break', 'Column Break', 'HTML', 'Button', 'Image', 'Fold', 'Heading') | |||
default_fields = ('doctype','name','owner','creation','modified','modified_by', | |||
'parent','parentfield','parenttype','idx','docstatus') | |||
optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen") | |||
table_fields = ('Table', 'Table MultiSelect') | |||
def delete_fields(args_dict, delete=0): | |||
""" | |||
@@ -7,7 +7,7 @@ from six import iteritems, string_types | |||
import frappe | |||
import datetime | |||
from frappe import _ | |||
from frappe.model import default_fields | |||
from frappe.model import default_fields, table_fields | |||
from frappe.model.naming import set_new_name | |||
from frappe.model.utils.link_count import notify_link_count | |||
from frappe.modules import load_doctype_module | |||
@@ -222,7 +222,7 @@ class BaseDocument(object): | |||
# unique empty field should be set to None | |||
d[fieldname] = None | |||
if isinstance(d[fieldname], list) and df.fieldtype != 'Table': | |||
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: | |||
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label))) | |||
if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)): | |||
@@ -398,7 +398,7 @@ class BaseDocument(object): | |||
def _get_missing_mandatory_fields(self): | |||
"""Get mandatory fields that do not have any values""" | |||
def get_msg(df): | |||
if df.fieldtype == "Table": | |||
if df.fieldtype in table_fields: | |||
return "{}: {}: {}".format(_("Error"), _("Data missing in table"), _(df.label)) | |||
elif self.parentfield: | |||
@@ -573,7 +573,7 @@ class BaseDocument(object): | |||
db_value = db_values.get(key) | |||
if df and not df.allow_on_submit and (self.get(key) or db_value): | |||
if df.fieldtype=="Table": | |||
if df.fieldtype in table_fields: | |||
# just check if the table size has changed | |||
# individual fields will be checked in the loop for children | |||
self_value = len(self.get(key)) | |||
@@ -141,8 +141,14 @@ def delete_from_table(doctype, name, ignore_doctypes, doc): | |||
else: | |||
def get_table_fields(field_doctype): | |||
return frappe.db.sql_list("""select options from `tab{}` where fieldtype='Table' | |||
and parent=%s""".format(field_doctype), doctype) | |||
return [r[0] for r in frappe.get_all(field_doctype, | |||
fields='options', | |||
filters={ | |||
'fieldtype': ['in', frappe.model.table_fields], | |||
'parent': doctype | |||
}, | |||
as_list=1 | |||
)] | |||
tables = get_table_fields("DocField") | |||
if not frappe.flags.in_install=="frappe": | |||
@@ -36,7 +36,7 @@ def update_table(f, new): | |||
def update_parent_field(f, new): | |||
"""update 'parentfield' in tables""" | |||
if f['fieldtype']=='Table': | |||
if f['fieldtype'] in frappe.model.table_fields: | |||
frappe.db.begin() | |||
frappe.db.sql("""update `tab%s` set parentfield=%s where parentfield=%s""" \ | |||
% (f['options'], '%s', '%s'), (new, f['fieldname'])) | |||
@@ -12,7 +12,7 @@ from frappe.model.naming import set_new_name | |||
from six import iteritems, string_types | |||
from werkzeug.exceptions import NotFound, Forbidden | |||
import hashlib, json | |||
from frappe.model import optional_fields | |||
from frappe.model import optional_fields, table_fields | |||
from frappe.model.workflow import validate_workflow | |||
from frappe.utils.global_search import update_global_search | |||
from frappe.integrations.doctype.webhook import run_webhooks | |||
@@ -489,7 +489,7 @@ class Document(BaseDocument): | |||
value = self.get(field.fieldname) | |||
original_value = self._doc_before_save.get(field.fieldname) | |||
if field.fieldtype=='Table': | |||
if field.fieldtype in table_fields: | |||
fail = not self.is_child_table_same(field.fieldname) | |||
elif field.fieldtype in ('Date', 'Datetime', 'Time'): | |||
fail = str(value) != str(original_value) | |||
@@ -756,7 +756,7 @@ class Document(BaseDocument): | |||
def get_all_children(self, parenttype=None): | |||
"""Returns all children documents from **Table** type field in a list.""" | |||
ret = [] | |||
for df in self.meta.get("fields", {"fieldtype": "Table"}): | |||
for df in self.meta.get("fields", {"fieldtype": ['in', table_fields]}): | |||
if parenttype: | |||
if df.options==parenttype: | |||
return self.get(df.fieldname) | |||
@@ -5,7 +5,7 @@ from __future__ import unicode_literals | |||
import frappe, json | |||
from frappe import _ | |||
from frappe.utils import cstr | |||
from frappe.model import default_fields | |||
from frappe.model import default_fields, table_fields | |||
from six import string_types | |||
@frappe.whitelist() | |||
@@ -129,8 +129,8 @@ def map_doc(source_doc, target_doc, table_map, source_parent=None): | |||
table_map["postprocess"](source_doc, target_doc, source_parent) | |||
def map_fields(source_doc, target_doc, table_map, source_parent): | |||
no_copy_fields = set([d.fieldname for d in source_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype=="Table")] | |||
+ [d.fieldname for d in target_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype=="Table")] | |||
no_copy_fields = set([d.fieldname for d in source_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype in table_fields)] | |||
+ [d.fieldname for d in target_doc.meta.get("fields") if (d.no_copy==1 or d.fieldtype in table_fields)] | |||
+ list(default_fields) | |||
+ list(table_map.get("field_no_map", []))) | |||
@@ -20,7 +20,7 @@ from datetime import datetime | |||
from six.moves import range | |||
import frappe, json, os | |||
from frappe.utils import cstr, cint | |||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes | |||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields | |||
from frappe.model.document import Document | |||
from frappe.model.base_document import BaseDocument | |||
from frappe.modules import load_doctype_module | |||
@@ -150,7 +150,7 @@ class Meta(Document): | |||
def get_table_fields(self): | |||
if not hasattr(self, "_table_fields"): | |||
if self.name!="DocType": | |||
self._table_fields = self.get('fields', {"fieldtype":"Table"}) | |||
self._table_fields = self.get('fields', {"fieldtype": ['in', table_fields]}) | |||
else: | |||
self._table_fields = doctype_table_fields | |||
@@ -451,7 +451,7 @@ def is_single(doctype): | |||
raise Exception('Cannot determine whether %s is single' % doctype) | |||
def get_parent_dt(dt): | |||
parent_dt = frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=dt), limit=1) | |||
parent_dt = frappe.db.get_all('DocField', 'parent', dict(fieldtype=['in', frappe.model.table_fields], options=dt), limit=1) | |||
return parent_dt and parent_dt[0].parent or '' | |||
def set_fieldname(field_id, fieldname): | |||
@@ -173,9 +173,11 @@ def validate_rename(doctype, new, meta, merge, force, ignore_permissions): | |||
return new | |||
def rename_doctype(doctype, old, new, force=False): | |||
# change options for fieldtype Table | |||
update_options_for_fieldtype("Table", old, new) | |||
update_options_for_fieldtype("Link", old, new) | |||
# change options for fieldtype Table, Table MultiSelect and Link | |||
fields_with_options = ("Link",) + frappe.model.table_fields | |||
for fieldtype in fields_with_options: | |||
update_options_for_fieldtype(fieldtype, old, new) | |||
# change options where select options are hardcoded i.e. listed | |||
select_fields = get_select_fields(old, new) | |||
@@ -352,13 +354,21 @@ def update_select_field_values(old, new): | |||
.format(frappe.db.escape('%' + '\n' + old + '%'), frappe.db.escape('%' + old + '\n' + '%')), (old, new, new)) | |||
def update_parenttype_values(old, new): | |||
child_doctypes = frappe.db.sql("""\ | |||
select options, fieldname from `tabDocField` | |||
where parent=%s and fieldtype='Table'""", (new,), as_dict=1) | |||
custom_child_doctypes = frappe.db.sql("""\ | |||
select options, fieldname from `tabCustom Field` | |||
where dt=%s and fieldtype='Table'""", (new,), as_dict=1) | |||
child_doctypes = frappe.db.get_all('DocField', | |||
fields=['options', 'fieldname'], | |||
filters={ | |||
'parent': new, | |||
'fieldtype': ['in', frappe.model.table_fields] | |||
} | |||
) | |||
custom_child_doctypes = frappe.db.get_all('Custom Field', | |||
fields=['options', 'fieldname'], | |||
filters={ | |||
'dt': new, | |||
'fieldtype': ['in', frappe.model.table_fields] | |||
} | |||
) | |||
child_doctypes += custom_child_doctypes | |||
fields = [d['fieldname'] for d in child_doctypes] | |||
@@ -5,7 +5,7 @@ from __future__ import unicode_literals, print_function | |||
import frappe | |||
import json | |||
from frappe.model import no_value_fields | |||
from frappe.model import no_value_fields, table_fields | |||
from frappe.utils.password import rename_password_field | |||
from frappe.model.utils.user_settings import update_user_settings_data, sync_user_settings | |||
@@ -19,7 +19,7 @@ def rename_field(doctype, old_fieldname, new_fieldname): | |||
print("rename_field: " + (new_fieldname) + " not found in " + doctype) | |||
return | |||
if new_field.fieldtype == "Table": | |||
if new_field.fieldtype in table_fields: | |||
# change parentfield of table mentioned in options | |||
frappe.db.sql("""update `tab%s` set parentfield=%s | |||
where parentfield=%s""" % (new_field.options.split("\n")[0], "%s", "%s"), | |||
@@ -85,7 +85,8 @@ | |||
"public/js/frappe/form/controls/barcode.js", | |||
"public/js/frappe/form/controls/geolocation.js", | |||
"public/js/frappe/form/controls/multiselect.js", | |||
"public/js/frappe/form/controls/multicheck.js" | |||
"public/js/frappe/form/controls/multicheck.js", | |||
"public/js/frappe/form/controls/table_multiselect.js" | |||
], | |||
"js/dialog.min.js": [ | |||
"public/js/frappe/dom.js", | |||
@@ -0,0 +1,130 @@ | |||
frappe.ui.form.ControlTableMultiSelect = frappe.ui.form.ControlLink.extend({ | |||
make_input() { | |||
this._super(); | |||
this.$input_area.addClass('form-control table-multiselect'); | |||
this.$input.removeClass('form-control'); | |||
this.$input.on("awesomplete-selectcomplete", () => { | |||
this.$input.val(''); | |||
}); | |||
// used as an internal model to store values | |||
this.rows = []; | |||
this.$input_area.on('click', '.btn-remove', (e) => { | |||
const $target = $(e.currentTarget); | |||
const $value = $target.closest('.tb-selected-value'); | |||
const value = decodeURIComponent($value.data().value); | |||
const link_field = this.get_link_field(); | |||
this.rows = this.rows.filter(row => row[link_field.fieldname] !== value); | |||
this.parse_validate_and_set_in_model(''); | |||
}); | |||
this.$input_area.on('click', '.btn-link-to-form', (e) => { | |||
const $target = $(e.currentTarget); | |||
const $value = $target.closest('.tb-selected-value'); | |||
const value = decodeURIComponent($value.data().value); | |||
const link_field = this.get_link_field(); | |||
frappe.set_route('Form', link_field.options, value); | |||
}); | |||
}, | |||
setup_buttons() { | |||
this.$input_area.find('.link-btn').remove(); | |||
}, | |||
parse(value) { | |||
const link_field = this.get_link_field(); | |||
if (value) { | |||
if (this.frm) { | |||
const new_row = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname); | |||
new_row[link_field.fieldname] = value; | |||
} else { | |||
this.rows.push({ | |||
[link_field.fieldname]: value | |||
}); | |||
} | |||
} | |||
return this.rows; | |||
}, | |||
validate(value) { | |||
const rows = (value || []).slice(); | |||
// validate the value just entered | |||
if (this.df.ignore_link_validation) { | |||
return rows; | |||
} | |||
const link_field = this.get_link_field(); | |||
if (rows.length === 0) { | |||
return rows; | |||
} | |||
const all_rows_except_last = rows.slice(0, rows.length - 1); | |||
const last_row = rows[rows.length - 1]; | |||
// validate the last value entered | |||
const link_value = last_row[link_field.fieldname]; | |||
// falsy value | |||
if (!link_value) { | |||
return all_rows_except_last; | |||
} | |||
// duplicate value | |||
if (all_rows_except_last.map(row => row[link_field.fieldname]).includes(link_value)) { | |||
return all_rows_except_last; | |||
} | |||
const validate_promise = this.validate_link_and_fetch(this.df, this.get_options(), | |||
this.docname, link_value); | |||
return validate_promise.then(validated_value => { | |||
if (validated_value === link_value) { | |||
return rows; | |||
} else { | |||
rows.pop(); | |||
return rows; | |||
} | |||
}); | |||
}, | |||
set_formatted_input(value) { | |||
this.rows = value || []; | |||
const link_field = this.get_link_field(); | |||
const values = this.rows.map(row => row[link_field.fieldname]); | |||
this.set_pill_html(values); | |||
}, | |||
set_pill_html(values) { | |||
const html = values | |||
.map(value => this.get_pill_html(value)) | |||
.join(''); | |||
this.$input_area.find('.tb-selected-value').remove(); | |||
this.$input_area.prepend(html); | |||
}, | |||
get_pill_html(value) { | |||
const encoded_value = encodeURIComponent(value); | |||
return `<div class="btn-group tb-selected-value" data-value="${encoded_value}"> | |||
<button class="btn btn-default btn-xs btn-link-to-form">${__(value)}</button> | |||
<button class="btn btn-default btn-xs btn-remove"> | |||
<i class="fa fa-remove text-muted"></i> | |||
</button> | |||
</div>`; | |||
}, | |||
get_options() { | |||
return (this.get_link_field() || {}).options; | |||
}, | |||
get_link_field() { | |||
if (!this._link_field) { | |||
const meta = frappe.get_meta(this.df.options); | |||
this._link_field = meta.fields.find(df => df.fieldtype === 'Link'); | |||
if (!this._link_field) { | |||
throw new Error('Table MultiSelect requires a Table with atleast one Link field'); | |||
} | |||
} | |||
return this._link_field; | |||
}, | |||
}); |
@@ -238,6 +238,16 @@ frappe.form.formatters = { | |||
value = flt(flt(value) / 1024, 1) + "K"; | |||
} | |||
return value; | |||
}, | |||
TableMultiSelect: function(rows, df, options) { | |||
rows = rows || []; | |||
const meta = frappe.get_meta(df.options); | |||
const link_field = meta.fields.find(df => df.fieldtype === 'Link'); | |||
const formatted_values = rows.map(row => { | |||
const value = row[link_field.fieldname]; | |||
return frappe.format(value, link_field, options, row); | |||
}); | |||
return formatted_values.join(', '); | |||
} | |||
} | |||
@@ -179,7 +179,7 @@ frappe.ui.form.ScriptManager = Class.extend({ | |||
// setup add fetch | |||
$.each(this.frm.fields, function(i, field) { | |||
setup_add_fetch(field.df); | |||
if(field.df.fieldtype==="Table") { | |||
if(frappe.model.table_fields.includes(field.df.fieldtype)) { | |||
$.each(frappe.meta.get_docfields(field.df.options, me.frm.docname), function(i, df) { | |||
setup_add_fetch(df); | |||
}); | |||
@@ -266,7 +266,7 @@ $.extend(frappe.model, { | |||
&& !(df && (!from_amend && cint(df.no_copy) == 1))) { | |||
var value = doc[key] || []; | |||
if (df.fieldtype === "Table") { | |||
if (frappe.model.table_fields.includes(df.fieldtype)) { | |||
for (var i = 0, j = value.length; i < j; i++) { | |||
var d = value[i]; | |||
frappe.model.copy_doc(d, from_amend, newdoc, df.fieldname); | |||
@@ -138,7 +138,7 @@ $.extend(frappe.meta, { | |||
get_table_fields: function(dt) { | |||
return $.map(frappe.meta.docfield_list[dt], function(d) { | |||
return d.fieldtype==='Table' ? d : null}); | |||
return frappe.model.table_fields.includes(d.fieldtype) ? d : null}); | |||
}, | |||
get_doctype_for_field: function(doctype, key) { | |||
@@ -168,8 +168,8 @@ $.extend(frappe.meta, { | |||
}, | |||
get_parentfield: function(parent_dt, child_dt) { | |||
var df = (frappe.get_doc("DocType", parent_dt).fields || []).filter(function(d) | |||
{ return d.fieldtype==="Table" && d.options===child_dt }) | |||
var df = (frappe.get_doc("DocType", parent_dt).fields || []) | |||
.filter(df => frappe.model.table_fields.includes(df.fieldtype) && df.options===child_dt) | |||
if(!df.length) | |||
throw "parentfield not found for " + parent_dt + ", " + child_dt; | |||
return df[0].fieldname; | |||
@@ -4,7 +4,7 @@ | |||
frappe.provide('frappe.model'); | |||
$.extend(frappe.model, { | |||
no_value_type: ['Section Break', 'Column Break', 'HTML', 'Table', | |||
no_value_type: ['Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect', | |||
'Button', 'Image', 'Fold', 'Heading'], | |||
layout_fields: ['Section Break', 'Column Break', 'Fold'], | |||
@@ -33,6 +33,8 @@ $.extend(frappe.model, { | |||
{fieldname:'parent', fieldtype:'Data', label:__('Parent')}, | |||
], | |||
table_fields: ['Table', 'Table MultiSelect'], | |||
new_names: {}, | |||
events: {}, | |||
user_settings: {}, | |||
@@ -96,10 +98,12 @@ $.extend(frappe.model, { | |||
if(locals.DocType[doctype]) { | |||
callback && callback(); | |||
} else { | |||
var cached_timestamp = null; | |||
let cached_timestamp = null; | |||
let cached_doc = null; | |||
if(localStorage["_doctype:" + doctype]) { | |||
let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]); | |||
let cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; | |||
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; | |||
if(cached_doc) { | |||
cached_timestamp = cached_doc.modified; | |||
} | |||
@@ -304,7 +308,7 @@ $.extend(frappe.model, { | |||
var val = locals[dt] && locals[dt][dn] && locals[dt][dn][fn]; | |||
var df = frappe.meta.get_docfield(dt, fn, dn); | |||
if(df.fieldtype=='Table') { | |||
if(frappe.model.table_fields.includes(df.fieldtype)) { | |||
var ret = false; | |||
$.each(locals[df.options] || {}, function(k,d) { | |||
if(d.parent==dn && d.parenttype==dt && d.parentfield==df.fieldname) { | |||
@@ -106,10 +106,10 @@ $.extend(frappe.model, { | |||
if (source[key] == undefined) delete target[key]; | |||
}); | |||
} | |||
for (let fieldname in doc) { | |||
let df = frappe.meta.get_field(doc.doctype, fieldname); | |||
if (df && df.fieldtype === 'Table') { | |||
if (df && frappe.model.table_fields.includes(df.fieldtype)) { | |||
// table | |||
if (!(doc[fieldname] instanceof Array)) { | |||
doc[fieldname] = []; | |||
@@ -118,7 +118,7 @@ $.extend(frappe.model, { | |||
if (!(local_doc[fieldname] instanceof Array)) { | |||
local_doc[fieldname] = []; | |||
} | |||
// child table, override each row and append new rows if required | |||
for (let i=0; i < doc[fieldname].length; i++ ) { | |||
let d = doc[fieldname][i]; | |||
@@ -144,7 +144,7 @@ $.extend(frappe.model, { | |||
// row exists, just copy the values | |||
Object.assign(local_d, d); | |||
clear_keys(d, local_d); | |||
} else { | |||
local_doc[fieldname].push(d); | |||
if (!d.parent) d.parent = doc.name; | |||
@@ -170,7 +170,7 @@ $.extend(frappe.model, { | |||
local_doc[fieldname] = doc[fieldname]; | |||
} | |||
} | |||
// clear keys on parent | |||
clear_keys(doc, local_doc); | |||
} | |||
@@ -138,7 +138,7 @@ frappe.ui.FieldSelect = Class.extend({ | |||
if(me.doctype && df.parent==me.doctype) { | |||
label = __(df.label); | |||
table = me.doctype; | |||
if(df.fieldtype=='Table') me.table_fields.push(df); | |||
if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df); | |||
} else { | |||
label = __(df.label) + ' (' + __(df.parent) + ')'; | |||
table = df.parent; | |||
@@ -658,7 +658,7 @@ frappe.ui.FieldSelect = Class.extend({ | |||
if(me.doctype && df.parent==me.doctype) { | |||
var label = __(df.label); | |||
var table = me.doctype; | |||
if(df.fieldtype=='Table') me.table_fields.push(df); | |||
if(frappe.model.table_fields.includes(df.fieldtype)) me.table_fields.push(df); | |||
} else { | |||
var label = __(df.label) + ' (' + __(df.parent) + ')'; | |||
var table = df.parent; | |||
@@ -149,7 +149,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||
// quick entry | |||
var mandatory = meta.fields.filter((df) => df.reqd && !doc[df.fieldname]); | |||
if (mandatory.some(df => df.fieldtype === 'Table') || mandatory.length > 1) { | |||
if (mandatory.some(df => frappe.model.table_fields.includes(df.fieldtype)) || mandatory.length > 1) { | |||
quick_entry = true; | |||
} | |||
@@ -279,7 +279,7 @@ _f.Frm.prototype.set_value = function(field, value, if_missing) { | |||
var fieldobj = me.fields_dict[f]; | |||
if(fieldobj) { | |||
if(!if_missing || !frappe.model.has_value(me.doctype, me.doc.name, f)) { | |||
if(fieldobj.df.fieldtype==="Table" && $.isArray(v)) { | |||
if(frappe.model.table_fields.includes(fieldobj.df.fieldtype) && $.isArray(v)) { | |||
frappe.model.clear_table(me.doc, fieldobj.df.fieldname); | |||
@@ -213,7 +213,9 @@ _f.Frm.prototype.watch_model_updates = function() { | |||
}); | |||
// on table fields | |||
var table_fields = frappe.get_children("DocType", me.doctype, "fields", {fieldtype:"Table"}); | |||
var table_fields = frappe.get_children("DocType", me.doctype, "fields", { | |||
fieldtype: ["in", frappe.model.table_fields] | |||
}); | |||
// using $.each to preserve df via closure | |||
$.each(table_fields, function(i, df) { | |||
@@ -54,3 +54,26 @@ | |||
.markdown-toggle, .html-toggle { | |||
margin-bottom: 5px; | |||
} | |||
/* table multiselect */ | |||
.table-multiselect { | |||
display: flex; | |||
align-items: center; | |||
flex-wrap: wrap; | |||
height: auto; | |||
padding: 10px; | |||
padding-bottom: 5px; | |||
} | |||
.table-multiselect.form-control input { | |||
outline: none; | |||
border: none; | |||
padding: 0; | |||
font-size: @text-medium; | |||
} | |||
.tb-selected-value { | |||
display: inline-block; | |||
margin-right: 5px; | |||
margin-bottom: 5px; | |||
} |
@@ -81,4 +81,10 @@ def format_value(value, df=None, doc=None, currency=None, translated=False): | |||
elif df.get("fieldtype") == "Markdown Editor": | |||
return frappe.utils.markdown(value) | |||
elif df.get("fieldtype") == "Table MultiSelect": | |||
meta = frappe.get_meta(df.options) | |||
link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0] | |||
values = [v.get(link_field.fieldname, 'asdf') for v in value] | |||
return ', '.join(values) | |||
return value |
@@ -75,7 +75,7 @@ def rebuild_for_doctype(doctype): | |||
meta = frappe.get_meta(doctype) | |||
if cint(meta.istable) == 1: | |||
parent_doctypes = frappe.get_all("DocField", fields="parent", filters={ | |||
"fieldtype": "Table", | |||
"fieldtype": ["in", frappe.model.table_fields], | |||
"options": doctype | |||
}) | |||
for p in parent_doctypes: | |||
@@ -229,7 +229,7 @@ def update_global_search(doc): | |||
content = [] | |||
for field in doc.meta.get_global_search_fields(): | |||
if doc.get(field.fieldname) and field.fieldtype != "Table": | |||
if doc.get(field.fieldname) and field.fieldtype not in frappe.model.table_fields: | |||
content.append(get_formatted_value(doc.get(field.fieldname), field)) | |||
tags = (doc.get('_user_tags') or '').strip() | |||