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
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2018 Lance Edgar
|
# Copyright © 2010-2019 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -176,6 +176,12 @@ class Grid(object):
|
||||||
if key in self.filters:
|
if key in self.filters:
|
||||||
self.filters[key].label = label
|
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):
|
def set_link(self, key, link=True):
|
||||||
if link:
|
if link:
|
||||||
if key not in self.linked_columns:
|
if key not in self.linked_columns:
|
||||||
|
@ -900,9 +906,16 @@ class Grid(object):
|
||||||
"""
|
"""
|
||||||
context = kwargs
|
context = kwargs
|
||||||
context['grid'] = self
|
context['grid'] = self
|
||||||
|
context['request'] = self.request
|
||||||
context.setdefault('allow_save_defaults', True)
|
context.setdefault('allow_save_defaults', True)
|
||||||
return render(template, context)
|
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):
|
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.
|
||||||
|
@ -982,6 +995,96 @@ class Grid(object):
|
||||||
# TODO: Make configurable or something...
|
# TODO: Make configurable or something...
|
||||||
return [5, 10, 20, 50, 100, 200]
|
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):
|
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">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
|
|
||||||
|
% if not use_buefy:
|
||||||
$('.grid-wrapper').gridwrapper();
|
$('.grid-wrapper').gridwrapper();
|
||||||
|
% endif
|
||||||
|
|
||||||
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
|
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
|
||||||
|
|
||||||
|
@ -170,4 +172,12 @@
|
||||||
|
|
||||||
</%def>
|
</%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
|
## Vue.js
|
||||||
${h.javascript_link('https://unpkg.com/vue')}
|
${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
|
## Buefy 0.7.3
|
||||||
${h.javascript_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.js')}
|
${h.javascript_link('https://unpkg.com/buefy@0.7.3/dist/buefy.min.js')}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2018 Lance Edgar
|
# Copyright © 2010-2019 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -44,10 +44,10 @@ class DataSyncChangesView(MasterView):
|
||||||
model_class = model.DataSyncChange
|
model_class = model.DataSyncChange
|
||||||
url_prefix = '/datasync/changes'
|
url_prefix = '/datasync/changes'
|
||||||
permission_prefix = 'datasync'
|
permission_prefix = 'datasync'
|
||||||
|
|
||||||
creatable = False
|
creatable = False
|
||||||
editable = False
|
editable = False
|
||||||
bulk_deletable = True
|
bulk_deletable = True
|
||||||
|
use_buefy = True
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'source',
|
'source',
|
||||||
|
|
|
@ -52,6 +52,7 @@ class ProfilesView(MasterView):
|
||||||
pageable = False
|
pageable = False
|
||||||
creatable = False
|
creatable = False
|
||||||
deletable = False
|
deletable = False
|
||||||
|
use_buefy = True
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'key',
|
'key',
|
||||||
|
|
|
@ -122,6 +122,8 @@ class MasterView(View):
|
||||||
|
|
||||||
grid_index = None
|
grid_index = None
|
||||||
use_index_links = False
|
use_index_links = False
|
||||||
|
# this should be turned on per-view as progress is made
|
||||||
|
use_buefy = False
|
||||||
|
|
||||||
has_versions = False
|
has_versions = False
|
||||||
help_url = None
|
help_url = None
|
||||||
|
@ -265,6 +267,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')
|
||||||
|
|
||||||
# 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.
|
||||||
|
@ -275,16 +278,28 @@ class MasterView(View):
|
||||||
if grid.pageable and hasattr(grid, 'pager'):
|
if grid.pageable and hasattr(grid, 'pager'):
|
||||||
self.first_visible_grid_index = grid.pager.first_item
|
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 self.request.params.get('partial'):
|
||||||
if six.PY3:
|
if use_buefy:
|
||||||
self.request.response.content_type = 'text/html'
|
# render grid data only, as JSON
|
||||||
else:
|
return render_to_response('json', grid.get_buefy_data(),
|
||||||
self.request.response.content_type = b'text/html'
|
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()
|
self.request.response.text = grid.render_grid()
|
||||||
return self.request.response
|
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):
|
def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -2246,9 +2261,11 @@ 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')
|
||||||
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
|
||||||
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
|
return actions
|
||||||
|
|
||||||
def get_view_index_url(self, row, i):
|
def get_view_index_url(self, row, i):
|
||||||
|
@ -2261,8 +2278,10 @@ 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')
|
||||||
if self.editable and self.request.has_perm('{}.edit'.format(prefix)):
|
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)):
|
if self.deletable and self.request.has_perm('{}.delete'.format(prefix)):
|
||||||
actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url))
|
actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url))
|
||||||
return actions
|
return actions
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2018 Lance Edgar
|
# Copyright © 2010-2019 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -42,6 +42,7 @@ class TablesView(MasterView):
|
||||||
viewable = False
|
viewable = False
|
||||||
filterable = False
|
filterable = False
|
||||||
pageable = False
|
pageable = False
|
||||||
|
use_buefy = True
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'name',
|
'name',
|
||||||
|
|
|
@ -68,6 +68,7 @@ class UpgradeView(MasterView):
|
||||||
executable = True
|
executable = True
|
||||||
execute_progress_template = '/upgrade.mako'
|
execute_progress_template = '/upgrade.mako'
|
||||||
execute_progress_initial_msg = "Upgrading"
|
execute_progress_initial_msg = "Upgrading"
|
||||||
|
use_buefy = True
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'executed_by': "Executed by",
|
'executed_by': "Executed by",
|
||||||
|
|
Loading…
Reference in a new issue