From 9bbce1015fe5747eace02e1c4eb115f392aa90e2 Mon Sep 17 00:00:00 2001
From: Petr Stribny <petr@stribny.name>
Date: Tue, 11 Mar 2025 11:22:03 +0000
Subject: [PATCH] Display single select values properly in chart group by
 single select field

---
 .../local_baserow/service_types.py            | 28 ++++++++++++++++
 ...grouped_aggregate_rows_data_source_type.py | 22 ++++++++++++-
 .../dashboard/chartFieldFormatting.js         | 32 +++++++++++++++++++
 .../dashboard/components/widget/Chart.vue     | 21 ++++++++++--
 .../modules/baserow_enterprise/plugin.js      |  5 +++
 5 files changed, 105 insertions(+), 3 deletions(-)
 create mode 100644 enterprise/web-frontend/modules/baserow_enterprise/dashboard/chartFieldFormatting.js

diff --git a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py
index 513e6d75b..5085041d3 100644
--- a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py
+++ b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py
@@ -5,7 +5,9 @@ from django.db.models import F
 
 from rest_framework.exceptions import ValidationError as DRFValidationError
 
+from baserow.contrib.database.api.fields.serializers import FieldSerializer
 from baserow.contrib.database.fields.exceptions import FieldTypeDoesNotExist
+from baserow.contrib.database.fields.registries import field_type_registry
 from baserow.contrib.database.views.exceptions import AggregationTypeDoesNotExist
 from baserow.contrib.database.views.utils import AnnotatedAggregation
 from baserow.contrib.integrations.local_baserow.integration_types import (
@@ -471,6 +473,32 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
 
         return service
 
+    def get_context_data(
+        self,
+        service: LocalBaserowGroupedAggregateRows,
+        allowed_fields: list[str] | None = None,
+    ) -> dict:
+        context_data = {}
+
+        if service.table:
+            model = service.table.get_model()
+
+            def get_group_by_field(field):
+                return model.get_primary_field() if field is None else field
+
+            def serialize_field(field):
+                return field_type_registry.get_serializer(field, FieldSerializer).data
+
+            fields = [
+                get_group_by_field(group_by.field)
+                for group_by in service.service_aggregation_group_bys.all()
+            ]
+            context_data["fields"] = {
+                field.db_column: serialize_field(field) for field in fields
+            }
+
+        return context_data
+
     def dispatch_data(
         self,
         service: LocalBaserowGroupedAggregateRows,
diff --git a/enterprise/backend/tests/baserow_enterprise_tests/api/dashboard/test_grouped_aggregate_rows_data_source_type.py b/enterprise/backend/tests/baserow_enterprise_tests/api/dashboard/test_grouped_aggregate_rows_data_source_type.py
index 53011f54d..1ff5c3f68 100644
--- a/enterprise/backend/tests/baserow_enterprise_tests/api/dashboard/test_grouped_aggregate_rows_data_source_type.py
+++ b/enterprise/backend/tests/baserow_enterprise_tests/api/dashboard/test_grouped_aggregate_rows_data_source_type.py
@@ -74,7 +74,27 @@ def test_grouped_aggregate_rows_get_dashboard_data_sources(
             {"aggregation_type": "sum", "field_id": field.id, "order": 1},
             {"aggregation_type": "sum", "field_id": field_2.id, "order": 1},
         ],
-        "context_data": {},
+        "context_data": {
+            "fields": {
+                f"field_{field_3.id}": {
+                    "description": None,
+                    "id": field_3.id,
+                    "immutable_properties": False,
+                    "immutable_type": False,
+                    "name": field_3.name,
+                    "number_decimal_places": 0,
+                    "number_negative": False,
+                    "number_prefix": "",
+                    "number_separator": "",
+                    "number_suffix": "",
+                    "order": 0,
+                    "primary": False,
+                    "read_only": False,
+                    "table_id": table.id,
+                    "type": "number",
+                },
+            },
+        },
         "context_data_schema": None,
         "dashboard_id": dashboard.id,
         "filter_type": "AND",
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/dashboard/chartFieldFormatting.js b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/chartFieldFormatting.js
new file mode 100644
index 000000000..0248802c3
--- /dev/null
+++ b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/chartFieldFormatting.js
@@ -0,0 +1,32 @@
+import { Registerable } from '@baserow/modules/core/registry'
+
+export class ChartFieldFormattingType extends Registerable {
+  constructor(...args) {
+    super(...args)
+    this.type = this.getType()
+
+    if (this.type === null) {
+      throw new Error('The type has to be set.')
+    }
+  }
+
+  formatGroupByFieldValue(field, value) {
+    return value ?? ''
+  }
+}
+
+export class SingleSelectFormattingType extends ChartFieldFormattingType {
+  static getType() {
+    return 'single_select'
+  }
+
+  formatGroupByFieldValue(field, value) {
+    const selectOption = field.select_options.find((item) => item.id === value)
+
+    if (selectOption) {
+      return selectOption.value
+    }
+
+    return value ?? ''
+  }
+}
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue
index 22b194a05..63ecb1ab8 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue
+++ b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue
@@ -120,9 +120,9 @@ export default {
       ).find((item) => item.metadata?.primary === true)
       const labels = this.result.map((item) => {
         if (item[`field_${groupByFieldId}`] !== undefined) {
-          return item[`field_${groupByFieldId}`] ?? ''
+          return this.getGroupByValue(`field_${groupByFieldId}`, item)
         }
-        return item[`field_${primaryField.metadata.id}`]
+        return this.getGroupByValue(`field_${primaryField.metadata.id}`, item)
       })
       const datasets = []
       for (const [index, series] of this.seriesConfig.entries()) {
@@ -212,6 +212,23 @@ export default {
       }
       return label
     },
+    getGroupByValue(fieldName, item) {
+      const serializedField = this.dataSource.context_data.fields[fieldName]
+      const fieldType = serializedField.type
+
+      if (this.$registry.exists('chartFieldFormatting', fieldType)) {
+        const fieldFormatter = this.$registry.get(
+          'chartFieldFormatting',
+          fieldType
+        )
+        return fieldFormatter.formatGroupByFieldValue(
+          serializedField,
+          item[fieldName]
+        )
+      }
+
+      return item[fieldName] ?? ''
+    },
   },
 }
 </script>
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/plugin.js b/enterprise/web-frontend/modules/baserow_enterprise/plugin.js
index 3ee15aa9d..013dea214 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/plugin.js
+++ b/enterprise/web-frontend/modules/baserow_enterprise/plugin.js
@@ -51,6 +51,7 @@ import {
   HubspotContactsDataSyncType,
 } from '@baserow_enterprise/dataSyncTypes'
 import { ChartWidgetType } from '@baserow_enterprise/dashboard/widgetTypes'
+import { SingleSelectFormattingType } from '@baserow_enterprise/dashboard/chartFieldFormatting'
 import { PeriodicIntervalFieldsConfigureDataSyncType } from '@baserow_enterprise/configureDataSyncTypes'
 import {
   CountViewAggregationType,
@@ -308,5 +309,9 @@ export default (context) => {
 
   if (app.$featureFlagIsEnabled(FF_DASHBOARDS)) {
     app.$registry.register('dashboardWidget', new ChartWidgetType(context))
+    app.$registry.register(
+      'chartFieldFormatting',
+      new SingleSelectFormattingType(context)
+    )
   }
 }