1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 06:15:36 +00:00

[2/3] Import Airtable view colors

This commit is contained in:
Bram Wiepjes 2025-03-14 11:15:29 +00:00
parent 29bfe98e54
commit a11883ca0c
6 changed files with 457 additions and 19 deletions
backend
src/baserow/contrib/database/airtable
tests/baserow/contrib/database/airtable
changelog/entries/unreleased/feature

View file

@ -13,16 +13,42 @@ AIRTABLE_EXPORT_JOB_DOWNLOADING_BASE = "downloading-base"
AIRTABLE_EXPORT_JOB_CONVERTING = "converting"
AIRTABLE_EXPORT_JOB_DOWNLOADING_FILES = "downloading-files"
AIRTABLE_BASEROW_COLOR_MAPPING = {
"blue": "blue",
"cyan": "light-blue",
"teal": "light-green",
"green": "green",
"yellow": "light-orange",
"orange": "orange",
"blue": "light-blue",
"cyan": "light-cyan",
"teal": "light-pink", # Baserow doesn't have teal, so we're using the left-over color
"green": "light-green",
"yellow": "light-yellow",
"orange": "light-orange",
"red": "light-red",
"pink": "red",
"purple": "dark-blue",
"purple": "light-purple",
"gray": "light-gray",
"blueMedium": "blue",
"cyanMedium": "cyan",
"tealMedium": "pink",
"greenMedium": "green",
"yellowMedium": "yellow",
"orangeMedium": "orange",
"redMedium": "red",
"purpleMedium": "purple",
"grayMedium": "gray",
"blueDark": "dark-blue",
"cyanDark": "dark-cyan",
"tealDark": "dark-pink",
"greenDark": "dark-green",
"yellowDark": "dark-yellow",
"orangeDark": "dark-orange",
"redDark": "dark-red",
"purpleDark": "dark-purple",
"grayDark": "dark-gray",
"blueDarker": "darker-blue",
"cyanDarker": "darker-cyan",
"tealDarker": "darker-pink",
"greenDarker": "darker-green",
"yellowDarker": "darker-yellow",
"orangeDarker": "darker-orange",
"redDarker": "darker-red",
"purpleDarker": "darker-purple",
"grayDarker": "darker-gray",
}
AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING = {
"commaPeriod": "COMMA_PERIOD",

View file

@ -25,17 +25,20 @@ SCOPE_VIEW_GROUP_BY = SelectOption(
SCOPE_VIEW_FILTER = SelectOption(
id="scope_view_filter", value="View filter", color="light-pink", order=6
)
SCOPE_VIEW_COLOR = SelectOption(
id="scope_view_color", value="View color", color="light-gray", order=7
)
SCOPE_VIEW_FIELD_OPTIONS = SelectOption(
id="scope_view_field_options",
value="View field options",
color="light-purple",
order=7,
order=8,
)
SCOPE_AUTOMATIONS = SelectOption(
id="scope_automations", value="Automations", color="light-orange", order=8
id="scope_automations", value="Automations", color="light-orange", order=9
)
SCOPE_INTERFACES = SelectOption(
id="scope_interfaces", value="Interfaces", color="light-yellow", order=9
id="scope_interfaces", value="Interfaces", color="light-yellow", order=10
)
ALL_SCOPES = [
SCOPE_FIELD,
@ -44,6 +47,7 @@ ALL_SCOPES = [
SCOPE_VIEW_SORT,
SCOPE_VIEW_GROUP_BY,
SCOPE_VIEW_FILTER,
SCOPE_VIEW_COLOR,
SCOPE_AUTOMATIONS,
SCOPE_INTERFACES,
]

View file

@ -1,14 +1,25 @@
from typing import Any, Dict, List, Optional, Tuple, Union
from baserow_premium.views.decorator_types import LeftBorderColorDecoratorType
from baserow_premium.views.decorator_value_provider_types import (
ConditionalColorValueProviderType,
SelectColorValueProviderType,
)
from baserow.contrib.database.airtable.config import AirtableImportConfig
from baserow.contrib.database.airtable.constants import AIRTABLE_ASCENDING_MAP
from baserow.contrib.database.airtable.constants import (
AIRTABLE_ASCENDING_MAP,
AIRTABLE_BASEROW_COLOR_MAPPING,
)
from baserow.contrib.database.airtable.exceptions import (
AirtableSkipCellValue,
AirtableSkipFilter,
)
from baserow.contrib.database.airtable.import_report import (
ERROR_TYPE_DATA_TYPE_MISMATCH,
ERROR_TYPE_UNSUPPORTED_FEATURE,
SCOPE_FIELD,
SCOPE_VIEW_COLOR,
SCOPE_VIEW_FILTER,
SCOPE_VIEW_GROUP_BY,
SCOPE_VIEW_SORT,
@ -27,6 +38,7 @@ from baserow.contrib.database.views.models import (
SORT_ORDER_ASC,
SORT_ORDER_DESC,
View,
ViewDecoration,
ViewFilter,
ViewFilterGroup,
ViewGroupBy,
@ -492,6 +504,174 @@ class AirtableViewType(Instance):
else:
return [baserow_filter], []
def get_select_column_decoration(
self,
field_mapping: dict,
view_type: ViewType,
row_id_mapping: Dict[str, Dict[str, int]],
raw_airtable_table: dict,
raw_airtable_view: dict,
raw_airtable_view_data: dict,
import_report: AirtableImportReport,
) -> Optional[ViewDecoration]:
color_config = raw_airtable_view_data["colorConfig"]
select_column_id = color_config["selectColumnId"]
if select_column_id not in field_mapping:
column_name = get_airtable_column_name(raw_airtable_table, select_column_id)
import_report.add_failed(
raw_airtable_view["name"],
SCOPE_VIEW_COLOR,
raw_airtable_table["name"],
ERROR_TYPE_DATA_TYPE_MISMATCH,
f'The select field coloring was ignored in {raw_airtable_view["name"]} '
f"because {column_name} does not exist.",
)
return None
return ViewDecoration(
id=f"{raw_airtable_view['id']}_decoration",
view_id=raw_airtable_view["id"],
type=LeftBorderColorDecoratorType.type,
value_provider_type=SelectColorValueProviderType.type,
value_provider_conf={"field_id": select_column_id},
order=1,
)
def get_color_definitions_decoration(
self,
field_mapping: dict,
view_type: ViewType,
row_id_mapping: Dict[str, Dict[str, int]],
raw_airtable_table: dict,
raw_airtable_view: dict,
raw_airtable_view_data: dict,
import_report: AirtableImportReport,
) -> Optional[ViewDecoration]:
color_config = raw_airtable_view_data["colorConfig"]
color_definitions = color_config["colorDefinitions"]
default_color = AIRTABLE_BASEROW_COLOR_MAPPING.get(
color_config.get("defaultColor", ""),
"",
)
baserow_colors = []
for color_definition in color_definitions:
filters, filter_groups = self.get_filters(
field_mapping,
row_id_mapping,
raw_airtable_view,
raw_airtable_table,
import_report,
color_definition,
)
# Pop the first group because that shouldn't be in Baserow, and the type is
# defined on the view.
root_group = filter_groups.pop(0)
color = AIRTABLE_BASEROW_COLOR_MAPPING.get(
color_definition.get("color", ""),
"blue",
)
baserow_colors.append(
{
"filter_groups": [
{
"id": filter_group.id,
"filter_type": filter_group.filter_type,
"parent_group": (
None
if filter_group.parent_group_id == root_group.id
else filter_group.parent_group_id
),
}
for filter_group in filter_groups
],
"filters": [
{
"id": filter_object.id,
"type": filter_object.type,
"field": filter_object.field_id,
"group": (
None
if filter_object.group_id == root_group.id
else filter_object.group_id
),
"value": filter_object.value,
}
for filter_object in filters
],
"operator": root_group.filter_type,
"color": color,
}
)
if default_color != "":
baserow_colors.append(
{
"filter_groups": [],
"filters": [],
"operator": "AND",
"color": default_color,
}
)
return ViewDecoration(
id=f"{raw_airtable_view['id']}_decoration",
view_id=raw_airtable_view["id"],
type=LeftBorderColorDecoratorType.type,
value_provider_type=ConditionalColorValueProviderType.type,
value_provider_conf={"colors": baserow_colors},
order=1,
)
def get_decorations(
self,
field_mapping: dict,
view_type: ViewType,
row_id_mapping: Dict[str, Dict[str, int]],
raw_airtable_table: dict,
raw_airtable_view: dict,
raw_airtable_view_data: dict,
import_report: AirtableImportReport,
) -> List[ViewDecoration]:
"""
Converts the raw Airtable color config into matching Baserow view decorations.
"""
color_config = raw_airtable_view_data.get("colorConfig", None)
if not view_type.can_decorate or color_config is None:
return []
color_config_type = color_config.get("type", "")
decoration = None
if color_config_type == "selectColumn":
decoration = self.get_select_column_decoration(
field_mapping,
view_type,
row_id_mapping,
raw_airtable_table,
raw_airtable_view,
raw_airtable_view_data,
import_report,
)
elif color_config_type == "colorDefinitions":
decoration = self.get_color_definitions_decoration(
field_mapping,
view_type,
row_id_mapping,
raw_airtable_table,
raw_airtable_view,
raw_airtable_view_data,
import_report,
)
if decoration:
return [decoration]
else:
return []
def to_serialized_baserow_view(
self,
field_mapping,
@ -527,7 +707,7 @@ class AirtableViewType(Instance):
import_report,
filters_object,
)
# Pop the first group because that shouldn't in Baserow, and the type is
# Pop the first group because that shouldn't be in Baserow, and the type is
# defined on the view.
view.filter_type = filter_groups.pop(0).filter_type
@ -547,6 +727,15 @@ class AirtableViewType(Instance):
raw_airtable_view_data,
import_report,
)
decorations = self.get_decorations(
field_mapping,
view_type,
row_id_mapping,
raw_airtable_table,
raw_airtable_view,
raw_airtable_view_data,
import_report,
)
view.get_field_options = lambda *args, **kwargs: []
view._prefetched_objects_cache = {
@ -554,7 +743,7 @@ class AirtableViewType(Instance):
"filter_groups": filter_groups,
"viewsort_set": sorts,
"viewgroupby_set": group_bys,
"viewdecoration_set": [],
"viewdecoration_set": decorations,
}
view = self.prepare_view_object(
field_mapping,
@ -587,7 +776,7 @@ class AirtableViewType(Instance):
Note that the common properties like name, filters, sorts, etc are added by
default depending on the Baserow view support for it.
:param field_mapping: @TODO
:param field_mapping: A dict containing all the imported fields.
:param view: The view object that must be prepared.
:param raw_airtable_table: The raw Airtable table data related to the column.
:param raw_airtable_view: The raw Airtable view values that must be

View file

@ -1413,11 +1413,11 @@ def test_airtable_import_multi_select_column(
assert len(select_options) == 2
assert select_options[0].id == "fldURNo0cvi6YWYcYj1_selEOJmenvqEd6pndFQ"
assert select_options[0].value == "Option 1"
assert select_options[0].color == "blue"
assert select_options[0].color == "light-blue"
assert select_options[0].order == 1
assert select_options[1].id == "fldURNo0cvi6YWYcYj1_sel5ekvuoNVvl03olMO"
assert select_options[1].value == "Option 2"
assert select_options[1].color == "light-blue"
assert select_options[1].color == "light-cyan"
assert select_options[1].order == 0
@ -2368,11 +2368,11 @@ def test_airtable_import_select_column(
assert len(select_options) == 2
assert select_options[0].id == "fldRd2Vkzgsf6X4z6B4_selbh6rEWaaiyQvWyfg"
assert select_options[0].value == "Option A"
assert select_options[0].color == "blue"
assert select_options[0].color == "light-blue"
assert select_options[0].order == 0
assert select_options[1].id == "fldRd2Vkzgsf6X4z6B4_selvZgpWhbkeRVphROT"
assert select_options[1].value == "Option B"
assert select_options[1].color == "light-blue"
assert select_options[1].color == "light-cyan"
assert select_options[1].order == 1

View file

@ -6,6 +6,7 @@ import pytest
from baserow.contrib.database.airtable.config import AirtableImportConfig
from baserow.contrib.database.airtable.import_report import (
SCOPE_VIEW_COLOR,
SCOPE_VIEW_GROUP_BY,
SCOPE_VIEW_SORT,
AirtableImportReport,
@ -133,6 +134,62 @@ RAW_VIEW_DATA_GROUPS = [
"emptyGroupState": "hidden",
}
]
RAW_VIEW_COLOR_CONFIG_SELECT_COLUMN = {
"type": "selectColumn",
"selectColumnId": "fldwSc9PqedIhTSqhi1",
"colorDefinitions": None,
"defaultColor": None,
}
RAW_VIEW_COLOR_CONFIG_COLOR_DEFINITIONS = {
"type": "colorDefinitions",
"colorDefinitions": [
{
"filterSet": [
{
"id": "fltp2gabc8P91234f",
"columnId": "fldwSc9PqedIhTSqhi1",
"operator": "isNotEmpty",
"value": None,
},
{
"id": "flthuYL0uubbDF2Xy",
"type": "nested",
"conjunction": "and",
"filterSet": [
{
"id": "flt70g1l245672xRi",
"columnId": "fldwSc9PqedIhTSqhi1",
"operator": "!=",
"value": "test",
},
{
"id": "fltVg238719fbIKqC",
"columnId": "fldwSc9PqedIhTSqhi2",
"operator": "!=",
"value": "test2",
},
{
"id": "flthuYL0uubbDF2Xz",
"type": "nested",
"conjunction": "or",
"filterSet": [
{
"id": "flt70g1l245672xRi",
"columnId": "fldwSc9PqedIhTSqhi1",
"operator": "!=",
"value": "test",
},
],
},
],
},
],
"conjunction": "or",
"color": "yellow",
}
],
"defaultColor": "teal",
}
def test_import_grid_view():
@ -508,3 +565,157 @@ def test_import_grid_view_filters_and_groups():
assert serialized_view["filter_groups"] == [
{"id": "flthuYL0uubbDF2Xy", "filter_type": "AND", "parent_group": None}
]
@pytest.mark.django_db
def test_import_grid_view_color_config_select_column_not_existing_column():
view_data = deepcopy(RAW_AIRTABLE_VIEW_DATA)
field_mapping = deepcopy(FIELD_MAPPING)
for field_object in field_mapping.values():
field_object["baserow_field"].content_type = ContentType.objects.get_for_model(
field_object["baserow_field"]
)
view_data["colorConfig"] = deepcopy(RAW_VIEW_COLOR_CONFIG_SELECT_COLUMN)
view_data["colorConfig"]["selectColumnId"] = "fld123"
airtable_view_type = airtable_view_type_registry.get("grid")
import_report = AirtableImportReport()
serialized_view = airtable_view_type.to_serialized_baserow_view(
field_mapping,
ROW_ID_MAPPING,
RAW_AIRTABLE_TABLE,
RAW_AIRTABLE_VIEW,
view_data,
AirtableImportConfig(),
import_report,
)
assert len(import_report.items) == 1
assert import_report.items[0].object_name == "Grid view"
assert import_report.items[0].scope == SCOPE_VIEW_COLOR
assert import_report.items[0].table == "Data"
@pytest.mark.django_db
def test_import_grid_view_color_config_select_column():
view_data = deepcopy(RAW_AIRTABLE_VIEW_DATA)
field_mapping = deepcopy(FIELD_MAPPING)
for field_object in field_mapping.values():
field_object["baserow_field"].content_type = ContentType.objects.get_for_model(
field_object["baserow_field"]
)
view_data["colorConfig"] = RAW_VIEW_COLOR_CONFIG_SELECT_COLUMN
airtable_view_type = airtable_view_type_registry.get("grid")
import_report = AirtableImportReport()
serialized_view = airtable_view_type.to_serialized_baserow_view(
field_mapping,
ROW_ID_MAPPING,
RAW_AIRTABLE_TABLE,
RAW_AIRTABLE_VIEW,
view_data,
AirtableImportConfig(),
import_report,
)
assert len(import_report.items) == 0
assert serialized_view["decorations"] == [
{
"id": "viwcpYeEpAs6kZspktV_decoration",
"type": "left_border_color",
"value_provider_type": "single_select_color",
"value_provider_conf": {"field_id": "fldwSc9PqedIhTSqhi1"},
"order": 1,
}
]
@pytest.mark.django_db
def test_import_grid_view_color_config_color_definitions():
view_data = deepcopy(RAW_AIRTABLE_VIEW_DATA)
field_mapping = deepcopy(FIELD_MAPPING)
for field_object in field_mapping.values():
field_object["baserow_field"].content_type = ContentType.objects.get_for_model(
field_object["baserow_field"]
)
view_data["colorConfig"] = RAW_VIEW_COLOR_CONFIG_COLOR_DEFINITIONS
airtable_view_type = airtable_view_type_registry.get("grid")
import_report = AirtableImportReport()
serialized_view = airtable_view_type.to_serialized_baserow_view(
field_mapping,
ROW_ID_MAPPING,
RAW_AIRTABLE_TABLE,
RAW_AIRTABLE_VIEW,
view_data,
AirtableImportConfig(),
import_report,
)
assert len(import_report.items) == 0
assert serialized_view["decorations"] == [
{
"id": "viwcpYeEpAs6kZspktV_decoration",
"type": "left_border_color",
"value_provider_type": "conditional_color",
"value_provider_conf": {
"colors": [
{
"filter_groups": [
{
"id": "flthuYL0uubbDF2Xy",
"filter_type": "AND",
"parent_group": None,
},
{
"id": "flthuYL0uubbDF2Xz",
"filter_type": "OR",
"parent_group": "flthuYL0uubbDF2Xy",
},
],
"filters": [
{
"id": "fltp2gabc8P91234f",
"type": "not_empty",
"field": "fldwSc9PqedIhTSqhi1",
"group": None,
"value": "",
},
{
"id": "flt70g1l245672xRi",
"type": "not_equal",
"field": "fldwSc9PqedIhTSqhi1",
"group": "flthuYL0uubbDF2Xy",
"value": "test",
},
{
"id": "fltVg238719fbIKqC",
"type": "not_equal",
"field": "fldwSc9PqedIhTSqhi2",
"group": "flthuYL0uubbDF2Xy",
"value": "test2",
},
{
"id": "flt70g1l245672xRi",
"type": "not_equal",
"field": "fldwSc9PqedIhTSqhi1",
"group": "flthuYL0uubbDF2Xz",
"value": "test",
},
],
"operator": "OR",
"color": "light-yellow",
},
{
"filter_groups": [],
"filters": [],
"operator": "AND",
"color": "light-pink",
},
]
},
"order": 1,
}
]

View file

@ -0,0 +1,8 @@
{
"type": "feature",
"message": "Import Airtable view colors.",
"domain": "database",
"issue_number": 793,
"bullet_points": [],
"created_at": "2025-03-13"
}