(core) Implementing row conditional formatting

Summary:
Conditional formatting can now be used for whole rows.
Related fix:
- Font styles weren't applicable for summary columns.
- Checkbox and slider weren't using colors properly

Test Plan: Existing and new tests

Reviewers: paulfitz, georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3547
This commit is contained in:
Jarosław Sadziński
2022-08-08 15:32:50 +02:00
parent fbba6b8f52
commit 9e4d802405
52 changed files with 823 additions and 439 deletions

View File

@@ -89,6 +89,7 @@ class MetaTableExtras(object):
usedByFields = _record_set('_grist_Views_section_field', 'displayCol')
ruleUsedByCols = _record_ref_list_set('_grist_Tables_column', 'rules')
ruleUsedByFields = _record_ref_list_set('_grist_Views_section_field', 'rules')
ruleUsedByTables = _record_ref_list_set('_grist_Views_section', 'rules')
def tableId(rec, table):
return rec.parentId.tableId
@@ -101,10 +102,16 @@ class MetaTableExtras(object):
def numRuleColUsers(rec, table):
"""
Returns the number of cols and fields using this col as a rule col
Returns the number of cols and fields using this col as a rule
"""
return len(rec.ruleUsedByCols) + len(rec.ruleUsedByFields)
def numRuleTableUsers(rec, table):
"""
Returns the number of tables using this col as a rule
"""
return len(rec.ruleUsedByTables)
def recalcOnChangesToSelf(rec, table):
"""
Whether the column is a trigger-formula column that depends on itself, used for
@@ -115,8 +122,11 @@ class MetaTableExtras(object):
def setAutoRemove(rec, table):
"""Marks the col for removal if it's a display/rule helper col with no more users."""
as_display = rec.colId.startswith('gristHelper_Display') and rec.numDisplayColUsers == 0
as_rule = rec.colId.startswith('gristHelper_Conditional') and rec.numRuleColUsers == 0
table.docmodel.setAutoRemove(rec, as_display or as_rule)
as_col_rule = rec.colId.startswith('gristHelper_ConditionalRule') and rec.numRuleColUsers == 0
as_row_rule = (
rec.colId.startswith('gristHelper_RowConditionalRule') and rec.numRuleTableUsers == 0
)
table.docmodel.setAutoRemove(rec, as_display or as_col_rule or as_row_rule)
class _grist_Views(object):

View File

@@ -28,6 +28,11 @@ log = logger.Logger(__name__, logger.INFO)
# This should make it at least barely possible to share documents by people who are not all on the
# same Grist version (even so, it will require more work). It should also make it somewhat safe to
# upgrade and then open the document with a previous version.
#
# After each migration you probably should run these commands:
# ./test/upgradeDocument public_samples/*.grist
# UPDATE_REGRESSION_DATA=1 GREP_TESTS=DocRegressionTests ./test/testrun.sh server
# ./test/upgradeDocument test/fixtures/docs/Hello.grist
all_migrations = {}
@@ -1081,3 +1086,9 @@ def migration31(tdset):
actions.UpdateRecord('_grist_ACLResources', resource.id, {'tableId': new_name})
)
return tdset.apply_doc_actions(doc_actions)
@migration(schema_version=32)
def migration32(tdset):
return tdset.apply_doc_actions([
add_column('_grist_Views_section', 'rules', 'RefList:_grist_Tables_column'),
])

View File

