Make Buefy grids use proper Vue.js component structure

at least, better than before...this lets each page have the final say about the
app logic etc.
This commit is contained in:
Lance Edgar 2019-05-23 12:10:11 -05:00
parent 6c3722737d
commit 7b1947914e
4 changed files with 340 additions and 317 deletions

View file

@ -0,0 +1,168 @@
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)
let TailboneGrid = {
template: '#tailbone-grid-template',
methods: {
getRowClass(row, index) {
return this.rowStatusMap[index]
},
loadAsyncData(params) {
if (params === undefined) {
params = [
'partial=true',
`sortkey=${this.sortField}`,
`sortdir=${this.sortOrder}`,
`pagesize=${this.perPage}`,
`page=${this.page}`
].join('&')
}
this.loading = true
this.$http.get(`?${params}`).then(({ data }) => {
this.data = data.data
this.rowStatusMap = data.row_status_map
this.total = data.total_items
this.firstItem = data.first_item
this.lastItem = data.last_item
this.loading = false
})
.catch((error) => {
this.data = []
this.total = 0
this.loading = false
throw error
})
},
onPageChange(page) {
this.page = page
this.loadAsyncData()
},
onSort(field, order) {
this.sortField = field
this.sortOrder = order
// always reset to first page when changing sort options
// TODO: i mean..right? would we ever not want that?
this.page = 1
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)
},
deleteResults(event) {
// submit form if user confirms
// TODO: how/where to get/show "plural model title" here?
// if (confirm("You are about to delete " + this.total + " ${grid.model_title_plural}.\n\nAre you sure?")) {
if (confirm("You are about to delete " + this.total + " objects.\n\nAre you sure?")) {
event.target.form.submit()
}
},
checkedRowUUIDs() {
var uuids = [];
for (var row of this.$data.checkedRows) {
uuids.push(row.uuid)
}
return uuids
}
}
}

View file

@ -1,24 +0,0 @@
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

