mirror of
https://github.com/frappe/books.git
synced 2024-11-12 16:36:27 +00:00
feat: Link field with create new doc
This commit is contained in:
parent
0f89720770
commit
59e79ab919
@ -68,6 +68,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
quickEditFields: [
|
quickEditFields: [
|
||||||
|
'rate',
|
||||||
'unit',
|
'unit',
|
||||||
'incomeAccount',
|
'incomeAccount',
|
||||||
'expenseAccount',
|
'expenseAccount',
|
||||||
|
@ -5,6 +5,7 @@ export default {
|
|||||||
title: _('Item'),
|
title: _('Item'),
|
||||||
columns: [
|
columns: [
|
||||||
'name',
|
'name',
|
||||||
'description'
|
'rate',
|
||||||
|
'tax'
|
||||||
]
|
]
|
||||||
}
|
}
|
11
src/components/Badge.vue
Normal file
11
src/components/Badge.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-blue-100 rounded text-blue-500 px-2 py-1 truncate">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Badge'
|
||||||
|
};
|
||||||
|
</script>
|
33
src/components/Controls/Base.vue
Normal file
33
src/components/Controls/Base.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
class="focus:outline-none w-full"
|
||||||
|
:class="inputClass"
|
||||||
|
:type="inputType"
|
||||||
|
:value="value"
|
||||||
|
@blur="e => triggerChange(e.target.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Base',
|
||||||
|
props: ['df', 'value', 'inputClass'],
|
||||||
|
inject: ['doctype', 'name'],
|
||||||
|
computed: {
|
||||||
|
inputType() {
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.input.focus();
|
||||||
|
},
|
||||||
|
triggerChange(value) {
|
||||||
|
this.$emit('change', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,26 +1,12 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
ref="input"
|
|
||||||
class="focus:outline-none w-full"
|
|
||||||
:class="inputClass"
|
|
||||||
type="text"
|
|
||||||
:value="value"
|
|
||||||
@blur="triggerChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Base from './Base';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Data',
|
name: 'Data',
|
||||||
props: ['df', 'value', 'inputClass'],
|
extends: Base,
|
||||||
methods: {
|
computed: {
|
||||||
focus() {
|
inputType() {
|
||||||
this.$refs.input.focus();
|
return 'text';
|
||||||
},
|
|
||||||
triggerChange(e) {
|
|
||||||
this.$emit('change', e.target.value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import Data from './Data';
|
import Data from './Data';
|
||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
|
import Link from './Link';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormControl',
|
name: 'FormControl',
|
||||||
render(h) {
|
render(h) {
|
||||||
let controls = {
|
let controls = {
|
||||||
Data,
|
Data,
|
||||||
Select
|
Select,
|
||||||
|
Link
|
||||||
};
|
};
|
||||||
let { df } = this.$attrs;
|
let { df } = this.$attrs;
|
||||||
return h(controls[df.fieldtype] || Data, {
|
return h(controls[df.fieldtype] || Data, {
|
||||||
|
185
src/components/Controls/Link.vue
Normal file
185
src/components/Controls/Link.vue
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
class="focus:outline-none w-full"
|
||||||
|
:class="inputClass"
|
||||||
|
type="text"
|
||||||
|
:value="linkValue"
|
||||||
|
@focus="onFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
@input="onInput"
|
||||||
|
@keydown.up="highlightItemUp"
|
||||||
|
@keydown.down="highlightItemDown"
|
||||||
|
@keydown.esc="$refs.input.blur"
|
||||||
|
@keydown.enter="selectHighlightedItem"
|
||||||
|
/>
|
||||||
|
<div class="mt-1 absolute left-0 z-10 bg-white rounded border w-full" v-if="isFocused">
|
||||||
|
<div class="p-1 max-h-64 overflow-auto" v-if="suggestions.length">
|
||||||
|
<a
|
||||||
|
ref="suggestionItems"
|
||||||
|
class="block p-2 rounded mt-1 first:mt-0 cursor-pointer"
|
||||||
|
v-for="(s, index) in suggestions"
|
||||||
|
:key="s.value"
|
||||||
|
:class="index === highlightedIndex ? 'bg-gray-200' : ''"
|
||||||
|
@mouseenter="highlightedIndex = index"
|
||||||
|
@mouseleave="highlightedIndex = -1"
|
||||||
|
@click="selectItem(s)"
|
||||||
|
>{{ s.label }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="border-t p-1">
|
||||||
|
<a
|
||||||
|
class="block px-2 rounded mt-1 first:mt-0 cursor-pointer flex items-center"
|
||||||
|
:class="{'bg-gray-200': highlightedIndex === suggestions.length, 'py-1': linkValue, 'py-2': !linkValue}"
|
||||||
|
>
|
||||||
|
<div>Create</div>
|
||||||
|
<Badge class="ml-2" v-if="isNewValue">{{ linkValue }}</Badge>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import frappe from 'frappejs';
|
||||||
|
import Base from './Base';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Link',
|
||||||
|
extends: Base,
|
||||||
|
components: {
|
||||||
|
Badge
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
linkValue: '',
|
||||||
|
isFocused: false,
|
||||||
|
suggestions: [],
|
||||||
|
highlightedIndex: -1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newValue) {
|
||||||
|
this.linkValue = this.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isNewValue() {
|
||||||
|
let values = this.suggestions.map(d => d.value);
|
||||||
|
return !values.includes(this.linkValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async updateSuggestions(e) {
|
||||||
|
let keyword;
|
||||||
|
if (e) {
|
||||||
|
keyword = e.target.value;
|
||||||
|
this.linkValue = keyword;
|
||||||
|
}
|
||||||
|
this.suggestions = await this.getSuggestions(keyword);
|
||||||
|
},
|
||||||
|
async getSuggestions(keyword = '') {
|
||||||
|
let doctype = this.df.target;
|
||||||
|
let meta = frappe.getMeta(doctype);
|
||||||
|
let filters = await this.getFilters(keyword);
|
||||||
|
if (keyword && !filters.keywords) {
|
||||||
|
filters.keywords = ['like', keyword];
|
||||||
|
}
|
||||||
|
let results = await frappe.db.getAll({
|
||||||
|
doctype,
|
||||||
|
filters,
|
||||||
|
fields: [...new Set(['name', meta.titleField, ...meta.keywordFields])]
|
||||||
|
});
|
||||||
|
return results.map(r => {
|
||||||
|
return { label: r[meta.titleField], value: r.name };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async getFilters(keyword) {
|
||||||
|
let doc = await frappe.getDoc(this.doctype, this.name);
|
||||||
|
return this.df.getFilters
|
||||||
|
? await this.df.getFilters(keyword, doc)
|
||||||
|
: {};
|
||||||
|
},
|
||||||
|
selectHighlightedItem() {
|
||||||
|
if (![-1, this.suggestions.length].includes(this.highlightedIndex)) {
|
||||||
|
// valid selection
|
||||||
|
let suggestion = this.suggestions[this.highlightedIndex];
|
||||||
|
this.setSuggestion(suggestion);
|
||||||
|
} else if (this.highlightedIndex === this.suggestions.length) {
|
||||||
|
// create new
|
||||||
|
this.openNewDoc();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectItem(suggestion) {
|
||||||
|
this.setSuggestion(suggestion);
|
||||||
|
},
|
||||||
|
setSuggestion(suggestion) {
|
||||||
|
this.triggerChange(suggestion.value);
|
||||||
|
this.isFocused = false;
|
||||||
|
},
|
||||||
|
async openNewDoc() {
|
||||||
|
let doctype = this.df.target;
|
||||||
|
let doc = await frappe.getNewDoc(doctype);
|
||||||
|
let currentPath = this.$route.path;
|
||||||
|
let filters = await this.getFilters();
|
||||||
|
this.$router.push({
|
||||||
|
path: `/list/${doctype}/${doc.name}`,
|
||||||
|
query: {
|
||||||
|
values: Object.assign({}, filters, {
|
||||||
|
name: this.linkValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
doc.once('afterInsert', () => {
|
||||||
|
this.$router.push({
|
||||||
|
path: currentPath,
|
||||||
|
query: {
|
||||||
|
values: {
|
||||||
|
[this.df.fieldname]: doc.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFocus() {
|
||||||
|
this.isFocused = true;
|
||||||
|
this.updateSuggestions();
|
||||||
|
},
|
||||||
|
onBlur() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isFocused = false;
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
onInput(e) {
|
||||||
|
this.updateSuggestions(e);
|
||||||
|
},
|
||||||
|
highlightItemUp() {
|
||||||
|
this.highlightedIndex -= 1;
|
||||||
|
if (this.highlightedIndex < 0) {
|
||||||
|
this.highlightedIndex = 0;
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
let index = this.highlightedIndex;
|
||||||
|
if (index !== 0) {
|
||||||
|
index -= 1;
|
||||||
|
}
|
||||||
|
let highlightedElement = this.$refs.suggestionItems[index];
|
||||||
|
highlightedElement && highlightedElement.scrollIntoView();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
highlightItemDown() {
|
||||||
|
this.highlightedIndex += 1;
|
||||||
|
if (this.highlightedIndex > this.suggestions.length) {
|
||||||
|
this.highlightedIndex = this.suggestions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
let index = this.highlightedIndex;
|
||||||
|
let highlightedElement = this.$refs.suggestionItems[index];
|
||||||
|
highlightedElement && highlightedElement.scrollIntoView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -4,7 +4,7 @@
|
|||||||
class="appearance-none bg-white rounded-none focus:outline-none w-full"
|
class="appearance-none bg-white rounded-none focus:outline-none w-full"
|
||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
:value="value"
|
:value="value"
|
||||||
@blur="triggerChange"
|
@blur="e => triggerChange(e.target.value)"
|
||||||
>
|
>
|
||||||
<option v-for="option in options" :value="option.value">{{ option.label }}</option>
|
<option v-for="option in options" :value="option.value">{{ option.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
@ -12,14 +12,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Base from './Base';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Select',
|
name: 'Select',
|
||||||
props: ['df', 'value', 'inputClass'],
|
extends: Base,
|
||||||
methods: {
|
|
||||||
triggerChange(e) {
|
|
||||||
this.$emit('change', e.target.value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
options() {
|
options() {
|
||||||
let options = this.df.options;
|
let options = this.df.options;
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<ListCell
|
<ListCell
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.label"
|
:key="column.label"
|
||||||
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right':''"
|
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right pr-10' : ''"
|
||||||
>{{ column.label }}</ListCell>
|
>{{ column.label }}</ListCell>
|
||||||
</ListRow>
|
</ListRow>
|
||||||
<ListRow
|
<ListRow
|
||||||
@ -14,7 +14,11 @@
|
|||||||
@click.native="openForm(doc.name)"
|
@click.native="openForm(doc.name)"
|
||||||
:columnCount="columns.length"
|
:columnCount="columns.length"
|
||||||
>
|
>
|
||||||
<ListCell v-for="column in columns" :key="column.label">
|
<ListCell
|
||||||
|
v-for="column in columns"
|
||||||
|
:key="column.label"
|
||||||
|
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right pr-10' : ''"
|
||||||
|
>
|
||||||
<indicator v-if="column.getIndicator" :color="column.getIndicator(doc)" class="mr-2" />
|
<indicator v-if="column.getIndicator" :color="column.getIndicator(doc)" class="mr-2" />
|
||||||
<span
|
<span
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@ -58,6 +62,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.setupColumnsAndData();
|
await this.setupColumnsAndData();
|
||||||
|
frappe.db.on(`change:${this.listConfig.doctype}`, obj => {
|
||||||
|
this.updateData();
|
||||||
|
});
|
||||||
frappe.listView.on('filterList', this.updateData.bind(this));
|
frappe.listView.on('filterList', this.updateData.bind(this));
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -86,6 +93,7 @@ export default {
|
|||||||
doctype: this.doctype,
|
doctype: this.doctype,
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
filters,
|
filters,
|
||||||
|
orderBy: 'creation',
|
||||||
limit: 13
|
limit: 13
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -27,8 +27,8 @@
|
|||||||
:key="df.fieldname"
|
:key="df.fieldname"
|
||||||
>
|
>
|
||||||
<div class="py-3 pl-4 text-gray-600">{{ df.label }}</div>
|
<div class="py-3 pl-4 text-gray-600">{{ df.label }}</div>
|
||||||
|
<div class="py-3 pr-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
class="py-3 pr-4"
|
|
||||||
input-class="focus:shadow-outline-px"
|
input-class="focus:shadow-outline-px"
|
||||||
:df="df"
|
:df="df"
|
||||||
:value="doc[df.fieldname]"
|
:value="doc[df.fieldname]"
|
||||||
@ -37,6 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -47,15 +48,20 @@ import FormControl from '@/components/Controls/FormControl';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'QuickEditForm',
|
name: 'QuickEditForm',
|
||||||
props: ['doctype', 'name'],
|
props: ['doctype', 'name', 'values'],
|
||||||
components: {
|
components: {
|
||||||
Button,
|
Button,
|
||||||
XIcon,
|
XIcon,
|
||||||
FormControl
|
FormControl
|
||||||
},
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
doctype: this.doctype,
|
||||||
|
name: this.name
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
title: '',
|
|
||||||
meta: null,
|
meta: null,
|
||||||
doc: {},
|
doc: {},
|
||||||
fields: [],
|
fields: [],
|
||||||
@ -75,6 +81,9 @@ export default {
|
|||||||
if (this.doc._notInserted) {
|
if (this.doc._notInserted) {
|
||||||
this.doc.set(this.titleDocField.fieldname, '');
|
this.doc.set(this.titleDocField.fieldname, '');
|
||||||
}
|
}
|
||||||
|
if (this.values) {
|
||||||
|
this.doc.set(this.values);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$refs.titleControl.focus()
|
this.$refs.titleControl.focus()
|
||||||
}, 300);
|
}, 300);
|
||||||
@ -97,7 +106,6 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchDoc() {
|
async fetchDoc() {
|
||||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
this.doc = await frappe.getDoc(this.doctype, this.name);
|
||||||
this.title = this.doc[this.meta.titleField];
|
|
||||||
},
|
},
|
||||||
async updateDoc() {
|
async updateDoc() {
|
||||||
try {
|
try {
|
||||||
|
@ -34,7 +34,7 @@ const routes = [
|
|||||||
const { listName } = route.params;
|
const { listName } = route.params;
|
||||||
return {
|
return {
|
||||||
listName,
|
listName,
|
||||||
filters: route.query
|
filters: route.query.filters
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@ -43,9 +43,11 @@ const routes = [
|
|||||||
component: QuickEditForm,
|
component: QuickEditForm,
|
||||||
props: route => {
|
props: route => {
|
||||||
const { listName, name } = route.params;
|
const { listName, name } = route.params;
|
||||||
|
let values = route.query.values || null;
|
||||||
return {
|
return {
|
||||||
doctype: listName,
|
doctype: listName,
|
||||||
name
|
name,
|
||||||
|
values
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter var experimental', 'sans-serif']
|
sans: ['Inter var experimental', 'sans-serif'],
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
maxHeight: {
|
||||||
|
'64': '16rem'
|
||||||
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
'72': '18rem',
|
'72': '18rem',
|
||||||
'80': '20rem'
|
'80': '20rem'
|
||||||
|
Loading…
Reference in New Issue
Block a user