mirror of
https://github.com/frappe/books.git
synced 2025-02-10 16:08:35 +00:00
Replace awesomplete with own component
- Add Tree View
This commit is contained in:
parent
667a966769
commit
110300005e
@ -26,7 +26,11 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
iconSVG() {
|
iconSVG() {
|
||||||
return feather.icons[this.name].toSvg({
|
const icon = feather.icons[this.name];
|
||||||
|
if (!icon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return icon.toSvg({
|
||||||
width: this.size,
|
width: this.size,
|
||||||
height: this.size
|
height: this.size
|
||||||
});
|
});
|
||||||
|
87
ui/components/Tree/TreeNode.vue
Normal file
87
ui/components/Tree/TreeNode.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tree-node">
|
||||||
|
<div class="tree-label px-3 py-2" @click.self="toggleChildren">
|
||||||
|
<div @click="toggleChildren">
|
||||||
|
<feather-icon :name="iconName" v-show="iconName" />
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="['tree-children', expanded ? '' : 'd-none']">
|
||||||
|
<tree-node v-for="child in children" :key="child.label"
|
||||||
|
:label="child.label"
|
||||||
|
:parentValue="child.name"
|
||||||
|
:doctype="doctype"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
const TreeNode = {
|
||||||
|
props: ['label', 'parentValue', 'doctype'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
children: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
iconName() {
|
||||||
|
if (this.children && this.children.length ==0) return 'chevron-right';
|
||||||
|
return this.expanded ? 'chevron-down' : 'chevron-right';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
TreeNode: () => Promise.resolve(TreeNode)
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.settings = frappe.getMeta(this.doctype).treeSettings;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async toggleChildren() {
|
||||||
|
await this.getChildren();
|
||||||
|
this.expanded = !this.expanded;
|
||||||
|
},
|
||||||
|
async getChildren() {
|
||||||
|
if (this.children) return;
|
||||||
|
|
||||||
|
this.children = [];
|
||||||
|
|
||||||
|
let filters = {
|
||||||
|
[this.settings.parentField]: this.parentValue
|
||||||
|
};
|
||||||
|
|
||||||
|
const children = await frappe.db.getAll({
|
||||||
|
doctype: this.doctype,
|
||||||
|
filters,
|
||||||
|
fields: [this.settings.parentField, 'isGroup', 'name'],
|
||||||
|
orderBy: 'name',
|
||||||
|
order: 'asc'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.children = children.map(c => {
|
||||||
|
c.label = c.name;
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TreeNode;
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../styles/variables";
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.tree-label {
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tree-label:hover {
|
||||||
|
background-color: $dropdown-link-hover-bg;;
|
||||||
|
}
|
||||||
|
.tree-children {
|
||||||
|
padding-left: 2.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
53
ui/components/Tree/index.vue
Normal file
53
ui/components/Tree/index.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-3 w-50" v-if="rootNode">
|
||||||
|
<tree-node :label="rootNode.label" :parentValue="''" :doctype="doctype" ref="rootNode"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import frappe from 'frappejs';
|
||||||
|
import TreeNode from './TreeNode';
|
||||||
|
import { setTimeout } from 'timers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['doctype'],
|
||||||
|
components: {
|
||||||
|
TreeNode,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rootNode: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.rootNode.find('.tree-label').click();
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.settings = frappe.getMeta(this.doctype).treeSettings;
|
||||||
|
this.rootNode = {
|
||||||
|
label: await this.settings.getRootLabel()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getChildren(parentValue) {
|
||||||
|
let filters = {
|
||||||
|
[this.settings.parentField]: parentValue
|
||||||
|
};
|
||||||
|
|
||||||
|
const children = await frappe.db.getAll({
|
||||||
|
doctype: this.doctype,
|
||||||
|
filters,
|
||||||
|
fields: [this.settings.parentField, 'isGroup', 'name'],
|
||||||
|
orderBy: 'name',
|
||||||
|
order: 'asc'
|
||||||
|
});
|
||||||
|
|
||||||
|
return children.map(c => {
|
||||||
|
c.label = c.name;
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,91 +1,104 @@
|
|||||||
<script>
|
<script>
|
||||||
import Awesomplete from 'awesomplete';
|
|
||||||
import Data from './Data';
|
import Data from './Data';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: Data,
|
extends: Data,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
awesomplete: null
|
popupOpen: false,
|
||||||
|
popupItems: [],
|
||||||
|
highlightedItem: -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.setupAwesomplete();
|
|
||||||
this.awesomplete.container.classList.add('form-control');
|
|
||||||
this.awesomplete.ul.classList.add('dropdown-menu');
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getInputListeners() {
|
getInputListeners() {
|
||||||
return {
|
return {
|
||||||
input: e => {
|
input: e => {
|
||||||
this.updateList(e.target.value);
|
this.updateList(e.target.value);
|
||||||
},
|
},
|
||||||
'awesomplete-select': e => {
|
keydown: e => {
|
||||||
const value = e.text.value;
|
if (e.keyCode === 38) {
|
||||||
this.handleChange(value);
|
// up
|
||||||
|
this.highlightedItem -= 1;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 40) {
|
||||||
|
// down
|
||||||
|
this.highlightedItem += 1;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
if (this.highlightedItem > -1) {
|
||||||
|
this.onItemClick(this.popupItems[this.highlightedItem]);
|
||||||
|
this.popupOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
focus: async e => {
|
focus: async e => {
|
||||||
await this.updateList();
|
await this.updateList();
|
||||||
this.awesomplete.evaluate();
|
},
|
||||||
this.awesomplete.open();
|
blur: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.popupOpen = false;
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async updateList(value) {
|
getChildrenElement(h) {
|
||||||
this.awesomplete.list = await this.getList(value);
|
return [
|
||||||
|
this.getLabelElement(h),
|
||||||
|
this.getInputElement(h),
|
||||||
|
this.getDropdownElement(h)
|
||||||
|
];
|
||||||
},
|
},
|
||||||
getList(text) {
|
getDropdownElement(h) {
|
||||||
return this.docfield.getList(text);
|
return h('div', {
|
||||||
|
class: ['dropdown-menu w-100', this.popupOpen ? 'show' : '']
|
||||||
|
}, this.getDropdownItems(h));
|
||||||
},
|
},
|
||||||
setupAwesomplete() {
|
getDropdownItems(h) {
|
||||||
const input = this.$refs.input;
|
return this.popupItems.map((item, i) => {
|
||||||
this.awesomplete = new Awesomplete(input, {
|
return h('a', {
|
||||||
minChars: 0,
|
class: ['dropdown-item', this.highlightedItem === i ? 'active text-dark' : ''],
|
||||||
maxItems: 99,
|
attrs: {
|
||||||
sort: this.sort(),
|
href: '#',
|
||||||
filter: this.filter(),
|
'data-value': item.value
|
||||||
item: (text, input) => {
|
},
|
||||||
const li = document.createElement('li');
|
on: {
|
||||||
li.classList.add('dropdown-item');
|
click: e => {
|
||||||
li.classList.add('d-flex');
|
e.preventDefault();
|
||||||
li.classList.add('align-items-center');
|
this.onItemClick(item);
|
||||||
li.innerHTML = text.label;
|
this.popupOpen = false;
|
||||||
|
|
||||||
return li;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
domProps: {
|
||||||
|
innerHTML: item.label
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
this.bindEvents();
|
|
||||||
},
|
},
|
||||||
bindEvents() {
|
onItemClick(item) {
|
||||||
|
this.handleChange(item.value);
|
||||||
},
|
},
|
||||||
sort() {
|
async updateList(keyword) {
|
||||||
// return a function that handles sorting of items
|
this.popupItems = await this.getList(keyword);
|
||||||
|
this.popupOpen = this.popupItems.length > 0;
|
||||||
},
|
},
|
||||||
filter() {
|
async getList(text='') {
|
||||||
// return a function that filters list suggestions based on input
|
let list = await this.docfield.getList(text);
|
||||||
return Awesomplete.FILTER_CONTAINS
|
list = list.map(item => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return {
|
||||||
|
label: item,
|
||||||
|
value: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return list.filter(item => {
|
||||||
|
const string = (item.label + ' ' + item.value).toLowerCase();
|
||||||
|
return string.includes(text.toLowerCase());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
|
||||||
@import "../../styles/variables";
|
|
||||||
@import "~awesomplete/awesomplete.base";
|
|
||||||
|
|
||||||
.awesomplete {
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
padding: $dropdown-padding-y 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu:not([hidden]) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item[aria-selected="true"] {
|
|
||||||
background-color: $dropdown-link-hover-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -37,7 +37,10 @@ export default {
|
|||||||
attrs: {
|
attrs: {
|
||||||
'data-fieldname': this.docfield.fieldname
|
'data-fieldname': this.docfield.fieldname
|
||||||
}
|
}
|
||||||
}, [this.getLabelElement(h), this.getInputElement(h)]);
|
}, this.getChildrenElement(h));
|
||||||
|
},
|
||||||
|
getChildrenElement(h) {
|
||||||
|
return [this.getLabelElement(h), this.getInputElement(h)]
|
||||||
},
|
},
|
||||||
getLabelElement(h) {
|
getLabelElement(h) {
|
||||||
return h('label', {
|
return h('label', {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import frappe from 'frappejs';
|
import frappe from 'frappejs';
|
||||||
import feather from 'feather-icons';
|
import feather from 'feather-icons';
|
||||||
import Awesomplete from 'awesomplete';
|
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import FeatherIcon from 'frappejs/ui/components/FeatherIcon';
|
import FeatherIcon from 'frappejs/ui/components/FeatherIcon';
|
||||||
import Form from '../Form/Form';
|
import Form from '../Form/Form';
|
||||||
@ -18,9 +17,11 @@ export default {
|
|||||||
async getList(query) {
|
async getList(query) {
|
||||||
const list = await frappe.db.getAll({
|
const list = await frappe.db.getAll({
|
||||||
doctype: this.getTarget(),
|
doctype: this.getTarget(),
|
||||||
filters: query ? {
|
filters: query
|
||||||
|
? {
|
||||||
keywords: ['like', query]
|
keywords: ['like', query]
|
||||||
} : null,
|
}
|
||||||
|
: null,
|
||||||
fields: ['name'],
|
fields: ['name'],
|
||||||
limit: 50
|
limit: 50
|
||||||
});
|
});
|
||||||
@ -41,31 +42,31 @@ export default {
|
|||||||
value: '__newItem'
|
value: '__newItem'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getWrapperElement(h) {
|
getChildrenElement(h) {
|
||||||
|
return [
|
||||||
|
this.getLabelElement(h),
|
||||||
|
this.getInputGroupElement(h),
|
||||||
|
this.getDropdownElement(h)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
getInputGroupElement(h) {
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
class: ['form-group', ...this.wrapperClass],
|
class: ['input-group']
|
||||||
attrs: {
|
|
||||||
'data-fieldname': this.docfield.fieldname
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
this.getLabelElement(h),
|
this.getInputElement(h),
|
||||||
this.getInputGroupElement(h)
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: ['input-group-append']
|
||||||
|
},
|
||||||
|
[this.getFollowLink(h)]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getInputGroupElement(h) {
|
|
||||||
return h('div', {
|
|
||||||
class: ['input-group']
|
|
||||||
}, [
|
|
||||||
this.getInputElement(h),
|
|
||||||
h('div', {
|
|
||||||
class: ['input-group-append']
|
|
||||||
}, [this.getFollowLink(h)])
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
getFollowLink(h) {
|
getFollowLink(h) {
|
||||||
const doctype = this.getTarget();
|
const doctype = this.getTarget();
|
||||||
const name = this.value;
|
const name = this.value;
|
||||||
@ -84,7 +85,9 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return h('button', {
|
return h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
class: ['btn btn-sm btn-outline-light border d-flex'],
|
class: ['btn btn-sm btn-outline-light border d-flex'],
|
||||||
attrs: {
|
attrs: {
|
||||||
type: 'button'
|
type: 'button'
|
||||||
@ -94,7 +97,9 @@ export default {
|
|||||||
this.$router.push(`/edit/${doctype}/${name}`);
|
this.$router.push(`/edit/${doctype}/${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [arrow])
|
},
|
||||||
|
[arrow]
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getTarget() {
|
getTarget() {
|
||||||
return this.docfield.target;
|
return this.docfield.target;
|
||||||
@ -126,21 +131,19 @@ export default {
|
|||||||
if (suggestion.value === '__newItem') {
|
if (suggestion.value === '__newItem') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return Awesomplete.FILTER_CONTAINS(suggestion, txt);
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
bindEvents() {
|
onItemClick(item) {
|
||||||
|
if (item.value === '__newItem') {
|
||||||
|
this.openFormModal();
|
||||||
|
} else {
|
||||||
|
this.handleChange(item.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openFormModal() {
|
||||||
const input = this.$refs.input;
|
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());
|
const newDoc = await frappe.getNewDoc(this.getTarget());
|
||||||
|
this.$formModal.open(newDoc, {
|
||||||
this.$formModal.open(
|
|
||||||
newDoc,
|
|
||||||
{
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: input.value !== '__newItem' ? input.value : null
|
name: input.value !== '__newItem' ? input.value : null
|
||||||
},
|
},
|
||||||
@ -151,8 +154,7 @@ export default {
|
|||||||
this.handleChange('');
|
this.handleChange('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
newDoc.on('afterInsert', data => {
|
newDoc.on('afterInsert', data => {
|
||||||
// if new doc was created
|
// if new doc was created
|
||||||
@ -161,8 +163,6 @@ export default {
|
|||||||
this.$formModal.close();
|
this.$formModal.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import ListAndForm from '../pages/ListAndForm';
|
import ListAndForm from '../pages/ListAndForm';
|
||||||
import ListAndPrintView from '../pages/ListAndPrintView';
|
import ListAndPrintView from '../pages/ListAndPrintView';
|
||||||
|
import Tree from '../components/Tree';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@ -14,6 +15,12 @@ export default [
|
|||||||
component: ListAndForm,
|
component: ListAndForm,
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tree/:doctype',
|
||||||
|
name: 'Tree',
|
||||||
|
component: Tree,
|
||||||
|
props: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/print/:doctype/:name',
|
path: '/print/:doctype/:name',
|
||||||
name: 'PrintView',
|
name: 'PrintView',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user