(core) Adding sort options for columns.

Summary:
Adding sort options for columns.
- Sort menu has a new option "More sort options" that opens up Sort left menu
- Each sort entry has an additional menu with 3 options
-- Order by choice index (for the Choice column, orders by choice position)
-- Empty last (puts empty values last in ascending order, first in descending order)
-- Natural sort (for Text column, compares strings with numbers as numbers)
Updated also CSV/Excel export and api sorting.
Most of the changes in this diff is a sort expression refactoring. Pulling out all the methods
that works on sortExpression array into a single namespace.

Test Plan: Browser tests

Reviewers: alexmojaki

Reviewed By: alexmojaki

Subscribers: dsagal, alexmojaki

Differential Revision: https://phab.getgrist.com/D3077
This commit is contained in:
Jarosław Sadziński
2021-11-03 12:44:28 +01:00
parent 0f946616b6
commit 3c72639e25
20 changed files with 992 additions and 267 deletions

View File

@@ -0,0 +1,40 @@
COL_SEPARATOR = ":"
"""
Helper module for sort expressions.
Sort expressions are encoded as a positive number for ascending column,
negative number for descending column. Can also be encoded as strings in a form:
'-1:flag' or '1:flag;flag'
Flags can be:
- emptyLast to put empty values at the end.
- orderByChoice: to order column by choice entry index rather then choice value.
- naturalSort: to treat strings containing numbers as numbers and sort them accordingly.
"""
def col_ref(col_spec):
"""
Gets column row id from column expression
"""
return abs(col_spec if isinstance(col_spec, int) else int(col_spec.split(COL_SEPARATOR)[0]))
def direction(col_spec):
"""
Gets direction for column expression (1 for ascending - 1 for descending).
"""
if isinstance(col_spec, int):
return 1 if col_spec >= 0 else -1
else:
assert col_spec
return 1 if col_spec[0] != "-" else -1
def swap_col_ref(col_spec, new_col_ref):
"""
Swaps colRef in colSpec preserving direction and options (used for display columns).
"""
new_spec = direction(col_spec) * new_col_ref
if isinstance(col_spec, int):
return new_spec
else:
parts = col_spec.split(COL_SEPARATOR)
parts[0] = str(new_spec)
return COL_SEPARATOR.join(parts)

View File

@@ -5,6 +5,8 @@ import re
import six
from column import is_visible_column
import sort_specs
import logger
log = logger.Logger(__name__, logger.INFO)
@@ -78,13 +80,14 @@ def _update_sort_spec(sort_spec, old_table, new_table):
# When adjusting, we take a possibly negated old colRef, and produce a new colRef.
# If anything is gone, we return 0, which will be excluded from the new sort spec.
def adjust(col_spec):
sign = 1 if col_spec >= 0 else -1
return sign * new_cols_map.get(old_cols_map.get(abs(col_spec)), 0)
old_colref = sort_specs.col_ref(col_spec)
new_colref = new_cols_map.get(old_cols_map.get(old_colref), 0)
return sort_specs.swap_col_ref(col_spec, new_colref)
try:
old_sort_spec = json.loads(sort_spec)
new_sort_spec = [adjust(col_spec) for col_spec in old_sort_spec]
new_sort_spec = [col_spec for col_spec in new_sort_spec if col_spec]
new_sort_spec = [col_spec for col_spec in new_sort_spec if sort_specs.col_ref(col_spec)]
return json.dumps(new_sort_spec, separators=(',', ':'))
except Exception:
log.warn("update_summary_section: can't parse sortColRefs JSON; clearing sortColRefs")

View File

@@ -0,0 +1,36 @@
# coding=utf-8
import unittest
import sort_specs
class TestSortSpec(unittest.TestCase):
def test_direction(self):
self.assertEqual(sort_specs.direction(1), 1)
self.assertEqual(sort_specs.direction(-1), -1)
self.assertEqual(sort_specs.direction('1'), 1)
self.assertEqual(sort_specs.direction('-1'), -1)
self.assertEqual(sort_specs.direction('1:emptyLast'), 1)
self.assertEqual(sort_specs.direction('1:emptyLast;orderByChoice'), 1)
self.assertEqual(sort_specs.direction('-1:emptyLast;orderByChoice'), -1)
def test_col_ref(self):
self.assertEqual(sort_specs.col_ref(1), 1)
self.assertEqual(sort_specs.col_ref(-1), 1)
self.assertEqual(sort_specs.col_ref('1'), 1)
self.assertEqual(sort_specs.col_ref('-1'), 1)
self.assertEqual(sort_specs.col_ref('1:emptyLast'), 1)
self.assertEqual(sort_specs.col_ref('1:emptyLast;orderByChoice'), 1)
self.assertEqual(sort_specs.col_ref('-1:emptyLast;orderByChoice'), 1)
def test_swap_col_ref(self):
self.assertEqual(sort_specs.swap_col_ref(1, 2), 2)
self.assertEqual(sort_specs.swap_col_ref(-1, 2), -2)
self.assertEqual(sort_specs.swap_col_ref('1', 2), '2')
self.assertEqual(sort_specs.swap_col_ref('-1', 2), '-2')
self.assertEqual(sort_specs.swap_col_ref('1:emptyLast', 2), '2:emptyLast')
self.assertEqual(
sort_specs.swap_col_ref('1:emptyLast;orderByChoice', 2),
'2:emptyLast;orderByChoice')
self.assertEqual(
sort_specs.swap_col_ref('-1:emptyLast;orderByChoice', 2),
'-2:emptyLast;orderByChoice')

View File

@@ -11,6 +11,7 @@ import acl
from acl_formula import parse_acl_formula_json
import actions
import column
import sort_specs
import identifiers
from objtypes import strict_equal, encode_object
import schema
@@ -877,7 +878,8 @@ class UserActions(object):
for section in parent_sections:
# Only iterates once for each section. Updated sort removes all columns being deleted.
sort = json.loads(section.sortColRefs) if section.sortColRefs else []
updated_sort = [sort_ref for sort_ref in sort if abs(sort_ref) not in removed_col_refs]
updated_sort = [col_spec for col_spec in sort
if sort_specs.col_ref(col_spec) not in removed_col_refs]
if sort != updated_sort:
re_sort_sections.append(section)
re_sort_specs.append(json.dumps(updated_sort))