Add "most of" Buefy support for grid filters

still a couple of details to wrap up yet, but this is most of it!
This commit is contained in:
Lance Edgar 2019-04-15 18:36:14 -05:00
parent 23c38e33d4
commit a0cd1f4cd0
8 changed files with 313 additions and 96 deletions

View file

@ -384,6 +384,7 @@ class Grid(object):
def render_grid(self, template='/grids/grid.mako', **kwargs): def render_grid(self, template='/grids/grid.mako', **kwargs):
context = kwargs context = kwargs
context['grid'] = self context['grid'] = self
context['request'] = self.request
grid_class = '' grid_class = ''
if self.width == 'full': if self.width == 'full':
grid_class = 'full' grid_class = 'full'
@ -912,10 +913,57 @@ class Grid(object):
def render_buefy(self, template='/grids/buefy.mako', **kwargs): def render_buefy(self, template='/grids/buefy.mako', **kwargs):
""" """
Render the Buefy grid, including filters. Render the Buefy grid, complete with filters. Note that this also
includes the context menu items and grid tools.
""" """
if 'grid_columns' not in kwargs:
kwargs['grid_columns'] = self.get_buefy_columns()
if 'grid_data' not in kwargs:
kwargs['grid_data'] = self.get_buefy_data()
if 'static_data' not in kwargs:
kwargs['static_data'] = self.has_static_data()
if self.filterable and 'filters_data' not in kwargs:
kwargs['filters_data'] = self.get_filters_data()
if self.filterable and 'filters_sequence' not in kwargs:
kwargs['filters_sequence'] = self.get_filters_sequence()
return self.render_complete(template=template, **kwargs) return self.render_complete(template=template, **kwargs)
def get_filters_sequence(self):
"""
Returns a list of filter keys (strings) in the sequence with which they
should be displayed in the UI.
"""
return list(self.filters)
def get_filters_data(self):
"""
Returns a dict of current filters data, for use with Buefy grid view.
"""
data = {}
for filtr in self.filters.values():
valueless = [v for v in filtr.valueless_verbs
if v in filtr.verbs]
data[filtr.key] = {
'key': filtr.key,
'label': filtr.label,
'active': filtr.active,
'visible': filtr.active,
'verbs': filtr.verbs,
'valueless_verbs': valueless,
'verb_labels': filtr.verb_labels,
'verb': filtr.verb or filtr.default_verb or filtr.verbs[0],
'value': filtr.value,
}
return data
def render_filters(self, template='/grids/filters.mako', **kwargs): def render_filters(self, template='/grids/filters.mako', **kwargs):
""" """
Render the filters to a Unicode string, using the specified template. Render the filters to a Unicode string, using the specified template.

View file

@ -0,0 +1,24 @@
const GridFilter = {
template: '#grid-filter-template',
props: {
filter: Object
},
methods: {
changeVerb() {
// set focus to value input, "as quickly as we can"
this.$nextTick(function() {
this.focusValue()
})
},
focusValue: function() {
this.$refs.valueInput.focus()
// this.$refs.valueInput.select()
}
}
}
Vue.component('grid-filter', GridFilter)

View file

@ -0,0 +1,8 @@
/******************************
* Grid Filters
******************************/
.filters .filter {
margin-bottom: 0.5rem;
}

View file

