mirror of
https://github.com/frappe/books.git
synced 2025-01-24 07:38:25 +00:00
feat: FilterDropdown in ListView
This commit is contained in:
parent
f52ee12e66
commit
5cc57d3748
@ -24,9 +24,18 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// label: _('View Invoices'),
|
label: _('View Invoices'),
|
||||||
// action: console.log
|
action: customer => {
|
||||||
// }
|
router.push({
|
||||||
|
path: `/list/SalesInvoice`,
|
||||||
|
query: {
|
||||||
|
filters: {
|
||||||
|
customer: customer.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
189
src/components/FilterDropdown.vue
Normal file
189
src/components/FilterDropdown.vue
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<Popover :right="true" @hide="emitFilterChange">
|
||||||
|
<template v-slot:target="{ toggleDropdown }">
|
||||||
|
<Button :icon="true" @click="toggleDropdown()">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<Icon name="filter" size="12" class="stroke-current text-gray-800" />
|
||||||
|
<span class="ml-2 text-base text-black">
|
||||||
|
<template v-if="activeFilterCount > 0">
|
||||||
|
{{ filterAppliedMessage }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ _('Filter') }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<div slot="content">
|
||||||
|
<div class="p-3">
|
||||||
|
<template v-if="filters.length">
|
||||||
|
<div
|
||||||
|
:key="filter.fieldname + filter.conditions + filter.value"
|
||||||
|
v-for="(filter, i) in filters"
|
||||||
|
class="flex items-center justify-between text-base"
|
||||||
|
:class="i !== 0 && 'mt-2'"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="w-24">
|
||||||
|
<FormControl
|
||||||
|
size="small"
|
||||||
|
:background="true"
|
||||||
|
:df="{
|
||||||
|
placeholder: 'Field',
|
||||||
|
fieldname: 'fieldname',
|
||||||
|
fieldtype: 'Select',
|
||||||
|
options: fieldOptions
|
||||||
|
}"
|
||||||
|
:value="filter.fieldname"
|
||||||
|
@change="value => (filter.fieldname = value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 w-24">
|
||||||
|
<FormControl
|
||||||
|
size="small"
|
||||||
|
:background="true"
|
||||||
|
:df="{
|
||||||
|
placeholder: 'Condition',
|
||||||
|
fieldname: 'condition',
|
||||||
|
fieldtype: 'Select',
|
||||||
|
options: conditions
|
||||||
|
}"
|
||||||
|
:value="filter.condition"
|
||||||
|
@change="value => (filter.condition = value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 w-24">
|
||||||
|
<FormControl
|
||||||
|
size="small"
|
||||||
|
:background="true"
|
||||||
|
:df="{
|
||||||
|
placeholder: 'Value',
|
||||||
|
fieldname: 'value',
|
||||||
|
fieldtype: 'Data'
|
||||||
|
}"
|
||||||
|
:value="filter.value"
|
||||||
|
@change="value => (filter.value = value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="ml-2 cursor-pointer w-5 h-5 flex items-center justify-center hover:bg-gray-100 rounded"
|
||||||
|
>
|
||||||
|
<feather-icon
|
||||||
|
name="x"
|
||||||
|
class="w-4 h-4"
|
||||||
|
@click="removeFilter(filter)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-base text-gray-600">{{
|
||||||
|
_('No filters selected')
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-base border-t px-3 py-2 flex items-center text-gray-600 cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="addNewFilter"
|
||||||
|
>
|
||||||
|
<feather-icon name="plus" class="w-4 h-4" />
|
||||||
|
<span class="ml-2">{{ _('Add a filter') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Popover from './Popover';
|
||||||
|
import Button from './Button';
|
||||||
|
import Icon from './Icon';
|
||||||
|
import FormControl from './Controls/FormControl';
|
||||||
|
|
||||||
|
let conditions = [
|
||||||
|
{ label: 'Is', value: '=' },
|
||||||
|
{ label: 'Is Not', value: '!=' },
|
||||||
|
{ label: 'Contains', value: 'like' },
|
||||||
|
{ label: 'Does Not Contain', value: 'not like' },
|
||||||
|
{ label: 'Greater Than', value: '>' },
|
||||||
|
{ label: 'Less Than', value: '<' },
|
||||||
|
{ label: 'Is Empty', value: 'is null' },
|
||||||
|
{ label: 'Is Not Empty', value: 'is not null' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FilterDropdown',
|
||||||
|
components: {
|
||||||
|
Popover,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
FormControl
|
||||||
|
},
|
||||||
|
props: ['fields'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filters: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.addNewFilter();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addNewFilter() {
|
||||||
|
let df = this.fields[0];
|
||||||
|
this.addFilter(df.fieldname, 'like', '');
|
||||||
|
},
|
||||||
|
addFilter(fieldname, condition, value) {
|
||||||
|
this.filters.push({ fieldname, condition, value });
|
||||||
|
},
|
||||||
|
removeFilter(filter) {
|
||||||
|
this.filters = this.filters.filter(f => f !== filter);
|
||||||
|
},
|
||||||
|
setFilter(filters) {
|
||||||
|
this.filters = [];
|
||||||
|
Object.keys(filters).map(fieldname => {
|
||||||
|
let parts = filters[fieldname];
|
||||||
|
let condition, value;
|
||||||
|
if (Array.isArray(parts)) {
|
||||||
|
condition = parts[0];
|
||||||
|
value = parts[1];
|
||||||
|
} else {
|
||||||
|
condition = '=';
|
||||||
|
value = parts;
|
||||||
|
}
|
||||||
|
this.addFilter(fieldname, condition, value);
|
||||||
|
});
|
||||||
|
this.emitFilterChange();
|
||||||
|
},
|
||||||
|
emitFilterChange() {
|
||||||
|
let filters = this.filters.reduce((acc, filter) => {
|
||||||
|
if (filter.value === '' && filter.condition) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc[filter.fieldname] = [filter.condition, filter.value];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.$emit('change', filters);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldOptions() {
|
||||||
|
return this.fields.map(df => ({ label: df.label, value: df.fieldname }));
|
||||||
|
},
|
||||||
|
conditions() {
|
||||||
|
return conditions;
|
||||||
|
},
|
||||||
|
activeFilterCount() {
|
||||||
|
return this.filters.filter(filter => filter.value).length;
|
||||||
|
},
|
||||||
|
filterAppliedMessage() {
|
||||||
|
if (this.activeFilterCount === 1) {
|
||||||
|
return this._('1 filter applied');
|
||||||
|
}
|
||||||
|
return this._('{0} filters applied', [this.activeFilterCount]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -4,7 +4,7 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M11.2 1.5L.8 1.5M3 5.75L9 5.75M5 10.5L7 10.5"
|
d="M11.2 1.5L.8 1.5M3 5.75L9 5.75M5 10.5L7 10.5"
|
||||||
transform="translate(0 -1)"
|
transform="translate(0 -2)"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
44
src/components/Popover.vue
Normal file
44
src/components/Popover.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative" v-on-outside-click="() => toggleDropdown(false)">
|
||||||
|
<div class="h-full">
|
||||||
|
<slot name="target" :toggleDropdown="toggleDropdown"></slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="right ? 'right-0' : 'left-0'"
|
||||||
|
class="mt-1 absolute z-10 bg-white rounded-5px border min-w-40 shadow-md"
|
||||||
|
v-if="isShown"
|
||||||
|
>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Popover',
|
||||||
|
props: ['right'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isShown: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isShown(newVal, oldVal) {
|
||||||
|
if (newVal === false) {
|
||||||
|
this.$emit('hide');
|
||||||
|
}
|
||||||
|
if (newVal === true) {
|
||||||
|
this.$emit('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleDropdown(flag, from) {
|
||||||
|
if (flag == null) {
|
||||||
|
flag = !this.isShown;
|
||||||
|
}
|
||||||
|
this.isShown = Boolean(flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -104,7 +104,6 @@ export default {
|
|||||||
frappe.db.on(`change:${this.listConfig.doctype}`, obj => {
|
frappe.db.on(`change:${this.listConfig.doctype}`, obj => {
|
||||||
this.updateData();
|
this.updateData();
|
||||||
});
|
});
|
||||||
frappe.listView.on('filterList', this.updateData.bind(this));
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async setupColumnsAndData() {
|
async setupColumnsAndData() {
|
||||||
@ -127,8 +126,6 @@ export default {
|
|||||||
},
|
},
|
||||||
async updateData(filters) {
|
async updateData(filters) {
|
||||||
if (!filters) filters = this.getFilters();
|
if (!filters) filters = this.getFilters();
|
||||||
// since passing filters as URL params which is String
|
|
||||||
filters = this.formatFilters(filters);
|
|
||||||
this.data = await frappe.db.getAll({
|
this.data = await frappe.db.getAll({
|
||||||
doctype: this.doctype,
|
doctype: this.doctype,
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
|
@ -34,7 +34,6 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
removeFilter(filter) {
|
removeFilter(filter) {
|
||||||
delete this.currentFilters[filter];
|
delete this.currentFilters[filter];
|
||||||
frappe.listView.trigger('filterList', this.currentFilters);
|
|
||||||
this.usedToReRender += 1;
|
this.usedToReRender += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,24 @@
|
|||||||
<PageHeader>
|
<PageHeader>
|
||||||
<h1 slot="title" class="text-2xl font-bold" v-if="title">{{ title }}</h1>
|
<h1 slot="title" class="text-2xl font-bold" v-if="title">{{ title }}</h1>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
<Button :icon="true" type="primary" @click="makeNewDoc">
|
<FilterDropdown
|
||||||
|
ref="filterDropdown"
|
||||||
|
@change="applyFilter"
|
||||||
|
:fields="meta.fields"
|
||||||
|
/>
|
||||||
|
<Button class="ml-2" :icon="true" type="primary" @click="makeNewDoc">
|
||||||
<feather-icon name="plus" class="w-4 h-4 text-white" />
|
<feather-icon name="plus" class="w-4 h-4 text-white" />
|
||||||
</Button>
|
</Button>
|
||||||
<SearchBar class="ml-2" />
|
<SearchBar class="ml-2" />
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div class="flex-1 flex h-full">
|
<div class="flex-1 flex h-full">
|
||||||
<List :listConfig="listConfig" :filters="filters" class="flex-1" />
|
<List
|
||||||
|
ref="list"
|
||||||
|
:listConfig="listConfig"
|
||||||
|
:filters="filters"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -22,6 +32,8 @@ import Button from '@/components/Button';
|
|||||||
import SearchBar from '@/components/SearchBar';
|
import SearchBar from '@/components/SearchBar';
|
||||||
import List from './List';
|
import List from './List';
|
||||||
import listConfigs from './listConfig';
|
import listConfigs from './listConfig';
|
||||||
|
import Icon from '@/components/Icon';
|
||||||
|
import FilterDropdown from '@/components/FilterDropdown';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListView',
|
name: 'ListView',
|
||||||
@ -30,10 +42,14 @@ export default {
|
|||||||
PageHeader,
|
PageHeader,
|
||||||
List,
|
List,
|
||||||
Button,
|
Button,
|
||||||
SearchBar
|
SearchBar,
|
||||||
|
Icon,
|
||||||
|
FilterDropdown
|
||||||
},
|
},
|
||||||
created() {
|
mounted() {
|
||||||
frappe.listView = new Observable();
|
if (typeof this.filters === 'object') {
|
||||||
|
this.$refs.filterDropdown.setFilter(this.filters);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async makeNewDoc() {
|
async makeNewDoc() {
|
||||||
@ -52,6 +68,9 @@ export default {
|
|||||||
this.$router.replace(path);
|
this.$router.replace(path);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
applyFilter(filters) {
|
||||||
|
this.$refs.list.updateData(filters);
|
||||||
|
},
|
||||||
getFormPath(name) {
|
getFormPath(name) {
|
||||||
if (this.listConfig.formRoute) {
|
if (this.listConfig.formRoute) {
|
||||||
let path = this.listConfig.formRoute(name);
|
let path = this.listConfig.formRoute(name);
|
||||||
@ -68,6 +87,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
meta() {
|
||||||
|
return frappe.getMeta(this.doctype);
|
||||||
|
},
|
||||||
listConfig() {
|
listConfig() {
|
||||||
if (listConfigs[this.doctype]) {
|
if (listConfigs[this.doctype]) {
|
||||||
return listConfigs[this.doctype];
|
return listConfigs[this.doctype];
|
||||||
@ -75,17 +97,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
title: this.doctype,
|
title: this.doctype,
|
||||||
doctype: this.doctype,
|
doctype: this.doctype,
|
||||||
columns: frappe.getMeta(this.doctype).getKeywordFields()
|
columns: this.meta.getKeywordFields()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.listConfig) {
|
return this.listConfig.title || this.doctype;
|
||||||
return typeof this.listConfig.title === 'function'
|
|
||||||
? this.listConfig.title(this.filters)
|
|
||||||
: this.listConfig.title;
|
|
||||||
}
|
|
||||||
return this.doctype;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user