mirror of
https://github.com/frappe/books.git
synced 2024-11-08 23:00:56 +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: [
|
||||
'rate',
|
||||
'unit',
|
||||
'incomeAccount',
|
||||
'expenseAccount',
|
||||
|
@ -5,6 +5,7 @@ export default {
|
||||
title: _('Item'),
|
||||
columns: [
|
||||
'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>
|
||||
import Base from './Base';
|
||||
|
||||
export default {
|
||||
name: 'Data',
|
||||
props: ['df', 'value', 'inputClass'],
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
triggerChange(e) {
|
||||
this.$emit('change', e.target.value);
|
||||
extends: Base,
|
||||
computed: {
|
||||
inputType() {
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,12 +1,14 @@
|
||||
import Data from './Data';
|
||||
import Select from './Select';
|
||||
import Link from './Link';
|
||||
|
||||
export default {
|
||||
name: 'FormControl',
|
||||
render(h) {
|
||||
let controls = {
|
||||
Data,
|
||||
Select
|
||||
Select,
|
||||
Link
|
||||
};
|
||||
let { df } = this.$attrs;
|
||||
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="inputClass"
|
||||
:value="value"
|
||||
@blur="triggerChange"
|
||||
@blur="e => triggerChange(e.target.value)"
|
||||
>
|
||||
<option v-for="option in options" :value="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
@ -12,14 +12,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Base from './Base';
|
||||
|
||||
export default {
|
||||
name: 'Select',
|
||||
props: ['df', 'value', 'inputClass'],
|
||||
methods: {
|
||||
triggerChange(e) {
|
||||
this.$emit('change', e.target.value);
|
||||
}
|
||||
},
|
||||
extends: Base,
|
||||
computed: {
|
||||
options() {
|
||||
let options = this.df.options;
|
||||
|
@ -4,7 +4,7 @@
|
||||
<ListCell
|
||||
v-for="column in columns"
|
||||
:key="column.label"
|
||||
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right':''"
|
||||
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right pr-10' : ''"
|
||||
>{{ column.label }}</ListCell>
|
||||
</ListRow>
|
||||
<ListRow
|
||||
@ -14,7 +14,11 @@
|
||||
@click.native="openForm(doc.name)"
|
||||
: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" />
|
||||
<span
|
||||
style="width: 100%"
|
||||
@ -58,6 +62,9 @@ export default {
|
||||
},
|
||||
async mounted() {
|
||||
await this.setupColumnsAndData();
|
||||
frappe.db.on(`change:${this.listConfig.doctype}`, obj => {
|
||||
this.updateData();
|
||||
});
|
||||
frappe.listView.on('filterList', this.updateData.bind(this));
|
||||
},
|
||||
methods: {
|
||||
@ -86,6 +93,7 @@ export default {
|
||||
doctype: this.doctype,
|
||||
fields: ['*'],
|
||||
filters,
|
||||
orderBy: 'creation',
|
||||
limit: 13
|
||||
});
|
||||
},
|
||||
|
@ -27,13 +27,14 @@
|
||||
:key="df.fieldname"
|
||||
>
|
||||
<div class="py-3 pl-4 text-gray-600">{{ df.label }}</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 class="py-3 pr-4">
|
||||
<FormControl
|
||||
input-class="focus:shadow-outline-px"
|
||||
:df="df"
|
||||
:value="doc[df.fieldname]"
|
||||
@change="value => valueChange(df, value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,15 +48,20 @@ import FormControl from '@/components/Controls/FormControl';
|
||||
|
||||
export default {
|
||||
name: 'QuickEditForm',
|
||||
props: ['doctype', 'name'],
|
||||
props: ['doctype', 'name', 'values'],
|
||||
components: {
|
||||
Button,
|
||||
XIcon,
|
||||
FormControl
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
doctype: this.doctype,
|
||||
name: this.name
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
meta: null,
|
||||
doc: {},
|
||||
fields: [],
|
||||
@ -75,6 +81,9 @@ export default {
|
||||
if (this.doc._notInserted) {
|
||||
this.doc.set(this.titleDocField.fieldname, '');
|
||||
}
|
||||
if (this.values) {
|
||||
this.doc.set(this.values);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$refs.titleControl.focus()
|
||||
}, 300);
|
||||
@ -97,7 +106,6 @@ export default {
|
||||
},
|
||||
async fetchDoc() {
|
||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
||||
this.title = this.doc[this.meta.titleField];
|
||||
},
|
||||
async updateDoc() {
|
||||
try {
|
||||
|
@ -34,7 +34,7 @@ const routes = [
|
||||
const { listName } = route.params;
|
||||
return {
|
||||
listName,
|
||||
filters: route.query
|
||||
filters: route.query.filters
|
||||
};
|
||||
},
|
||||
children: [
|
||||
@ -43,9 +43,11 @@ const routes = [
|
||||
component: QuickEditForm,
|
||||
props: route => {
|
||||
const { listName, name } = route.params;
|
||||
let values = route.query.values || null;
|
||||
return {
|
||||
doctype: listName,
|
||||
name
|
||||
name,
|
||||
values
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
module.exports = {
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['Inter var experimental', 'sans-serif']
|
||||
sans: ['Inter var experimental', 'sans-serif'],
|
||||
},
|
||||
extend: {
|
||||
maxHeight: {
|
||||
'64': '16rem'
|
||||
},
|
||||
spacing: {
|
||||
'72': '18rem',
|
||||
'80': '20rem'
|
||||
|
Loading…
Reference in New Issue
Block a user