@@ -15,7 +15,7 @@ import six
import actions
SCHEMA_VERSION = 31
SCHEMA_VERSION = 32
def make_column(col_id, col_type, formula='', isFormula=False):
return {
@@ -194,6 +194,8 @@ def schema_create_actions():
make_column("linkTargetColRef", "Ref:_grist_Tables_column"),
# embedId is deprecated as of version 12. Do not remove or reuse.
make_column("embedId", "Text"),
# Points to formula columns that hold conditional formatting rules for this view section.
make_column("rules", "RefList:_grist_Tables_column"),
]),
# The fields of a view section.
actions.AddTable("_grist_Views_section_field", [

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
import test_engine
class TestGridRules(test_engine.EngineTestCase):
# Helper for rules action
def add_empty(self):
return self.apply_user_action(['AddEmptyRule', "Table1", 0, 0])
def set_rule(self, rule_index, formula):
rules = self.engine.docmodel.tables.lookupOne(tableId='Table1').rawViewSectionRef.rules
rule = list(rules)[rule_index]
return self.apply_user_action(['UpdateRecord', '_grist_Tables_column',
rule.id, {"formula": formula}])
def remove_rule(self, rule_index):
rules = self.engine.docmodel.tables.lookupOne(tableId='Table1').rawViewSectionRef.rules
rule = list(rules)[rule_index]
return self.apply_user_action(['RemoveColumn', 'Table1', rule.colId])
def test_simple_rules(self):
self.apply_user_action(['AddEmptyTable', None])
self.apply_user_action(['AddRecord', "Table1", None, {"A": 1}])
self.apply_user_action(['AddRecord', "Table1", None, {"A": 2}])
self.apply_user_action(['AddRecord', "Table1", None, {"A": 3}])
out_actions = self.add_empty()
self.assertPartialOutActions(out_actions, {"stored": [
["AddColumn", "Table1", "gristHelper_RowConditionalRule",
{"formula": "", "isFormula": True, "type": "Any"}],
["AddRecord", "_grist_Tables_column", 5,
{"colId": "gristHelper_RowConditionalRule", "formula": "", "isFormula": True,
"label": "gristHelper_RowConditionalRule", "parentId": 1, "parentPos": 5.0,
"type": "Any",
"widgetOptions": ""}],
["UpdateRecord", "_grist_Views_section", 2, {"rules": ["L", 5]}],
]})
out_actions = self.set_rule(0, "$A == 1")
self.assertPartialOutActions(out_actions, {"stored": [
["ModifyColumn", "Table1", "gristHelper_RowConditionalRule",
{"formula": "$A == 1"}],
["UpdateRecord", "_grist_Tables_column", 5, {"formula": "$A == 1"}],
["BulkUpdateRecord", "Table1", [1, 2, 3],
{"gristHelper_RowConditionalRule": [True, False, False]}],
]})
# Replace this rule with another rule to mark A = 2
out_actions = self.set_rule(0, "$A == 2")
self.assertPartialOutActions(out_actions, {"stored": [
["ModifyColumn", "Table1", "gristHelper_RowConditionalRule",
{"formula": "$A == 2"}],
["UpdateRecord", "_grist_Tables_column", 5, {"formula": "$A == 2"}],
["BulkUpdateRecord", "Table1", [1, 2],
{"gristHelper_RowConditionalRule": [False, True]}],
]})
# Add another rule A = 3
self.add_empty()
out_actions = self.set_rule(1, "$A == 3")
self.assertPartialOutActions(out_actions, {"stored": [
["ModifyColumn", "Table1", "gristHelper_RowConditionalRule2",
{"formula": "$A == 3"}],
["UpdateRecord", "_grist_Tables_column", 6, {"formula": "$A == 3"}],
["BulkUpdateRecord", "Table1", [1, 2, 3],
{"gristHelper_RowConditionalRule2": [False, False, True]}],
]})
# Remove the last rule
out_actions = self.remove_rule(1)
self.assertPartialOutActions(out_actions, {"stored": [
["RemoveRecord", "_grist_Tables_column", 6],
["UpdateRecord", "_grist_Views_section", 2, {"rules": ["L", 5]}],
["RemoveColumn", "Table1", "gristHelper_RowConditionalRule2"]
]})
# Remove last rule
out_actions = self.remove_rule(0)
self.assertPartialOutActions(out_actions, {"stored": [
["RemoveRecord", "_grist_Tables_column", 5],
["UpdateRecord", "_grist_Views_section", 2, {"rules": None}],
["RemoveColumn", "Table1", "gristHelper_RowConditionalRule"]
]})

View File

@@ -200,6 +200,7 @@ def allowed_summary_change(key, updated, original):
allowed_to_change = {'widget', 'dateFormat', 'timeFormat', 'isCustomDateFormat', 'alignment',
'fillColor', 'textColor', 'isCustomTimeFormat', 'isCustomDateFormat',
'numMode', 'numSign', 'decimals', 'maxDecimals', 'currency',
'fontBold', 'fontItalic', 'fontUnderline', 'fontStrikethrough',
'rulesOptions'}
# Helper function to remove protected keys from dictionary.
def trim(options):
@@ -456,7 +457,7 @@ class UserActions(object):
table_id == "_grist_Views_section"
and any(rec.isRaw for i, rec in self._bulk_action_iter(table_id, row_ids))
):
allowed_fields = {"title", "options", "sortColRefs"}
allowed_fields = {"title", "options", "sortColRefs", "rules"}
has_summary_section = any(rec.tableRef.summarySourceTable
for i, rec in self._bulk_action_iter(table_id, row_ids))
if has_summary_section:
@@ -1637,23 +1638,26 @@ class UserActions(object):
Adds empty conditional style rule to a field or column.
"""
assert table_id, "table_id is required"
assert field_ref or col_ref, "field_ref or col_ref is required"
assert not field_ref or not col_ref, "can't set both field_ref and col_ref"
col_name = "gristHelper_ConditionalRule"
if field_ref:
field_or_col = self._docmodel.view_fields.table.get_record(field_ref)
rule_owner = self._docmodel.view_fields.table.get_record(field_ref)
elif col_ref:
rule_owner = self._docmodel.columns.table.get_record(col_ref)
else:
field_or_col = self._docmodel.columns.table.get_record(col_ref)
col_name = "gristHelper_RowConditionalRule"
rule_owner = self._docmodel.get_table_rec(table_id).rawViewSectionRef
col_info = self.AddHiddenColumn(table_id, 'gristHelper_ConditionalRule', {
col_info = self.AddHiddenColumn(table_id, col_name, {
"type": "Any",
"isFormula": True,
"formula": ''
})
new_rule = col_info['colRef']
existing_rules = field_or_col.rules._get_encodable_row_ids() if field_or_col.rules else []
existing_rules = rule_owner.rules._get_encodable_row_ids() if rule_owner.rules else []
updated_rules = existing_rules + [new_rule]
self._docmodel.update([field_or_col], rules=[encode_object(updated_rules)])
self._docmodel.update([rule_owner], rules=[encode_object(updated_rules)])
#----------------------------------------