Add basic "Buefy" support for grids (master index view)

still pretty experimental at this point, but making progress
This commit is contained in:
Lance Edgar 2019-03-24 17:24:43 -05:00
parent 3cef591719
commit 8d6ecc3ec7
9 changed files with 296 additions and 15 deletions

View file

@ -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):
""" """

View 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>

View file

@ -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

View file

@ -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')}

View file

@ -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',

View file

@ -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',

View file

@ -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

View file

@ -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',

View file

@ -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",