├── README.md ├── content.js ├── examples └── bulk-update-speed.webm ├── frappe ├── form.js ├── listview.js └── papaparse.min.js ├── license.txt ├── logo └── logo.png ├── manifest.json └── service-worker.js /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | A simple Browser Extension that help you work & develop in Frappe/ERPNext Framework 3 | 4 | ## Get Started 5 | Install the extention from the [Chrome Web Store](https://chrome.google.com/webstore/detail/frappe-development-utils/mfpfeokebfgddkaemagjigbjkmohmpab) 6 | or download this repo and load it to your browser in developer mode Then open any doctype and click the options icon near the field label to open the options dialog. 7 | 8 | 9 | ![Alt text](https://iili.io/HTp8G87.jpg "Preview") 10 | 11 | ## Features 12 | 13 | - Support Frappe v13, v14 14 | - Show & change hidden fields in any DocType 15 | - Force Save Non changed forms, Submittable forms & Submitted forms (re submit) 16 | - Highlight the hidden and custom fields in any DocType 17 | - Show forms fields & tables fields details 18 | - Fast Access to Customize Form/DocType for field Options of type Link, Table & Table MultiSelect 19 | - Copy/Insert any child table all/specific rows from/to any ERPNext site 20 | - Copy/Insert any Customize Form custom fields from/to any ERPNext site 21 | - Copy/Insert Doctypes data between ERPNext Sites 22 | - Simple CSV Files bulk import (usefull also for triggering python db apis, webhooks, server scripts etc) 23 | 24 | 25 | #### Credits 26 | - [Frappe Framework](https://frappeframework.com/) 27 | - [PapaParse](https://www.papaparse.com/) 28 | 29 | 30 | ## License 31 | 32 | The MIT License (MIT) -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | function get_fieldname(label, docfields) { 2 | const field = docfields.find((df) => { 3 | if (df.label && df.label === label) { 4 | return df 5 | } 6 | }) 7 | return field["fieldname"] 8 | } 9 | 10 | async function csvToolReadFile(doctype, docfields, file) { 11 | Papa.parse(file, { 12 | header: true, 13 | transformHeader: function (header, idx) { 14 | if (header === "ID") { 15 | return "docname" 16 | } 17 | return get_fieldname(header, docfields) 18 | }, 19 | complete: function (results) { 20 | console.log(results); 21 | let docs = [] 22 | 23 | for (let i = 0; i < results.data.length; i++) { 24 | let doc = results.data[i] 25 | if (!doc.docname) continue 26 | doc["doctype"] = doctype 27 | 28 | docs.push(doc) 29 | } 30 | backgroundMessage('idf_bg_request__csv-tool-bulk_update', docs); 31 | } 32 | }); 33 | } 34 | 35 | 36 | // Listen for page scripts 37 | window.addEventListener("message", async (event) => { 38 | if (event.origin === window.origin) { 39 | // console.log("CS: ", evt.data.eventName); 40 | switch (event.data.eventName) { 41 | case "idf_cs_request__form_trigger": 42 | backgroundMessage("idf_bg_request__form_trigger", event.data.payload); 43 | break; 44 | case "idf_cs_request__listview_setup": 45 | backgroundMessage("idf_bg_request__listview_setup", event.data.payload); 46 | break; 47 | case "idf_cs_request__show_options_dialog": 48 | backgroundMessage("idf_bg_request__show_options_dialog", event.data.payload); 49 | break; 50 | case "idf_cs_request__save-data": 51 | backgroundMessage('idf_bg_request__save-data', event.data.payload); 52 | break; 53 | case "idf_cs_request__listview_show-insert-doc-data-dialog": 54 | backgroundMessage('idf_bg_request__listview_show-insert-doc-data-dialog', event.data.payload); 55 | break; 56 | case "idf_cs_request__listview_show-csv-tool-dialog": 57 | backgroundMessage('idf_bg_request__listview_show-csv-tool-dialog', event.data.payload); 58 | break; 59 | case "idf_cs_request__csv-tool-read_file": 60 | csvToolReadFile(event.data.payload.doctype, event.data.payload.docfields, event.data.payload.file) 61 | break; 62 | case "idf_cs_request__childtable_save": 63 | backgroundMessage('idf_bg_request__childtable_save', event.data.payload); 64 | break; 65 | case "idf_cs_request__childtable_insert": 66 | backgroundMessage('idf_bg_request__childtable_insert', event.data.payload); 67 | break; 68 | case "idf_cs_request__customized_fields_save": 69 | backgroundMessage('idf_bg_request__customized_fields_save', event.data.payload); 70 | break; 71 | case "idf_cs_request__customized_fields_insert": 72 | backgroundMessage('idf_bg_request__customized_fields_insert'); 73 | break; 74 | } 75 | } 76 | }); 77 | 78 | // utils 79 | async function backgroundMessage(eventName, payload) { 80 | chrome.runtime.sendMessage( 81 | { 82 | eventName: eventName, 83 | payload: payload 84 | } 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /examples/bulk-update-speed.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascorbic-acid/frappe_chrome_dev_utils/8a9c571d635c32db96ac1c8d34c837e53d9cd3ac/examples/bulk-update-speed.webm -------------------------------------------------------------------------------- /frappe/form.js: -------------------------------------------------------------------------------- 1 | 2 | function formGridRender(tabId, doctype, docname) { 3 | idfExec(({ doctype, docname }) => { 4 | let frm = cur_frm; 5 | 6 | let parentField = frappe.meta.get_parentfield(frm.doctype, doctype); 7 | 8 | let openFormGrid = frm.fields_dict[parentField].grid.open_grid_row 9 | 10 | for (let k in openFormGrid.fields_dict) { 11 | let field = openFormGrid.fields_dict[k]; 12 | 13 | if (field.df.fieldtype === "Column Break" || 14 | field.df.fieldtype === "Section Break" || 15 | field.df.fieldtype === "Tab Break" 16 | ) continue; 17 | 18 | let opsDiv = document.createElement("div"); 19 | opsDiv.classList.add("idf-child-control") 20 | opsDiv.style.display = "inline-block" 21 | opsDiv.style.cursor = "pointer"; 22 | opsDiv.style.marginRight = "3px"; 23 | opsDiv.style.marginLeft = "3px"; 24 | opsDiv.innerHTML = window.idfState.idfLogoUrl2; 25 | 26 | opsDiv.addEventListener("click", function (event) { 27 | event.stopPropagation(); 28 | postMessage({ 29 | eventName: "idf_cs_request__show_options_dialog", 30 | payload: { 31 | doctype: doctype, 32 | fieldname: field.df.fieldname 33 | } 34 | }); 35 | }); 36 | if (field.wrapper.firstElementChild) { 37 | // checkbox fields 38 | if (field.wrapper.firstElementChild.classList.contains("checkbox")) { 39 | const label = field.wrapper.firstElementChild.querySelector("label") 40 | label.appendChild(opsDiv); 41 | // standard fields 42 | } else if (field.wrapper.firstElementChild.classList.contains("form-group")) { 43 | const label = field.wrapper.firstElementChild.querySelector(".form-group > .clearfix") 44 | label.appendChild(opsDiv); 45 | } 46 | } 47 | } 48 | 49 | }, 50 | { doctype, docname }, tabId); 51 | } 52 | 53 | function formRefresh(tabId, doctype, docname) { 54 | idfExec(({ doctype, name }) => { 55 | let frm = cur_frm; 56 | 57 | // add force save button 58 | if (!frm.is_dirty() && (frm.doc.docstatus === 0 || frm.doc.docstatus === 1)) { 59 | // temp fix to avoid duplicate buttons 60 | if (document.querySelector(".idf__force-save-btn")) { 61 | document.querySelector(".idf__force-save-btn").remove(); 62 | } 63 | frm.page.add_button(frm.doc.docstatus === 0 ? "Force Save" : "Force Submit", function () { 64 | frm.dirty(); 65 | frm.save_or_update(); 66 | }, { btn_class: "btn-warning idf__force-save-btn" }); 67 | } 68 | 69 | if (frm["idf_inited"]) { 70 | return; 71 | } else { 72 | frm["idf_inited"] = true; 73 | } 74 | 75 | // patch fields 76 | for (let i = 0; i < frm.fields.length; i++) { 77 | let field = frm.fields[i]; 78 | if (!field.wrapper.querySelector) 79 | continue; 80 | 81 | let opsDiv = document.createElement("div"); 82 | opsDiv.style.display = "inline-block" 83 | opsDiv.style.cursor = "pointer"; 84 | opsDiv.style.marginRight = "3px"; 85 | opsDiv.style.marginLeft = "3px"; 86 | opsDiv.innerHTML = window.idfState.idfLogoUrl2; 87 | 88 | opsDiv.addEventListener("click", function (event) { 89 | postMessage({ 90 | eventName: "idf_cs_request__show_options_dialog", 91 | payload: { 92 | doctype: frm.doctype, 93 | fieldname: field.df.fieldname 94 | } 95 | }); 96 | }); 97 | 98 | if (field.wrapper.firstElementChild) { 99 | // checkbox fields 100 | if (field.wrapper.firstElementChild.classList.contains("checkbox")) { 101 | const label = field.wrapper.firstElementChild.querySelector("label") 102 | label.appendChild(opsDiv); 103 | // standard fields 104 | } else if (field.wrapper.firstElementChild.classList.contains("form-group")) { 105 | const label = field.wrapper.firstElementChild.querySelector(".form-group > .clearfix") 106 | label.appendChild(opsDiv); 107 | // table field 108 | } else if (field.wrapper.firstElementChild.classList.contains("control-label")) { 109 | const label = field.wrapper.firstElementChild; 110 | label.appendChild(opsDiv); 111 | // table field v14 112 | } else if (field.wrapper.firstElementChild.classList.contains("grid-field")) { 113 | const label = field.wrapper.firstElementChild; 114 | label.prepend(opsDiv); 115 | } 116 | } 117 | 118 | if (!field.df.is_custom_field) 119 | field.df.is_custom_field = "0"; 120 | if (!field.df.hidden) 121 | field.df.hidden = "0"; 122 | 123 | if (location.pathname.includes("/app/customize-form")) 124 | field.df.old_hidden = field.df.hidden; 125 | 126 | // show all hidden fields & highlight custom fields 127 | if (field.df.hidden === 1) { 128 | //save old hidden value 129 | field.df.old_hidden = 1; 130 | field.df.hidden = 0; 131 | 132 | let control_label = field.wrapper.querySelector(".control-label"); 133 | if (field.df.is_custom_field === 1) { 134 | field.df.label += " (HIDDEN)"; 135 | field.wrapper.style.color = "darksalmon"; 136 | if (control_label) { 137 | control_label.style.color = "darksalmon"; 138 | } 139 | } else { 140 | field.df.label += " (HIDDEN)"; 141 | field.wrapper.style.color = "brown"; 142 | if (control_label) { 143 | control_label.style.color = "brown"; 144 | } 145 | } 146 | } 147 | } 148 | frm.refresh_fields(); 149 | }, 150 | { doctype, docname }, tabId); 151 | } 152 | 153 | function formEvent(tabId, eventName, doctype, docname) { 154 | if (eventName === "refresh") { 155 | formRefresh(tabId, doctype, docname) 156 | } else if (eventName == "form_render") { 157 | formGridRender(tabId, doctype, docname) 158 | } 159 | } 160 | 161 | function idfShowOptionsDialog(args, tabId) { 162 | idfExec((args) => { 163 | let fieldData = frappe.meta.get_docfield(args.doctype, args.fieldname); 164 | 165 | // prepare field info 166 | if (!fieldData.options) { 167 | fieldData.options = ""; 168 | } 169 | 170 | let openDocButtonsHTML = ""; 171 | 172 | if (["Link", "Table", "Table MultiSelect"].includes(fieldData.fieldtype)) { 173 | openDocButtonsHTML = ` 174 | 178 | 182 | `; 183 | } 184 | 185 | var dialog = new frappe.ui.Dialog({ 186 | title: `${window.idfState.idfLogoUrl2} Field Details`, 187 | fields: [{ 188 | label: `Details:`, 189 | fieldname: "tables_options_section", 190 | fieldtype: "Section Break" 191 | }, { 192 | label: `Details:`, 193 | fieldname: "field_details_html", 194 | fieldtype: "HTML", 195 | options: `
196 | 197 |

