2
0
mirror of https://github.com/frappe/books.git synced 2025-01-23 07:08:36 +00:00

Merge branch 'master' into master

This commit is contained in:
Faris Ansari 2018-07-12 21:15:43 +05:30 committed by GitHub
commit d102d14448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 642 additions and 316 deletions

View File

@ -1,6 +1,7 @@
const BaseDocument = require('./document');
const frappe = require('frappejs');
const model = require('./index')
const indicatorColor = require('frappejs/ui/constants/indicators');
module.exports = class BaseMeta extends BaseDocument {
constructor(data) {
@ -185,8 +186,8 @@ module.exports = class BaseMeta extends BaseDocument {
this.indicators = {
key: 'submitted',
colors: {
0: 'gray',
1: 'blue'
0: indicatorColor.GRAY,
1: indicatorColor.BLUE
}
}
}
@ -195,17 +196,17 @@ module.exports = class BaseMeta extends BaseDocument {
getIndicatorColor(doc) {
if (frappe.isDirty(this.name, doc.name)) {
return 'orange';
return indicatorColor.ORANGE;
} else {
if (this.indicators) {
let value = doc[this.indicators.key];
if (value) {
return this.indicators.colors[value] || 'gray';
return this.indicators.colors[value] || indicatorColor.GRAY;
} else {
return 'gray';
return indicatorColor.GRAY;
}
} else {
return 'gray';
return indicatorColor.GRAY;
}
}
}

View File

@ -1,3 +1,5 @@
const indicatorColor = require('frappejs/ui/constants/indicators');
module.exports = {
name: "ToDo",
label: "To Do",
@ -14,8 +16,8 @@ module.exports = {
indicators: {
key: 'status',
colors: {
Open: 'gray',
Closed: 'green'
Open: indicatorColor.BLUE,
Closed: indicatorColor.GREEN
}
},
"fields": [

View File

@ -22,7 +22,7 @@
"eslint": "^4.19.1",
"express": "^4.16.2",
"flatpickr": "^4.3.2",
"frappe-datatable": "frappe/datatable",
"frappe-datatable": "^1.1.2",
"jquery": "^3.3.1",
"jwt-simple": "^0.5.1",
"luxon": "^1.0.0",

View File

@ -1,6 +1,6 @@
<template functional>
<button type="button"
:class="['btn btn-sm', 'btn-' + Object.keys(props).find(key => ['primary', 'secondary', 'light', 'dark'].includes(key))]"
:class="['btn btn-sm', 'btn-' + Object.keys(props).find(key => ['primary', 'secondary', 'light', 'dark', 'danger'].includes(key))]"
v-bind="data.attrs"
v-on="listeners"
>

View File

@ -2,11 +2,10 @@
<div class="frappe-form">
<form-actions
v-if="shouldRenderForm"
:doctype="doctype"
:name="name"
:title="formTitle"
:isDirty="isDirty"
:doc="doc"
@save="save"
@submit="submit"
@revert="revert"
/>
<div class="p-3">
<form-layout
@ -28,7 +27,7 @@ import { _ } from 'frappejs/utils';
export default {
name: 'Form',
props: ['doctype', 'name'],
props: ['doctype', 'name', 'defaultValues'],
components: {
FormActions,
FormLayout
@ -38,7 +37,6 @@ export default {
docLoaded: false,
notFound: false,
invalid: false,
isDirty: false,
invalidFields: []
}
},
@ -48,21 +46,26 @@ 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,
// it should be unset since it is autogenerated
this.doc.set('name', '');
}
if (this.defaultValues) {
for (let fieldname in this.defaultValues) {
const value = this.defaultValues[fieldname];
this.doc.set(fieldname, value);
}
}
this.docLoaded = true;
} catch(e) {
this.notFound = true;
@ -88,6 +91,16 @@ export default {
}
},
async submit() {
this.doc.set('submitted', 1);
await this.save();
},
async revert() {
this.doc.set('submitted', 0);
await this.save();
},
onValidate(fieldname, isValid) {
if (!isValid && !this.invalidFields.includes(fieldname)) {
this.invalidFields.push(fieldname);

View File

@ -1,11 +1,59 @@
<template>
<div class="frappe-form-actions d-flex justify-content-between align-items-center p-3 border-bottom">
<h5 class="m-0">{{ title || name }}</h5>
<f-button primary :disabled="!isDirty" @click="$emit('save')">{{ _('Save') }}</f-button>
<h5 class="m-0">{{ title }}</h5>
<f-button primary v-if="isDirty" @click="$emit('save')">{{ _('Save') }}</f-button>
<f-button primary v-if="showSubmit" @click="$emit('submit')">{{ _('Submit') }}</f-button>
<f-button secondary v-if="showRevert" @click="$emit('revert')">{{ _('Revert') }}</f-button>
</div>
</template>
<script>
import frappe from 'frappejs';
export default {
props: ['doctype', 'name', 'title', 'isDirty']
props: ['doc'],
data() {
return {
isDirty: false,
showSubmit: false,
showRevert: false
}
},
created() {
this.doc.on('change', () => {
this.isDirty = this.doc._dirty;
this.updateShowSubmittable();
});
this.updateShowSubmittable();
},
methods: {
updateShowSubmittable() {
this.showSubmit =
this.meta.isSubmittable
&& !this.isDirty
&& !this.doc._notInserted
&& this.doc.submitted === 0;
this.showRevert =
this.meta.isSubmittable
&& !this.isDirty
&& !this.doc._notInserted
&& this.doc.submitted === 1;
}
},
computed: {
meta() {
return frappe.getMeta(this.doc.doctype);
},
title() {
const _ = this._;
if (this.doc._notInserted) {
return _('New {0}', _(this.doc.doctype));
}
const titleField = this.meta.titleField || 'name';
return this.doc[titleField];
}
}
}
</script>

View File

@ -7,22 +7,15 @@
<div class="col" v-for="(column, j) in section.columns" :key="j">
<frappe-control
v-for="fieldname in column.fields"
v-if="shouldRenderField(fieldname)"
:key="fieldname"
:docfield="getDocField(fieldname)"
:value="$data[fieldname]"
:doc="doc"
@change="value => updateDoc(fieldname, value)"
/>
</div>
</div>
<div v-if="!layout">
<frappe-control
v-for="docfield in fields"
:key="docfield.fieldname"
:docfield="docfield"
:value="$data[docfield.fieldname]"
@change="value => updateDoc(docfield.fieldname, value)"
/>
</div>
</form>
</template>
<script>
@ -58,6 +51,19 @@ export default {
getDocField(fieldname) {
return this.fields.find(df => df.fieldname === fieldname);
},
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);
},
@ -70,17 +76,21 @@ export default {
},
computed: {
layoutConfig() {
if (!this.layout) return false;
let layout = this.layout;
let config = this.layout;
if (Array.isArray(config)) {
config = {
sections: config
}
if (!layout) {
const fields = this.fields.map(df => df.fieldname);
layout = [{
columns: [{ fields }]
}];
}
return config;
if (Array.isArray(layout)) {
layout = {
sections: layout
}
}
return layout;
}
}
};

View File

@ -2,8 +2,24 @@
<span :class="['indicator', 'indicator-' + color]"></span>
</template>
<script>
import indicatorColor from 'frappejs/ui/constants/indicators';
export default {
props: ['color']
props: {
color: {
type: String,
required: true,
default: indicatorColor.GREY,
validator(value) {
const validColors = Object.values(indicatorColor);
const valid = validColors.includes(value);
if (!valid) {
console.warn(`color must be one of `, validColors);
}
return valid;
}
}
}
}
</script>
@ -14,11 +30,31 @@ export default {
display: inline-block;
width: 0.5rem;
height: 0.5rem;
background-color: $gray-400;
border-radius: 50%;
}
.indicator-grey {
background-color: $gray-300;
}
.indicator-blue {
background-color: $primary;
background-color: $blue;
}
.indicator-red {
background-color: $red;
}
.indicator-green {
background-color: $green;
}
.indicator-orange {
background-color: $orange;
}
.indicator-purple {
background-color: $purple;
}
.indicator-yellow {
background-color: $yellow;
}
.indicator-black {
background-color: $gray-800;
}
</style>

View File

@ -12,8 +12,12 @@
:isActive="doc.name === $route.params.name"
:isChecked="isChecked(doc.name)"
@clickItem="openForm(doc.name)"
@checkItem="toggleCheck(doc.name)">
{{ doc[meta.titleField || 'name'] }}
@checkItem="toggleCheck(doc.name)"
>
<indicator v-if="hasIndicator" :color="getIndicatorColor(doc)" />
<span class="d-inline-block ml-2">
{{ doc[meta.titleField || 'name'] }}
</span>
</list-item>
</ul>
</div>
@ -27,20 +31,23 @@ export default {
name: 'List',
props: ['doctype', 'filters'],
components: {
ListActions,
ListItem
ListActions,
ListItem
},
data() {
return {
data: [],
checkList: [],
activeItem: ''
}
return {
data: [],
checkList: [],
activeItem: ''
};
},
computed: {
meta() {
return frappe.getMeta(this.doctype);
}
meta() {
return frappe.getMeta(this.doctype);
},
hasIndicator() {
return Boolean(this.meta.indicators);
}
},
created() {
frappe.db.on(`change:${this.doctype}`, () => {
@ -52,21 +59,29 @@ export default {
},
methods: {
async newDoc() {
let doc = await frappe.getNewDoc(this.doctype);
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
let doc = await frappe.getNewDoc(this.doctype);
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
},
async updateList() {
const indicatorField = this.hasIndicator ? this.meta.indicators.key : null;
const fields = [
'name',
indicatorField,
this.meta.titleField,
...this.meta.keywordFields
].filter(Boolean);
const data = await frappe.db.getAll({
doctype: this.doctype,
fields: ['name', ...this.meta.keywordFields, this.meta.titleField],
fields,
filters: this.filters || null
});
this.data = data;
},
openForm(name) {
this.activeItem = name;
this.$router.push(`/edit/${this.doctype}/${name}`);
this.activeItem = name;
this.$router.push(`/edit/${this.doctype}/${name}`);
},
async deleteCheckedItems() {
await frappe.db.deleteMany(this.doctype, this.checkList);
@ -74,31 +89,34 @@ export default {
},
toggleCheck(name) {
if (this.checkList.includes(name)) {
this.checkList = this.checkList.filter(docname => docname !== name);
this.checkList = this.checkList.filter(docname => docname !== name);
} else {
this.checkList = this.checkList.concat(name);
this.checkList = this.checkList.concat(name);
}
},
isChecked(name) {
return this.checkList.includes(name);
},
getIndicatorColor(doc) {
return this.meta.getIndicatorColor(doc);
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../styles/variables";
.list-group-item {
border-left: none;
border-right: none;
border-radius: 0;
border-left: none;
border-right: none;
border-radius: 0;
}
.list-group-item:first-child {
border-top: none;
border-top: none;
}
.list-group-item:not(.active):hover {
background-color: $light;
background-color: $light;
}
</style>

View File

@ -1,70 +0,0 @@
<template>
<div>
<div :class="['modal fade', modalClasses]" :style="{display: show ? 'block' : ''}" id="frappe-modal"
tabindex="-1" role="dialog" aria-labelledby="frappe-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="frappe-modal-label">{{ title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="closeModal">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body modal-height">
<component :is="bodyComponent" v-bind="bodyProps"/>
</div>
<div class="modal-footer">
<component :is="footerComponent" v-bind="footerProps"/>
<f-button secondary @click="closeModal">Close</f-button>
</div>
</div>
</div>
</div>
<div class="modal-backdrop show" v-show="show" @click="closeModal"></div>
</div>
</template>
<script>
export default {
props: {
show: Boolean,
title: {
type: String,
default: "Modal Title"
},
bodyComponent: {
type: Object,
default: null
},
bodyProps: {
type: Object,
default: null
},
footerComponent: {
type: Object,
default: null
},
footerProps: {
type: Object,
default: null
},
},
computed: {
modalClasses() {
return {
show: this.show
};
}
},
methods: {
closeModal() {
this.$emit('close-modal');
}
}
};
</script>
<style scoped>
.modal-height {
max-height: 80vh;
overflow: auto;
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div :class="['modal fade show d-block']"
tabindex="-1" role="dialog" aria-labelledby="frappe-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content shadow">
<div class="modal-header">
<h5 class="modal-title">{{ title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="closeModal">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body modal-height">
<component :is="component" v-bind="props" v-on="events"/>
</div>
<div class="modal-footer">
<f-button secondary @click="closeModal">{{ _('Close') }}</f-button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "Modal Title"
},
component: {
type: Object
},
props: {
type: Object
},
events: {
type: Object
}
},
methods: {
closeModal() {
this.$emit('close-modal');
}
}
};
</script>
<style scoped>
.modal-height {
max-height: 80vh;
overflow: auto;
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div class="modal-container">
<modal
:key="modal.id"
v-for="modal in modals"
v-bind="modal"
@close-modal="onModalClose(modal.id)"
></modal>
<div class="modal-backdrop show" v-show="modals.length"></div>
</div>
</template>
<script>
import Modal from './Modal';
import Plugin from './plugin';
export default {
name: 'ModalContainer',
components: {
Modal
},
data() {
return {
currentId: 0,
modals: []
}
},
created() {
Plugin.modalContainer = this;
Plugin.event.$on('hide', (id) => {
if (!id) {
console.warn(`id not provided in $modal.hide method, the last modal in the stack will be hidden`);
}
this.onModalClose(id);
});
},
methods: {
add(component, props, events) {
this.currentId++;
this.modals.push({
id: this.currentId,
component,
props,
events
});
return this.currentId;
},
removeModal(id) {
if (!id) {
id = this.currentId;
}
this.currentId--;
this.modals = this.modals.filter(modal => modal.id !== id);
},
onModalClose(id) {
if (id) {
const modal = this.modals.find(modal => modal.id === id);
modal.props.onClose && modal.props.onClose();
}
this.removeModal(id);
}
}
}
</script>

View File

@ -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;

View File

@ -1,8 +1,9 @@
<template>
<div>
<div ref="wrapper" class="datatable-wrapper"></div>
<div class="table-actions mt-1">
<button type="button" @click="addRow" class="btn btn-sm btn-light border">Add Row</button>
<div class="table-actions mt-1" v-if="!disabled">
<f-button danger @click="removeCheckedRows" v-if="checkedRows.length">Remove</f-button>
<f-button light @click="addRow" v-if="!checkedRows.length">Add Row</f-button>
</div>
</div>
</template>
@ -15,10 +16,11 @@ import FrappeControl from './controls/FrappeControl';
import { convertFieldsToDatatableColumns } from 'frappejs/client/ui/utils';
export default {
props: ['doctype', 'rows'],
props: ['doctype', 'rows', 'disabled'],
data() {
return {
docs: this.getRowDocs()
docs: this.getRowDocs(),
checkedRows: []
}
},
computed: {
@ -33,6 +35,13 @@ export default {
layout: 'fluid',
checkboxColumn: true,
checkedRowStatus: false,
events: {
onCheckRow: () => {
this.checkedRows = this.datatable.rowmanager
.getCheckedRows()
.map(i => parseInt(i, 10));
}
},
getEditor: (colIndex, rowIndex, value, parent) => {
let inputComponent = null;
@ -111,13 +120,38 @@ export default {
});
},
getColumns() {
return convertFieldsToDatatableColumns(this.meta.fields);
const fieldsToShow = this.meta.fields.filter(df => !df.hidden);
const columns = convertFieldsToDatatableColumns(fieldsToShow);
if (this.disabled) {
columns.forEach(col => col.editable = false);
}
return columns;
},
addRow() {
const doc = new Observable();
doc.set('idx', this.docs.length);
this.docs.push(doc);
},
removeCheckedRows() {
this.removeRows(this.checkedRows);
this.checkedRows = [];
this.datatable.rowmanager.checkAll(false);
},
removeRows(indices) {
// convert to array
if (!Array.isArray(indices)) {
indices = [indices];
}
// convert string to number
indices = indices.map(i => parseInt(i, 10));
// filter
this.docs = this.docs.filter(doc => !indices.includes(parseInt(doc.idx, 10)));
// recalculate idx
this.docs.forEach((doc, i) => {
doc.set('idx', i);
});
},
emitChange(doc) {
this.$emit('update:rows', this.docs, doc);
}

View File

@ -12,7 +12,8 @@ export default {
onlyInput: {
type: Boolean,
default: false
}
},
disabled: Boolean
},
computed: {
id() {
@ -70,7 +71,8 @@ export default {
type: 'text',
placeholder: '',
value: this.value,
required: this.docfield.required
required: this.docfield.required,
disabled: this.disabled
}
},
getInputListeners() {

View File

@ -1,7 +1,12 @@
<template>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" :id="id" v-model="checkboxValue" @change="emitChange">
<input class="custom-control-input" type="checkbox"
:id="id"
v-model="checkboxValue"
@change="emitChange"
:disabled="disabled"
>
<label class="custom-control-label" :for="id">{{ docfield.label }}</label>
</div>
</div>

View File

@ -1,3 +1,4 @@
<script>
import Base from './Base';

View File

@ -1,19 +1,12 @@
<template>
<div class="form-group" v-if="!onlyInput">
<label>{{ docfield.label }}</label>
<div :class="{'form-group': !onlyInput}">
<label v-if="!onlyInput">{{ docfield.label }}</label>
<flat-pickr
:value="value"
class="form-control"
@on-change="emitChange">
</flat-pickr>
</div>
<flat-pickr
v-else
:value="value"
class="form-control"
@on-change="emitChange"
>
</flat-pickr>
</template>
<script>
import flatPickr from 'vue-flatpickr-component';

View File

@ -38,7 +38,8 @@ export default {
id: this.id,
type: 'file',
value: this.value,
required: this.docfield.required
required: this.docfield.required,
disabled: this.disabled
}
}
}

View File

@ -1,12 +1,15 @@
<template>
<component :is="component"
:docfield="docfield"
:value="value"
:onlyInput="onlyInput"
@change="$emit('change', $event)"
/>
<component
:is="component"
:docfield="docfield"
:value="value"
:onlyInput="onlyInput"
:disabled="isDisabled"
@change="$emit('change', $event)"
/>
</template>
<script>
import Base from './Base';
import Autocomplete from './Autocomplete';
import Check from './Check';
import Code from './Code';
@ -22,31 +25,59 @@ import Password from './Password';
import Select from './Select';
import Table from './Table';
import Text from './Text';
import Time from './Time'
import Time from './Time';
export default {
props: ['docfield', 'value', 'onlyInput'],
computed: {
component() {
return {
Autocomplete,
Check,
Code,
Currency,
Data,
Date,
DynamicLink,
File,
Float,
Int,
Link,
Password,
Select,
Table,
Text,
Time,
}[this.docfield.fieldtype];
}
props: ['docfield', 'value', 'onlyInput', 'doc'],
computed: {
component() {
if (this.docfield.template) {
// for controls with their own template
// create a vue object for it
return {
extends: Base,
render: null,
template: this.docfield.template()
};
}
return {
Autocomplete,
Check,
Code,
Currency,
Data,
Date,
DynamicLink,
File,
Float,
Int,
Link,
Password,
Select,
Table,
Text,
Time
}[this.docfield.fieldtype];
},
isDisabled() {
let disabled = this.docfield.disabled;
if (this.doc && this.doc.submitted) {
disabled = true;
}
if (this.docfield.formula && this.docfield.fieldtype !== 'Table') {
disabled = true;
}
return Boolean(disabled);
}
}
}
};
</script>
<style scoped>
.form-group {
position: relative;
}
</style>

View File

@ -3,87 +3,150 @@ import frappe from 'frappejs';
import feather from 'feather-icons';
import Awesomplete from 'awesomplete';
import Autocomplete from './Autocomplete';
import FeatherIcon from 'frappejs/ui/components/FeatherIcon';
import Form from '../Form/Form';
import { _ } from 'frappejs/utils';
export default {
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'
})
},
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,
},
});
newDoc.on('afterInsert', (data) => {
this.handleChange(newDoc.name);
this.$modal.hide();
});
}
})
}
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'
});
},
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;
}
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();
});
}
});
}
}
};
</script>

