mirror of
https://github.com/frappe/books.git
synced 2025-02-02 20:18:26 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
d102d14448
@ -1,6 +1,7 @@
|
|||||||
const BaseDocument = require('./document');
|
const BaseDocument = require('./document');
|
||||||
const frappe = require('frappejs');
|
const frappe = require('frappejs');
|
||||||
const model = require('./index')
|
const model = require('./index')
|
||||||
|
const indicatorColor = require('frappejs/ui/constants/indicators');
|
||||||
|
|
||||||
module.exports = class BaseMeta extends BaseDocument {
|
module.exports = class BaseMeta extends BaseDocument {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
@ -185,8 +186,8 @@ module.exports = class BaseMeta extends BaseDocument {
|
|||||||
this.indicators = {
|
this.indicators = {
|
||||||
key: 'submitted',
|
key: 'submitted',
|
||||||
colors: {
|
colors: {
|
||||||
0: 'gray',
|
0: indicatorColor.GRAY,
|
||||||
1: 'blue'
|
1: indicatorColor.BLUE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,17 +196,17 @@ module.exports = class BaseMeta extends BaseDocument {
|
|||||||
|
|
||||||
getIndicatorColor(doc) {
|
getIndicatorColor(doc) {
|
||||||
if (frappe.isDirty(this.name, doc.name)) {
|
if (frappe.isDirty(this.name, doc.name)) {
|
||||||
return 'orange';
|
return indicatorColor.ORANGE;
|
||||||
} else {
|
} else {
|
||||||
if (this.indicators) {
|
if (this.indicators) {
|
||||||
let value = doc[this.indicators.key];
|
let value = doc[this.indicators.key];
|
||||||
if (value) {
|
if (value) {
|
||||||
return this.indicators.colors[value] || 'gray';
|
return this.indicators.colors[value] || indicatorColor.GRAY;
|
||||||
} else {
|
} else {
|
||||||
return 'gray';
|
return indicatorColor.GRAY;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return 'gray';
|
return indicatorColor.GRAY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
const indicatorColor = require('frappejs/ui/constants/indicators');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "ToDo",
|
name: "ToDo",
|
||||||
label: "To Do",
|
label: "To Do",
|
||||||
@ -14,8 +16,8 @@ module.exports = {
|
|||||||
indicators: {
|
indicators: {
|
||||||
key: 'status',
|
key: 'status',
|
||||||
colors: {
|
colors: {
|
||||||
Open: 'gray',
|
Open: indicatorColor.BLUE,
|
||||||
Closed: 'green'
|
Closed: indicatorColor.GREEN
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fields": [
|
"fields": [
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"flatpickr": "^4.3.2",
|
"flatpickr": "^4.3.2",
|
||||||
"frappe-datatable": "frappe/datatable",
|
"frappe-datatable": "^1.1.2",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
"jwt-simple": "^0.5.1",
|
"jwt-simple": "^0.5.1",
|
||||||
"luxon": "^1.0.0",
|
"luxon": "^1.0.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template functional>
|
<template functional>
|
||||||
<button type="button"
|
<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-bind="data.attrs"
|
||||||
v-on="listeners"
|
v-on="listeners"
|
||||||
>
|
>
|
||||||
|
@ -2,11 +2,10 @@
|
|||||||
<div class="frappe-form">
|
<div class="frappe-form">
|
||||||
<form-actions
|
<form-actions
|
||||||
v-if="shouldRenderForm"
|
v-if="shouldRenderForm"
|
||||||
:doctype="doctype"
|
:doc="doc"
|
||||||
:name="name"
|
|
||||||
:title="formTitle"
|
|
||||||
:isDirty="isDirty"
|
|
||||||
@save="save"
|
@save="save"
|
||||||
|
@submit="submit"
|
||||||
|
@revert="revert"
|
||||||
/>
|
/>
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<form-layout
|
<form-layout
|
||||||
@ -28,7 +27,7 @@ import { _ } from 'frappejs/utils';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Form',
|
name: 'Form',
|
||||||
props: ['doctype', 'name'],
|
props: ['doctype', 'name', 'defaultValues'],
|
||||||
components: {
|
components: {
|
||||||
FormActions,
|
FormActions,
|
||||||
FormLayout
|
FormLayout
|
||||||
@ -38,7 +37,6 @@ export default {
|
|||||||
docLoaded: false,
|
docLoaded: false,
|
||||||
notFound: false,
|
notFound: false,
|
||||||
invalid: false,
|
invalid: false,
|
||||||
isDirty: false,
|
|
||||||
invalidFields: []
|
invalidFields: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -48,21 +46,26 @@ export default {
|
|||||||
},
|
},
|
||||||
shouldRenderForm() {
|
shouldRenderForm() {
|
||||||
return this.name && this.docLoaded;
|
return this.name && this.docLoaded;
|
||||||
},
|
|
||||||
formTitle() {
|
|
||||||
if (this.doc._notInserted) {
|
|
||||||
return _('New {0}', _(this.doctype));
|
|
||||||
}
|
|
||||||
return this.doc[this.meta.titleField];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
if (!this.name) return;
|
if (!this.name) return;
|
||||||
try {
|
try {
|
||||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
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;
|
this.docLoaded = true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.notFound = true;
|
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) {
|
onValidate(fieldname, isValid) {
|
||||||
if (!isValid && !this.invalidFields.includes(fieldname)) {
|
if (!isValid && !this.invalidFields.includes(fieldname)) {
|
||||||
this.invalidFields.push(fieldname);
|
this.invalidFields.push(fieldname);
|
||||||
|
@ -1,11 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="frappe-form-actions d-flex justify-content-between align-items-center p-3 border-bottom">
|
<div class="frappe-form-actions d-flex justify-content-between align-items-center p-3 border-bottom">
|
||||||
<h5 class="m-0">{{ title || name }}</h5>
|
<h5 class="m-0">{{ title }}</h5>
|
||||||
<f-button primary :disabled="!isDirty" @click="$emit('save')">{{ _('Save') }}</f-button>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import frappe from 'frappejs';
|
||||||
|
|
||||||
export default {
|
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>
|
</script>
|
||||||
|
@ -7,22 +7,15 @@
|
|||||||
<div class="col" v-for="(column, j) in section.columns" :key="j">
|
<div class="col" v-for="(column, j) in section.columns" :key="j">
|
||||||
<frappe-control
|
<frappe-control
|
||||||
v-for="fieldname in column.fields"
|
v-for="fieldname in column.fields"
|
||||||
|
v-if="shouldRenderField(fieldname)"
|
||||||
:key="fieldname"
|
:key="fieldname"
|
||||||
:docfield="getDocField(fieldname)"
|
:docfield="getDocField(fieldname)"
|
||||||
:value="$data[fieldname]"
|
:value="$data[fieldname]"
|
||||||
|
:doc="doc"
|
||||||
@change="value => updateDoc(fieldname, value)"
|
@change="value => updateDoc(fieldname, value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@ -58,6 +51,19 @@ export default {
|
|||||||
getDocField(fieldname) {
|
getDocField(fieldname) {
|
||||||
return this.fields.find(df => df.fieldname === 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) {
|
updateDoc(fieldname, value) {
|
||||||
this.doc.set(fieldname, value);
|
this.doc.set(fieldname, value);
|
||||||
},
|
},
|
||||||
@ -70,17 +76,21 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
layoutConfig() {
|
layoutConfig() {
|
||||||
if (!this.layout) return false;
|
let layout = this.layout;
|
||||||
|
|
||||||
let config = this.layout;
|
if (!layout) {
|
||||||
|
const fields = this.fields.map(df => df.fieldname);
|
||||||
if (Array.isArray(config)) {
|
layout = [{
|
||||||
config = {
|
columns: [{ fields }]
|
||||||
sections: config
|
}];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
if (Array.isArray(layout)) {
|
||||||
|
layout = {
|
||||||
|
sections: layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return layout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2,8 +2,24 @@
|
|||||||
<span :class="['indicator', 'indicator-' + color]"></span>
|
<span :class="['indicator', 'indicator-' + color]"></span>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import indicatorColor from 'frappejs/ui/constants/indicators';
|
||||||
|
|
||||||
export default {
|
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>
|
</script>
|
||||||
|
|
||||||
@ -14,11 +30,31 @@ export default {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
background-color: $gray-400;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indicator-grey {
|
||||||
|
background-color: $gray-300;
|
||||||
|
}
|
||||||
.indicator-blue {
|
.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>
|
</style>
|
||||||
|
@ -12,8 +12,12 @@
|
|||||||
:isActive="doc.name === $route.params.name"
|
:isActive="doc.name === $route.params.name"
|
||||||
:isChecked="isChecked(doc.name)"
|
:isChecked="isChecked(doc.name)"
|
||||||
@clickItem="openForm(doc.name)"
|
@clickItem="openForm(doc.name)"
|
||||||
@checkItem="toggleCheck(doc.name)">
|
@checkItem="toggleCheck(doc.name)"
|
||||||
{{ doc[meta.titleField || 'name'] }}
|
>
|
||||||
|
<indicator v-if="hasIndicator" :color="getIndicatorColor(doc)" />
|
||||||
|
<span class="d-inline-block ml-2">
|
||||||
|
{{ doc[meta.titleField || 'name'] }}
|
||||||
|
</span>
|
||||||
</list-item>
|
</list-item>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -27,20 +31,23 @@ export default {
|
|||||||
name: 'List',
|
name: 'List',
|
||||||
props: ['doctype', 'filters'],
|
props: ['doctype', 'filters'],
|
||||||
components: {
|
components: {
|
||||||
ListActions,
|
ListActions,
|
||||||
ListItem
|
ListItem
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
data: [],
|
data: [],
|
||||||
checkList: [],
|
checkList: [],
|
||||||
activeItem: ''
|
activeItem: ''
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
meta() {
|
meta() {
|
||||||
return frappe.getMeta(this.doctype);
|
return frappe.getMeta(this.doctype);
|
||||||
}
|
},
|
||||||
|
hasIndicator() {
|
||||||
|
return Boolean(this.meta.indicators);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
frappe.db.on(`change:${this.doctype}`, () => {
|
frappe.db.on(`change:${this.doctype}`, () => {
|
||||||
@ -52,21 +59,29 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async newDoc() {
|
async newDoc() {
|
||||||
let doc = await frappe.getNewDoc(this.doctype);
|
let doc = await frappe.getNewDoc(this.doctype);
|
||||||
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
|
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
|
||||||
},
|
},
|
||||||
async updateList() {
|
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({
|
const data = await frappe.db.getAll({
|
||||||
doctype: this.doctype,
|
doctype: this.doctype,
|
||||||
fields: ['name', ...this.meta.keywordFields, this.meta.titleField],
|
fields,
|
||||||
filters: this.filters || null
|
filters: this.filters || null
|
||||||
});
|
});
|
||||||
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
},
|
},
|
||||||
openForm(name) {
|
openForm(name) {
|
||||||
this.activeItem = name;
|
this.activeItem = name;
|
||||||
this.$router.push(`/edit/${this.doctype}/${name}`);
|
this.$router.push(`/edit/${this.doctype}/${name}`);
|
||||||
},
|
},
|
||||||
async deleteCheckedItems() {
|
async deleteCheckedItems() {
|
||||||
await frappe.db.deleteMany(this.doctype, this.checkList);
|
await frappe.db.deleteMany(this.doctype, this.checkList);
|
||||||
@ -74,31 +89,34 @@ export default {
|
|||||||
},
|
},
|
||||||
toggleCheck(name) {
|
toggleCheck(name) {
|
||||||
if (this.checkList.includes(name)) {
|
if (this.checkList.includes(name)) {
|
||||||
this.checkList = this.checkList.filter(docname => docname !== name);
|
this.checkList = this.checkList.filter(docname => docname !== name);
|
||||||
} else {
|
} else {
|
||||||
this.checkList = this.checkList.concat(name);
|
this.checkList = this.checkList.concat(name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isChecked(name) {
|
isChecked(name) {
|
||||||
return this.checkList.includes(name);
|
return this.checkList.includes(name);
|
||||||
|
},
|
||||||
|
getIndicatorColor(doc) {
|
||||||
|
return this.meta.getIndicatorColor(doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../../styles/variables";
|
@import "../../styles/variables";
|
||||||
|
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:first-child {
|
.list-group-item:first-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:not(.active):hover {
|
.list-group-item:not(.active):hover {
|
||||||
background-color: $light;
|
background-color: $light;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -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">×</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>
|
|
51
ui/components/Modal/Modal.vue
Normal file
51
ui/components/Modal/Modal.vue
Normal 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">×</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>
|
64
ui/components/Modal/ModalContainer.vue
Normal file
64
ui/components/Modal/ModalContainer.vue
Normal 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>
|
25
ui/components/Modal/plugin.js
Normal file
25
ui/components/Modal/plugin.js
Normal 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;
|
@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div ref="wrapper" class="datatable-wrapper"></div>
|
<div ref="wrapper" class="datatable-wrapper"></div>
|
||||||
<div class="table-actions mt-1">
|
<div class="table-actions mt-1" v-if="!disabled">
|
||||||
<button type="button" @click="addRow" class="btn btn-sm btn-light border">Add Row</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -15,10 +16,11 @@ import FrappeControl from './controls/FrappeControl';
|
|||||||
import { convertFieldsToDatatableColumns } from 'frappejs/client/ui/utils';
|
import { convertFieldsToDatatableColumns } from 'frappejs/client/ui/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['doctype', 'rows'],
|
props: ['doctype', 'rows', 'disabled'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
docs: this.getRowDocs()
|
docs: this.getRowDocs(),
|
||||||
|
checkedRows: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -33,6 +35,13 @@ export default {
|
|||||||
layout: 'fluid',
|
layout: 'fluid',
|
||||||
checkboxColumn: true,
|
checkboxColumn: true,
|
||||||
checkedRowStatus: false,
|
checkedRowStatus: false,
|
||||||
|
events: {
|
||||||
|
onCheckRow: () => {
|
||||||
|
this.checkedRows = this.datatable.rowmanager
|
||||||
|
.getCheckedRows()
|
||||||
|
.map(i => parseInt(i, 10));
|
||||||
|
}
|
||||||
|
},
|
||||||
getEditor: (colIndex, rowIndex, value, parent) => {
|
getEditor: (colIndex, rowIndex, value, parent) => {
|
||||||
|
|
||||||
let inputComponent = null;
|
let inputComponent = null;
|
||||||
@ -111,13 +120,38 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
getColumns() {
|
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() {
|
addRow() {
|
||||||
const doc = new Observable();
|
const doc = new Observable();
|
||||||
doc.set('idx', this.docs.length);
|
doc.set('idx', this.docs.length);
|
||||||
this.docs.push(doc);
|
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) {
|
emitChange(doc) {
|
||||||
this.$emit('update:rows', this.docs, doc);
|
this.$emit('update:rows', this.docs, doc);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,8 @@ export default {
|
|||||||
onlyInput: {
|
onlyInput: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
},
|
||||||
|
disabled: Boolean
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
id() {
|
id() {
|
||||||
@ -70,7 +71,8 @@ export default {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
value: this.value,
|
value: this.value,
|
||||||
required: this.docfield.required
|
required: this.docfield.required,
|
||||||
|
disabled: this.disabled
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getInputListeners() {
|
getInputListeners() {
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="custom-control custom-checkbox">
|
<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>
|
<label class="custom-control-label" :for="id">{{ docfield.label }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Base from './Base';
|
import Base from './Base';
|
||||||
|
|
||||||
|
@ -1,19 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-group" v-if="!onlyInput">
|
<div :class="{'form-group': !onlyInput}">
|
||||||
<label>{{ docfield.label }}</label>
|
<label v-if="!onlyInput">{{ docfield.label }}</label>
|
||||||
<flat-pickr
|
<flat-pickr
|
||||||
:value="value"
|
:value="value"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@on-change="emitChange">
|
@on-change="emitChange">
|
||||||
</flat-pickr>
|
</flat-pickr>
|
||||||
</div>
|
</div>
|
||||||
<flat-pickr
|
|
||||||
v-else
|
|
||||||
:value="value"
|
|
||||||
class="form-control"
|
|
||||||
@on-change="emitChange"
|
|
||||||
>
|
|
||||||
</flat-pickr>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import flatPickr from 'vue-flatpickr-component';
|
import flatPickr from 'vue-flatpickr-component';
|
||||||
|
@ -38,7 +38,8 @@ export default {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
value: this.value,
|
value: this.value,
|
||||||
required: this.docfield.required
|
required: this.docfield.required,
|
||||||
|
disabled: this.disabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="component"
|
<component
|
||||||
:docfield="docfield"
|
:is="component"
|
||||||
:value="value"
|
:docfield="docfield"
|
||||||
:onlyInput="onlyInput"
|
:value="value"
|
||||||
@change="$emit('change', $event)"
|
:onlyInput="onlyInput"
|
||||||
/>
|
:disabled="isDisabled"
|
||||||
|
@change="$emit('change', $event)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import Base from './Base';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import Check from './Check';
|
import Check from './Check';
|
||||||
import Code from './Code';
|
import Code from './Code';
|
||||||
@ -22,31 +25,59 @@ import Password from './Password';
|
|||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
import Table from './Table';
|
import Table from './Table';
|
||||||
import Text from './Text';
|
import Text from './Text';
|
||||||
import Time from './Time'
|
import Time from './Time';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['docfield', 'value', 'onlyInput'],
|
props: ['docfield', 'value', 'onlyInput', 'doc'],
|
||||||
computed: {
|
computed: {
|
||||||
component() {
|
component() {
|
||||||
return {
|
if (this.docfield.template) {
|
||||||
Autocomplete,
|
// for controls with their own template
|
||||||
Check,
|
// create a vue object for it
|
||||||
Code,
|
return {
|
||||||
Currency,
|
extends: Base,
|
||||||
Data,
|
render: null,
|
||||||
Date,
|
template: this.docfield.template()
|
||||||
DynamicLink,
|
};
|
||||||
File,
|
}
|
||||||
Float,
|
|
||||||
Int,
|
return {
|
||||||
Link,
|
Autocomplete,
|
||||||
Password,
|
Check,
|
||||||
Select,
|
Code,
|
||||||
Table,
|
Currency,
|
||||||
Text,
|
Data,
|
||||||
Time,
|
Date,
|
||||||
}[this.docfield.fieldtype];
|
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>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.form-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -3,87 +3,150 @@ import frappe from 'frappejs';
|
|||||||
import feather from 'feather-icons';
|
import feather from 'feather-icons';
|
||||||
import Awesomplete from 'awesomplete';
|
import Awesomplete from 'awesomplete';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
|
import FeatherIcon from 'frappejs/ui/components/FeatherIcon';
|
||||||
import Form from '../Form/Form';
|
import Form from '../Form/Form';
|
||||||
import { _ } from 'frappejs/utils';
|
import { _ } from 'frappejs/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: Autocomplete,
|
extends: Autocomplete,
|
||||||
watch: {
|
watch: {
|
||||||
value(newValue) {
|
value(newValue) {
|
||||||
this.$refs.input.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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
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>
|
</script>
|
||||||
|
@ -10,7 +10,8 @@ export default {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
type: 'password',
|
type: 'password',
|
||||||
value: this.value,
|
value: this.value,
|
||||||
required: this.docfield.required
|
required: this.docfield.required,
|
||||||
|
disabled: this.disabled
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,8 @@ export default {
|
|||||||
getInputAttrs() {
|
getInputAttrs() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
required: this.docfield.required
|
required: this.docfield.required,
|
||||||
|
disabled: this.disabled
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInputChildren(h) {
|
getInputChildren(h) {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<model-table
|
<model-table
|
||||||
:doctype="docfield.childtype"
|
:doctype="docfield.childtype"
|
||||||
:rows="value"
|
:rows="value"
|
||||||
|
:disabled="disabled"
|
||||||
@update:rows="emitChange"
|
@update:rows="emitChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
required: this.docfield.required,
|
required: this.docfield.required,
|
||||||
rows: 3
|
rows: 3,
|
||||||
|
disabled: this.disabled
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getDomProps() {
|
getDomProps() {
|
||||||
|
11
ui/constants/indicators.js
Normal file
11
ui/constants/indicators.js
Normal 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
27
ui/plugins/formModal.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user