mirror of
https://github.com/frappe/books.git
synced 2025-01-23 07:08:36 +00:00
fix: QuickEditForm
- Create a new document - Rename a document - Auto save document on value change
This commit is contained in:
parent
9b9181146e
commit
eb348c7210
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="text-sm px-4 py-2 focus:outline-none rounded-lg" :style="style">
|
<button class="text-sm px-4 py-2 focus:outline-none rounded-lg" :style="style" v-bind="$attrs" v-on="$listeners">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@ -15,11 +15,12 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
style() {
|
style() {
|
||||||
return {
|
return {
|
||||||
'background-image': this.type === 'primary'
|
'background-image':
|
||||||
? 'linear-gradient(180deg, #4AC3F8 0%, #2490EF 100%)'
|
this.type === 'primary'
|
||||||
: 'linear-gradient(180deg, #FFFFFF 0%, #F4F4F6 100%)'
|
? 'linear-gradient(180deg, #4AC3F8 0%, #2490EF 100%)'
|
||||||
}
|
: 'linear-gradient(180deg, #FFFFFF 0%, #F4F4F6 100%)'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
27
src/components/Controls/Data.vue
Normal file
27
src/components/Controls/Data.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
class="focus:outline-none w-full"
|
||||||
|
:class="inputClass"
|
||||||
|
type="text"
|
||||||
|
:value="value"
|
||||||
|
@blur="triggerChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Data',
|
||||||
|
props: ['df', 'value', 'inputClass'],
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
},
|
||||||
|
triggerChange(e) {
|
||||||
|
this.$emit('change', e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
23
src/components/Controls/FormControl.js
Normal file
23
src/components/Controls/FormControl.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Data from './Data';
|
||||||
|
import Select from './Select';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FormControl',
|
||||||
|
render(h) {
|
||||||
|
let controls = {
|
||||||
|
Data,
|
||||||
|
Select
|
||||||
|
};
|
||||||
|
let { df } = this.$attrs;
|
||||||
|
return h(controls[df.fieldtype] || Data, {
|
||||||
|
props: this.$attrs,
|
||||||
|
on: this.$listeners,
|
||||||
|
ref: 'control'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.control.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
35
src/components/Controls/Select.vue
Normal file
35
src/components/Controls/Select.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
class="appearance-none bg-white rounded-none focus:outline-none w-full"
|
||||||
|
:class="inputClass"
|
||||||
|
:value="value"
|
||||||
|
@blur="triggerChange"
|
||||||
|
>
|
||||||
|
<option v-for="option in options" :value="option.value">{{ option.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Select',
|
||||||
|
props: ['df', 'value', 'inputClass'],
|
||||||
|
methods: {
|
||||||
|
triggerChange(e) {
|
||||||
|
this.$emit('change', e.target.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
options() {
|
||||||
|
let options = this.df.options;
|
||||||
|
return options.map(o => {
|
||||||
|
if (typeof o === 'string') {
|
||||||
|
return { label: o, value: o };
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,22 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="border-l h-full">
|
<div class="border-l h-full">
|
||||||
<div class="flex justify-end px-4 pt-4">
|
<div class="flex justify-end px-4 pt-4">
|
||||||
<Button>
|
<Button @click="routeToList">
|
||||||
<XIcon class="w-3 h-3 stroke-current text-gray-700" />
|
<XIcon class="w-3 h-3 stroke-current text-gray-700" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button @click="insertDoc" type="primary" v-if="doc._notInserted" class="ml-2 flex">
|
||||||
|
<feather-icon name="check" class="text-white" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 pt-2 pb-4 border-b">
|
<div class="px-4 pt-2 pb-4 border-b flex items-center justify-between">
|
||||||
<h2 class="font-medium">{{ title }}</h2>
|
<FormControl
|
||||||
|
ref="titleControl"
|
||||||
|
v-if="titleDocField"
|
||||||
|
input-class="focus:shadow-outline-px"
|
||||||
|
:df="titleDocField"
|
||||||
|
:value="doc[titleDocField.fieldname]"
|
||||||
|
@change="value => valueChange(titleDocField, value)"
|
||||||
|
/>
|
||||||
|
<span v-if="showSaved" class="text-xs text-gray-600">{{ _('Saved') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
<div
|
<div
|
||||||
class="grid border-b px-4 py-3"
|
class="grid border-b"
|
||||||
style="grid-template-columns: 1fr 2fr"
|
style="grid-template-columns: 1fr 2fr"
|
||||||
v-for="df in fields"
|
v-for="df in fields"
|
||||||
:key="df.fieldname"
|
:key="df.fieldname"
|
||||||
>
|
>
|
||||||
<div class="text-gray-600">{{ df.label }}</div>
|
<div class="py-3 pl-4 text-gray-600">{{ df.label }}</div>
|
||||||
<div class="text-gray-900">{{ doc[df.fieldname] }}</div>
|
<FormControl
|
||||||
|
class="py-3 pr-4"
|
||||||
|
input-class="focus:shadow-outline-px"
|
||||||
|
:df="df"
|
||||||
|
:value="doc[df.fieldname]"
|
||||||
|
@change="value => valueChange(df, value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -26,28 +43,80 @@
|
|||||||
import frappe from 'frappejs';
|
import frappe from 'frappejs';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import XIcon from '@/components/Icons/X';
|
import XIcon from '@/components/Icons/X';
|
||||||
|
import FormControl from '@/components/Controls/FormControl';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'QuickEditForm',
|
name: 'QuickEditForm',
|
||||||
props: ['doctype', 'name'],
|
props: ['doctype', 'name'],
|
||||||
components: {
|
components: {
|
||||||
Button,
|
Button,
|
||||||
XIcon
|
XIcon,
|
||||||
|
FormControl
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
title: '',
|
title: '',
|
||||||
|
meta: null,
|
||||||
doc: {},
|
doc: {},
|
||||||
fields: []
|
fields: [],
|
||||||
|
titleDocField: null,
|
||||||
|
showSaved: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let meta = frappe.getMeta(this.doctype);
|
this.meta = frappe.getMeta(this.doctype);
|
||||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
this.fields = this.meta
|
||||||
this.title = this.doc[meta.titleField];
|
|
||||||
this.fields = meta
|
|
||||||
.getQuickEditFields()
|
.getQuickEditFields()
|
||||||
.map(fieldname => meta.getField(fieldname));
|
.map(fieldname => this.meta.getField(fieldname));
|
||||||
|
this.titleDocField = this.meta.getField(this.meta.titleField);
|
||||||
|
await this.fetchDoc();
|
||||||
|
|
||||||
|
// setup the title field
|
||||||
|
if (this.doc._notInserted) {
|
||||||
|
this.doc.set(this.titleDocField.fieldname, '');
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.titleControl.focus()
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
valueChange(df, value) {
|
||||||
|
if (!value) return;
|
||||||
|
let oldValue = this.doc.get(df.fieldname);
|
||||||
|
if (df.fieldname === 'name' && oldValue !== value && !this.doc._notInserted) {
|
||||||
|
this.doc.rename(value);
|
||||||
|
this.doc.once('afterRename', () => {
|
||||||
|
this.$router.push(`/list/${this.doctype}/${this.doc.name}`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.doc.set(df.fieldname, value);
|
||||||
|
if (this.doc._dirty && !this.doc._notInserted) {
|
||||||
|
this.updateDoc();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchDoc() {
|
||||||
|
this.doc = await frappe.getDoc(this.doctype, this.name);
|
||||||
|
this.title = this.doc[this.meta.titleField];
|
||||||
|
},
|
||||||
|
async updateDoc() {
|
||||||
|
try {
|
||||||
|
await this.doc.update();
|
||||||
|
this.triggerSaved();
|
||||||
|
} catch (e) {
|
||||||
|
await this.fetchDoc();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerSaved() {
|
||||||
|
this.showSaved = true;
|
||||||
|
setTimeout(() => (this.showSaved = false), 1000);
|
||||||
|
},
|
||||||
|
insertDoc() {
|
||||||
|
this.doc.insert();
|
||||||
|
},
|
||||||
|
routeToList() {
|
||||||
|
this.$router.push(`/list/${this.doctype}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
'sans': ['Inter var experimental', 'sans-serif'],
|
sans: ['Inter var experimental', 'sans-serif']
|
||||||
},
|
},
|
||||||
extend: { }
|
extend: {
|
||||||
|
spacing: {
|
||||||
|
'72': '18rem',
|
||||||
|
'80': '20rem'
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'outline-px': '0 0 0 1px rgba(66,153,225,0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
margin: ['responsive', 'first', 'hover', 'focus'],
|
margin: ['responsive', 'first', 'hover', 'focus']
|
||||||
},
|
},
|
||||||
plugins: []
|
plugins: []
|
||||||
}
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user