View File

@ -10,7 +10,8 @@ export default {
id: this.id,
type: 'password',
value: this.value,
required: this.docfield.required
required: this.docfield.required,
disabled: this.disabled
};
},
}

View File

@ -9,7 +9,8 @@ export default {
getInputAttrs() {
return {
id: this.id,
required: this.docfield.required
required: this.docfield.required,
disabled: this.disabled
};
},
getInputChildren(h) {

View File

@ -3,6 +3,7 @@
<model-table
:doctype="docfield.childtype"
:rows="value"
:disabled="disabled"
@update:rows="emitChange"
/>
</div>

View File

@ -10,7 +10,8 @@ export default {
return {
id: this.id,
required: this.docfield.required,
rows: 3
rows: 3,
disabled: this.disabled
};
},
getDomProps() {

View File

@ -0,0 +1,11 @@
module.exports = {
GRAY: 'grey',
GREY: 'grey',
BLUE: 'blue',
RED: 'red',
GREEN: 'green',
ORANGE: 'orange',
PURPLE: 'purple',
YELLOW: 'yellow',
BLACK: 'black',
}

27
ui/plugins/formModal.js Normal file
View File

@ -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
}
}
}
})
}

View File

@ -1,44 +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');
}
}
Vue.mixin({
data() {
return {
registered: false,
modalVisible: false,
modalOptions: {}
}
},
created: function () {
if (this.registered) return;
Bus.on('showModal', (options = {}) => {
this.modalVisible = true;
this.modalOptions = options;
});
Bus.on('hideModal', () => {
this.modalVisible = false;
});
this.registered = true;
}
});
}
}