Add basic "Buefy" support for grids (master index view)
still pretty experimental at this point, but making progress
This commit is contained in:
parent
3cef591719
commit
8d6ecc3ec7
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2018 Lance Edgar
|
||||
# Copyright © 2010-2019 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -176,6 +176,12 @@ class Grid(object):
|
|||
if key in self.filters:
|
||||
self.filters[key].label = label
|
||||
|
||||
def get_label(self, key):
|
||||
"""
|
||||
Returns the label text for given field key.
|
||||
"""
|
||||
return self.labels.get(key, prettify(key))
|
||||
|
||||
def set_link(self, key, link=True):
|
||||
if link:
|
||||
if key not in self.linked_columns:
|
||||
|
@ -900,9 +906,16 @@ class Grid(object):
|
|||
"""
|
||||
context = kwargs
|
||||
context['grid'] = self
|
||||
context['request'] = self.request
|
||||
context.setdefault('allow_save_defaults', True)
|
||||
return render(template, context)
|
||||
|
||||
def render_buefy(self, template='/grids/buefy.mako', **kwargs):
|
||||
"""
|
||||
Render the Buefy grid, including filters.
|
||||
"""
|
||||
return self.render_complete(template=template, **kwargs)
|
||||
|
||||
def render_filters(self, template='/grids/filters.mako', **kwargs):
|
||||
"""
|
||||
Render the filters to a Unicode string, using the specified template.
|
||||
|
@ -982,6 +995,96 @@ class Grid(object):
|
|||
# TODO: Make configurable or something...
|
||||
return [5, 10, 20, 50, 100, 200]
|
||||
|
||||
def has_static_data(self):
|
||||
"""
|
||||
Should return ``True`` if the grid data can be considered "static"
|
||||
(i.e. a list of values). Will return ``False`` otherwise, e.g. if the
|
||||
data is represented as a SQLAlchemy query.
|
||||
"""
|
||||
# TODO: should make this smarter?
|
||||
if isinstance(self.data, list):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_buefy_columns(self):
|
||||
"""
|
||||
Return a list of dicts representing all grid columns. Meant for use
|
||||
with Buefy table.
|
||||
"""
|
||||
columns = []
|
||||
for name in self.columns:
|
||||
columns.append({
|
||||
'field': name,
|
||||
'label': self.get_label(name),
|
||||
'sortable': self.sortable and name in self.sorters,
|
||||
})
|
||||
return columns
|
||||
|
||||
def get_buefy_data(self):
|
||||
"""
|
||||
Returns a list of data rows for the grid, for use with Buefy table.
|
||||
"""
|
||||
# filter / sort / paginate to get "visible" data
|
||||
raw_data = self.make_visible_data()
|
||||
data = []
|
||||
|
||||
# iterate over data rows
|
||||
for i in range(len(raw_data)):
|
||||
rowobj = raw_data[i]
|
||||
row = {}
|
||||
|
||||
# iterate over data fields
|
||||
for name in self.columns:
|
||||
|
||||
# leverage configured rendering logic where applicable;
|
||||
# otherwise use "raw" data value as string
|
||||
if self.renderers and name in self.renderers:
|
||||
row[name] = self.renderers[name](rowobj, name)
|
||||
else:
|
||||
value = self.obtain_value(rowobj, name)
|
||||
if value is None:
|
||||
value = ""
|
||||
row[name] = six.text_type(value)
|
||||
|
||||
# set action URL(s) for row, as needed
|
||||
self.set_action_urls(row, rowobj, i)
|
||||
|
||||
data.append(row)
|
||||
|
||||
results = {
|
||||
'data': data,
|
||||
}
|
||||
|
||||
if self.pageable and self.pager is not None:
|
||||
results['total_items'] = self.pager.item_count
|
||||
results['per_page'] = self.pager.items_per_page
|
||||
results['page'] = self.pager.page
|
||||
results['pages'] = self.pager.page_count
|
||||
results['first_item'] = self.pager.first_item
|
||||
results['last_item'] = self.pager.last_item
|
||||
|
||||
return results
|
||||
|
||||
def set_action_urls(self, row, rowobj, i):
|
||||
"""
|
||||
Pre-generate all action URLs for the given data row. Meant for use
|
||||
with Buefy table, since we can't generate URLs from JS.
|
||||
"""
|
||||
for action in (self.main_actions + self.more_actions):
|
||||
url = action.get_url(rowobj, i)
|
||||
row['_action_url_{}'.format(action.key)] = url
|
||||
|
||||
def is_linked(self, name):
|
||||
"""
|
||||
Should return ``True`` if the given column name is configured to be
|
||||
"linked" (i.e. table cell should contain a link to "view object"),
|
||||
otherwise ``False``.
|
||||
"""
|
||||
if self.linked_columns:
|
||||
if name in self.linked_columns:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class CustomWebhelpersGrid(webhelpers2_grid.Grid):
|
||||
"""
|
||||
|
|
142
tailbone/templates/grids/buefy.mako
Normal file
142
tailbone/templates/grids/buefy.mako
Normal file
|
@ -0,0 +1,142 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
<div id="buefy-table-app">
|
||||
<b-table
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
|
||||
: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"
|
||||
:striped="true"
|
||||
: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:
|
||||
{{ props.row.${column['field']} }}
|
||||
% 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">
|
||||
<div class="has-text-right">showing {{ firstItem }} - {{ lastItem }} of {{ total }} results</div>
|
||||
</template>
|
||||
% endif
|
||||
|
||||
</b-table>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
new Vue({
|
||||
el: '#buefy-table-app',
|
||||
data() {
|
||||
return {
|
||||
data: ${json.dumps(grid_data['data'])|n},
|
||||
loading: false,
|
||||
sortField: '${grid.sortkey}',
|
||||
sortOrder: '${grid.sortdir}',
|
||||
% 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: ${grid_data['first_item']},
|
||||
lastItem: ${grid_data['last_item']},
|
||||
% endif
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
loadAsyncData() {
|
||||
|
||||
const 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.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()
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
</script>
|
|
@ -17,7 +17,9 @@
|
|||
<script type="text/javascript">
|
||||
$(function() {
|
||||
|
||||
% if not use_buefy:
|
||||
$('.grid-wrapper').gridwrapper();
|
||||
% endif
|
||||
|
||||
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
|
||||
|
||||
|
@ -170,4 +172,12 @@
|
|||
|
||||
</%def>
|
||||
|
||||
${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
|
||||
|
||||
% if use_buefy:
|
||||
${grid.render_buefy(grid_columns=grid_columns, grid_data=grid_data, static_data=static_data)|n}
|
||||
|
||||
% else:
|
||||
## no buefy, so do the traditional thing
|
||||
${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
|
||||
|
||||
% endif
|
||||
|
|
|
@ -256,6 +256,10 @@
|
|||
## Vue.js
|
||||
${h.javascript_link('https://unpkg.com/vue')}
|
||||
|
||||
## vue-resource
|
||||
## (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')}
|
||||
|
||||
## Buefy 0.7.3
|
||||
${h.javascript_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.js')}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2018 Lance Edgar
|
||||
# Copyright © 2010-2019 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -44,10 +44,10 @@ class DataSyncChangesView(MasterView):
|
|||
model_class = model.DataSyncChange
|
||||
url_prefix = '/datasync/changes'
|
||||
permission_prefix = 'datasync'
|
||||
|
||||
creatable = False
|
||||
editable = False
|
||||
bulk_deletable = True
|
||||
use_buefy = True
|
||||
|
||||
grid_columns = [
|
||||
'source',
|
||||
|
|
|
@ -52,6 +52,7 @@ class ProfilesView(MasterView):
|
|||
pageable = False
|
||||
creatable = False
|
||||
deletable = False
|
||||
use_buefy = True
|
||||
|
||||
grid_columns = [
|
||||
'key',
|
||||
|
|
|
@ -122,6 +122,8 @@ class MasterView(View):
|
|||
|
||||
grid_index = None
|
||||
use_index_links = False
|
||||
# this should be turned on per-view as progress is made
|
||||
use_buefy = False
|
||||
|
||||
has_versions = False
|
||||
help_url = None
|
||||
|
@ -265,6 +267,7 @@ class MasterView(View):
|
|||
"""
|
||||
self.listing = True
|
||||
grid = self.make_grid()
|
||||
use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy')
|
||||
|
||||
# If user just refreshed the page with a reset instruction, issue a
|
||||
# redirect in order to clear out the query string.
|
||||
|
@ -275,16 +278,28 @@ class MasterView(View):
|
|||
if grid.pageable and hasattr(grid, 'pager'):
|
||||
self.first_visible_grid_index = grid.pager.first_item
|
||||
|
||||
# Return grid only, if partial page was requested.
|
||||
# return grid only, if partial page was requested
|
||||
if self.request.params.get('partial'):
|
||||
if six.PY3:
|
||||
self.request.response.content_type = 'text/html'
|
||||
else:
|
||||
self.request.response.content_type = b'text/html'
|
||||
if use_buefy:
|
||||
# render grid data only, as JSON
|
||||
return render_to_response('json', grid.get_buefy_data(),
|
||||
request=self.request)
|
||||
else: # just do traditional thing, render grid HTML
|
||||
self.request.response.content_type = str('text/html')
|
||||
self.request.response.text = grid.render_grid()
|
||||
return self.request.response
|
||||
|
||||
return self.render_to_response('index', {'grid': grid})
|
||||
context = {
|
||||
'grid': grid,
|
||||
'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)
|
||||
|
||||
def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||
"""
|
||||
|
@ -2246,9 +2261,11 @@ class MasterView(View):
|
|||
"""
|
||||
actions = []
|
||||
prefix = self.get_permission_prefix()
|
||||
use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy')
|
||||
if self.viewable and self.request.has_perm('{}.view'.format(prefix)):
|
||||
url = self.get_view_index_url if self.use_index_links else None
|
||||
actions.append(self.make_action('view', icon='zoomin', url=url))
|
||||
icon = 'eye' if use_buefy else 'zoomin'
|
||||
actions.append(self.make_action('view', icon=icon, url=url))
|
||||
return actions
|
||||
|
||||
def get_view_index_url(self, row, i):
|
||||
|
@ -2261,8 +2278,10 @@ class MasterView(View):
|
|||
"""
|
||||
actions = []
|
||||
prefix = self.get_permission_prefix()
|
||||
use_buefy = self.use_buefy and self.rattail_config.getbool('tailbone', 'grids.use_buefy')
|
||||
if self.editable and self.request.has_perm('{}.edit'.format(prefix)):
|
||||
actions.append(self.make_action('edit', icon='pencil', url=self.default_edit_url))
|
||||
icon = 'edit' if use_buefy else 'pencil'
|
||||
actions.append(self.make_action('edit', icon=icon, url=self.default_edit_url))
|
||||
if self.deletable and self.request.has_perm('{}.delete'.format(prefix)):
|
||||
actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url))
|
||||
return actions
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2018 Lance Edgar
|
||||
# Copyright © 2010-2019 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -42,6 +42,7 @@ class TablesView(MasterView):
|
|||
viewable = False
|
||||
filterable = False
|
||||
pageable = False
|
||||
use_buefy = True
|
||||
|
||||
grid_columns = [
|
||||
'name',
|
||||
|
|
|
@ -68,6 +68,7 @@ class UpgradeView(MasterView):
|
|||
executable = True
|
||||
execute_progress_template = '/upgrade.mako'
|
||||
execute_progress_initial_msg = "Upgrading"
|
||||
use_buefy = True
|
||||
|
||||
labels = {
|
||||
'executed_by': "Executed by",
|
||||
|
|
Loading…
Reference in a new issue