From edb5393cdc4f64b830548cd180d59b69ea408c27 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 8 Oct 2023 16:38:13 -0500 Subject: [PATCH] Add front-end support for multi-column grid sorting user must ctrl-click column header to engage multi-sort --- tailbone/grids/core.py | 66 ++++++++++++++------- tailbone/templates/grids/buefy.mako | 92 ++++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 30 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 984307b3..e42f8714 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -830,10 +830,10 @@ class Grid(object): if self.sortable: self.active_sorters = [] for i in range(1, settings['sorters.length'] + 1): - self.active_sorters.append(( - settings[f'sorters.{i}.key'], - settings[f'sorters.{i}.dir'], - )) + self.active_sorters.append({ + 'field': settings[f'sorters.{i}.key'], + 'order': settings[f'sorters.{i}.dir'], + }) if self.pageable: self.pagesize = settings['pagesize'] self.page = settings['page'] @@ -1229,28 +1229,52 @@ class Grid(object): if not self.active_sorters: return data - # convert sort settings into a 'sortspec' for use with sa-filters - full_spec = [] - for sortkey, sortdir in self.active_sorters: - sortfunc = self.sorters.get(sortkey) - if sortfunc: - spec = { - 'sortkey': sortkey, - 'model': sortfunc._class.__name__, - 'field': sortfunc._column.name, - 'direction': sortdir or 'asc', - } - # spec.sortkey = sortkey - full_spec.append(spec) + # TODO: is there a better way to check for SA sorting? + if self.model_class: - # apply joins needed for this sort spec - for spec in full_spec: - sortkey = spec['sortkey'] + # convert sort settings into a 'sortspec' for use with sa-filters + full_spec = [] + for sorter in self.active_sorters: + sortkey = sorter['field'] + sortdir = sorter['order'] + sortfunc = self.sorters.get(sortkey) + if sortfunc: + spec = { + 'sortkey': sortkey, + 'model': sortfunc._class.__name__, + 'field': sortfunc._column.name, + 'direction': sortdir or 'asc', + } + full_spec.append(spec) + + # apply joins needed for this sort spec + for spec in full_spec: + sortkey = spec['sortkey'] + if sortkey in self.joiners and sortkey not in self.joined: + data = self.joiners[sortkey](data) + self.joined.add(sortkey) + + return apply_sort(data, full_spec) + + else: + # not a SQLAlchemy grid, custom sorter + + assert len(self.active_sorters) < 2 + + sortkey = self.active_sorters[0]['field'] + sortdir = self.active_sorters[0]['order'] or 'asc' + + # Cannot sort unless we have a sort function. + sortfunc = self.sorters.get(sortkey) + if not sortfunc: + return data + + # apply joins needed for this sorter if sortkey in self.joiners and sortkey not in self.joined: data = self.joiners[sortkey](data) self.joined.add(sortkey) - return apply_sort(data, full_spec) + return sortfunc(data, sortdir) def paginate_data(self, data): """ diff --git a/tailbone/templates/grids/buefy.mako b/tailbone/templates/grids/buefy.mako index 1203b9de..5b21b42a 100644 --- a/tailbone/templates/grids/buefy.mako +++ b/tailbone/templates/grids/buefy.mako @@ -202,9 +202,25 @@ % endif % if grid.sortable: - :default-sort="sortingPriority[0]" - backend-sorting - @sort="onSort" + backend-sorting + @sort="onSort" + @sorting-priority-removed="sortingPriorityRemoved" + + ## TODO: there is a bug (?) which prevents the arrow from + ## displaying for simple default single-column sort. so to + ## work around that, we *disable* multi-sort until the + ## component is mounted. seems to work for now..see also + ## https://github.com/buefy/buefy/issues/2584 + :sort-multiple="allowMultiSort" + + ## nb. specify default sort only if single-column + :default-sort="backendSorters.length == 1 ? [backendSorters[0].field, backendSorters[0].order] : null" + + ## nb. otherwise there may be default multi-column sort + :sort-multiple-data="sortingPriority" + + ## user must ctrl-click column header to do multi-sort + sort-multiple-key="ctrlKey" % endif % if grid.click_handlers: @@ -353,7 +369,25 @@ lastItem: ${json.dumps(grid_data['last_item'] if grid.pageable else None)|n}, % if grid.sortable: - sortingPriority: ${json.dumps(grid.active_sorters)|n}, + + ## TODO: there is a bug (?) which prevents the arrow from + ## displaying for simple default single-column sort. so to + ## work around that, we *disable* multi-sort until the + ## component is mounted. seems to work for now..see also + ## https://github.com/buefy/buefy/issues/2584 + allowMultiSort: false, + + ## nb. this contains all truly active sorters + backendSorters: ${json.dumps(grid.active_sorters)|n}, + + ## nb. whereas this will only contain multi-column sorters, + ## but will be *empty* for single-column sorting + % if len(grid.active_sorters) > 1: + sortingPriority: ${json.dumps(grid.active_sorters)|n}, + % else: + sortingPriority: [], + % endif + % endif ## filterable: ${json.dumps(grid.filterable)|n}, @@ -395,6 +429,15 @@ }, }, + mounted() { + ## TODO: there is a bug (?) which prevents the arrow from + ## displaying for simple default single-column sort. so to + ## work around that, we *disable* multi-sort until the + ## component is mounted. seems to work for now..see also + ## https://github.com/buefy/buefy/issues/2584 + this.allowMultiSort = true + }, + methods: { % if grid.click_handlers: @@ -455,9 +498,9 @@ getBasicParams() { let params = {} % if grid.sortable: - for (let i = 1; i <= this.sortingPriority.length; i++) { - params['sort'+i+'key'] = this.sortingPriority[i-1][0] - params['sort'+i+'dir'] = this.sortingPriority[i-1][1] + for (let i = 1; i <= this.backendSorters.length; i++) { + params['sort'+i+'key'] = this.backendSorters[i-1].field + params['sort'+i+'dir'] = this.backendSorters[i-1].order } % endif % if grid.pageable: @@ -537,14 +580,45 @@ this.loadAsyncData() }, - onSort(field, order) { - this.sortingPriority = [[field, order]] + onSort(field, order, event) { + + if (event.ctrlKey) { + + // engage or enhance multi-column sorting + let sorter = this.backendSorters.filter(i => i.field === field)[0] + if (sorter) { + sorter.order = sorter.order === 'desc' ? 'asc' : 'desc' + } else { + this.backendSorters.push({field, order}) + } + this.sortingPriority = this.backendSorters + + } else { + + // sort by single column only + this.backendSorters = [{field, order}] + this.sortingPriority = [] + } + // always reset to first page when changing sort options // TODO: i mean..right? would we ever not want that? this.currentPage = 1 this.loadAsyncData() }, + sortingPriorityRemoved(field) { + + // prune field from active sorters + this.backendSorters = this.backendSorters.filter( + (sorter) => sorter.field !== field) + + // nb. must keep active sorter list "as-is" even if + // there is only one sorter; buefy seems to expect it + this.sortingPriority = this.backendSorters + + this.loadAsyncData() + }, + resetView() { this.loading = true