@ -60,311 +60,170 @@
</script> </script>
<script type="text/x-template" id="tailbone-grid-template">
<div>
<div id="buefy-grid-app"> <div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;"> <div style="display: flex; flex-direction: column; justify-content: space-between;">
<div></div>
<div style="display: flex; flex-direction: column; justify-content: space-between;"> <div class="filters">
<div></div> % if grid.filterable:
<div class="filters">
% if grid.filterable:
## TODO: stop using |n filter
${grid.render_filters(template='/grids/filters_buefy.mako', allow_save_defaults=allow_save_defaults)|n}
% endif
</div>
</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 ## TODO: stop using |n filter
${context_menu|n} ${grid.render_filters(template='/grids/filters_buefy.mako', allow_save_defaults=allow_save_defaults)|n}
</ul> % endif
% endif
</div>
<div class="grid-tools-wrapper">
% if tools:
<div class="grid-tools field is-grouped">
## TODO: stop using |n filter
${tools|n}
</div>
% endif
</div>
</div>
</div>
<b-table
:data="data"
:columns="columns"
:loading="loading"
:row-class="getRowClass"
% if grid.checkboxes:
checkable
:checked-rows.sync="checkedRows"
## TODO: definitely will be wanting this...
## :is-row-checkable=""
% endif
:default-sort="[sortField, sortOrder]"
backend-sorting
@sort="onSort"
% if grid.pageable:
paginated
:per-page="perPage"
:current-page="page"
backend-pagination
:total="total"
@page-change="onPageChange"
% endif
## TODO: should let grid (or master view) decide how to set these?
icon-pack="fas"
## note that :striped="true" was interfering with row status (e.g. warning) styles
:striped="false"
:hoverable="true"
:narrowed="true">
<template slot-scope="props">
% for column in grid_columns:
<b-table-column field="${column['field']}" label="${column['label']}" ${'sortable' if column['sortable'] else ''}>
% if grid.is_linked(column['field']):
<a :href="props.row._action_url_view" v-html="props.row.${column['field']}"></a>
% else:
<span v-html="props.row.${column['field']}"></span>
% endif
</b-table-column>
% endfor
% if grid.main_actions or grid.more_actions:
<b-table-column field="actions" label="Actions">
% for action in grid.main_actions:
<a :href="props.row._action_url_${action.key}"><i class="fas fa-${action.icon}"></i>
${action.label}
</a>
% endfor
</b-table-column>
% endif
</template>
<template slot="empty">
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>
<b-icon
pack="fas"
icon="fas fa-sad-tear"
size="is-large">
</b-icon>
</p>
<p>Nothing here.</p>
</div> </div>
</section> </div>
</template>
% if grid.pageable and grid.pager: <div style="display: flex; flex-direction: column; justify-content: space-between;">
<template slot="footer">
<b-field grouped position="is-right">
<span class="control">
showing {{ firstItem }} - {{ lastItem }} of {{ total }} results,
</span>
<b-select v-model="perPage"
size="is-small"
@input="loadAsyncData()">
% for value in grid.get_pagesize_options():
<option value="${value}">${value}</option>
% endfor
</b-select>
<span class="control">
per page
</span>
</b-field>
</template>
% endif
</b-table> <div class="context-menu">
</div> % 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 field is-grouped">
## TODO: stop using |n filter
${tools|n}
</div>
% endif
</div>
</div>
</div>
<b-table
:data="data"
## :columns="columns"
:loading="loading"
:row-class="getRowClass"
:checkable="checkable"
:checked-rows.sync="checkedRows"
## TODO: definitely will be wanting this...
## :is-row-checkable=""
:default-sort="[sortField, sortOrder]"
backend-sorting
@sort="onSort"
## % if grid.pageable:
## paginated
:paginated="paginated"
:per-page="perPage"
:current-page="currentPage"
backend-pagination
:total="total"
@page-change="onPageChange"
## % endif
## TODO: should let grid (or master view) decide how to set these?
icon-pack="fas"
## note that :striped="true" was interfering with row status (e.g. warning) styles
:striped="false"
:hoverable="true"
:narrowed="true">
<template slot-scope="props">
% for column in grid_columns:
<b-table-column field="${column['field']}" label="${column['label']}" ${'sortable' if column['sortable'] else ''}>
% if grid.is_linked(column['field']):
<a :href="props.row._action_url_view" v-html="props.row.${column['field']}"></a>
% else:
<span v-html="props.row.${column['field']}"></span>
% endif
</b-table-column>
% endfor
% if grid.main_actions or grid.more_actions:
<b-table-column field="actions" label="Actions">
% for action in grid.main_actions:
<a :href="props.row._action_url_${action.key}"><i class="fas fa-${action.icon}"></i>
${action.label}
</a>
% endfor
</b-table-column>
% endif
</template>
<template slot="empty">
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>
<b-icon
pack="fas"
icon="fas fa-sad-tear"
size="is-large">
</b-icon>
</p>
<p>Nothing here.</p>
</div>
</section>
</template>
% if grid.pageable and grid.pager:
<template slot="footer">
<b-field grouped position="is-right">
<span class="control">
showing {{ firstItem }} - {{ lastItem }} of {{ total }} results,
</span>
<b-select v-model="perPage"
size="is-small"
@input="loadAsyncData()">
% for value in grid.get_pagesize_options():
<option value="${value}">${value}</option>
% endfor
</b-select>
<span class="control">
per page
</span>
</b-field>
</template>
% endif
</b-table>
</div>
</script>
<script type="text/javascript"> <script type="text/javascript">
new Vue({ let TailboneGridData = {
el: '#buefy-grid-app', loading: false,
data() { checkedRows: [],
return { selectedFilter: null,
data: ${json.dumps(grid_data['data'])|n},
loading: false,
rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n},
## TODO: should be dumping json from server here
checkedRows: [],
% if grid.sortable: data: ${json.dumps(grid_data['data'])|n},
sortField: '${grid.sortkey}', rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n},
sortOrder: '${grid.sortdir}',
% endif
% if grid.pageable: checkable: ${json.dumps(grid.checkboxes)|n},
% if static_data: ## TODO: should be dumping json from server here?
total: ${len(grid_data['data'])}, ## checkedRows: [],
% else:
total: ${grid_data['total_items']},
% endif
perPage: ${grid.pagesize},
page: ${grid.page},
firstItem: ${json.dumps(grid_data['first_item'])|n},
lastItem: ${json.dumps(grid_data['last_item'])|n},
% endif
% if grid.filterable: paginated: ${json.dumps(grid.pageable)|n},
filters: ${json.dumps(filters_data)|n}, total: ${len(grid_data['data']) if static_data else grid_data['total_items']},
filtersSequence: ${json.dumps(filters_sequence)|n}, perPage: ${json.dumps(grid.pagesize if grid.pageable else None)|n},
selectedFilter: null, currentPage: ${json.dumps(grid.page if grid.pageable else None)|n},
% endif firstItem: ${json.dumps(grid_data['first_item'] if grid.pageable else None)|n},
} lastItem: ${json.dumps(grid_data['last_item'] if grid.pageable else None)|n},
},
methods: {
getRowClass(row, index) { sortField: ${json.dumps(grid.sortkey if grid.sortable else None)|n},
return this.rowStatusMap[index] sortOrder: ${json.dumps(grid.sortdir if grid.sortable else None)|n},
},
loadAsyncData(params) { ## filterable: ${json.dumps(grid.filterable)|n},
filters: ${json.dumps(filters_data if grid.filterable else None)|n},
if (params === undefined) { filtersSequence: ${json.dumps(filters_sequence if grid.filterable else None)|n},
params = [ selectedFilter: null,
'partial=true', }
`sortkey=${'$'}{this.sortField}`,
`sortdir=${'$'}{this.sortOrder}`,
`pagesize=${'$'}{this.perPage}`,
`page=${'$'}{this.page}`
].join('&')
}
this.loading = true
this.$http.get(`${request.current_route_url(_query=None)}?${'$'}{params}`).then(({ data }) => {
this.data = data.data
this.rowStatusMap = data.row_status_map
this.total = data.total_items
this.firstItem = data.first_item
this.lastItem = data.last_item
this.loading = false
})
.catch((error) => {
this.data = []
this.total = 0
this.loading = false
throw error
})
},
onPageChange(page) {
this.page = page
this.loadAsyncData()
},
onSort(field, order) {
this.sortField = field
this.sortOrder = order
// always reset to first page when changing sort options
// TODO: i mean..right? would we ever not want that?
this.page = 1
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)
},
deleteResults(event) {
// submit form if user confirms
if (confirm("You are about to delete " + this.total + " ${grid.model_title_plural}.\n\nAre you sure?")) {
event.target.form.submit()
}
},
checkedRowUUIDs() {
var uuids = [];
for (var row of this.$data.checkedRows) {
uuids.push(row.uuid)
}
return uuids
}
}
});
</script> </script>
<div id="tailbone-grid-app">
<tailbone-grid></tailbone-grid>
</div>

View file

@ -15,7 +15,7 @@
<%def name="extra_javascript()"> <%def name="extra_javascript()">
${parent.extra_javascript()} ${parent.extra_javascript()}
% if use_buefy: % if use_buefy:
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.gridfilters.js') + '?ver={}'.format(tailbone.__version__))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.grid.js') + '?ver={}'.format(tailbone.__version__))}
% endif % endif
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
@ -200,10 +200,30 @@
</%def> </%def>
<%def name="modify_tailbone_grid()">
## NOTE: if you override this, must use <script> tags
</%def>
<%def name="make_tailbone_grid_app()">
${self.modify_tailbone_grid()}
<script type="text/javascript">
TailboneGrid.data = function() { return TailboneGridData }
Vue.component('tailbone-grid', TailboneGrid)
new Vue({
el: '#tailbone-grid-app'
});
</script>
</%def>
% if use_buefy: % if use_buefy:
## TODO: stop using |n filter ## TODO: stop using |n filter
${grid.render_buefy(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n} ${grid.render_buefy(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
${self.make_tailbone_grid_app()}
% else: % else:
## no buefy, so do the traditional thing ## no buefy, so do the traditional thing