From 8f2c48c3df6c349072724b49775945f390012bc5 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 10 Jul 2018 19:05:43 +0530 Subject: [PATCH] Modal - Simplify modal plugin - Support multiple stacked modals in a modal container - Add formModal plugin --- package.json | 2 +- ui/components/Form/Form.vue | 15 +- ui/components/Form/FormActions.vue | 31 ++- ui/components/Form/FormLayout.vue | 18 +- ui/components/Modal.vue | 70 ------- ui/components/Modal/Modal.vue | 51 +++++ ui/components/Modal/ModalContainer.vue | 64 +++++++ ui/components/Modal/plugin.js | 25 +++ ui/components/controls/Link.vue | 255 +++++++++++++------------ ui/plugins/formModal.js | 27 +++ ui/plugins/modal.js | 61 ------ 11 files changed, 345 insertions(+), 274 deletions(-) delete mode 100644 ui/components/Modal.vue create mode 100644 ui/components/Modal/Modal.vue create mode 100644 ui/components/Modal/ModalContainer.vue create mode 100644 ui/components/Modal/plugin.js create mode 100644 ui/plugins/formModal.js delete mode 100644 ui/plugins/modal.js diff --git a/package.json b/package.json index 1c4555fc..552803eb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "eslint": "^4.19.1", "express": "^4.16.2", "flatpickr": "^4.3.2", - "frappe-datatable": "^1.1.1", + "frappe-datatable": "^1.1.2", "jquery": "^3.3.1", "jwt-simple": "^0.5.1", "luxon": "^1.0.0", diff --git a/ui/components/Form/Form.vue b/ui/components/Form/Form.vue index 2d0aabda..3466ed13 100644 --- a/ui/components/Form/Form.vue +++ b/ui/components/Form/Form.vue @@ -2,10 +2,7 @@
@@ -38,7 +35,6 @@ export default { docLoaded: false, notFound: false, invalid: false, - isDirty: false, invalidFields: [] } }, @@ -48,21 +44,12 @@ export default { }, shouldRenderForm() { return this.name && this.docLoaded; - }, - formTitle() { - if (this.doc._notInserted) { - return _('New {0}', _(this.doctype)); - } - return this.doc[this.meta.titleField]; } }, async created() { if (!this.name) return; try { this.doc = await frappe.getDoc(this.doctype, this.name); - this.doc.on('change', () => { - this.isDirty = this.doc._dirty; - }); if (this.doc._notInserted && this.meta.fields.map(df => df.fieldname).includes('name')) { // For a user editable name field, diff --git a/ui/components/Form/FormActions.vue b/ui/components/Form/FormActions.vue index 46b963fc..b39b3cb9 100644 --- a/ui/components/Form/FormActions.vue +++ b/ui/components/Form/FormActions.vue @@ -1,11 +1,38 @@ diff --git a/ui/components/Form/FormLayout.vue b/ui/components/Form/FormLayout.vue index b7b51290..ca5703fa 100644 --- a/ui/components/Form/FormLayout.vue +++ b/ui/components/Form/FormLayout.vue @@ -7,7 +7,7 @@
df.fieldname === fieldname); }, - fieldIsNotHidden(fieldname) { - return !Boolean(this.getDocField(fieldname).hidden); + shouldRenderField(fieldname) { + const hidden = Boolean(this.getDocField(fieldname).hidden); + + if (hidden) { + return false; + } + + if (fieldname === 'name' && !this.doc._notInserted) { + return false; + } + + return true; }, updateDoc(fieldname, value) { this.doc.set(fieldname, value); diff --git a/ui/components/Modal.vue b/ui/components/Modal.vue deleted file mode 100644 index ad9dbaf7..00000000 --- a/ui/components/Modal.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/ui/components/Modal/Modal.vue b/ui/components/Modal/Modal.vue new file mode 100644 index 00000000..7d2ba527 --- /dev/null +++ b/ui/components/Modal/Modal.vue @@ -0,0 +1,51 @@ + + + diff --git a/ui/components/Modal/ModalContainer.vue b/ui/components/Modal/ModalContainer.vue new file mode 100644 index 00000000..3628a599 --- /dev/null +++ b/ui/components/Modal/ModalContainer.vue @@ -0,0 +1,64 @@ + + diff --git a/ui/components/Modal/plugin.js b/ui/components/Modal/plugin.js new file mode 100644 index 00000000..ec0930d2 --- /dev/null +++ b/ui/components/Modal/plugin.js @@ -0,0 +1,25 @@ +import ModalContainer from './ModalContainer'; + +const Plugin = { + install (Vue) { + + this.event = new Vue(); + + Vue.prototype.$modal = { + show(...args) { + Plugin.modalContainer.add(...args); + }, + + hide(id) { + Plugin.event.$emit('hide', id); + } + } + + // create modal container + const div = document.createElement('div'); + document.body.appendChild(div); + new Vue({ render: h => h(ModalContainer) }).$mount(div); + } +} + +export default Plugin; \ No newline at end of file diff --git a/ui/components/controls/Link.vue b/ui/components/controls/Link.vue index 0621d504..09e6831f 100644 --- a/ui/components/controls/Link.vue +++ b/ui/components/controls/Link.vue @@ -8,134 +8,145 @@ import Form from '../Form/Form'; import { _ } from 'frappejs/utils'; export default { - extends: Autocomplete, - watch: { - value(newValue) { - this.$refs.input.value = newValue; - } + extends: Autocomplete, + watch: { + value(newValue) { + this.$refs.input.value = newValue; + } + }, + methods: { + async getList(query) { + const list = await frappe.db.getAll({ + doctype: this.getTarget(), + filters: { + keywords: ['like', query] + }, + fields: ['name'], + limit: 50 + }); + + const plusIcon = feather.icons.plus.toSvg({ + class: 'm-1', + width: 16, + height: 16 + }); + + return list + .map(d => ({ + label: d.name, + value: d.name + })) + .concat({ + label: plusIcon + ' New ' + this.getTarget(), + value: '__newItem' + }); }, - methods: { - async getList(query) { - const list = await frappe.db.getAll({ - doctype: this.getTarget(), - filters: { - keywords: ["like", query] - }, - fields: ['name'], - limit: 50 - }); - - const plusIcon = feather.icons.plus.toSvg({ - class: 'm-1', - width: 16, - height: 16 - }); - - return list - .map(d => ({ - label: d.name, - value: d.name - })) - .concat({ - label: plusIcon + ' New ' + this.getTarget(), - value: '__newItem' - }) - }, - getWrapperElement(h) { - return h('div', { - class: ['form-group', ...this.wrapperClass], - attrs: { - 'data-fieldname': this.docfield.fieldname - } - }, [ - this.getLabelElement(h), - this.getInputElement(h), - this.getFollowLink(h) - ]); - }, - getFollowLink(h) { - const doctype = this.getTarget(); - const name = this.value; - - if (!name) { - return null; + getWrapperElement(h) { + return h( + 'div', + { + class: ['form-group', ...this.wrapperClass], + attrs: { + 'data-fieldname': this.docfield.fieldname } + }, + [ + this.getLabelElement(h), + this.getInputElement(h), + this.getFollowLink(h) + ] + ); + }, + getFollowLink(h) { + const doctype = this.getTarget(); + const name = this.value; - return h(FeatherIcon, { - props: { - name: 'arrow-right-circle' - }, - class: ['text-muted'], - style: { - position: 'absolute', - right: '8px', - bottom: '4px', - cursor: 'pointer' - }, - nativeOn: { - click: () => { - this.$router.push(`/edit/${doctype}/${name}`); + if (!name) { + return null; + } + + return h(FeatherIcon, { + props: { + name: 'arrow-right-circle' + }, + class: ['text-muted'], + style: { + position: 'absolute', + right: '8px', + bottom: '4px', + cursor: 'pointer' + }, + nativeOn: { + click: () => { + this.$router.push(`/edit/${doctype}/${name}`); + } + } + }); + }, + getTarget() { + return this.docfield.target; + }, + sort() { + return (a, b) => { + if (a.value === '__newItem') { + return 1; + } + if (b.value === '___newItem') { + return -1; + } + if (a.value === b.value) { + return 0; + } + if (a.value < b.value) { + return -1; + } + if (a.value > b.value) { + return 1; + } + }; + }, + filter() { + return (suggestion, txt) => { + if (suggestion.value === '__newItem') { + return true; + } + return Awesomplete.FILTER_CONTAINS(suggestion, txt); + }; + }, + bindEvents() { + const input = this.$refs.input; + + input.addEventListener('awesomplete-select', async e => { + // new item action + if (e.text && e.text.value === '__newItem') { + e.preventDefault(); + const newDoc = await frappe.getNewDoc(this.getTarget()); + + this.$formModal.open( + newDoc, + { + defaultValues: { + name: input.value !== '__newItem' ? input.value : null + }, + onClose: () => { + // if new doc was not created + // then reset the input value + if (this.value === '__newItem') { + this.handleChange(''); + } } } + ); + + newDoc.on('afterInsert', data => { + // if new doc was created + // then set the name of the doc in input + this.handleChange(newDoc.name); + this.$formModal.close(); }); - }, - getTarget() { - return this.docfield.target; - }, - sort() { - return (a, b) => { - if (a.value === '__newitem' || b.value === '__newitem') { - return -1; - } - return a.value > b.value; - } - }, - filter() { - return (suggestion, txt) => { - if (suggestion.value === '__newItem') { - return true; - } - return Awesomplete.FILTER_CONTAINS(suggestion, txt); - } - }, - bindEvents() { - const input = this.$refs.input; - - input.addEventListener('awesomplete-select', async (e) => { - // new item action - if (e.text && e.text.value === '__newItem') { - e.preventDefault(); - const newDoc = await frappe.getNewDoc(this.getTarget()); - - this.$modal.show({ - title: _('New {0}', _(newDoc.doctype)), - bodyComponent: Form, - bodyProps: { - doctype: newDoc.doctype, - name: newDoc.name, - defaultValues: { - name: input.value - } - }, - }); - - newDoc.on('afterInsert', (data) => { - // if new doc was created - // then set the name of the doc in input - this.handleChange(newDoc.name); - this.$modal.hide(); - }); - - this.$modal.observable().once('modal.hide', () => { - // if new doc was not created - // then reset the input value - if (this.value === '__newItem') { - this.handleChange(''); - } - }) - } - }) } + }); } -} + } +}; diff --git a/ui/plugins/formModal.js b/ui/plugins/formModal.js new file mode 100644 index 00000000..7a6691ac --- /dev/null +++ b/ui/plugins/formModal.js @@ -0,0 +1,27 @@ +import Form from '../components/Form/Form'; + +export default function installFormModal(Vue) { + + Vue.mixin({ + computed: { + $formModal() { + const open = (doc, options = {}) => { + const { defaultValues = null, onClose = null } = options; + this.$modal.show(Form, { + doctype: doc.doctype, + name: doc.name, + defaultValues, + onClose + }); + } + + const close = () => this.$modal.hide(); + + return { + open, + close + } + } + } + }) +} diff --git a/ui/plugins/modal.js b/ui/plugins/modal.js deleted file mode 100644 index 25088690..00000000 --- a/ui/plugins/modal.js +++ /dev/null @@ -1,61 +0,0 @@ -import Observable from 'frappejs/utils/observable'; - -const Bus = new Observable(); - -export default { - // enable use of this.$modal in every component - // this also keeps only one modal in the DOM at any time - // which is the recommended way by bootstrap - install (Vue) { - Vue.prototype.$modal = { - show(options) { - Bus.trigger('showModal', options); - }, - - hide() { - Bus.trigger('hideModal'); - }, - - observable() { - return Bus; - } - } - - Vue.mixin({ - data() { - return { - registered: false, - modalVisible: false, - modalOptions: {}, - modalListeners: {} - } - }, - - watch: { - modalVisible(value) { - if (value === true) { - Bus.trigger('modal.show'); - } else { - Bus.trigger('modal.hide'); - } - } - }, - - created: function () { - if (this.registered) return; - - Bus.on('showModal', (options = {}) => { - this.modalVisible = true; - this.modalOptions = options; - }); - - Bus.on('hideModal', () => { - this.modalVisible = false; - this.modalOptions = {}; - }); - - this.registered = true; - } - }); - } -} \ No newline at end of file