Name: ${fieldData.fieldname}

198 |

Field No.: ${fieldData.idx}

199 | 200 |

Type: ${fieldData.fieldtype}

201 |

In ListView: ${fieldData.in_list_view}

202 | 203 |
204 |

207 | Options: ${fieldData.options} 208 | ${openDocButtonsHTML} 209 |

210 |

211 |

Is Custom: ${fieldData.is_custom_field}

212 |
213 | 223 | ` 224 | }, { 225 | fieldtype: "Section Break" 226 | }, { 227 | label: "Extra Actions:", 228 | fieldname: "field_options_section", 229 | fieldtype: "Section Break" 230 | }, 231 | { 232 | label: 'Copy Table Data', 233 | fieldname: 'copy_table_data', 234 | fieldtype: 'Button', 235 | click: (val) => { 236 | if (fieldData.fieldtype == "Table") { 237 | let table; 238 | if (dialog.get_value("only_selected_rows")) { 239 | table = fieldData.grid.get_selected_children() 240 | } else { 241 | table = cur_frm.doc[fieldData.fieldname]; 242 | } 243 | postMessage({ 244 | eventName: "idf_cs_request__childtable_save", 245 | payload: table 246 | }); 247 | 248 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Date of table: (${fieldData.fieldname}) saved to browser storage`, 8); 249 | cur_dialog.hide(); 250 | } else { 251 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Field: (${fieldData.fieldname}) is not a table`, 8); 252 | } 253 | } 254 | }, 255 | { 256 | label: 'Only Selected Rows', 257 | fieldname: 'only_selected_rows', 258 | fieldtype: 'Check', 259 | description: 'if not checked copy all rows' 260 | } 261 | , { 262 | fieldtype: "Column Break" 263 | }, { 264 | label: 'Insert Saved Table Data', 265 | fieldname: 'copy_table_data', 266 | fieldtype: 'Button', 267 | click: (val) => { 268 | if (fieldData.fieldtype == "Table") { 269 | postMessage({ 270 | eventName: "idf_cs_request__childtable_insert", 271 | payload: fieldData.fieldname 272 | }); 273 | 274 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Data of table: (${fieldData.fieldname}) has been inserted`, 8); 275 | cur_dialog.hide(); 276 | } else { 277 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Field: (${fieldData.fieldname}) is not a table`, 8); 278 | } 279 | } 280 | }, // Customize Form 281 | { 282 | fieldtype: "Section Break" 283 | }, { 284 | label: 'Copy Customized Fields', 285 | fieldname: 'copy_customized_fields', 286 | fieldtype: 'Button', 287 | click: (val) => { 288 | if (fieldData.parent == "Customize Form" && fieldData.fieldname == "fields") { 289 | let fields = cur_frm.doc.fields; 290 | let customFields = []; 291 | 292 | for (let i = 0; i < fields.length; i++) { 293 | if (fields[i].is_custom_field === 1) { 294 | fields[i].insert_after_fieldname = fields[i === 0 ? 0 : (i - 1)].fieldname; 295 | customFields.push(fields[i]); 296 | } 297 | } 298 | postMessage({ 299 | eventName: "idf_cs_request__customized_fields_save", 300 | payload: customFields 301 | }); 302 | 303 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Customized Fields saved to browser storage`, 8); 304 | cur_dialog.hide(); 305 | } else { 306 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Only works on (fields) table in Customize Form doctype.`, 8); 307 | } 308 | } 309 | }, { 310 | fieldtype: "Column Break" 311 | }, { 312 | label: 'Insert Saved Customized Fields', 313 | fieldname: 'inser_customized_fields', 314 | fieldtype: 'Button', 315 | click: (val) => { 316 | if (fieldData.parent == "Customize Form" && fieldData.fieldname == "fields") { 317 | postMessage({ 318 | eventName: "idf_cs_request__customized_fields_insert" 319 | }); 320 | 321 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Customized Fields has been inserted`, 8); 322 | cur_dialog.hide(); 323 | } else { 324 | frappe.show_alert(`${window.idfState.idfLogoUrl2} Only works on (fields) table in Customize Form doctype.`, 8); 325 | } 326 | } 327 | }, { 328 | fieldtype: "Section Break" 329 | },], 330 | primary_action_label: 'Done', 331 | primary_action(values) { 332 | cur_dialog.hide(); 333 | } 334 | }); 335 | dialog.show(); 336 | }, 337 | args, tabId); 338 | } 339 | -------------------------------------------------------------------------------- /frappe/listview.js: -------------------------------------------------------------------------------- 1 | function ListviewSetup(tabId) { 2 | idfExec((args) => { 3 | if (cur_list) { 4 | // add bulk edit to list views 5 | // if (cur_list.page.clear_custom_actions) { 6 | // cur_list.page.clear_custom_actions(); 7 | // } 8 | let idfGroup = cur_list.page.add_custom_button_group("IDF") 9 | 10 | // set group btn icon 11 | idfGroup.parent().find("span.custom-btn-group-label").html(window.idfState.idfLogoUrl1); 12 | 13 | // save docs 14 | cur_list.page.add_custom_menu_item(idfGroup, "Save Docs", async () => { 15 | let selected_docs = cur_list.get_checked_items(); 16 | 17 | if (selected_docs.length == 0) { 18 | frappe.show_alert(`${window.idfState.idfLogoUrl1} Please Select at least one row`, 2); 19 | return; 20 | } 21 | 22 | let docs_insert_dialog = new frappe.ui.Dialog({ 23 | title: `${window.idfState.idfLogoUrl1} Saving (${selected_docs.length}) Doc Data of (${cur_list.doctype})`, 24 | fields: [ 25 | { 26 | label: __("Include Cancelled Docs"), 27 | fieldname: "include_cancelled", 28 | fieldtype: "Check", 29 | description: __("Note: Cancelled Docs will be inserted as Draft"), 30 | }, 31 | { fieldtype: "Column Break" }, 32 | { 33 | label: __("Storage Bucket"), 34 | fieldname: "storage_bucket", 35 | fieldtype: "Select", 36 | description: __("Select bucket to store to"), 37 | options: ["General", "Current Doc"], 38 | default: "Current Doc" 39 | }, 40 | { fieldtype: "Section Break" }, 41 | { 42 | label: __("Keep Old"), 43 | fieldname: "keep_old_data", 44 | fieldtype: "Check", 45 | description: __("Append to Old Docs Data"), 46 | }, 47 | { fieldtype: "Column Break" }, 48 | { 49 | label: __("Add to Storage Top"), 50 | fieldname: "add_data_to_top", 51 | fieldtype: "Check", 52 | description: __("Useful if you want later insert this group of data first before the old one"), 53 | depends_on: "eval:doc.keep_old_data==1", 54 | } 55 | ], 56 | primary_action_label: `${window.idfState.idfLogoUrl1} Save Docs`, 57 | primary_action: async function (values) { 58 | let docs = [] 59 | 60 | for (let i = 0; i < selected_docs.length; i++) { 61 | if (!values.include_cancelled && selected_docs[i].docstatus === 2) continue; 62 | 63 | let res = await frappe.call({ 64 | method: "frappe.client.get", 65 | args: { doctype: cur_list.doctype, name: selected_docs[i].name }, 66 | }) 67 | let res_docs = res.message; 68 | 69 | if (res_docs) { 70 | frappe.show_progress( 71 | `${window.idfState.idfLogoUrl1} Fetching DocType Data`, 72 | i + 1, 73 | selected_docs.length, 74 | `Fetching ${selected_docs[i].name}`, 75 | true 76 | ) 77 | docs.push(res_docs) 78 | } 79 | 80 | } 81 | postMessage({ 82 | eventName: "idf_cs_request__save-data", 83 | payload: { 84 | doctype: cur_list.doctype, 85 | data: docs, 86 | bucket: values.storage_bucket, 87 | keepOld: values.keep_old_data, 88 | addToTop: values.add_data_to_top 89 | } 90 | }); 91 | frappe.show_alert(`${window.idfState.idfLogoUrl1} Saved ${docs.length} DocType Data Successfully`) 92 | docs_insert_dialog.hide() 93 | } 94 | }) 95 | docs_insert_dialog.show() 96 | }); 97 | 98 | // insert docs 99 | cur_list.page.add_custom_menu_item(idfGroup, "Insert Docs", () => { 100 | postMessage({ 101 | eventName: "idf_cs_request__listview_show-insert-doc-data-dialog", 102 | payload: { 103 | doctype: cur_list.doctype 104 | } 105 | }); 106 | }); 107 | 108 | // insert docs 109 | cur_list.page.add_custom_menu_item(idfGroup, "CSV Tool", () => { 110 | postMessage({ 111 | eventName: "idf_cs_request__listview_show-csv-tool-dialog", 112 | payload: { 113 | doctype: cur_list.doctype 114 | } 115 | }); 116 | }); 117 | } 118 | } 119 | , {}, tabId); 120 | } 121 | -------------------------------------------------------------------------------- /frappe/papaparse.min.js: -------------------------------------------------------------------------------- 1 | /* @license 2 | Papa Parse 3 | v5.0.2 4 | https://github.com/mholt/PapaParse 5 | License: MIT 6 | */ 7 | !function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var r=(t=t||{}).dynamicTyping||!1;q(r)&&(t.dynamicTypingFunction=r,r={});if(t.dynamicTyping=r,t.transform=!!q(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var i=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(r=f.URL||f.webkitURL||null,i=s.toString(),b.BLOB_URL||(b.BLOB_URL=r.createObjectURL(new Blob(["(",i,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var r,i;return t.onmessage=_,t.id=h++,a[t.id]=t}();return i.userStep=t.step,i.userChunk=t.chunk,i.userComplete=t.complete,i.userError=t.error,t.step=q(t.step),t.chunk=q(t.chunk),t.complete=q(t.complete),t.error=q(t.error),delete t.worker,void i.postMessage({input:e,config:t,workerId:i.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&q(e.read)&&q(e.on)?n=new m(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var i=!1,_=!0,g=",",v="\r\n",n='"',s=n+n,r=!1,a=null;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(g=t.delimiter);("boolean"==typeof t.quotes||Array.isArray(t.quotes))&&(i=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(r=t.skipEmptyLines);"string"==typeof t.newline&&(v=t.newline);"string"==typeof t.quoteChar&&(n=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");a=t.columns}void 0!==t.escapeChar&&(s=t.escapeChar+n)}();var o=new RegExp(U(n),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,r);if("object"==typeof e[0])return u(a||h(e[0]),e,r)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:h(e.data[0])),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],r);throw new Error("Unable to serialize unrecognized input");function h(e){if("object"!=typeof e)return[];var t=[];for(var r in e)t.push(r);return t}function u(e,t,r){var i="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(q(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!q(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){q(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var i;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(i=new XMLHttpRequest,this._config.withCredentials&&(i.withCredentials=this._config.withCredentials),n||(i.onload=y(this._chunkLoaded,this),i.onerror=y(this._chunkError,this)),i.open("GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)i.setRequestHeader(t,e[t])}if(this._config.chunkSize){var r=this._start+this._config.chunkSize-1;i.setRequestHeader("Range","bytes="+this._start+"-"+r)}try{i.send()}catch(e){this._chunkError(e.message)}n&&0===i.status?this._chunkError():this._start+=this._config.chunkSize}},this._chunkLoaded=function(){4===i.readyState&&(i.status<200||400<=i.status?this._chunkError():(this._finished=!this._config.chunkSize||this._start>function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substr(t.lastIndexOf("/")+1))}(i),this.parseChunk(i.responseText)))},this._chunkError=function(e){var t=i.statusText||e;this._sendError(new Error(t))}}function c(e){var i,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((i=new FileReader).onload=y(this._chunkLoaded,this),i.onerror=y(this._chunkError,this)):i=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(i.error)}}function p(e){var r;u.call(this,e=e||{}),this.stream=function(e){return r=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e=this._config.chunkSize,t=e?r.substr(0,e):r;return r=e?r.substr(e):"",this._finished=!r,this.parseChunk(t)}}}function m(e){u.call(this,e=e||{});var t=[],r=!0,i=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){i&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):r=!0},this._streamData=y(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),r&&(r=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=y(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=y(function(){this._streamCleanUp(),i=!0,this._streamData("")},this),this._streamCleanUp=y(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function r(g){var a,o,h,i=Math.pow(2,53),n=-i,s=/^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i,u=/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/,t=this,r=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(q(g.step)){var p=g.step;g.step=function(e){if(c=e,_())m();else{if(m(),0===c.data.length)return;r+=e.data.length,g.preview&&r>g.preview?o.abort():p(c,t)}}}function v(e){return"greedy"===g.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function m(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),g.skipEmptyLines)for(var e=0;e=l.length?"__parsed_extra":l[r]),g.transform&&(s=g.transform(s,n)),s=y(n,s),"__parsed_extra"===n?(i[n]=i[n]||[],i[n].push(s)):i[n]=s}return g.header&&(r>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+r,f+t):r=i.length/2?"\r\n":"\r"}(e,i)),h=!1,g.delimiter)q(g.delimiter)&&(g.delimiter=g.delimiter(e),c.meta.delimiter=g.delimiter);else{var n=function(e,t,r,i,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=L)return R(!0)}else for(g=M,M++;;){if(-1===(g=a.indexOf(O,g+1)))return t||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:M}),w();if(g===i-1)return w(a.substring(M,g).replace(_,O));if(O!==z||a[g+1]!==z){if(O===z||0===g||a[g-1]!==z){var y=E(-1===m?p:Math.min(p,m));if(a[g+1+y]===D){f.push(a.substring(M,g).replace(_,O)),a[M=g+1+y+e]!==O&&(g=a.indexOf(O,M)),p=a.indexOf(D,M),m=a.indexOf(I,M);break}var k=E(m);if(a.substr(g+1+k,n)===I){if(f.push(a.substring(M,g).replace(_,O)),C(g+1+k+n),p=a.indexOf(D,M),g=a.indexOf(O,M),o&&(S(),j))return R();if(L&&h.length>=L)return R(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:M}),g++}}else g++}return w();function b(e){h.push(e),d=M}function E(e){var t=0;if(-1!==e){var r=a.substring(g+1,e);r&&""===r.trim()&&(t=r.length)}return t}function w(e){return t||(void 0===e&&(e=a.substr(M)),f.push(e),M=i,b(f),o&&S()),R()}function C(e){M=e,b(f),f=[],m=a.indexOf(I,M)}function R(e,t){return{data:t||!1?h[0]:h,errors:u,meta:{delimiter:D,linebreak:I,aborted:j,truncated:!!e,cursor:d+(r||0)}}}function S(){A(R(void 0,!0)),h=[],u=[]}function x(e,t,r){var i={nextDelim:void 0,quoteSearch:void 0},n=a.indexOf(O,t+1);if(t" ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('frappe/form.js'); 2 | importScripts('frappe/listview.js') 3 | 4 | 5 | function formTrigger(tabId, eventName, doctype, docname) { 6 | formEvent(tabId, eventName, doctype, docname); 7 | } 8 | 9 | function listviewSetup(tabId) { 10 | ListviewSetup(tabId); 11 | } 12 | 13 | async function saveDocData(doctype, data = [], bucket = "General", keepOld = 0, addToTop = 0) { 14 | 15 | let storageObject = {}; 16 | let storageKey = "storage__doc_data-"; 17 | 18 | if (bucket === "General") { 19 | storageKey += "General"; 20 | } else { 21 | storageKey += doctype; 22 | } 23 | 24 | if (keepOld) { 25 | let oldData = await chrome.storage.local.get(storageKey); 26 | oldData = oldData[storageKey]; 27 | 28 | if (addToTop) { 29 | oldData.unshift(...data); 30 | } else { 31 | oldData.push(...data); 32 | } 33 | 34 | oldData = oldData.filter((v, i, a) => a.findIndex(v2 => (v2.name === v.name)) === i); 35 | storageObject[storageKey] = oldData; 36 | await chrome.storage.local.set(storageObject); 37 | } else { 38 | storageObject[storageKey] = data; 39 | await chrome.storage.local.set(storageObject); 40 | } 41 | 42 | let res = await chrome.storage.local.get(storageKey); 43 | } 44 | 45 | async function showInsertDocDataDialog(doctype, bucket = "Current Doc", tabId) { 46 | let storageKey = "storage__doc_data-"; 47 | 48 | if (bucket === "General") { 49 | storageKey += "General"; 50 | } else { 51 | storageKey += doctype; 52 | } 53 | 54 | var data = await chrome.storage.local.get(storageKey); 55 | var args = { data: data[storageKey], bucket }; 56 | 57 | idfExec((args) => { 58 | if (args.data === undefined || (args.data && args.data.length === 0)) { 59 | frappe.show_alert(`${window.idfState.idfLogoUrl1} No Data Saved`, 2); 60 | } 61 | let doc_insert_dialog = new frappe.ui.Dialog({ 62 | title: `${window.idfState.idfLogoUrl1} Insert (${args.data.length}) Saved Doc Data of (${cur_list.doctype})`, 63 | fields: [ 64 | { 65 | label: __("Dont Submit Records"), 66 | fieldname: "submitted_as_draft", 67 | fieldtype: "Check", 68 | description: __("Insert Submitted Docs as Draft"), 69 | read_only_depends_on: "eval:doc.insert_doc_as_local===1", 70 | default: 0 71 | }, 72 | { fieldtype: "Column Break" }, 73 | { 74 | label: __("Storage Bucket"), 75 | fieldname: "storage_bucket", 76 | fieldtype: "Select", 77 | description: __("Select bucket to fetch from"), 78 | options: ["General", "Current Doc"], 79 | default: args.bucket, 80 | onchange: function (e) { 81 | if (e && e.type == "change") { 82 | this.layout.hide(); 83 | postMessage({ 84 | eventName: "idf_cs_request__listview_show-insert-doc-data-dialog", 85 | payload: { 86 | doctype: cur_list.doctype, 87 | bucket: this.value 88 | } 89 | }); 90 | } 91 | } 92 | }, 93 | { fieldtype: "Section Break" }, 94 | { 95 | label: __("Insert One as Local"), 96 | fieldname: "insert_doc_as_local", 97 | fieldtype: "Check", 98 | description: __("Inserting one doc record localy to edit"), 99 | default: 0, 100 | onchange: function (e) { 101 | if (e && e.type == "change") { 102 | if (this.value) { 103 | // this.layout.set_df_property("submitted_as_draft", "read_only", 1); 104 | // this.layout.set_df_property("stored_doc_name", "hidden", 0); 105 | this.layout.set_value("stored_doc_name", args.data[0].name); 106 | } else { 107 | // this.layout.set_df_property("submitted_as_draft", "read_only", 0); 108 | // this.layout.set_df_property("stored_doc_name", "hidden", 1) 109 | this.layout.set_value("stored_doc_name", ""); 110 | } 111 | } 112 | } 113 | }, 114 | { fieldtype: "Column Break" }, 115 | { 116 | label: __("Stored Doc to Insert"), 117 | fieldname: "stored_doc_name", 118 | fieldtype: "Data", 119 | depends_on: "eval:doc.insert_doc_as_local===1", 120 | read_only: 1, 121 | } 122 | ], 123 | primary_action_label: `${window.idfState.idfLogoUrl1} Insert Docs`, 124 | primary_action: async function (values) { 125 | doc_insert_dialog.hide(); 126 | let inserted_docs = []; 127 | let remaining_docs = args.data.slice(); 128 | 129 | for (let i = 0; i < args.data.length; i++) { 130 | 131 | if (values.insert_doc_as_local) { 132 | let new_doc = frappe.model.copy_doc(args.data[0]); 133 | 134 | frappe.set_route("Form", new_doc.doctype, new_doc.name); 135 | frappe.show_alert(`${window.idfState.idfLogoUrl1} Inserted doc (${args.data[0].name}) As a New Local Doc`); 136 | remaining_docs.splice(0, 1); 137 | 138 | postMessage({ 139 | eventName: "idf_cs_request__save-data", 140 | payload: { 141 | doctype: cur_list.doctype, 142 | data: remaining_docs, 143 | bucket: args.bucket 144 | } 145 | }); 146 | return; 147 | } 148 | 149 | frappe.show_progress(`${window.idfState.idfLogoUrl1} Inserting Data`, i + 1, args.data.length, `Inserting ${args.data[i].name}`, true); 150 | 151 | let doc_exists = await frappe.db.exists(cur_list.doctype, args.data[i].name); 152 | 153 | if (doc_exists) { 154 | let exist_doc_values = await new Promise((resolve, reject) => { 155 | let exist_doc_dialog = new frappe.ui.Dialog({ 156 | title: __(`${window.idfState.idfLogoUrl1} Please Select Action`), 157 | fields: [{ 158 | label: __("Info:"), 159 | fieldname: "info_data", 160 | fieldtype: "Data", 161 | read_only: 1 162 | }, { 163 | fieldtype: "Section Break" 164 | }, { 165 | label: __("Skip This Doc"), 166 | fieldname: "skip_doc", 167 | fieldtype: "Check", 168 | default: 1, 169 | onchange: function (e) { 170 | if (e && e.type == "change") { 171 | if (this.value) { 172 | this.layout.set_value("insert_new_name", 0); 173 | this.layout.set_df_property("new_name", "read_only", 1); 174 | } else { 175 | this.layout.set_value("insert_new_name", 1); 176 | 177 | } 178 | } 179 | } 180 | }, { 181 | fieldtype: "Column Break" 182 | }, { 183 | label: __("Insert with New Name"), 184 | fieldname: "insert_new_name", 185 | fieldtype: "Check", 186 | onchange: function (e) { 187 | if (e && e.type == "change") { 188 | if (this.value) { 189 | this.layout.set_value("skip_doc", 0); 190 | this.layout.set_df_property("new_name", "read_only", 0); 191 | } else { 192 | this.layout.set_value("skip_doc", 1); 193 | this.layout.set_df_property("new_name", "read_only", 1); 194 | } 195 | } 196 | } 197 | }, { 198 | fieldtype: "Section Break" 199 | }, { 200 | label: __("New Name"), 201 | fieldname: "new_name", 202 | fieldtype: "Data", 203 | description: __("Note: System may force auto unique naming, this depend on your config"), 204 | read_only: 1 205 | }], 206 | primary_action_label: __("Apply"), 207 | primary_action: function (values) { 208 | if (this.get_value("insert_new_name") && this.get_value("new_name") === args.data[i].name) { 209 | frappe.show_alert(`${window.idfState.idfLogoUrl1} Please select a new name`); 210 | return; 211 | } 212 | resolve(values); 213 | } 214 | }); 215 | exist_doc_dialog.show(); 216 | exist_doc_dialog.set_value("new_name", args.data[i].name) 217 | exist_doc_dialog.set_value("info_data", `Doc: (${args.data[i].name}) Already Exist`); 218 | }) 219 | 220 | cur_dialog.hide(); 221 | frappe.hide_progress(); 222 | 223 | if (exist_doc_values.skip_doc) { 224 | continue 225 | } else if (exist_doc_values.insert_new_name) { 226 | // args.data[i]["__newname"] = exist_doc_values.new_name; 227 | args.data[i]["name"] = exist_doc_values.new_name; 228 | } 229 | } 230 | 231 | if (values.submitted_as_draft) { 232 | args.data[i]["docstatus"] = 0; 233 | args.data[i]["status"] = null; 234 | } 235 | if (args.data[i]["docstatus"] === 2) { 236 | args.data[i]["docstatus"] = 0 237 | } 238 | let res = null; 239 | try { 240 | 241 | res = await frappe.call({ 242 | method: "frappe.client.insert", 243 | args: { 244 | doc: args.data[i] 245 | } 246 | }); 247 | 248 | inserted_docs.push(args.data[i]); 249 | remaining_docs.splice(i, i + 1); 250 | } catch (e) { 251 | // handle insertion errors 252 | let insertion_error_dialog_values = await new Promise((resolve, reject) => { 253 | let insertion_error_dialog = new frappe.ui.Dialog({ 254 | title: __(`${window.idfState.idfLogoUrl1} Insertion Error`), 255 | fields: [ 256 | { 257 | label: __("Sugessted Solutions"), 258 | fieldtype: "Section Break" 259 | }, { 260 | label: __("Skip This Doc"), 261 | fieldname: "ed_skip_doc", 262 | fieldtype: "Check", 263 | default: 1, 264 | change: function () { 265 | if (this.value) { 266 | this.layout.set_value("ed_stop_operation", 0); 267 | this.layout.set_value("ed_retry_current_operation", 0); 268 | this.layout.set_value("ed_insert_as_local_doc", 0); 269 | 270 | 271 | } 272 | 273 | } 274 | }, 275 | { fieldtype: "Column Break" }, 276 | { 277 | label: __("Stop the Operation"), 278 | fieldname: "ed_stop_operation", 279 | fieldtype: "Check", 280 | default: 0, 281 | change: function () { 282 | if (this.value) { 283 | this.layout.set_value("ed_skip_doc", 0); 284 | this.layout.set_value("ed_retry_current_operation", 0); 285 | this.layout.set_value("ed_insert_as_local_doc", 0); 286 | 287 | } 288 | } 289 | }, 290 | { fieldtype: "Section Break" }, 291 | { 292 | label: __("Try Inserting as Local Doc"), 293 | fieldname: "ed_insert_as_local_doc", 294 | fieldtype: "Check", 295 | change: function () { 296 | if (this.value) { 297 | this.layout.set_value("ed_stop_operation", 0); 298 | this.layout.set_value("ed_retry_current_operation", 0); 299 | this.layout.set_value("ed_skip_doc", 0); 300 | } 301 | } 302 | }, 303 | { fieldtype: "Column Break" }, 304 | { 305 | label: __("Retry Current Operation"), 306 | fieldname: "ed_retry_current_operation", 307 | fieldtype: "Check", 308 | change: function () { 309 | if (this.value) { 310 | this.layout.set_value("ed_stop_operation", 0); 311 | this.layout.set_value("ed_insert_as_local_doc", 0); 312 | this.layout.set_value("ed_skip_doc", 0); 313 | } 314 | } 315 | }, { 316 | label: __("Extra Details"), 317 | fieldtype: "Section Break" 318 | }, 319 | { 320 | label: __("Error Doc"), 321 | fieldname: "ed_insertion_error_doc_name", 322 | fieldtype: "Data", 323 | read_only: 1 324 | }, { 325 | label: __("Error Reason"), 326 | fieldname: "ed_insertion_error_traceback", 327 | fieldtype: "Small Text", 328 | read_only: 1 329 | } 330 | ], 331 | primary_action_label: __("Apply"), 332 | primary_action: (values) => resolve(values) 333 | }); 334 | 335 | insertion_error_dialog.show(); 336 | insertion_error_dialog.wrapper.find(".form-section:nth-child(2)"). 337 | css("border-top", "0px").css("margin-top", "-20px"); 338 | 339 | insertion_error_dialog.add_custom_action("Help", () => { 340 | frappe.msgprint( 341 | __(`
342 | Some DocTypes depend on link fields which may not exist on your destination site, 343 | For example DocTypes amended from others or return Sales Invoices which has return aginst, etc 344 | One Solution to solve this problem is by trying to insert the required docs first which then will solve them problem. 345 | 346 |

347 | To insert the required docs data follow steps: 348 |

349 | 350 | When saving docs select the General Bucket then copy your required docs
351 | and whenever you get insertion error due to required docs select (Stop Operation) 352 | And go to the source site and save the required doc data with the options (Keep Old) 353 | & (Add to Storage Top) both selected then you can go back to the destination site 354 | and try to insert again, this time the required docs will be inserted first. 355 |
`), 356 | __(`${window.idfState.idfLogoUrl1} Insertion Error`) 357 | ) 358 | }); 359 | 360 | setTimeout(() => { 361 | frappe.hide_progress(); 362 | frappe.hide_msgprint(); 363 | }, 1000); 364 | 365 | if (e.responseJSON._server_messages) { 366 | let errorMessage = JSON.parse(JSON.parse(e.responseJSON._server_messages)[0]).message 367 | insertion_error_dialog.set_value("ed_insertion_error_traceback", errorMessage); 368 | } 369 | insertion_error_dialog.set_value("ed_insertion_error_doc_name", args.data[i].name); 370 | 371 | }) 372 | 373 | cur_dialog.hide(); 374 | 375 | if (insertion_error_dialog_values.ed_retry_current_operation) { 376 | cur_dialog.hide(); 377 | i -= 1; continue; 378 | } 379 | else if (insertion_error_dialog_values.ed_insert_as_local_doc) { 380 | let new_doc = frappe.model.copy_doc(args.data[i]); 381 | 382 | frappe.set_route("Form", new_doc.doctype, new_doc.name); 383 | 384 | frappe.show_alert(`${window.idfState.idfLogoUrl1} Inserted doc (${args.data[i].name}) As a New Local Doc`); 385 | remaining_docs.splice(i, i + 1); 386 | 387 | postMessage({ 388 | eventName: "idf_cs_request__save-data", 389 | payload: { 390 | doctype: cur_list.doctype, 391 | data: remaining_docs, 392 | bucket: args.bucket 393 | } 394 | }); 395 | 396 | return; 397 | } else if (insertion_error_dialog_values.ed_skip_doc) { 398 | postMessage({ 399 | eventName: "idf_cs_request__save-data", 400 | payload: { 401 | doctype: cur_list.doctype, 402 | data: remaining_docs, 403 | bucket: args.bucket 404 | } 405 | }); 406 | continue; 407 | } else if (insertion_error_dialog_values.ed_stop_operation) { 408 | cur_dialog.hide(); 409 | return; 410 | } 411 | } 412 | } 413 | 414 | postMessage({ 415 | eventName: "idf_cs_request__save-data", 416 | payload: { 417 | doctype: cur_list.doctype, 418 | data: remaining_docs, 419 | bucket: args.bucket 420 | } 421 | }); 422 | if (inserted_docs.length > 0 && args.bucket == "General") { 423 | let insertion_report = ""; 424 | 425 | for (let insert_doc of inserted_docs) { 426 | insertion_report += `(DocType: ${insert_doc.doctype} | DocName: ${insert_doc.name})
`; 427 | } 428 | frappe.msgprint( 429 | insertion_report, 430 | __(`${window.idfState.idfLogoUrl1} General Bucket Insert info`) 431 | ) 432 | } 433 | frappe.show_alert(`${window.idfState.idfLogoUrl1} Inserted (${inserted_docs.length}) DocType Data Successfully`); 434 | } 435 | }) 436 | doc_insert_dialog.show(); 437 | doc_insert_dialog.set_df_property("stored_doc_name", "hidden", 1); 438 | } 439 | , args, tabId); 440 | } 441 | 442 | async function showCSVToolDialog(payload, tabId) { 443 | let args = {} 444 | idfExec((args) => { 445 | let csv_tool_dialog = new frappe.ui.Dialog({ 446 | title: `${window.idfState.idfLogoUrl1} CSV Tool`, 447 | fields: [ 448 | { label: __("Doctype"), fieldtype: "Data", fieldname: "current_doctype", read_only: 1, default: cur_list.doctype }, 449 | { fieldtype: "Column Break" }, 450 | { 451 | label: __("Get Import Template"), 452 | fieldtype: "Button", 453 | description: __("Note: Child Table Fields not supported when importing with this tool"), 454 | click: () => { 455 | frappe.new_doc("Data Import", { 456 | reference_doctype: csv_tool_dialog.get_field("current_doctype").value, 457 | import_type: __("Update Existing Records") 458 | }, () => { 459 | setTimeout(() => { 460 | cur_frm.script_manager.trigger("download_template") 461 | }, 2000) 462 | }) 463 | 464 | 465 | } 466 | }, 467 | { label: __("Data Import"), fieldtype: "Section Break" }, 468 | { label: __("Import Type"), fieldtype: "Select", options: ["Create Records", "Update Records"], default: "Update Records", read_only: 1 }, 469 | { 470 | fieldtype: "HTML", 471 | fieldname: "file_type", 472 | options: ` 473 |
474 | 475 | 476 |
477 | ` 478 | }, 479 | ], 480 | primary_action_label: `${window.idfState.idfLogoUrl1} Apply`, 481 | primary_action: async function (values) { 482 | csv_tool_dialog.hide(); 483 | let csvFile = document.querySelector("#formFileLg").files[0]; 484 | postMessage({ 485 | eventName: "idf_cs_request__csv-tool-read_file", 486 | payload: { 487 | doctype: cur_list.doctype, 488 | docfields: frappe.meta.get_docfields(cur_list.doctype).map(df => { 489 | return { label: df.label, fieldname: df.fieldname } 490 | }), 491 | file: csvFile 492 | } 493 | }); 494 | frappe.show_alert(`${window.idfState.idfLogoUrl1} in progress, pelase wait...`); 495 | } 496 | }) 497 | csv_tool_dialog.show(); 498 | } 499 | , args, tabId); 500 | } 501 | 502 | async function handleCSVToolBulkUpdate(docs, tabId) { 503 | const args = { docs: docs } 504 | 505 | idfExec((args) => { 506 | frappe.call({ 507 | method: "frappe.client.bulk_update", 508 | args: { 509 | docs: args.docs 510 | }, 511 | callback: function (r) { 512 | console.log(r.message); 513 | frappe.show_alert(`${window.idfState.idfLogoUrl1} Imports (${args.docs.length}), Fails(${r.message.failed_docs.length})`); 514 | } 515 | }) 516 | } 517 | , args, tabId); 518 | 519 | } 520 | 521 | async function saveChildTableData(payload) { 522 | await chrome.storage.local.set({ 523 | "storage__childtable_data": payload 524 | }); 525 | } 526 | 527 | async function insertChildtableData(fieldname, tabId) { 528 | var childData = await chrome.storage.local.get('storage__childtable_data'); 529 | var args = { 530 | fieldname, 531 | childData: childData.storage__childtable_data 532 | } 533 | 534 | idfExec((args) => { 535 | cur_frm.clear_table(args.fieldname); 536 | for (row of args.childData) { 537 | // skip insertion of fields 538 | delete row["creation"]; 539 | delete row["modified"]; 540 | delete row["modified_by"]; 541 | delete row["name"]; 542 | delete row["owner"]; 543 | delete row["parent"]; 544 | delete row["parentfield"]; 545 | delete row["parenttype"]; 546 | 547 | cur_frm.add_child(args.fieldname, row); 548 | } 549 | cur_frm.refresh_field(args.fieldname); 550 | } 551 | , args, tabId); 552 | } 553 | 554 | // Customization 555 | async function saveCustomizedFields(payload) { 556 | await chrome.storage.local.set({ 557 | "storage__customized_fields_data": payload 558 | }); 559 | } 560 | 561 | async function insertCustomizedFields(tabId) { 562 | var savedData = await chrome.storage.local.get('storage__customized_fields_data'); 563 | var args = { 564 | savedData: savedData.storage__customized_fields_data 565 | } 566 | 567 | idfExec((args) => { 568 | let rows = args.savedData; 569 | 570 | for (let i = 0; i < rows.length; i++) { 571 | delete rows[i]["name"]; 572 | 573 | let nr = cur_frm.add_child("fields", rows[i]); 574 | let new_index = cur_frm.doc.fields.findIndex(o => o.fieldname === rows[i].insert_after_fieldname) + 1 575 | let new_row = cur_frm.doc.fields.splice(cur_frm.doc.fields.length - 1, 1); 576 | 577 | cur_frm.doc.fields.splice(new_index, 0, new_row[0]); 578 | nr.idx = new_index + 1; 579 | 580 | // update custom rows highlights 581 | for (let i = 0; i < cur_frm.doc.fields.length; i++) { 582 | if (cur_frm.doc.fields[i].is_custom_field === 1) { 583 | let grid_rows = cur_frm.grids[0].wrapper.querySelector(".form-grid > .grid-body > .rows"); 584 | if (grid_rows.childNodes[i]) { 585 | grid_rows.childNodes[i].firstChild.classList.add("highlight"); 586 | } 587 | } 588 | } 589 | } 590 | cur_frm.refresh_fields(); 591 | } 592 | , args, tabId); 593 | } 594 | 595 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 596 | if (changeInfo.status == 'complete' && tab.active) { 597 | let args = {} 598 | args.idfLogoUrl1 = ` 599 | `; 601 | args.idfLogoUrl2 = ` 602 | `; 603 | 604 | idfExec((args) => { 605 | // init idf state 606 | if (!window["idfState"]) { 607 | window["idfState"] = { 608 | pageInit: false, 609 | idfLogoUrl1: args.idfLogoUrl1, 610 | idfLogoUrl2: args.idfLogoUrl2 611 | } 612 | } 613 | 614 | // perfect & official way to detect frappe form events and run scripts 615 | // patch script manager to listen for frappe forms onload, refresh etc 616 | if (!frappe.ui.form.ScriptManager) return; 617 | 618 | if (!window.idfState.pageInit) { 619 | window.idfState.pageInit = true; 620 | 621 | // hooking frappe script manager to add forms scripting features 622 | let oriTrigger = frappe.ui.form.ScriptManager.prototype.trigger; 623 | frappe.ui.form.ScriptManager.prototype.trigger = function (...args) { 624 | setTimeout(() => { 625 | postMessage({ 626 | eventName: "idf_cs_request__form_trigger", 627 | payload: args 628 | }); 629 | } 630 | , 100); 631 | return oriTrigger.call(this, ...args); 632 | } 633 | // hooking frappe setup_view to add listview scripting features 634 | let oriViewSetup = frappe.views.ListView.prototype.setup_view; 635 | frappe.views.ListView.prototype.setup_view = function (...args) { 636 | setTimeout(() => { 637 | postMessage({ 638 | eventName: "idf_cs_request__listview_setup", 639 | payload: args 640 | }); 641 | } 642 | , 500); 643 | return oriViewSetup.call(this, ...args); 644 | } 645 | } 646 | }, 647 | args, tab.id); 648 | } 649 | }) 650 | 651 | // listen for content script messages 652 | chrome.runtime.onMessage.addListener(async (event, sender, sendResponse) => { 653 | const tabId = sender.tab.id; 654 | // console.log("BG: ", event.eventName); 655 | switch (event.eventName) { 656 | case "idf_bg_request__form_trigger": 657 | formTrigger(tabId, ...event.payload); 658 | break; 659 | case "idf_bg_request__listview_setup": 660 | listviewSetup(tabId); 661 | break; 662 | case "idf_bg_request__show_options_dialog": 663 | idfShowOptionsDialog(event.payload, tabId); 664 | break; 665 | case "idf_bg_request__save-data": 666 | saveDocData( 667 | event.payload.doctype, 668 | event.payload.data, 669 | event.payload.bucket, 670 | event.payload.keepOld, 671 | event.payload.addToTop 672 | ); 673 | break; 674 | case "idf_bg_request__listview_show-insert-doc-data-dialog": 675 | showInsertDocDataDialog(event.payload.doctype, event.payload.bucket, tabId); 676 | break; 677 | case "idf_bg_request__listview_show-csv-tool-dialog": 678 | showCSVToolDialog(event.payload, tabId); 679 | break; 680 | case "idf_bg_request__csv-tool-bulk_update": 681 | handleCSVToolBulkUpdate(event.payload, tabId); 682 | case "idf_bg_request__childtable_save": 683 | saveChildTableData(event.payload); 684 | break; 685 | case "idf_bg_request__childtable_insert": 686 | insertChildtableData(event.payload, tabId); 687 | break; 688 | case "idf_bg_request__customized_fields_save": 689 | saveCustomizedFields(event.payload); 690 | break; 691 | case "idf_bg_request__customized_fields_insert": 692 | insertCustomizedFields(tabId); 693 | break; 694 | } 695 | }); 696 | 697 | // execute functions in page 698 | async function idfExec(handler, args, tabId) { 699 | const ret = await chrome.scripting.executeScript({ 700 | target: { 701 | tabId: tabId 702 | }, 703 | func: handler, 704 | args: [args], 705 | world: "MAIN" 706 | }); 707 | return ret; 708 | } 709 | --------------------------------------------------------------------------------