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:
parent
6c3722737d
commit
7b1947914e
168
tailbone/static/js/tailbone.buefy.grid.js
Normal file
168
tailbone/static/js/tailbone.buefy.grid.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -60,311 +60,170 @@
|
|||
|
||||
</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 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">
|
||||
<div style="display: flex; flex-direction: column; justify-content: space-between;">
|
||||
<div></div>
|
||||
<div class="filters">
|
||||
% if grid.filterable:
|
||||
## 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"
|
||||
|
||||
% 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>
|
||||
${grid.render_filters(template='/grids/filters_buefy.mako', allow_save_defaults=allow_save_defaults)|n}
|
||||
% endif
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
% 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
|
||||
<div style="display: flex; flex-direction: column; justify-content: space-between;">
|
||||
|
||||
</b-table>
|
||||
</div>
|
||||
<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 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">
|
||||
|
||||
new Vue({
|
||||
el: '#buefy-grid-app',
|
||||
data() {
|
||||
return {
|
||||
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: [],
|
||||
let TailboneGridData = {
|
||||
loading: false,
|
||||
checkedRows: [],
|
||||
selectedFilter: null,
|
||||
|
||||
% if grid.sortable:
|
||||
sortField: '${grid.sortkey}',
|
||||
sortOrder: '${grid.sortdir}',
|
||||
% endif
|
||||
data: ${json.dumps(grid_data['data'])|n},
|
||||
rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n},
|
||||
|
||||
% if grid.pageable:
|
||||
% if static_data:
|
||||
total: ${len(grid_data['data'])},
|
||||
% 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
|
||||
checkable: ${json.dumps(grid.checkboxes)|n},
|
||||
## TODO: should be dumping json from server here?
|
||||
## checkedRows: [],
|
||||
|
||||
% if grid.filterable:
|
||||
filters: ${json.dumps(filters_data)|n},
|
||||
filtersSequence: ${json.dumps(filters_sequence)|n},
|
||||
selectedFilter: null,
|
||||
% endif
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
paginated: ${json.dumps(grid.pageable)|n},
|
||||
total: ${len(grid_data['data']) if static_data else grid_data['total_items']},
|
||||
perPage: ${json.dumps(grid.pagesize if grid.pageable else None)|n},
|
||||
currentPage: ${json.dumps(grid.page if grid.pageable else None)|n},
|
||||
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},
|
||||
|
||||
getRowClass(row, index) {
|
||||
return this.rowStatusMap[index]
|
||||
},
|
||||
sortField: ${json.dumps(grid.sortkey if grid.sortable else None)|n},
|
||||
sortOrder: ${json.dumps(grid.sortdir if grid.sortable else None)|n},
|
||||
|
||||
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(`${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
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
## filterable: ${json.dumps(grid.filterable)|n},
|
||||
filters: ${json.dumps(filters_data if grid.filterable else None)|n},
|
||||
filtersSequence: ${json.dumps(filters_sequence if grid.filterable else None)|n},
|
||||
selectedFilter: null,
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div id="tailbone-grid-app">
|
||||
<tailbone-grid></tailbone-grid>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<%def name="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__))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.grid.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
% endif
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
|
@ -200,10 +200,30 @@
|
|||
|
||||
</%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:
|
||||
## TODO: stop using |n filter
|
||||
${grid.render_buefy(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
|
||||
${self.make_tailbone_grid_app()}
|
||||
|
||||
% else:
|
||||
## no buefy, so do the traditional thing
|
||||
|
|
Loading…
Reference in a new issue