@ -1,6 +1,80 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<div id="buefy-table-app"> <script type="text/x-template" id="grid-filter-template">
<div class="level filter" v-show="filter.visible">
<div class="level-left">
<div class="level-item">
<b-field>
<b-checkbox-button v-model="filter.active" native-value="IGNORED">
<b-icon pack="fas" icon="check" v-show="filter.active"></b-icon>
<span>{{ filter.label }}</span>
</b-checkbox-button>
</b-field>
</div>
<b-field grouped v-show="filter.active" custom-class="level-item">
<b-select v-model="filter.verb"
@input="focusValue()">
<option v-for="verb in filter.verbs"
:key="verb"
:value="verb">
{{ filter.verb_labels[verb] }}
</option>
</b-select>
<b-input v-model="filter.value"
v-show="! (filter.valueless_verbs && filter.valueless_verbs.includes(filter.verb))"
ref="valueInput">
</b-input>
</b-field>
</div><!-- level-left -->
</div><!-- level -->
</script>
<div id="buefy-grid-app">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;">
<div class="filters">
% if grid.filterable:
## TODO: stop using |n filter
${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
% endif
</div>
<div style="display: flex; flex-direction: column; justify-content: space-between;">
<div class="context-menu">
% if context_menu:
<ul id="context-menu">
## TODO: stop using |n filter
${context_menu|n}
</ul>
% endif
</div>
<div class="grid-tools-wrapper">
% if tools:
<div class="grid-tools">
## TODO: stop using |n filter
${tools|n}
</div>
% endif
</div>
</div>
</div>
<b-table <b-table
:data="data" :data="data"
:columns="columns" :columns="columns"
@ -76,7 +150,7 @@
<script type="text/javascript"> <script type="text/javascript">
new Vue({ new Vue({
el: '#buefy-table-app', el: '#buefy-grid-app',
data() { data() {
return { return {
data: ${json.dumps(grid_data['data'])|n}, data: ${json.dumps(grid_data['data'])|n},
@ -84,6 +158,7 @@
sortField: '${grid.sortkey}', sortField: '${grid.sortkey}',
sortOrder: '${grid.sortdir}', sortOrder: '${grid.sortdir}',
rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n}, rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n},
% if grid.pageable: % if grid.pageable:
% if static_data: % if static_data:
total: ${len(grid_data['data'])}, total: ${len(grid_data['data'])},
@ -92,8 +167,14 @@
% endif % endif
perPage: ${grid.pagesize}, perPage: ${grid.pagesize},
page: ${grid.page}, page: ${grid.page},
firstItem: ${grid_data['first_item']}, firstItem: ${json.dumps(grid_data['first_item'])|n},
lastItem: ${grid_data['last_item']}, lastItem: ${json.dumps(grid_data['last_item'])|n},
% endif
% if grid.filterable:
filters: ${json.dumps(filters_data)|n},
filtersSequence: ${json.dumps(filters_sequence)|n},
selectedFilter: null,
% endif % endif
} }
}, },
@ -103,15 +184,17 @@
return this.rowStatusMap[index] return this.rowStatusMap[index]
}, },
loadAsyncData() { loadAsyncData(params) {
const params = [ if (params === undefined) {
'partial=true', params = [
`sortkey=${'$'}{this.sortField}`, 'partial=true',
`sortdir=${'$'}{this.sortOrder}`, `sortkey=${'$'}{this.sortField}`,
`pagesize=${'$'}{this.perPage}`, `sortdir=${'$'}{this.sortOrder}`,
`page=${'$'}{this.page}` `pagesize=${'$'}{this.perPage}`,
].join('&') `page=${'$'}{this.page}`
].join('&')
}
this.loading = true this.loading = true
this.$http.get(`${request.current_route_url(_query=None)}?${'$'}{params}`).then(({ data }) => { this.$http.get(`${request.current_route_url(_query=None)}?${'$'}{params}`).then(({ data }) => {
@ -142,6 +225,77 @@
// TODO: i mean..right? would we ever not want that? // TODO: i mean..right? would we ever not want that?
this.page = 1 this.page = 1
this.loadAsyncData() this.loadAsyncData()
},
resetView() {
this.loading = true
location.href = '?reset-to-default-filters=true'
},
addFilter(filter_key) {
// reset dropdown so user again sees "Add Filter" placeholder
this.$nextTick(function() {
this.selectedFilter = null
})
// show corresponding grid filter
this.filters[filter_key].visible = true
this.filters[filter_key].active = true
// track down the component
var gridFilter = null
for (var gf of this.$refs.gridFilters) {
if (gf.filter.key == filter_key) {
gridFilter = gf
break
}
}
// tell component to focus the value field, ASAP
this.$nextTick(function() {
gridFilter.focusValue()
})
},
applyFilters(params) {
if (params === undefined) {
params = []
}
params.push('partial=true')
params.push('filter=true')
for (var key in this.filters) {
var filter = this.filters[key]
if (filter.active) {
params.push(key + '=' + encodeURIComponent(filter.value))
params.push(key + '.verb=' + encodeURIComponent(filter.verb))
} else {
filter.visible = false
}
}
this.loadAsyncData(params.join('&'))
},
clearFilters() {
// explicitly deactivate all filters
for (var key in this.filters) {
this.filters[key].active = false
}
// then just "apply" as normal
this.applyFilters()
},
saveDefaults() {
// apply current filters as normal, but add special directive
const params = ['save-current-filters-as-defaults=true']
this.applyFilters(params)
} }
} }

View file

@ -14,6 +14,9 @@
<%def name="extra_javascript()"> <%def name="extra_javascript()">
${parent.extra_javascript()} ${parent.extra_javascript()}
% if use_buefy:
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.gridfilters.js') + '?ver={}'.format(tailbone.__version__))}
% endif
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
@ -174,7 +177,8 @@
% if use_buefy: % if use_buefy:
${grid.render_buefy(grid_columns=grid_columns, grid_data=grid_data, static_data=static_data)|n} ## TODO: stop using |n filter
${grid.render_buefy(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
% else: % else:
## no buefy, so do the traditional thing ## no buefy, so do the traditional thing

View file

@ -268,8 +268,8 @@
## (needed for e.g. this.$http.get() calls, used by grid at least) ## (needed for e.g. this.$http.get() calls, used by grid at least)
${h.javascript_link('https://cdn.jsdelivr.net/npm/vue-resource@1.5.1')} ${h.javascript_link('https://cdn.jsdelivr.net/npm/vue-resource@1.5.1')}
## Buefy 0.7.3 ## Buefy 0.7.4
${h.javascript_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.js')} ${h.javascript_link('https://unpkg.com/buefy@0.7.4/dist/buefy.min.js')}
## FontAwesome 5.3.1 ## FontAwesome 5.3.1
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script> <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
@ -331,6 +331,7 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css') + '?ver={}'.format(tailbone.__version__))} ${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css') + '?ver={}'.format(tailbone.__version__))}
${h.stylesheet_link(request.static_url('tailbone:static/themes/falafel/css/grids.rowstatus.css') + '?ver={}'.format(tailbone.__version__))} ${h.stylesheet_link(request.static_url('tailbone:static/themes/falafel/css/grids.rowstatus.css') + '?ver={}'.format(tailbone.__version__))}
## ${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css') + '?ver={}'.format(tailbone.__version__))} ## ${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css') + '?ver={}'.format(tailbone.__version__))}
${h.stylesheet_link(request.static_url('tailbone:static/themes/falafel/css/filters.css') + '?ver={}'.format(tailbone.__version__))}
${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/forms.css') + '?ver={}'.format(tailbone.__version__))} ${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/forms.css') + '?ver={}'.format(tailbone.__version__))}
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))} ${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
</%def> </%def>

View file

@ -1,76 +1,57 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<div class="newfilters">
${h.form(form.action_url, method='get')} <form action="${form.action_url}" method="GET" v-on:submit.prevent="applyFilters()">
${h.hidden('reset-to-default-filters', value='false')}
${h.hidden('save-current-filters-as-defaults', value='false')}
<fieldset> <grid-filter v-for="key in filtersSequence"
<legend>Filters</legend> :key="key"
% for filtr in form.iter_filters(): :filter="filters[key]"
<div class="filter" id="filter-${filtr.key}" data-key="${filtr.key}"${' style="display: none;"' if not filtr.active else ''|n}> ref="gridFilters">
${h.checkbox('{}-active'.format(filtr.key), class_='active', id='filter-active-{}'.format(filtr.key), checked=filtr.active)} </grid-filter>
<label for="filter-active-${filtr.key}">${filtr.label}</label>
<div class="inputs">
${form.filter_verb(filtr)}
${form.filter_value(filtr)}
</div>
</div>
% endfor
</fieldset>
<div class="level"> <b-field grouped>
<div class="level-left">
<div class="level-item">
## <button type="submit" class="button is-primary" id="apply-filters">Apply Filters</button>
<a class="button is-primary">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>Apply Filters</span>
</a>
</div>
<div class="level-item">
<div class="select">
<select id="add-filter">
<option value="">Add Filter</option>
% for filtr in form.iter_filters():
<option value="${filtr.key}"${' disabled="disabled"' if filtr.active else ''|n}>${filtr.label}</option>
% endfor
</select>
</div>
</div>
<div class="level-item">
## <button type="button" class="button" id="default-filters">Default View</button>
<a class="button">
<span class="icon is-small">
<i class="fas fa-home"></i>
</span>
<span>Default View</span>
</a>
</div>
<div class="level-item">
## <button type="button" class="button" id="clear-filters">No Filters</button>
<a class="button">
<span class="icon is-small">
<i class="fas fa-trash"></i>
</span>
<span>No Filters</span>
</a>
</div>
% if allow_save_defaults and request.user:
<div class="level-item">
## <button type="button" class="button" id="save-defaults">Save Defaults</button>
<a class="button">
<span class="icon is-small">
<i class="fas fa-save"></i>
</span>
<span>Save Defaults</span>
</a>
</div>
% endif
</div>
</div>
${h.end_form()} <b-button type="is-primary"
</div><!-- newfilters --> native-type="submit"
icon-pack="fas"
icon-left="check"
class="control">
Apply Filters
</b-button>
<b-select @input="addFilter"
placeholder="Add Filter"
v-model="selectedFilter">
<option v-for="key in filtersSequence"
:key="key"
:value="key"
:disabled="filters[key].visible">
{{ filters[key].label }}
</option>
</b-select>
<b-button @click="resetView()"
icon-pack="fas"
icon-left="home"
class="control">
Default View
</b-button>
<b-button @click="clearFilters()"
icon-pack="fas"
icon-left="trash"
class="control">
No Filters
</b-button>
% if allow_save_defaults and request.user:
<b-button @click="saveDefaults()"
icon-pack="fas"
icon-left="save"
class="control">
Save Defaults
</b-button>
% endif
</b-field>
</form>

View file

@ -253,6 +253,9 @@ class MasterView(View):
labels.update(cls.row_labels) labels.update(cls.row_labels)
return labels return labels
def get_use_buefy(self):
return self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy')
############################## ##############################
# Available Views # Available Views
############################## ##############################
@ -267,7 +270,7 @@ class MasterView(View):
""" """
self.listing = True self.listing = True
grid = self.make_grid() grid = self.make_grid()
use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy') use_buefy = self.get_use_buefy()
# If user just refreshed the page with a reset instruction, issue a # If user just refreshed the page with a reset instruction, issue a
# redirect in order to clear out the query string. # redirect in order to clear out the query string.
@ -293,12 +296,6 @@ class MasterView(View):
'grid': grid, 'grid': grid,
'use_buefy': use_buefy, 'use_buefy': use_buefy,
} }
if use_buefy:
context['grid_columns'] = grid.get_buefy_columns()
context['grid_data'] = grid.get_buefy_data()
context['static_data'] = grid.has_static_data()
return self.render_to_response('index', context) return self.render_to_response('index', context)
def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
@ -2253,7 +2250,7 @@ class MasterView(View):
""" """
actions = [] actions = []
prefix = self.get_permission_prefix() prefix = self.get_permission_prefix()
use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy') use_buefy = self.get_use_buefy()
if self.viewable and self.request.has_perm('{}.view'.format(prefix)): if self.viewable and self.request.has_perm('{}.view'.format(prefix)):
url = self.get_view_index_url if self.use_index_links else None url = self.get_view_index_url if self.use_index_links else None
icon = 'eye' if use_buefy else 'zoomin' icon = 'eye' if use_buefy else 'zoomin'
@ -2270,7 +2267,7 @@ class MasterView(View):
""" """
actions = [] actions = []
prefix = self.get_permission_prefix() prefix = self.get_permission_prefix()
use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy') use_buefy = self.get_use_buefy()
if self.editable and self.request.has_perm('{}.edit'.format(prefix)): if self.editable and self.request.has_perm('{}.edit'.format(prefix)):
icon = 'edit' if use_buefy else 'pencil' icon = 'edit' if use_buefy else 'pencil'
actions.append(self.make_action('edit', icon=icon, url=self.default_edit_url)) actions.append(self.make_action('edit', icon=icon, url=self.default_edit_url))