mirror of
https://github.com/frappe/books.git
synced 2025-01-08 17:24:05 +00:00
feat: FilterDropdown in ListView
This commit is contained in:
parent
f52ee12e66
commit
5cc57d3748
@ -24,9 +24,18 @@ module.exports = {
|
||||
});
|
||||
}
|
||||
},
|
||||
// {
|
||||
// label: _('View Invoices'),
|
||||
// action: console.log
|
||||
// }
|
||||
{
|
||||
label: _('View Invoices'),
|
||||
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-linejoin="round"
|
||||
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>
|
||||
</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 => {
|
||||
this.updateData();
|
||||
});
|
||||
frappe.listView.on('filterList', this.updateData.bind(this));
|
||||
},
|
||||
methods: {
|
||||
async setupColumnsAndData() {
|
||||
@ -127,8 +126,6 @@ export default {
|
||||
},
|
||||
async updateData(filters) {
|
||||
if (!filters) filters = this.getFilters();
|
||||
// since passing filters as URL params which is String
|
||||
filters = this.formatFilters(filters);
|
||||
this.data = await frappe.db.getAll({
|
||||
doctype: this.doctype,
|
||||
fields: ['*'],
|
||||
|
@ -34,7 +34,6 @@ export default {
|
||||
methods: {
|
||||
removeFilter(filter) {
|
||||
delete this.currentFilters[filter];
|
||||
frappe.listView.trigger('filterList', this.currentFilters);
|
||||
this.usedToReRender += 1;
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,24 @@
|
||||
<PageHeader>
|
||||
<h1 slot="title" class="text-2xl font-bold" v-if="title">{{ title }}</h1>
|
||||
<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" />
|
||||
</Button>
|
||||
<SearchBar class="ml-2" />
|
||||
</template>
|
||||
</PageHeader>
|
||||
<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>
|
||||
</template>
|
||||
@ -22,6 +32,8 @@ import Button from '@/components/Button';
|
||||
import SearchBar from '@/components/SearchBar';
|
||||
import List from './List';
|
||||
import listConfigs from './listConfig';
|
||||
import Icon from '@/components/Icon';
|
||||
import FilterDropdown from '@/components/FilterDropdown';
|
||||
|
||||
export default {
|
||||
name: 'ListView',
|
||||
@ -30,10 +42,14 @@ export default {
|
||||
PageHeader,
|
||||
List,
|
||||
Button,
|
||||
SearchBar
|
||||
SearchBar,
|
||||
Icon,
|
||||
FilterDropdown
|
||||
},
|
||||
created() {
|
||||
frappe.listView = new Observable();
|
||||
mounted() {
|
||||
if (typeof this.filters === 'object') {
|
||||
this.$refs.filterDropdown.setFilter(this.filters);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async makeNewDoc() {
|
||||
@ -52,6 +68,9 @@ export default {
|
||||
this.$router.replace(path);
|
||||
});
|
||||
},
|
||||
applyFilter(filters) {
|
||||
this.$refs.list.updateData(filters);
|
||||
},
|
||||
getFormPath(name) {
|
||||
if (this.listConfig.formRoute) {
|
||||
let path = this.listConfig.formRoute(name);
|
||||
@ -68,6 +87,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
meta() {
|
||||
return frappe.getMeta(this.doctype);
|
||||
},
|
||||
listConfig() {
|
||||
if (listConfigs[this.doctype]) {
|
||||
return listConfigs[this.doctype];
|
||||
@ -75,17 +97,12 @@ export default {
|
||||
return {
|
||||
title: this.doctype,
|
||||
doctype: this.doctype,
|
||||
columns: frappe.getMeta(this.doctype).getKeywordFields()
|
||||
columns: this.meta.getKeywordFields()
|
||||
};
|
||||
}
|
||||
},
|
||||
title() {
|
||||
if (this.listConfig) {
|
||||
return typeof this.listConfig.title === 'function'
|
||||
? this.listConfig.title(this.filters)
|
||||
: this.listConfig.title;
|
||||
}
|
||||
return this.doctype;
|
||||
return this.listConfig.title || this.doctype;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user