2
0
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:
Faris Ansari 2019-10-06 18:03:21 +05:30
parent 0f89720770
commit 59e79ab919
12 changed files with 282 additions and 45 deletions

View File

@ -68,6 +68,7 @@ module.exports = {
}
],
quickEditFields: [
'rate',
'unit',
'incomeAccount',
'expenseAccount',

View File

@ -5,6 +5,7 @@ export default {
title: _('Item'),
columns: [
'name',
'description'
'rate',
'tax'
]
}

11
src/components/Badge.vue Normal file
View 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>

View 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>

View File

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

View File

@ -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, {

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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