From 92ace8884c5afc5904bf37c9671a5cd50cfab9d8 Mon Sep 17 00:00:00 2001
From: Jrmi <jrmi+gitlab@jeremiez.net>
Date: Tue, 28 Dec 2021 12:37:58 +0000
Subject: [PATCH] Resolve "Add ratings field"

---
 .gitignore                                    |    3 +-
 .nvmrc                                        |    1 +
 backend/src/baserow/contrib/database/apps.py  |    2 +
 .../contrib/database/fields/field_helpers.py  |    3 +
 .../contrib/database/fields/field_types.py    |  111 +-
 .../baserow/contrib/database/fields/models.py |   50 +
 .../database/migrations/0054_ratingfield.py   |   72 +
 .../src/baserow/contrib/database/models.py    |    2 +
 .../contrib/database/views/view_filters.py    |    5 +
 .../src/baserow/test_utils/fixtures/field.py  |   18 +
 backend/src/baserow/test_utils/helpers.py     |    1 +
 .../database/api/rows/test_row_serializers.py |    1 +
 .../database/api/rows/test_row_views.py       |   20 +
 .../database/export/test_export_handler.py    |   12 +-
 .../database/field/test_field_types.py        |    2 +
 .../field/test_multiple_select_field_type.py  |   29 +-
 .../database/field/test_rating_field_type.py  |  297 ++++
 changelog.md                                  |    5 +-
 .../export/test_premium_export_types.py       |    4 +
 premium/web-frontend/jsconfig.json            |   14 +
 .../views/kanban/KanbanViewOptionForm.vue     |    4 +-
 web-frontend/.eslintrc.js                     |    1 +
 web-frontend/jsconfig.json                    |   14 +
 web-frontend/locales/en.js                    |    3 +
 web-frontend/locales/fr.js                    |    3 +
 .../core/assets/scss/components/all.scss      |    5 +-
 .../assets/scss/components/color_select.scss  |   22 +-
 .../core/assets/scss/components/colors.scss   |   60 +
 .../assets/scss/components/fields/rating.scss |   26 +
 .../core/assets/scss/components/filters.scss  |    6 +
 .../core/assets/scss/components/rating.scss   |   51 +
 .../scss/components/views/grid/rating.scss    |    8 +
 .../core/components/ColorSelectContext.vue    |   48 +-
 .../modules/core/components/Dropdown.vue      |   14 +-
 .../modules/core/components/DropdownItem.vue  |   14 +-
 .../modules/core/mixins/dropdownItem.js       |    3 +
 web-frontend/modules/core/utils/colors.js     |    4 +
 .../modules/database/components/Rating.vue    |   53 +
 .../components/card/RowCardFieldRating.vue    |   20 +
 .../components/field/FieldRatingSubForm.vue   |  134 ++
 .../components/field/FieldSelectOptions.vue   |    4 +-
 .../components/row/RowEditFieldRating.vue     |   30 +
 .../database/components/view/ViewFilter.vue   |   23 +-
 ...ewFilterContext.vue => ViewFilterForm.vue} |   24 +-
 .../components/view/ViewFilterTypeRating.vue  |   26 +
 .../fields/FunctionalGridViewFieldRating.vue  |   29 +
 .../view/grid/fields/GridViewFieldRating.vue  |   67 +
 web-frontend/modules/database/fieldTypes.js   |  115 +-
 .../modules/database/mixins/selectOptions.js  |    6 +-
 web-frontend/modules/database/plugin.js       |    2 +
 web-frontend/modules/database/viewFilters.js  |   40 +-
 web-frontend/package.json                     |    6 +-
 web-frontend/test/fixtures/mockServer.js      |    6 +
 web-frontend/test/helpers/testApp.js          |    6 +-
 .../__snapshots__/rating.spec.js.snap         |   39 +
 .../unit/database/components/rating.spec.js   |   65 +
 .../__snapshots__/viewFilterForm.spec.js.snap | 1353 +++++++++++++++++
 .../components/view/viewFilterForm.spec.js    |  177 +++
 .../test/unit/database/fieldTypes.spec.js     |    8 +
 web-frontend/test/unit/jest.config.js         |    1 +
 web-frontend/yarn.lock                        |    9 +-
 61 files changed, 3071 insertions(+), 110 deletions(-)
 create mode 100644 .nvmrc
 create mode 100644 backend/src/baserow/contrib/database/migrations/0054_ratingfield.py
 create mode 100644 backend/tests/baserow/contrib/database/field/test_rating_field_type.py
 create mode 100644 premium/web-frontend/jsconfig.json
 create mode 100644 web-frontend/jsconfig.json
 create mode 100644 web-frontend/modules/core/assets/scss/components/fields/rating.scss
 create mode 100644 web-frontend/modules/core/assets/scss/components/rating.scss
 create mode 100644 web-frontend/modules/core/assets/scss/components/views/grid/rating.scss
 create mode 100644 web-frontend/modules/database/components/Rating.vue
 create mode 100644 web-frontend/modules/database/components/card/RowCardFieldRating.vue
 create mode 100644 web-frontend/modules/database/components/field/FieldRatingSubForm.vue
 create mode 100644 web-frontend/modules/database/components/row/RowEditFieldRating.vue
 rename web-frontend/modules/database/components/view/{ViewFilterContext.vue => ViewFilterForm.vue} (95%)
 create mode 100644 web-frontend/modules/database/components/view/ViewFilterTypeRating.vue
 create mode 100644 web-frontend/modules/database/components/view/grid/fields/FunctionalGridViewFieldRating.vue
 create mode 100644 web-frontend/modules/database/components/view/grid/fields/GridViewFieldRating.vue
 create mode 100644 web-frontend/test/unit/database/components/__snapshots__/rating.spec.js.snap
 create mode 100644 web-frontend/test/unit/database/components/rating.spec.js
 create mode 100644 web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap
 create mode 100644 web-frontend/test/unit/database/components/view/viewFilterForm.spec.js

diff --git a/.gitignore b/.gitignore
index b754e1f37..093a5b4fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -112,7 +112,6 @@ out/
 
 # vscode config files
 .vscode
-jsconfig.json
 vetur.config.js
 
-formula/out/
\ No newline at end of file
+formula/out/
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..48082f72f
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+12
diff --git a/backend/src/baserow/contrib/database/apps.py b/backend/src/baserow/contrib/database/apps.py
index e22418fc1..eeb63bccb 100644
--- a/backend/src/baserow/contrib/database/apps.py
+++ b/backend/src/baserow/contrib/database/apps.py
@@ -69,6 +69,7 @@ class DatabaseConfig(AppConfig):
             LongTextFieldType,
             URLFieldType,
             NumberFieldType,
+            RatingFieldType,
             BooleanFieldType,
             DateFieldType,
             LastModifiedFieldType,
@@ -88,6 +89,7 @@ class DatabaseConfig(AppConfig):
         field_type_registry.register(URLFieldType())
         field_type_registry.register(EmailFieldType())
         field_type_registry.register(NumberFieldType())
+        field_type_registry.register(RatingFieldType())
         field_type_registry.register(BooleanFieldType())
         field_type_registry.register(DateFieldType())
         field_type_registry.register(LastModifiedFieldType())
diff --git a/backend/src/baserow/contrib/database/fields/field_helpers.py b/backend/src/baserow/contrib/database/fields/field_helpers.py
index af4630fc0..1694823ab 100644
--- a/backend/src/baserow/contrib/database/fields/field_helpers.py
+++ b/backend/src/baserow/contrib/database/fields/field_helpers.py
@@ -35,6 +35,9 @@ def construct_all_possible_field_kwargs(
                 "number_negative": False,
             },
         ],
+        "rating": [
+            {"name": "rating", "max_value": 5, "color": "blue", "style": "star"}
+        ],
         "boolean": [{"name": "boolean"}],
         "date": [
             {"name": "datetime_us", "date_include_time": True, "date_format": "US"},
diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py
index 63dd7dc43..c64e46123 100644
--- a/backend/src/baserow/contrib/database/fields/field_types.py
+++ b/backend/src/baserow/contrib/database/fields/field_types.py
@@ -18,6 +18,7 @@ from django.db.models import Case, When, Q, F, Func, Value, CharField
 from django.db.models.expressions import RawSQL
 from django.db.models.functions import Coalesce
 from django.utils.timezone import make_aware
+
 from pytz import timezone
 from rest_framework import serializers
 
@@ -85,6 +86,7 @@ from .models import (
     LongTextField,
     URLField,
     NumberField,
+    RatingField,
     BooleanField,
     DateField,
     LastModifiedField,
@@ -436,6 +438,111 @@ class NumberFieldType(FieldType):
         )
 
 
+class RatingFieldType(FieldType):
+    type = "rating"
+    model_class = RatingField
+    allowed_fields = ["max_value", "color", "style"]
+    serializer_field_names = ["max_value", "color", "style"]
+
+    def prepare_value_for_db(self, instance, value):
+        if not value:
+            return 0
+
+        if value < 0:
+            raise ValidationError("Ensure this value is greater than or equal to 0.")
+        if value > instance.max_value:
+            raise ValidationError(
+                f"Ensure this value is less than or equal to {instance.max_value}."
+            )
+
+        return value
+
+    def get_serializer_field(self, instance, **kwargs):
+        return serializers.IntegerField(
+            **{
+                "required": False,
+                "allow_null": False,
+                "min_value": 0,
+                "default": 0,
+                "max_value": instance.max_value,
+                **kwargs,
+            }
+        )
+
+    def force_same_type_alter_column(self, from_field, to_field):
+        """
+        Force field alter column hook to be called when chaging max_value.
+        """
+
+        return to_field.max_value != from_field.max_value
+
+    def get_alter_column_prepare_new_value(self, connection, from_field, to_field):
+        """
+        Prepare value for Rating field. Clamp between 0 and field max_value.
+        Also convert Null value to 0.
+        """
+
+        if connection.vendor == "postgresql":
+            from_field_type = field_type_registry.get_by_model(from_field)
+
+            if from_field_type.type in ["number", "text", "rating"]:
+                # Convert and clamp values on field conversion
+                return (
+                    f"p_in = least(greatest(round(p_in::numeric), 0)"
+                    f", {to_field.max_value});"
+                )
+
+            if from_field_type.type == "boolean":
+                return """
+                    IF p_in THEN
+                        p_in = 1;
+                    ELSE
+                        p_in = 0;
+                    END IF;
+                """
+
+        return super().get_alter_column_prepare_new_value(
+            connection, from_field, to_field
+        )
+
+    def get_alter_column_prepare_old_value(self, connection, from_field, to_field):
+        """
+        Prepare value from Rating field.
+        """
+
+        if connection.vendor == "postgresql":
+            to_field_type = field_type_registry.get_by_model(to_field)
+
+            if to_field_type.type == "boolean":
+                return "p_in = least(p_in::numeric, 1);"
+
+        return super().get_alter_column_prepare_old_value(
+            connection, from_field, to_field
+        )
+
+    def get_model_field(self, instance, **kwargs):
+        return models.PositiveSmallIntegerField(
+            blank=False,
+            null=False,
+            default=0,
+            **kwargs,
+        )
+
+    def random_value(self, instance, fake, cache):
+        return fake.random_int(0, instance.max_value)
+
+    def contains_query(self, *args):
+        return contains_filter(*args)
+
+    def to_baserow_formula_type(self, field) -> BaserowFormulaType:
+        return BaserowFormulaNumberType(0)
+
+    def from_baserow_formula_type(
+        self, formula_type: BaserowFormulaNumberType
+    ) -> "RatingField":
+        return RatingField()
+
+
 class BooleanFieldType(FieldType):
     type = "boolean"
     model_class = BooleanField
@@ -769,7 +876,7 @@ class CreatedOnLastModifiedBaseFieldType(DateFieldType):
         before,
     ):
         """
-        If the field type has changed, we need to update the values from the from
+        If the field type has changed, we need to update the values from
         the source_field_name column.
         """
 
@@ -1705,7 +1812,7 @@ class SingleSelectFieldType(SelectOptionBaseFieldType):
                 variables,
             )
 
-        return super().get_alter_column_prepare_old_value(
+        return super().get_alter_column_prepare_new_value(
             connection, from_field, to_field
         )
 
diff --git a/backend/src/baserow/contrib/database/fields/models.py b/backend/src/baserow/contrib/database/fields/models.py
index 5f0400419..962593d20 100644
--- a/backend/src/baserow/contrib/database/fields/models.py
+++ b/backend/src/baserow/contrib/database/fields/models.py
@@ -1,6 +1,7 @@
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.utils.functional import cached_property
+from django.core.validators import MinValueValidator, MaxValueValidator
 
 from baserow.contrib.database.fields.mixins import (
     BaseDateMixin,
@@ -39,6 +40,14 @@ NUMBER_DECIMAL_PLACES_CHOICES = [
     (NUMBER_MAX_DECIMAL_PLACES, "1.00000"),
 ]
 
+RATING_STYLE_CHOICES = [
+    ("star", "Star"),
+    ("heart", "Heart"),
+    ("thumbs-up", "Thumbs-up"),
+    ("flag", "Flags"),
+    ("smile", "Smile"),
+]
+
 
 def get_default_field_content_type():
     return ContentType.objects.get_for_model(Field)
@@ -214,6 +223,47 @@ class NumberField(Field):
         super(NumberField, self).save(*args, **kwargs)
 
 
+class RatingField(Field):
+    max_value = models.PositiveSmallIntegerField(
+        default=5,
+        help_text="Maximum value the rating can take.",
+        validators=[MinValueValidator(1), MaxValueValidator(10)],
+    )
+    color = models.CharField(
+        max_length=50,
+        blank=False,
+        help_text="Color of the symbols.",
+        default="dark-orange",
+    )
+    style = models.CharField(
+        choices=RATING_STYLE_CHOICES,
+        default="star",
+        max_length=50,
+        blank=False,
+        help_text=(
+            "Rating style. Allowed values: "
+            f"{', '.join([value for (value, _) in RATING_STYLE_CHOICES])}."
+        ),
+    )
+
+    def save(self, *args, **kwargs):
+        """
+        Check if the max_value, color and style have a valid value.
+        """
+
+        if not any(self.style in _tuple for _tuple in RATING_STYLE_CHOICES):
+            raise ValueError(f"{self.style} is not a valid choice.")
+        if not self.color:
+            raise ValueError(f"color should be defined.")
+
+        if self.max_value < 1:
+            raise ValueError("Ensure this value is greater than or equal to 1.")
+        if self.max_value > 10:
+            raise ValueError(f"Ensure this value is less than or equal to 10.")
+
+        super().save(*args, **kwargs)
+
+
 class BooleanField(Field):
     pass
 
diff --git a/backend/src/baserow/contrib/database/migrations/0054_ratingfield.py b/backend/src/baserow/contrib/database/migrations/0054_ratingfield.py
new file mode 100644
index 000000000..8aac7012a
--- /dev/null
+++ b/backend/src/baserow/contrib/database/migrations/0054_ratingfield.py
@@ -0,0 +1,72 @@
+# Generated by Django 3.2.6 on 2021-12-21 13:15
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("database", "0053_add_and_move_public_flags"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="RatingField",
+            fields=[
+                (
+                    "field_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="database.field",
+                    ),
+                ),
+                (
+                    "max_value",
+                    models.PositiveSmallIntegerField(
+                        default=5,
+                        help_text="Maximum value the rating can take.",
+                        validators=[
+                            django.core.validators.MinValueValidator(1),
+                            django.core.validators.MaxValueValidator(10),
+                        ],
+                    ),
+                ),
+                (
+                    "color",
+                    models.CharField(
+                        default="dark-orange",
+                        help_text="Color of the symbols.",
+                        max_length=50,
+                    ),
+                ),
+                (
+                    "style",
+                    models.CharField(
+                        choices=[
+                            ("star", "Star"),
+                            ("heart", "Heart"),
+                            ("thumbs-up", "Thumbs-up"),
+                            ("flag", "Flags"),
+                            ("smile", "Smile"),
+                        ],
+                        default="star",
+                        help_text=(
+                            "Rating style. Allowed values: star, heart, "
+                            "thumbs-up, flag, smile."
+                        ),
+                        max_length=50,
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("database.field",),
+        ),
+    ]
diff --git a/backend/src/baserow/contrib/database/models.py b/backend/src/baserow/contrib/database/models.py
index 27f95f5cb..b7c607205 100644
--- a/backend/src/baserow/contrib/database/models.py
+++ b/backend/src/baserow/contrib/database/models.py
@@ -15,6 +15,7 @@ from .fields.models import (
     Field,
     TextField,
     NumberField,
+    RatingField,
     LongTextField,
     BooleanField,
     DateField,
@@ -48,6 +49,7 @@ __all__ = [
     "Field",
     "TextField",
     "NumberField",
+    "RatingField",
     "LongTextField",
     "BooleanField",
     "DateField",
diff --git a/backend/src/baserow/contrib/database/views/view_filters.py b/backend/src/baserow/contrib/database/views/view_filters.py
index 143f562fb..51c24c572 100644
--- a/backend/src/baserow/contrib/database/views/view_filters.py
+++ b/backend/src/baserow/contrib/database/views/view_filters.py
@@ -23,6 +23,7 @@ from baserow.contrib.database.fields.field_types import (
     LongTextFieldType,
     URLFieldType,
     NumberFieldType,
+    RatingFieldType,
     DateFieldType,
     LastModifiedFieldType,
     LinkRowFieldType,
@@ -64,6 +65,7 @@ class EqualViewFilterType(ViewFilterType):
         LongTextFieldType.type,
         URLFieldType.type,
         NumberFieldType.type,
+        RatingFieldType.type,
         EmailFieldType.type,
         PhoneNumberFieldType.type,
         FormulaFieldType.compatible_with_formula_types(
@@ -210,6 +212,7 @@ class HigherThanViewFilterType(ViewFilterType):
     type = "higher_than"
     compatible_field_types = [
         NumberFieldType.type,
+        RatingFieldType.type,
         FormulaFieldType.compatible_with_formula_types(
             BaserowFormulaNumberType.type,
         ),
@@ -246,6 +249,7 @@ class LowerThanViewFilterType(ViewFilterType):
     type = "lower_than"
     compatible_field_types = [
         NumberFieldType.type,
+        RatingFieldType.type,
         FormulaFieldType.compatible_with_formula_types(
             BaserowFormulaNumberType.type,
         ),
@@ -728,6 +732,7 @@ class EmptyViewFilterType(ViewFilterType):
         LongTextFieldType.type,
         URLFieldType.type,
         NumberFieldType.type,
+        RatingFieldType.type,
         BooleanFieldType.type,
         DateFieldType.type,
         LastModifiedFieldType.type,
diff --git a/backend/src/baserow/test_utils/fixtures/field.py b/backend/src/baserow/test_utils/fixtures/field.py
index d435b3ccb..9cf3df846 100644
--- a/backend/src/baserow/test_utils/fixtures/field.py
+++ b/backend/src/baserow/test_utils/fixtures/field.py
@@ -5,6 +5,7 @@ from baserow.contrib.database.fields.models import (
     TextField,
     LongTextField,
     NumberField,
+    RatingField,
     BooleanField,
     DateField,
     LinkRowField,
@@ -94,6 +95,23 @@ class FieldFixtures:
 
         return field
 
+    def create_rating_field(self, user=None, create_field=True, **kwargs):
+        if "table" not in kwargs:
+            kwargs["table"] = self.create_database_table(user=user)
+
+        if "name" not in kwargs:
+            kwargs["name"] = self.fake.name()
+
+        if "order" not in kwargs:
+            kwargs["order"] = 0
+
+        field = RatingField.objects.create(**kwargs)
+
+        if create_field:
+            self.create_model_field(kwargs["table"], field)
+
+        return field
+
     def create_boolean_field(self, user=None, create_field=True, **kwargs):
         if "table" not in kwargs:
             kwargs["table"] = self.create_database_table(user=user)
diff --git a/backend/src/baserow/test_utils/helpers.py b/backend/src/baserow/test_utils/helpers.py
index 20d5c4ddd..ae0b39174 100644
--- a/backend/src/baserow/test_utils/helpers.py
+++ b/backend/src/baserow/test_utils/helpers.py
@@ -90,6 +90,7 @@ def setup_interesting_test_table(data_fixture, user_kwargs=None):
         "positive_int": 1,
         "negative_decimal": Decimal("-1.2"),
         "positive_decimal": Decimal("1.2"),
+        "rating": 3,
         "boolean": "True",
         "datetime_us": datetime,
         "date_us": date,
diff --git a/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py b/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py
index 142a3f413..fe77320d1 100644
--- a/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py
+++ b/backend/tests/baserow/contrib/database/api/rows/test_row_serializers.py
@@ -279,6 +279,7 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
         "phone_number": "+4412345678",
         "positive_decimal": "1.2",
         "positive_int": "1",
+        "rating": 3,
         "single_select": {
             "color": "red",
             "id": SelectOption.objects.get(value="A").id,
diff --git a/backend/tests/baserow/contrib/database/api/rows/test_row_views.py b/backend/tests/baserow/contrib/database/api/rows/test_row_views.py
index 43c36f616..ca5ff2406 100644
--- a/backend/tests/baserow/contrib/database/api/rows/test_row_views.py
+++ b/backend/tests/baserow/contrib/database/api/rows/test_row_views.py
@@ -13,6 +13,7 @@ from baserow.contrib.database.fields.handler import FieldHandler
 from baserow.contrib.database.fields.registries import field_type_registry
 from baserow.contrib.database.rows.handler import RowHandler
 from baserow.contrib.database.tokens.handler import TokenHandler
+from baserow.test_utils.helpers import setup_interesting_test_table
 
 
 @pytest.mark.django_db
@@ -622,6 +623,25 @@ def test_create_row(api_client, data_fixture):
     }
 
 
+@pytest.mark.django_db
+def test_create_empty_row_for_interesting_fields(api_client, data_fixture):
+    """
+    Test a common case: create a row with empty values.
+    """
+
+    table, user, row, _ = setup_interesting_test_table(data_fixture)
+    jwt_token = data_fixture.generate_token(user)
+
+    response = api_client.post(
+        reverse("api:database:rows:list", kwargs={"table_id": table.id}),
+        {},
+        format="json",
+        HTTP_AUTHORIZATION=f"JWT {jwt_token}",
+    )
+
+    assert response.status_code == HTTP_200_OK
+
+
 @pytest.mark.django_db
 def test_get_row(api_client, data_fixture):
     user, jwt_token = data_fixture.create_user_and_token()
diff --git a/backend/tests/baserow/contrib/database/export/test_export_handler.py b/backend/tests/baserow/contrib/database/export/test_export_handler.py
index 6b8c37975..49dc4783b 100644
--- a/backend/tests/baserow/contrib/database/export/test_export_handler.py
+++ b/backend/tests/baserow/contrib/database/export/test_export_handler.py
@@ -220,16 +220,16 @@ def test_can_export_every_interesting_different_field_to_csv(
     # noinspection HttpUrlsUsage
     expected = (
         "\ufeffid,text,long_text,url,email,negative_int,positive_int,"
-        "negative_decimal,positive_decimal,boolean,datetime_us,date_us,datetime_eu,"
-        "date_eu,last_modified_datetime_us,last_modified_date_us,"
+        "negative_decimal,positive_decimal,rating,boolean,datetime_us,date_us,"
+        "datetime_eu,date_eu,last_modified_datetime_us,last_modified_date_us,"
         "last_modified_datetime_eu,last_modified_date_eu,created_on_datetime_us,"
         "created_on_date_us,created_on_datetime_eu,created_on_date_eu,link_row,"
-        "decimal_link_row,file_link_row,file,single_select,multiple_select,"
-        "phone_number,formula,lookup\r\n"
-        "1,,,,,,,,,False,,,,,01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,"
+        "decimal_link_row,file_link_row,file,single_select,"
+        "multiple_select,phone_number,formula,lookup\r\n"
+        "1,,,,,,,,,0,False,,,,,01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,"
         "01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,,,,,,,,test FORMULA,"
         "\r\n"
-        "2,text,long_text,https://www.google.com,test@example.com,-1,1,-1.2,1.2,True,"
+        "2,text,long_text,https://www.google.com,test@example.com,-1,1,-1.2,1.2,3,True,"
         "02/01/2020 01:23,02/01/2020,01/02/2020 01:23,01/02/2020,"
         "01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,"
         "01/02/2021 13:00,01/02/2021,02/01/2021 13:00,02/01/2021,"
diff --git a/backend/tests/baserow/contrib/database/field/test_field_types.py b/backend/tests/baserow/contrib/database/field/test_field_types.py
index ff3930321..a4c0cb19a 100644
--- a/backend/tests/baserow/contrib/database/field/test_field_types.py
+++ b/backend/tests/baserow/contrib/database/field/test_field_types.py
@@ -541,6 +541,7 @@ def test_human_readable_values(data_fixture):
         "phone_number": "",
         "positive_decimal": "",
         "positive_int": "",
+        "rating": "0",
         "single_select": "",
         "multiple_select": "",
         "text": "",
@@ -573,6 +574,7 @@ def test_human_readable_values(data_fixture):
         "phone_number": "+4412345678",
         "positive_decimal": "1.2",
         "positive_int": "1",
+        "rating": "3",
         "single_select": "A",
         "multiple_select": "D, C, E",
         "text": "text",
diff --git a/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py b/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py
index 764448234..e5357e0b5 100644
--- a/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py
+++ b/backend/tests/baserow/contrib/database/field/test_multiple_select_field_type.py
@@ -1340,11 +1340,11 @@ def test_conversion_date_to_multiple_select_field(data_fixture):
         assert field_type.type == "multiple_select"
         assert len(select_options) == 1
 
-    model = table.get_model()
+    model = table.get_model(attribute_names=True)
     rows = list(model.objects.all().enhance_by_fields())
 
     for index, field in enumerate(all_fields):
-        cell = getattr(rows[0], f"field_{field.id}").all()
+        cell = getattr(rows[0], field.model_attribute_name).all()
         assert len(cell) == 1
         assert cell[0].value == all_results[index]
 
@@ -1354,11 +1354,20 @@ def test_conversion_date_to_multiple_select_field(data_fixture):
     new_select_option = data_fixture.create_select_option(
         field=date_field_eu, value="01/09/2021", color="green"
     )
-    select_options = date_field_eu.select_options.all()
+    select_options = list(date_field_eu.select_options.all())
 
-    row_handler.create_row(
+    row = row_handler.create_row(
         user=user,
         table=table,
+        values={
+            f"field_{date_field_eu.id}": [select_options[0].id],
+        },
+    )
+
+    row_handler.update_row(
+        user=user,
+        table=table,
+        row_id=row.id,
         values={
             f"field_{date_field_eu.id}": [getattr(x, "id") for x in select_options],
         },
@@ -1377,18 +1386,14 @@ def test_conversion_date_to_multiple_select_field(data_fixture):
         field=date_field_eu,
         new_type_name="date",
         date_format="EU",
-        name="date_field_eu",
     )
 
-    model = table.get_model()
+    model = table.get_model(attribute_names=True)
     rows = list(model.objects.all().enhance_by_fields())
 
-    field_cell_row_0 = getattr(rows[0], f"field_{date_field_eu.id}")
-    field_cell_row_1 = getattr(rows[1], f"field_{date_field_eu.id}")
-    field_cell_row_2 = getattr(rows[2], f"field_{date_field_eu.id}")
-    assert field_cell_row_0 == date(2021, 8, 31)
-    assert field_cell_row_1 == date(2021, 8, 31)
-    assert field_cell_row_2 == date(2021, 9, 1)
+    assert rows[0].datefieldeu == date(2021, 8, 31)
+    assert rows[1].datefieldeu == date(2021, 8, 31)
+    assert rows[2].datefieldeu == date(2021, 9, 1)
 
 
 @pytest.mark.django_db
diff --git a/backend/tests/baserow/contrib/database/field/test_rating_field_type.py b/backend/tests/baserow/contrib/database/field/test_rating_field_type.py
new file mode 100644
index 000000000..f4f9f8342
--- /dev/null
+++ b/backend/tests/baserow/contrib/database/field/test_rating_field_type.py
@@ -0,0 +1,297 @@
+import pytest
+from django.core.exceptions import ValidationError
+from faker import Faker
+
+from decimal import Decimal
+from baserow.contrib.database.fields.handler import FieldHandler
+from baserow.contrib.database.fields.models import (
+    RatingField,
+)
+from baserow.contrib.database.rows.handler import RowHandler
+
+
+@pytest.mark.django_db
+def test_field_creation(data_fixture):
+    user = data_fixture.create_user()
+    table = data_fixture.create_database_table(user=user)
+    field = data_fixture.create_text_field(table=table, order=1, name="name")
+
+    handler = FieldHandler()
+    field = handler.create_field(
+        user=user,
+        table=table,
+        type_name="rating",
+        name="rating",
+        max_value=4,
+        color="red",
+        style="flag",
+    )
+
+    assert len(RatingField.objects.all()) == 1
+    from_db = RatingField.objects.get(name="rating")
+    assert from_db.color == "red"
+    assert from_db.max_value == 4
+    assert from_db.style == "flag"
+
+    fake = Faker()
+    value = fake.random_int(1, 4)
+    model = table.get_model(attribute_names=True)
+    row = model.objects.create(rating=value, name="Test")
+
+    assert row.rating == value
+    assert row.name == "Test"
+
+    handler.delete_field(user=user, field=field)
+    assert len(RatingField.objects.all()) == 0
+
+    for invalid_value in [
+        {"max_value": 11},
+        {"max_value": 0},
+        {"max_value": -2},
+        {"style": "invalid"},
+        {"style": ""},
+        {"color": None},
+        {"color": ""},
+    ]:
+        with pytest.raises(ValueError):
+            handler.create_field(
+                user=user,
+                table=table,
+                type_name="rating",
+                name="rating invalid",
+                **invalid_value
+            )
+
+
+@pytest.mark.django_db
+def test_row_creation(data_fixture):
+    user = data_fixture.create_user()
+    table = data_fixture.create_database_table(user=user)
+    data_fixture.create_database_table(user=user, database=table.database)
+    data_fixture.create_text_field(table=table, order=1, name="name")
+    field_handler = FieldHandler()
+    row_handler = RowHandler()
+
+    field_handler.create_field(
+        user=user, table=table, type_name="rating", name="rating"
+    )
+    assert len(RatingField.objects.all()) == 1
+
+    model = table.get_model(attribute_names=True)
+
+    row1 = row_handler.create_row(
+        user=user, table=table, values={"rating": 3}, model=model
+    )
+    row_handler.create_row(user=user, table=table, values={"rating": 0}, model=model)
+    row_handler.create_row(user=user, table=table, values={"rating": None}, model=model)
+    row_handler.create_row(user=user, table=table, values={}, model=model)
+
+    assert [(f.id, f.rating) for f in model.objects.all()] == [
+        (1, 3),
+        (2, 0),
+        (3, 0),
+        (4, 0),
+    ]
+
+    row_handler.update_row(
+        user_field_names=True,
+        user=user,
+        row_id=row1.id,
+        table=table,
+        values={"rating": 1},
+    )
+
+    assert [(f.id, f.rating) for f in model.objects.all()] == [
+        (1, 1),
+        (2, 0),
+        (3, 0),
+        (4, 0),
+    ]
+
+    for invalid_value in [-1, 6]:
+        with pytest.raises(ValidationError):
+            row_handler.create_row(
+                user=user, table=table, values={"rating": invalid_value}, model=model
+            )
+            row_handler.update_row(
+                user=user,
+                row_id=row1.id,
+                table=table,
+                values={"rating": invalid_value},
+            )
+
+
+@pytest.mark.django_db
+def test_rating_field_modification(data_fixture):
+    user = data_fixture.create_user()
+    table = data_fixture.create_database_table(user=user)
+    data_fixture.create_database_table(user=user, database=table.database)
+    text_field = data_fixture.create_text_field(table=table, order=1, name="text")
+    field_handler = FieldHandler()
+    row_handler = RowHandler()
+
+    rating_field = field_handler.create_field(
+        user=user, table=table, type_name="rating", name="Rating"
+    )
+
+    integer_field = data_fixture.create_number_field(
+        table=table,
+        name="integer",
+        number_type="INTEGER",
+        number_negative=True,
+    )
+
+    decimal_field = data_fixture.create_number_field(
+        table=table,
+        name="decimal",
+        number_type="DECIMAL",
+        number_negative=True,
+    )
+
+    boolean_field = data_fixture.create_boolean_field(table=table, name="boolean")
+
+    model = table.get_model(attribute_names=True)
+
+    row_handler.create_row(
+        user=user,
+        table=table,
+        values={
+            "text": "5",
+            "rating": 5,
+            "integer": 5,
+            "decimal": 4.5,
+            "boolean": True,
+        },
+        model=model,
+    )
+    row_handler.create_row(
+        user=user,
+        table=table,
+        values={
+            "text": "3",
+            "rating": 4,
+            "integer": 3,
+            "decimal": 2.5,
+            "boolean": False,
+        },
+        model=model,
+    )
+    row3 = row_handler.create_row(
+        user=user,
+        table=table,
+        values={"text": "1", "rating": 3, "integer": 1, "decimal": 1.3},
+        model=model,
+    )
+    row_handler.create_row(
+        user=user,
+        table=table,
+        values={"text": "1.5", "rating": 2, "integer": -1, "decimal": -1.2},
+        model=model,
+    )
+    row_handler.create_row(
+        user=user,
+        table=table,
+        values={"text": "invalid", "rating": 1, "integer": -5, "decimal": -7},
+        model=model,
+    )
+    row_handler.create_row(
+        user=user,
+        table=table,
+        values={"text": "0", "rating": 0, "integer": 0, "decimal": 0},
+        model=model,
+    )
+    row_handler.create_row(
+        user=user,
+        table=table,
+        values={
+            "text": None,
+            "rating": None,
+            "integer": None,
+            "number": None,
+        },
+        model=model,
+    )
+
+    # Convert text field to rating
+    field_handler.update_field(
+        user=user, field=text_field, new_type_name="rating", max_value=3
+    )
+    # Change max_value
+    field_handler.update_field(user=user, field=rating_field, max_value=3)
+    # Change field type from number -> rating
+    field_handler.update_field(
+        user=user,
+        field=integer_field,
+        new_type_name="rating",
+        max_value=3,
+    )
+    field_handler.update_field(
+        user=user,
+        field=decimal_field,
+        new_type_name="rating",
+        max_value=3,
+    )
+    field_handler.update_field(
+        user=user,
+        field=boolean_field,
+        new_type_name="rating",
+        max_value=3,
+    )
+
+    # Check value clamping on max_value modification
+    assert [
+        (f.id, f.text, f.rating, f.integer, f.decimal, f.boolean)
+        for f in model.objects.all()
+    ] == [
+        (1, 3, 3, 3, 3, 1),
+        (2, 3, 3, 3, 3, 0),
+        (3, 1, 3, 1, 1, 0),
+        (4, 2, 2, 0, 0, 0),
+        (5, 0, 1, 0, 0, 0),
+        (6, 0, 0, 0, 0, 0),
+        (7, 0, 0, 0, 0, 0),
+    ]
+
+    # Change boolean field to test conversion back with value != [0,1]
+    row_handler.update_row(
+        user=user,
+        row_id=row3.id,
+        table=table,
+        user_field_names=True,
+        values={"boolean": 3},
+    )
+
+    # Convert back field to original type
+    field_handler.update_field(user=user, field=text_field, new_type_name="text")
+    field_handler.update_field(
+        user=user,
+        field=integer_field,
+        new_type_name="number",
+        number_type="INTEGER",
+        number_negative=True,
+    )
+    field_handler.update_field(
+        user=user,
+        field=decimal_field,
+        new_type_name="number",
+        number_type="DECIMAL",
+        number_negative=True,
+        number_decimal_places=2,
+    )
+    field_handler.update_field(
+        user=user,
+        field=boolean_field,
+        new_type_name="boolean",
+    )
+
+    assert [
+        (f.id, f.text, f.integer, f.decimal, f.boolean) for f in model.objects.all()
+    ] == [
+        (1, "3", Decimal("3"), Decimal("3.00"), True),
+        (2, "3", Decimal("3"), Decimal("3.00"), False),
+        (3, "1", Decimal("1"), Decimal("1.00"), True),
+        (4, "2", Decimal("0"), Decimal("0.00"), False),
+        (5, "0", Decimal("0"), Decimal("0.00"), False),
+        (6, "0", Decimal("0"), Decimal("0.00"), False),
+        (7, "0", Decimal("0"), Decimal("0.00"), False),
+    ]
diff --git a/changelog.md b/changelog.md
index 20cf77d7e..000240dc6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+* Added rating field type.
 * Fix deleted options that appear in the command line JSON file export.
 * Fix subtracting date intervals from dates in formulas in some situations not working.
 * Added day of month filter to date field.
@@ -24,12 +25,12 @@
 
 * Fixed a bug where the frontend would fail hard if a table with no views was accessed.
 * Tables can now be opened in new browser tabs.
-* **Breaking Change**: Baserow's `docker-compose.yml` now allows setting the MEDIA_URL 
+* **Breaking Change**: Baserow's `docker-compose.yml` now allows setting the MEDIA_URL
   env variable. If using MEDIA_PORT you now need to set MEDIA_URL also.
 * **Breaking Change**: Baserow's `docker-compose.yml` container names have changed to
   no longer be hardcoded to prevent naming clashes.
 * Added a licensing system for the premium version.
-* Fixed bug where it was possible to create duplicate trash entries. 
+* Fixed bug where it was possible to create duplicate trash entries.
 * Fixed propType validation error when converting from a date field to a boolean field.
 * Deprecate internal formula field function field_by_id.
 * Made it possible to change user information.
diff --git a/premium/backend/tests/baserow_premium/export/test_premium_export_types.py b/premium/backend/tests/baserow_premium/export/test_premium_export_types.py
index 8146498ec..5c3d1293c 100644
--- a/premium/backend/tests/baserow_premium/export/test_premium_export_types.py
+++ b/premium/backend/tests/baserow_premium/export/test_premium_export_types.py
@@ -47,6 +47,7 @@ def test_can_export_every_interesting_different_field_to_json(
     "positive_int": "",
     "negative_decimal": "",
     "positive_decimal": "",
+    "rating": 0,
     "boolean": false,
     "datetime_us": "",
     "date_us": "",
@@ -80,6 +81,7 @@ def test_can_export_every_interesting_different_field_to_json(
     "positive_int": 1,
     "negative_decimal": "-1.2",
     "positive_decimal": "1.2",
+    "rating": 3,
     "boolean": true,
     "datetime_us": "02/01/2020 01:23",
     "date_us": "02/01/2020",
@@ -208,6 +210,7 @@ def test_can_export_every_interesting_different_field_to_xml(
     <positive-int/>
     <negative-decimal/>
     <positive-decimal/>
+    <rating>0</rating>
     <boolean>false</boolean>
     <datetime-us/>
     <date-us/>
@@ -241,6 +244,7 @@ def test_can_export_every_interesting_different_field_to_xml(
     <positive-int>1</positive-int>
     <negative-decimal>-1.2</negative-decimal>
     <positive-decimal>1.2</positive-decimal>
+    <rating>3</rating>
     <boolean>true</boolean>
     <datetime-us>02/01/2020 01:23</datetime-us>
     <date-us>02/01/2020</date-us>
diff --git a/premium/web-frontend/jsconfig.json b/premium/web-frontend/jsconfig.json
new file mode 100644
index 000000000..9b232de72
--- /dev/null
+++ b/premium/web-frontend/jsconfig.json
@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@baserow_premium/*": [
+        "./*"
+      ]
+    }
+  },
+  "exclude": [
+    "node_modules",
+    ".nuxt"
+  ]
+}
\ No newline at end of file
diff --git a/premium/web-frontend/modules/baserow_premium/components/views/kanban/KanbanViewOptionForm.vue b/premium/web-frontend/modules/baserow_premium/components/views/kanban/KanbanViewOptionForm.vue
index 452524118..49962b88f 100644
--- a/premium/web-frontend/modules/baserow_premium/components/views/kanban/KanbanViewOptionForm.vue
+++ b/premium/web-frontend/modules/baserow_premium/components/views/kanban/KanbanViewOptionForm.vue
@@ -45,7 +45,7 @@
 <script>
 import { required } from 'vuelidate/lib/validators'
 import form from '@baserow/modules/core/mixins/form'
-import { colors } from '@baserow/modules/core/utils/colors'
+import { randomColor } from '@baserow/modules/core/utils/colors'
 import ColorSelectContext from '@baserow/modules/core/components/ColorSelectContext'
 
 export default {
@@ -56,7 +56,7 @@ export default {
     return {
       allowedValues: ['color', 'value'],
       values: {
-        color: colors[Math.floor(Math.random() * colors.length)],
+        color: randomColor(),
         value: '',
       },
     }
diff --git a/web-frontend/.eslintrc.js b/web-frontend/.eslintrc.js
index 737d5b257..6a1e1a2c5 100644
--- a/web-frontend/.eslintrc.js
+++ b/web-frontend/.eslintrc.js
@@ -30,5 +30,6 @@ module.exports = {
       },
     ],
     'import/order': 'off',
+    'vue/html-self-closing': 'off',
   },
 }
diff --git a/web-frontend/jsconfig.json b/web-frontend/jsconfig.json
new file mode 100644
index 000000000..d5c98b39f
--- /dev/null
+++ b/web-frontend/jsconfig.json
@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@baserow/*": [
+        "./*"
+      ]
+    }
+  },
+  "exclude": [
+    "node_modules",
+    ".nuxt"
+  ]
+}
diff --git a/web-frontend/locales/en.js b/web-frontend/locales/en.js
index 86040d5d8..e0ddb3998 100644
--- a/web-frontend/locales/en.js
+++ b/web-frontend/locales/en.js
@@ -3,6 +3,7 @@ export default {
     yes: 'yes',
     no: 'no',
     wrong: 'Something went wrong',
+    none: 'None',
   },
   action: {
     upload: 'Upload',
@@ -62,6 +63,7 @@ export default {
     longText: 'Long text',
     linkToTable: 'Link to table',
     number: 'Number',
+    rating: 'Rating',
     boolean: 'Boolean',
     date: 'Date',
     lastModified: 'Last modified',
@@ -95,6 +97,7 @@ export default {
     decimal: 'Accepts a decimal with {places} decimal places after the dot.',
     decimalPositive:
       'Accepts a positive decimal with {places} decimal places after the dot.',
+    rating: 'Accepts a number.',
     boolean: 'Accepts a boolean.',
     date: 'Accepts a date time in ISO format.',
     dateTime: 'Accepts a date in ISO format.',
diff --git a/web-frontend/locales/fr.js b/web-frontend/locales/fr.js
index 3ce3ef084..c27a61184 100644
--- a/web-frontend/locales/fr.js
+++ b/web-frontend/locales/fr.js
@@ -3,6 +3,7 @@ export default {
     yes: 'oui',
     no: 'non',
     wrong: 'Une erreur est survenue',
+    none: 'Aucun(e)',
   },
   action: {
     upload: 'Envoyer',
@@ -62,6 +63,7 @@ export default {
     longText: 'Texte long',
     linkToTable: 'Lien vers une table',
     number: 'Nombre',
+    rating: 'Classement',
     boolean: 'Booléen',
     date: 'Date',
     lastModified: 'Dernière modification',
@@ -95,6 +97,7 @@ export default {
     numberPositive: 'Accepte un entier positive.',
     decimal: 'Accepte un nombre décimal.',
     decimalPositive: 'Accepte un nombre décimal positif.',
+    rating: 'Accepte un nombre entier',
     boolean: 'Accepte une valeur booléenne.',
     date: 'Accepte une date au format ISO.',
     dateTime: 'Accepte une date/heure au format ISO.',
diff --git a/web-frontend/modules/core/assets/scss/components/all.scss b/web-frontend/modules/core/assets/scss/components/all.scss
index c9d25c041..d1d249dce 100644
--- a/web-frontend/modules/core/assets/scss/components/all.scss
+++ b/web-frontend/modules/core/assets/scss/components/all.scss
@@ -14,8 +14,10 @@
 @import 'select';
 @import 'dropdown';
 @import 'tooltip';
+@import 'rating';
 @import 'fields/boolean';
 @import 'fields/number';
+@import 'fields/rating';
 @import 'fields/long_text';
 @import 'fields/date';
 @import 'fields/link_row';
@@ -27,8 +29,9 @@
 @import 'views/grid';
 @import 'views/grid/text';
 @import 'views/grid/long_text';
-@import 'views/grid/boolean';
 @import 'views/grid/number';
+@import 'views/grid/rating';
+@import 'views/grid/boolean';
 @import 'views/grid/date';
 @import 'views/grid/link_row';
 @import 'views/grid/file';
diff --git a/web-frontend/modules/core/assets/scss/components/color_select.scss b/web-frontend/modules/core/assets/scss/components/color_select.scss
index 84b07c9b1..cfa947765 100644
--- a/web-frontend/modules/core/assets/scss/components/color_select.scss
+++ b/web-frontend/modules/core/assets/scss/components/color_select.scss
@@ -1,24 +1,22 @@
-.color-select-context {
-  width: 212px;
-}
-
 .color-select-context__colors {
   display: flex;
-  flex-wrap: wrap;
-  padding: 6px;
+  flex-direction: column;
+  padding: 12px;
+  gap: 12px;
+}
+
+.color-select-context__row {
+  display: flex;
+  flex-direction: row;
+  gap: 12px;
 }
 
 .color-select-context__color {
   position: relative;
-  flex: 0 0 28px;
   height: 28px;
-  margin: 6px;
+  width: 28px;
   border-radius: 3px;
 
-  &:nth-child(5n+5) {
-    margin-right: 0;
-  }
-
   &:not(.active):hover {
     box-shadow: 0 0 2px rgba(0, 0, 0, 0.08);
   }
diff --git a/web-frontend/modules/core/assets/scss/components/colors.scss b/web-frontend/modules/core/assets/scss/components/colors.scss
index 0bbf9dd52..a4ed05017 100644
--- a/web-frontend/modules/core/assets/scss/components/colors.scss
+++ b/web-frontend/modules/core/assets/scss/components/colors.scss
@@ -57,3 +57,63 @@
 .background-color--dark-red {
   background-color: $color-error-300;
 }
+
+.color--light-blue {
+  color: $color-primary-100;
+}
+
+.color--light-gray {
+  color: $color-neutral-100;
+}
+
+.color--light-green {
+  color: $color-success-100;
+}
+
+.color--light-orange {
+  color: $color-warning-100;
+}
+
+.color--light-red {
+  color: $color-error-100;
+}
+
+.color--blue {
+  color: $color-primary-200;
+}
+
+.color--gray {
+  color: $color-neutral-200;
+}
+
+.color--green {
+  color: $color-success-200;
+}
+
+.color--orange {
+  color: $color-warning-200;
+}
+
+.color--red {
+  color: $color-error-200;
+}
+
+.color--dark-blue {
+  color: $color-primary-300;
+}
+
+.color--dark-gray {
+  color: $color-neutral-300;
+}
+
+.color--dark-green {
+  color: $color-success-300;
+}
+
+.color--dark-orange {
+  color: $color-warning-300;
+}
+
+.color--dark-red {
+  color: $color-error-300;
+}
diff --git a/web-frontend/modules/core/assets/scss/components/fields/rating.scss b/web-frontend/modules/core/assets/scss/components/fields/rating.scss
new file mode 100644
index 000000000..8c91d0c7c
--- /dev/null
+++ b/web-frontend/modules/core/assets/scss/components/fields/rating.scss
@@ -0,0 +1,26 @@
+.rating-field-form {
+  display: flex;
+  justify-content: space-between;
+}
+
+.rating-field-form__dropdown-style .select__item-name {
+  color: $color-neutral-600;
+}
+
+.rating-field__color {
+  padding: 10px;
+  text-align: center;
+  color: $color-primary-900;
+  border-radius: 3px;
+  font-size: 14px;
+  display: block;
+}
+
+.field-rating {
+  @extend %ellipsis;
+
+  @include fixed-height(32px, 13px);
+
+  padding-left: 5px;
+  user-select: none;
+}
diff --git a/web-frontend/modules/core/assets/scss/components/filters.scss b/web-frontend/modules/core/assets/scss/components/filters.scss
index 37f927bdb..2ba1a3e4b 100644
--- a/web-frontend/modules/core/assets/scss/components/filters.scss
+++ b/web-frontend/modules/core/assets/scss/components/filters.scss
@@ -104,6 +104,12 @@
   width: 130px;
 }
 
+.filters__value-rating {
+  border: solid 1px $color-neutral-400;
+  border-radius: 3px;
+  padding: 6px 12px;
+}
+
 .filters__value-link-row {
   @extend %ellipsis;
 
diff --git a/web-frontend/modules/core/assets/scss/components/rating.scss b/web-frontend/modules/core/assets/scss/components/rating.scss
new file mode 100644
index 000000000..859d48ef4
--- /dev/null
+++ b/web-frontend/modules/core/assets/scss/components/rating.scss
@@ -0,0 +1,51 @@
+.rating__star {
+  font-size: 18px;
+  padding-left: 2px;
+  pointer-events: initial;
+}
+
+.rating {
+  @extend %ellipsis;
+
+  display: block;
+  min-height: 18px;
+  user-select: none;
+  pointer-events: none;
+
+  & > .rating__star:first-child {
+    padding-left: 0;
+  }
+
+  &.editing {
+    & > .rating__star {
+      color: $color-neutral-200;
+      cursor: pointer;
+    }
+
+    // If we hover the rating, all stars should be colored and semi-transparent
+    // by default
+    &:hover > .rating__star {
+      color: inherit;
+      opacity: 0.6;
+    }
+
+    // but selected stars should have full opacity.
+    & > .rating__star.rating__star--selected {
+      color: inherit;
+      opacity: 1;
+    }
+
+    // stars after the hovered one should be grey and have full opacity
+    & > .rating__star:hover ~ .rating__star {
+      color: $color-neutral-200;
+      opacity: 1;
+    }
+
+    // selected star after the hovered star should be colored and be
+    // semi-transparent
+    & > .rating__star:hover ~ .rating__star.rating__star--selected {
+      color: inherit;
+      opacity: 0.6;
+    }
+  }
+}
diff --git a/web-frontend/modules/core/assets/scss/components/views/grid/rating.scss b/web-frontend/modules/core/assets/scss/components/views/grid/rating.scss
new file mode 100644
index 000000000..480b72088
--- /dev/null
+++ b/web-frontend/modules/core/assets/scss/components/views/grid/rating.scss
@@ -0,0 +1,8 @@
+.grid-field-rating {
+  @extend %ellipsis;
+
+  @include fixed-height(32px, 13px);
+
+  padding-left: 5px;
+  user-select: none;
+}
diff --git a/web-frontend/modules/core/components/ColorSelectContext.vue b/web-frontend/modules/core/components/ColorSelectContext.vue
index 7e0026acd..e1d90058e 100644
--- a/web-frontend/modules/core/components/ColorSelectContext.vue
+++ b/web-frontend/modules/core/components/ColorSelectContext.vue
@@ -1,39 +1,51 @@
 <template>
   <Context ref="context" class="color-select-context">
     <div class="color-select-context__colors">
-      <a
-        v-for="(color, index) in colors"
-        :key="color + '-' + index"
-        class="color-select-context__color"
-        :class="
-          'background-color--' +
-          color +
-          ' ' +
-          (color === active ? 'active' : '')
-        "
-        @click="select(color)"
-      ></a>
+      <div
+        v-for="(colorRow, rowIndex) in colors"
+        :key="`color-row-${rowIndex}`"
+        class="color-select-context__row"
+      >
+        <a
+          v-for="(color, index) in colorRow"
+          :key="`color-${index}`"
+          class="color-select-context__color"
+          :class="[
+            `background-color--${color}`,
+            color === active ? 'active' : '',
+          ]"
+          @click="select(color)"
+        ></a>
+      </div>
     </div>
   </Context>
 </template>
 
 <script>
 import context from '@baserow/modules/core/mixins/context'
-import { colors } from '@baserow/modules/core/utils/colors'
+import { colors as colorList } from '@baserow/modules/core/utils/colors'
+
+const defaultColors = [
+  colorList.slice(0, 5),
+  colorList.slice(5, 10),
+  colorList.slice(10, 15),
+]
 
 export default {
   name: 'ColorSelectContext',
   mixins: [context],
+  props: {
+    colors: {
+      type: Array,
+      default: () => defaultColors,
+      required: false,
+    },
+  },
   data() {
     return {
       active: '',
     }
   },
-  computed: {
-    colors() {
-      return colors
-    },
-  },
   methods: {
     setActive(color) {
       this.active = color
diff --git a/web-frontend/modules/core/components/Dropdown.vue b/web-frontend/modules/core/components/Dropdown.vue
index 442d63d41..d0fedce37 100644
--- a/web-frontend/modules/core/components/Dropdown.vue
+++ b/web-frontend/modules/core/components/Dropdown.vue
@@ -8,12 +8,14 @@
   >
     <a v-if="showInput" class="dropdown__selected" @click="show()">
       <template v-if="hasValue()">
-        <i
-          v-if="selectedIcon"
-          class="dropdown__selected-icon fas"
-          :class="'fa-' + selectedIcon"
-        ></i>
-        {{ selectedName }}
+        <slot name="value">
+          <i
+            v-if="selectedIcon"
+            class="dropdown__selected-icon fas"
+            :class="'fa-' + selectedIcon"
+          />
+          {{ selectedName }}
+        </slot>
       </template>
       <template v-else>{{ $t('action.makeChoice') }}</template>
       <i class="dropdown__toggle-icon fas fa-caret-down"></i>
diff --git a/web-frontend/modules/core/components/DropdownItem.vue b/web-frontend/modules/core/components/DropdownItem.vue
index b397d33a7..3b9e55b74 100644
--- a/web-frontend/modules/core/components/DropdownItem.vue
+++ b/web-frontend/modules/core/components/DropdownItem.vue
@@ -14,12 +14,14 @@
       @mousemove="hover(value, disabled)"
     >
       <div class="select__item-name">
-        <i
-          v-if="icon"
-          class="select__item-icon fas fa-fw"
-          :class="'fa-' + icon"
-        ></i>
-        {{ name }}
+        <slot>
+          <i
+            v-if="icon"
+            class="select__item-icon fas fa-fw"
+            :class="'fa-' + icon"
+          />
+          {{ name }}
+        </slot>
       </div>
       <div v-if="description !== null" class="select__item-description">
         {{ description }}
diff --git a/web-frontend/modules/core/mixins/dropdownItem.js b/web-frontend/modules/core/mixins/dropdownItem.js
index f0773438b..ff4af84b9 100644
--- a/web-frontend/modules/core/mixins/dropdownItem.js
+++ b/web-frontend/modules/core/mixins/dropdownItem.js
@@ -47,6 +47,9 @@ export default {
       return this.isVisible(query)
     },
     isVisible(query) {
+      if (!query) {
+        return true
+      }
       const regex = new RegExp('(' + escapeRegExp(query) + ')', 'i')
       return this.name.match(regex)
     },
diff --git a/web-frontend/modules/core/utils/colors.js b/web-frontend/modules/core/utils/colors.js
index 617a47b5b..fcce4b7fa 100644
--- a/web-frontend/modules/core/utils/colors.js
+++ b/web-frontend/modules/core/utils/colors.js
@@ -15,3 +15,7 @@ export const colors = [
   'dark-red',
   'dark-gray',
 ]
+
+export const randomColor = () => {
+  return colors[Math.floor(Math.random() * colors.length)]
+}
diff --git a/web-frontend/modules/database/components/Rating.vue b/web-frontend/modules/database/components/Rating.vue
new file mode 100644
index 000000000..6c5c8cb4a
--- /dev/null
+++ b/web-frontend/modules/database/components/Rating.vue
@@ -0,0 +1,53 @@
+<template functional>
+  <div
+    class="rating"
+    :class="[
+      data.staticClass,
+      `color--${props.color}`,
+      props.readOnly ? '' : 'editing',
+    ]"
+  >
+    <i
+      v-for="index in props.readOnly ? props.value : props.maxValue"
+      :key="index"
+      class="fas rating__star"
+      :class="{
+        [`fa-${props.ratingStyle}`]: true,
+        'rating__star--selected': index <= props.value,
+      }"
+      @click="
+        !props.readOnly &&
+          listeners['update'] &&
+          listeners['update'](index === props.value ? 0 : index)
+      "
+    />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Rating',
+  props: {
+    readOnly: {
+      default: false,
+      type: Boolean,
+    },
+    value: {
+      required: true,
+      validator: () => true,
+    },
+    maxValue: {
+      required: true,
+      type: Number,
+    },
+    ratingStyle: {
+      default: 'star',
+      type: String,
+    },
+    color: {
+      default: 'dark-orange',
+      type: String,
+    },
+  },
+}
+</script>
diff --git a/web-frontend/modules/database/components/card/RowCardFieldRating.vue b/web-frontend/modules/database/components/card/RowCardFieldRating.vue
new file mode 100644
index 000000000..6daa3c868
--- /dev/null
+++ b/web-frontend/modules/database/components/card/RowCardFieldRating.vue
@@ -0,0 +1,20 @@
+<template functional>
+  <component
+    :is="$options.components.Rating"
+    :read-only="true"
+    :rating-style="props.field.style"
+    :color="props.field.color"
+    :value="props.value"
+    :max-value="props.field.max_value"
+    class="card-rating"
+  ></component>
+</template>
+
+<script>
+import Rating from '@baserow/modules/database/components/Rating'
+
+export default {
+  height: 18,
+  components: { Rating },
+}
+</script>
diff --git a/web-frontend/modules/database/components/field/FieldRatingSubForm.vue b/web-frontend/modules/database/components/field/FieldRatingSubForm.vue
new file mode 100644
index 000000000..7b871b660
--- /dev/null
+++ b/web-frontend/modules/database/components/field/FieldRatingSubForm.vue
@@ -0,0 +1,134 @@
+<template>
+  <div class="rating-field-form">
+    <div class="control">
+      <label class="control__label control__label--small">{{
+        $t('fieldRatingSubForm.color')
+      }}</label>
+      <div class="control__elements">
+        <a
+          :ref="'color-select'"
+          :class="'rating-field__color' + ' background-color--' + values.color"
+          @click="openColor()"
+        >
+          <i class="fas fa-caret-down"></i>
+        </a>
+      </div>
+    </div>
+    <div class="control">
+      <label class="control__label control__label--small">{{
+        $t('fieldRatingSubForm.style')
+      }}</label>
+      <div class="control__elements">
+        <Dropdown
+          v-model="values.style"
+          class="dropdown--floating rating-field-form__dropdown-style"
+          :class="{ 'dropdown--error': $v.values.style.$error }"
+          :show-search="false"
+          @hide="$v.values.style.$touch()"
+        >
+          <DropdownItem
+            v-for="style in styles"
+            :key="style"
+            name=""
+            :value="style"
+            :icon="style"
+          />
+        </Dropdown>
+      </div>
+    </div>
+    <div class="control">
+      <label class="control__label control__label--small">{{
+        $t('fieldRatingSubForm.maxValue')
+      }}</label>
+      <div class="control__elements">
+        <Dropdown
+          v-model="values.max_value"
+          class="dropdown--floating"
+          :class="{ 'dropdown--error': $v.values.max_value.$error }"
+          :show-search="false"
+          @hide="$v.values.max_value.$touch()"
+        >
+          <DropdownItem
+            v-for="index in 10"
+            :key="index"
+            :name="`${index}`"
+            :value="index"
+          ></DropdownItem>
+        </Dropdown>
+      </div>
+    </div>
+    <ColorSelectContext
+      ref="colorContext"
+      :colors="colors"
+      @selected="updateColor($event)"
+    ></ColorSelectContext>
+  </div>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators'
+
+import form from '@baserow/modules/core/mixins/form'
+import fieldSubForm from '@baserow/modules/database/mixins/fieldSubForm'
+import ColorSelectContext from '@baserow/modules/core/components/ColorSelectContext'
+
+const colors = [['dark-blue', 'dark-green', 'dark-orange', 'dark-red']]
+
+export default {
+  name: 'FieldRatingSubForm',
+  components: { ColorSelectContext },
+  mixins: [form, fieldSubForm],
+  data() {
+    return {
+      allowedValues: ['max_value', 'color', 'style'],
+      values: {
+        max_value: 5,
+        color: 'dark-orange',
+        style: 'star',
+      },
+      colors,
+      styles: ['star', 'heart', 'thumbs-up', 'flag', 'smile'],
+    }
+  },
+  methods: {
+    openColor() {
+      this.$refs.colorContext.setActive(this.values.color)
+      this.$refs.colorContext.toggle(
+        this.$refs['color-select'],
+        'bottom',
+        'left',
+        4
+      )
+    },
+    updateColor(color) {
+      this.values.color = color
+    },
+  },
+  validations: {
+    values: {
+      max_value: { required },
+      color: { required },
+      style: { required },
+    },
+  },
+}
+</script>
+
+<i18n>
+{
+  "en": {
+    "fieldRatingSubForm": {
+      "maxValue": "Max",
+      "color": "Color",
+      "style": "Style"
+    }
+  },
+  "fr": {
+    "fieldRatingSubForm": {
+      "maxValue": "Max",
+      "color": "Couleur",
+      "style": "Style"
+    }
+  }
+}
+</i18n>
diff --git a/web-frontend/modules/database/components/field/FieldSelectOptions.vue b/web-frontend/modules/database/components/field/FieldSelectOptions.vue
index c0d4318ae..5591ec570 100644
--- a/web-frontend/modules/database/components/field/FieldSelectOptions.vue
+++ b/web-frontend/modules/database/components/field/FieldSelectOptions.vue
@@ -47,7 +47,7 @@
 import { required } from 'vuelidate/lib/validators'
 
 import ColorSelectContext from '@baserow/modules/core/components/ColorSelectContext'
-import { colors } from '@baserow/modules/core/utils/colors'
+import { randomColor } from '@baserow/modules/core/utils/colors'
 
 export default {
   name: 'FieldSelectOptions',
@@ -72,7 +72,7 @@ export default {
     add() {
       this.value.push({
         value: '',
-        color: colors[Math.floor(Math.random() * colors.length)],
+        color: randomColor(),
       })
       this.$emit('input', this.value)
     },
diff --git a/web-frontend/modules/database/components/row/RowEditFieldRating.vue b/web-frontend/modules/database/components/row/RowEditFieldRating.vue
new file mode 100644
index 000000000..5856ca839
--- /dev/null
+++ b/web-frontend/modules/database/components/row/RowEditFieldRating.vue
@@ -0,0 +1,30 @@
+<template>
+  <div class="control__elements">
+    <div class="field-rating">
+      <Rating
+        :rating-style="field.style"
+        :color="field.color"
+        :value="value"
+        :max-value="field.max_value"
+        :read-only="readOnly"
+        @update="update"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import rowEditField from '@baserow/modules/database/mixins/rowEditField'
+
+import Rating from '@baserow/modules/database/components/Rating'
+
+export default {
+  components: { Rating },
+  mixins: [rowEditField],
+  methods: {
+    update(newValue) {
+      this.$emit('update', newValue, this.value)
+    },
+  },
+}
+</script>
diff --git a/web-frontend/modules/database/components/view/ViewFilter.vue b/web-frontend/modules/database/components/view/ViewFilter.vue
index c72f94e5a..f4e7f6f76 100644
--- a/web-frontend/modules/database/components/view/ViewFilter.vue
+++ b/web-frontend/modules/database/components/view/ViewFilter.vue
@@ -15,23 +15,28 @@
         })
       }}</span>
     </a>
-    <ViewFilterContext
+    <Context
       ref="context"
-      :view="view"
-      :fields="fields"
-      :primary="primary"
-      :read-only="readOnly"
-      @changed="$emit('changed')"
-    ></ViewFilterContext>
+      class="filters"
+      :class="{ 'context--loading-overlay': view._.loading }"
+    >
+      <ViewFilterForm
+        :primary="primary"
+        :fields="fields"
+        :view="view"
+        :read-only="readOnly"
+        @changed="$emit('changed')"
+      />
+    </Context>
   </div>
 </template>
 
 <script>
-import ViewFilterContext from '@baserow/modules/database/components/view/ViewFilterContext'
+import ViewFilterForm from '@baserow/modules/database/components/view/ViewFilterForm'
 
 export default {
   name: 'ViewFilter',
-  components: { ViewFilterContext },
+  components: { ViewFilterForm },
   props: {
     primary: {
       type: Object,
diff --git a/web-frontend/modules/database/components/view/ViewFilterContext.vue b/web-frontend/modules/database/components/view/ViewFilterForm.vue
similarity index 95%
rename from web-frontend/modules/database/components/view/ViewFilterContext.vue
rename to web-frontend/modules/database/components/view/ViewFilterForm.vue
index de91a5863..a5688f9f5 100644
--- a/web-frontend/modules/database/components/view/ViewFilterContext.vue
+++ b/web-frontend/modules/database/components/view/ViewFilterForm.vue
@@ -1,9 +1,5 @@
 <template>
-  <Context
-    ref="context"
-    class="filters"
-    :class="{ 'context--loading-overlay': view._.loading }"
-  >
+  <div>
     <div v-show="view.filters.length === 0">
       <div class="filters__none">
         <div class="filters__none-title">
@@ -104,7 +100,7 @@
       </div>
       <div class="filters__value">
         <component
-          :is="getInputComponent(filter.type)"
+          :is="getInputComponent(filter.type, filter.field)"
           :ref="'filter-' + filter.id + '-value'"
           :filter="filter"
           :fields="fields"
@@ -116,9 +112,8 @@
     </div>
     <div v-if="!readOnly" class="filters_footer">
       <a class="filters__add" @click.prevent="addFilter()">
-        <i class="fas fa-plus"></i>
-        {{ $t('viewFilterContext.addFilter') }}
-      </a>
+        <i class="fas fa-plus"></i>{{ $t('viewFilterContext.addFilter') }}</a
+      >
       <div v-if="view.filters.length > 0">
         <SwitchInput
           :value="view.filters_disabled"
@@ -127,16 +122,14 @@
         >
       </div>
     </div>
-  </Context>
+  </div>
 </template>
 
 <script>
 import { notifyIf } from '@baserow/modules/core/utils/error'
-import context from '@baserow/modules/core/mixins/context'
 
 export default {
-  name: 'ViewFilterContext',
-  mixins: [context],
+  name: 'ViewFilterForm',
   props: {
     primary: {
       type: Object,
@@ -308,8 +301,9 @@ export default {
      * Returns the input component related to the filter type. This component is
      * responsible for updating the filter value.
      */
-    getInputComponent(type) {
-      return this.$registry.get('viewFilter', type).getInputComponent()
+    getInputComponent(type, fieldId) {
+      const field = this.fields.find(({ id }) => id === fieldId)
+      return this.$registry.get('viewFilter', type).getInputComponent(field)
     },
   },
 }
diff --git a/web-frontend/modules/database/components/view/ViewFilterTypeRating.vue b/web-frontend/modules/database/components/view/ViewFilterTypeRating.vue
new file mode 100644
index 000000000..486fb365d
--- /dev/null
+++ b/web-frontend/modules/database/components/view/ViewFilterTypeRating.vue
@@ -0,0 +1,26 @@
+<template>
+  <div class="filters__value-rating">
+    <Rating
+      :rating-style="field.style"
+      :color="field.color"
+      :value="copy"
+      :max-value="field.max_value"
+      @update="delayedUpdate($event, true)"
+    />
+  </div>
+</template>
+
+<script>
+import filterTypeInput from '@baserow/modules/database/mixins/filterTypeInput'
+import Rating from '@baserow/modules/database/components/Rating'
+
+export default {
+  name: 'ViewFilterTypeText',
+  components: { Rating },
+  mixins: [filterTypeInput],
+  created() {
+    // Value from server is always a string, we need to parse it.
+    this.copy = parseInt(this.filter.value, 10)
+  },
+}
+</script>
diff --git a/web-frontend/modules/database/components/view/grid/fields/FunctionalGridViewFieldRating.vue b/web-frontend/modules/database/components/view/grid/fields/FunctionalGridViewFieldRating.vue
new file mode 100644
index 000000000..347d538eb
--- /dev/null
+++ b/web-frontend/modules/database/components/view/grid/fields/FunctionalGridViewFieldRating.vue
@@ -0,0 +1,29 @@
+<template functional>
+  <div
+    class="grid-view__cell"
+    :class="{
+      ...(data.staticClass && {
+        [data.staticClass]: true,
+      }),
+    }"
+  >
+    <div class="grid-field-rating">
+      <component
+        :is="$options.components.Rating"
+        :read-only="true"
+        :rating-style="props.field.style"
+        :color="props.field.color"
+        :value="props.value"
+        :max-value="props.field.max_value"
+      ></component>
+    </div>
+  </div>
+</template>
+
+<script>
+import Rating from '@baserow/modules/database/components/Rating'
+
+export default {
+  components: { Rating },
+}
+</script>
diff --git a/web-frontend/modules/database/components/view/grid/fields/GridViewFieldRating.vue b/web-frontend/modules/database/components/view/grid/fields/GridViewFieldRating.vue
new file mode 100644
index 000000000..d816cce17
--- /dev/null
+++ b/web-frontend/modules/database/components/view/grid/fields/GridViewFieldRating.vue
@@ -0,0 +1,67 @@
+<template>
+  <div class="grid-view__cell active">
+    <div class="grid-field-rating">
+      <Rating
+        :read-only="readOnly"
+        :rating-style="field.style"
+        :color="field.color"
+        :value="value"
+        :max-value="field.max_value"
+        @update="update"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import gridField from '@baserow/modules/database/mixins/gridField'
+import Rating from '@baserow/modules/database/components/Rating'
+
+export default {
+  components: { Rating },
+  mixins: [gridField],
+  mounted() {
+    window.addEventListener('keyup', this.keyup)
+  },
+  beforeDestroy() {
+    window.removeEventListener('keyup', this.keyup)
+  },
+  methods: {
+    update(newValue) {
+      this.$emit('update', newValue, this.value)
+    },
+    keyup(event) {
+      // Allow keyboard modification
+      const { key } = event
+      let newValue = this.value
+
+      if ('0123456789'.includes(key)) {
+        // Transform digit in value
+        newValue = parseInt(key, 10)
+      } else {
+        // +, > to increase -, < to decrease
+        switch (key) {
+          case '+':
+          case '>':
+            newValue = this.value + 1
+            break
+          case '-':
+          case '<':
+            newValue = this.value - 1
+            break
+        }
+      }
+      if (newValue !== this.value) {
+        // Clamp value
+        if (newValue > this.field.max_value) {
+          newValue = this.field.max_value
+        }
+        if (newValue < 0) {
+          newValue = 0
+        }
+        this.$emit('update', newValue, this.value)
+      }
+    },
+  },
+}
+</script>
diff --git a/web-frontend/modules/database/fieldTypes.js b/web-frontend/modules/database/fieldTypes.js
index 7f2fe5ca6..6ce281dae 100644
--- a/web-frontend/modules/database/fieldTypes.js
+++ b/web-frontend/modules/database/fieldTypes.js
@@ -10,6 +10,7 @@ import {
 import { Registerable } from '@baserow/modules/core/registry'
 
 import FieldNumberSubForm from '@baserow/modules/database/components/field/FieldNumberSubForm'
+import FieldRatingSubForm from '@baserow/modules/database/components/field/FieldRatingSubForm'
 import FieldTextSubForm from '@baserow/modules/database/components/field/FieldTextSubForm'
 import FieldDateSubForm from '@baserow/modules/database/components/field/FieldDateSubForm'
 import FieldCreatedOnLastModifiedSubForm from '@baserow/modules/database/components/field/FieldCreatedOnLastModifiedSubForm'
@@ -22,6 +23,7 @@ import GridViewFieldURL from '@baserow/modules/database/components/view/grid/fie
 import GridViewFieldEmail from '@baserow/modules/database/components/view/grid/fields/GridViewFieldEmail'
 import GridViewFieldLinkRow from '@baserow/modules/database/components/view/grid/fields/GridViewFieldLinkRow'
 import GridViewFieldNumber from '@baserow/modules/database/components/view/grid/fields/GridViewFieldNumber'
+import GridViewFieldRating from '@baserow/modules/database/components/view/grid/fields/GridViewFieldRating'
 import GridViewFieldBoolean from '@baserow/modules/database/components/view/grid/fields/GridViewFieldBoolean'
 import GridViewFieldDate from '@baserow/modules/database/components/view/grid/fields/GridViewFieldDate'
 import GridViewFieldDateReadOnly from '@baserow/modules/database/components/view/grid/fields/GridViewFieldDateReadOnly'
@@ -34,6 +36,7 @@ import FunctionalGridViewFieldText from '@baserow/modules/database/components/vi
 import FunctionalGridViewFieldLongText from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldLongText'
 import FunctionalGridViewFieldLinkRow from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldLinkRow'
 import FunctionalGridViewFieldNumber from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldNumber'
+import FunctionalGridViewFieldRating from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldRating'
 import FunctionalGridViewFieldBoolean from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldBoolean'
 import FunctionalGridViewFieldDate from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldDate'
 import FunctionalGridViewFieldFile from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldFile'
@@ -48,6 +51,7 @@ import RowEditFieldURL from '@baserow/modules/database/components/row/RowEditFie
 import RowEditFieldEmail from '@baserow/modules/database/components/row/RowEditFieldEmail'
 import RowEditFieldLinkRow from '@baserow/modules/database/components/row/RowEditFieldLinkRow'
 import RowEditFieldNumber from '@baserow/modules/database/components/row/RowEditFieldNumber'
+import RowEditFieldRating from '@baserow/modules/database/components/row/RowEditFieldRating'
 import RowEditFieldBoolean from '@baserow/modules/database/components/row/RowEditFieldBoolean'
 import RowEditFieldDate from '@baserow/modules/database/components/row/RowEditFieldDate'
 import RowEditFieldDateReadOnly from '@baserow/modules/database/components/row/RowEditFieldDateReadOnly'
@@ -64,6 +68,7 @@ import RowCardFieldFormula from '@baserow/modules/database/components/card/RowCa
 import RowCardFieldLinkRow from '@baserow/modules/database/components/card/RowCardFieldLinkRow'
 import RowCardFieldMultipleSelect from '@baserow/modules/database/components/card/RowCardFieldMultipleSelect'
 import RowCardFieldNumber from '@baserow/modules/database/components/card/RowCardFieldNumber'
+import RowCardFieldRating from '@baserow/modules/database/components/card/RowCardFieldRating'
 import RowCardFieldPhoneNumber from '@baserow/modules/database/components/card/RowCardFieldPhoneNumber'
 import RowCardFieldSingleSelect from '@baserow/modules/database/components/card/RowCardFieldSingleSelect'
 import RowCardFieldText from '@baserow/modules/database/components/card/RowCardFieldText'
@@ -855,7 +860,7 @@ export class NumberFieldType extends FieldType {
 
   /**
    * Formats the value based on the field's settings. The number will be rounded
-   * if to much decimal places are provided and if negative numbers aren't allowed
+   * if too much decimal places are provided and if negative numbers aren't allowed
    * they will be set to 0.
    */
   static formatNumber(field, value) {
@@ -905,6 +910,114 @@ export class NumberFieldType extends FieldType {
   }
 }
 
+export class RatingFieldType extends FieldType {
+  static getMaxNumberLength() {
+    return 2
+  }
+
+  static getType() {
+    return 'rating'
+  }
+
+  getIconClass() {
+    return 'star'
+  }
+
+  getName() {
+    const { i18n } = this.app
+    return i18n.t('fieldType.rating')
+  }
+
+  getFormComponent() {
+    return FieldRatingSubForm
+  }
+
+  getGridViewFieldComponent() {
+    return GridViewFieldRating
+  }
+
+  getFunctionalGridViewFieldComponent() {
+    return FunctionalGridViewFieldRating
+  }
+
+  getRowEditFieldComponent() {
+    return RowEditFieldRating
+  }
+
+  getCardComponent() {
+    return RowCardFieldRating
+  }
+
+  getSortIndicator() {
+    return ['text', '1', '9']
+  }
+
+  getEmptyValue(field) {
+    return 0
+  }
+
+  getSort(name, order) {
+    return (a, b) => {
+      if (a[name] === b[name]) {
+        return 0
+      }
+
+      const numberA = a[name]
+      const numberB = b[name]
+
+      return order === 'ASC'
+        ? numberA < numberB
+          ? -1
+          : 1
+        : numberB < numberA
+        ? -1
+        : 1
+    }
+  }
+
+  /**
+   * First checks if the value is numeric, if that is the case, the number is going
+   * to be formatted.
+   */
+  prepareValueForPaste(field, clipboardData) {
+    const pastedValue = clipboardData.getData('text')
+    const value = parseInt(pastedValue, 10)
+
+    if (isNaN(value) || !isFinite(value)) {
+      return
+    }
+
+    // Clamp the value
+    if (value < 0) {
+      return 0
+    }
+    if (value > field.max_value) {
+      return field.max_value
+    }
+    return value
+  }
+
+  getDocsDataType(field) {
+    return 'number'
+  }
+
+  getDocsDescription(field) {
+    return this.app.i18n.t(`fieldDocs.rating`)
+  }
+
+  getDocsRequestExample(field) {
+    return 3
+  }
+
+  getContainsFilterFunction() {
+    return genericContainsFilter
+  }
+
+  canBeReferencedByFormulaField() {
+    return true
+  }
+}
+
 export class BooleanFieldType extends FieldType {
   static getType() {
     return 'boolean'
diff --git a/web-frontend/modules/database/mixins/selectOptions.js b/web-frontend/modules/database/mixins/selectOptions.js
index ec8d13cd9..0e3164d2f 100644
--- a/web-frontend/modules/database/mixins/selectOptions.js
+++ b/web-frontend/modules/database/mixins/selectOptions.js
@@ -1,7 +1,7 @@
 import { notifyIf } from '@baserow/modules/core/utils/error'
 import { clone } from '@baserow/modules/core/utils/object'
 import { isPrintableUnicodeCharacterKeyPress } from '@baserow/modules/core/utils/events'
-import { colors } from '@baserow/modules/core/utils/colors'
+import { randomColor } from '@baserow/modules/core/utils/colors'
 import FieldSelectOptionsDropdown from '@baserow/modules/database/components/field/FieldSelectOptionsDropdown'
 
 export default {
@@ -9,14 +9,14 @@ export default {
   methods: {
     /**
      * Adds a new select option to the field and then updates the field. This method is
-     * called from the dropdown, the user can create a new optionfrom  there if no
+     * called from the dropdown, the user can create a new option from there if no
      * options are found matching his search query.
      */
     async createOption({ value, done }) {
       const values = { select_options: clone(this.field.select_options) }
       values.select_options.push({
         value,
-        color: colors[Math.floor(Math.random() * colors.length)],
+        color: randomColor(),
       })
 
       try {
diff --git a/web-frontend/modules/database/plugin.js b/web-frontend/modules/database/plugin.js
index 116507e41..947ed8df0 100644
--- a/web-frontend/modules/database/plugin.js
+++ b/web-frontend/modules/database/plugin.js
@@ -11,6 +11,7 @@ import {
   EmailFieldType,
   LinkRowFieldType,
   NumberFieldType,
+  RatingFieldType,
   BooleanFieldType,
   DateFieldType,
   LastModifiedFieldType,
@@ -219,6 +220,7 @@ export default (context) => {
   app.$registry.register('field', new LongTextFieldType(context))
   app.$registry.register('field', new LinkRowFieldType(context))
   app.$registry.register('field', new NumberFieldType(context))
+  app.$registry.register('field', new RatingFieldType(context))
   app.$registry.register('field', new BooleanFieldType(context))
   app.$registry.register('field', new DateFieldType(context))
   app.$registry.register('field', new LastModifiedFieldType(context))
diff --git a/web-frontend/modules/database/viewFilters.js b/web-frontend/modules/database/viewFilters.js
index 839f7855b..ef6d0b4b7 100644
--- a/web-frontend/modules/database/viewFilters.js
+++ b/web-frontend/modules/database/viewFilters.js
@@ -2,6 +2,7 @@ import moment from '@baserow/modules/core/moment'
 import { Registerable } from '@baserow/modules/core/registry'
 import ViewFilterTypeText from '@baserow/modules/database/components/view/ViewFilterTypeText'
 import ViewFilterTypeNumber from '@baserow/modules/database/components/view/ViewFilterTypeNumber'
+import ViewFilterTypeRating from '@baserow/modules/database/components/view/ViewFilterTypeRating'
 import ViewFilterTypeSelectOptions from '@baserow/modules/database/components/view/ViewFilterTypeSelectOptions'
 import ViewFilterTypeBoolean from '@baserow/modules/database/components/view/ViewFilterTypeBoolean'
 import ViewFilterTypeDate from '@baserow/modules/database/components/view/ViewFilterTypeDate'
@@ -10,7 +11,10 @@ import ViewFilterTypeLinkRow from '@baserow/modules/database/components/view/Vie
 import { trueString } from '@baserow/modules/database/utils/constants'
 import { isNumeric } from '@baserow/modules/core/utils/string'
 import ViewFilterTypeFileTypeDropdown from '@baserow/modules/database/components/view/ViewFilterTypeFileTypeDropdown'
-import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
+import {
+  FormulaFieldType,
+  RatingFieldType,
+} from '@baserow/modules/database/fieldTypes'
 
 export class ViewFilterType extends Registerable {
   /**
@@ -133,7 +137,10 @@ export class EqualViewFilterType extends ViewFilterType {
     return i18n.t('viewFilter.is')
   }
 
-  getInputComponent() {
+  getInputComponent(field) {
+    if (field?.type === RatingFieldType.getType()) {
+      return ViewFilterTypeRating
+    }
     return ViewFilterTypeText
   }
 
@@ -144,6 +151,7 @@ export class EqualViewFilterType extends ViewFilterType {
       'url',
       'email',
       'number',
+      'rating',
       'phone_number',
       FormulaFieldType.compatibleWithFormulaTypes('text', 'char', 'number'),
     ]
@@ -170,7 +178,10 @@ export class NotEqualViewFilterType extends ViewFilterType {
     return i18n.t('viewFilter.isNot')
   }
 
-  getInputComponent() {
+  getInputComponent(field) {
+    if (field?.type === RatingFieldType.getType()) {
+      return ViewFilterTypeRating
+    }
     return ViewFilterTypeText
   }
 
@@ -181,6 +192,7 @@ export class NotEqualViewFilterType extends ViewFilterType {
       'url',
       'email',
       'number',
+      'rating',
       'phone_number',
       FormulaFieldType.compatibleWithFormulaTypes('text', 'char', 'number'),
     ]
@@ -712,12 +724,19 @@ export class HigherThanViewFilterType extends ViewFilterType {
     return '100'
   }
 
-  getInputComponent() {
+  getInputComponent(field) {
+    if (field?.type === RatingFieldType.getType()) {
+      return ViewFilterTypeRating
+    }
     return ViewFilterTypeNumber
   }
 
   getCompatibleFieldTypes() {
-    return ['number', FormulaFieldType.compatibleWithFormulaTypes('number')]
+    return [
+      'number',
+      'rating',
+      FormulaFieldType.compatibleWithFormulaTypes('number'),
+    ]
   }
 
   matches(rowValue, filterValue, field, fieldType) {
@@ -745,12 +764,19 @@ export class LowerThanViewFilterType extends ViewFilterType {
     return '100'
   }
 
-  getInputComponent() {
+  getInputComponent(field) {
+    if (field?.type === RatingFieldType.getType()) {
+      return ViewFilterTypeRating
+    }
     return ViewFilterTypeNumber
   }
 
   getCompatibleFieldTypes() {
-    return ['number', FormulaFieldType.compatibleWithFormulaTypes('number')]
+    return [
+      'number',
+      'rating',
+      FormulaFieldType.compatibleWithFormulaTypes('number'),
+    ]
   }
 
   matches(rowValue, filterValue, field, fieldType) {
diff --git a/web-frontend/package.json b/web-frontend/package.json
index b06bc6d12..39744d7bb 100644
--- a/web-frontend/package.json
+++ b/web-frontend/package.json
@@ -12,19 +12,21 @@
     "start": "nuxt start --hostname 0.0.0.0",
     "eslint": "eslint -c .eslintrc.js --ext .js,.vue . ../premium/web-frontend",
     "stylelint": "stylelint **/*.scss ../premium/web-frontend/**/*.scss --syntax scss",
-    "jest": "jest -i --verbose false"
+    "jest": "jest -i --verbose false",
+    "test": "yarn jest"
   },
   "dependencies": {
     "@fortawesome/fontawesome-free": "^5.15.3",
     "@nuxtjs/axios": "^5.13.6",
     "@nuxtjs/i18n": "^7.0.1",
-    "async-mutex": "^0.3.1",
     "antlr4": "4.8.0",
+    "async-mutex": "^0.3.1",
     "axios": "^0.21.0",
     "bignumber.js": "^9.0.1",
     "chart.js": "2.9.4",
     "cookie-universal-nuxt": "^2.1.5",
     "cross-env": "^7.0.2",
+    "jest-serializer-vue": "^2.0.2",
     "jwt-decode": "^3.1.2",
     "lodash": "^4.17.21",
     "moment": "^2.26.0",
diff --git a/web-frontend/test/fixtures/mockServer.js b/web-frontend/test/fixtures/mockServer.js
index bd92bea11..9213f49be 100644
--- a/web-frontend/test/fixtures/mockServer.js
+++ b/web-frontend/test/fixtures/mockServer.js
@@ -74,6 +74,12 @@ export class MockServer {
     this.mock.onPost(`/database/rows/table/${table.id}/`).reply(200, result)
   }
 
+  updateViewFilter(filterId, newValue) {
+    this.mock
+      .onPatch(`/database/views/filter/${filterId}/`, { value: newValue })
+      .reply(200)
+  }
+
   resetMockEndpoints() {
     this.mock.reset()
   }
diff --git a/web-frontend/test/helpers/testApp.js b/web-frontend/test/helpers/testApp.js
index 5d2d3ce5a..4e41fe1c8 100644
--- a/web-frontend/test/helpers/testApp.js
+++ b/web-frontend/test/helpers/testApp.js
@@ -148,7 +148,11 @@ export class TestApp {
       attachTo: rootDiv,
       ...kwargs,
     })
-    await this.callFetchOnChildren(wrapper.vm)
+
+    // The vm property doesn't alway exist. See https://vue-test-utils.vuejs.org/api/wrapper/#properties
+    if (wrapper.vm) {
+      await this.callFetchOnChildren(wrapper.vm)
+    }
     return wrapper
   }
 
diff --git a/web-frontend/test/unit/database/components/__snapshots__/rating.spec.js.snap b/web-frontend/test/unit/database/components/__snapshots__/rating.spec.js.snap
new file mode 100644
index 000000000..a488ecb2b
--- /dev/null
+++ b/web-frontend/test/unit/database/components/__snapshots__/rating.spec.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Rating component Customized rating component 1`] = `
+<div
+  class="rating color--dark-blue editing"
+>
+  <i
+    class="fas rating__star fa-star rating__star--selected"
+  />
+  <i
+    class="fas rating__star fa-star rating__star--selected"
+  />
+  <i
+    class="fas rating__star fa-star rating__star--selected"
+  />
+  <i
+    class="fas rating__star fa-star"
+  />
+  <i
+    class="fas rating__star fa-star"
+  />
+</div>
+`;
+
+exports[`Rating component Default rating component 1`] = `
+<div
+  class="rating color--dark-orange"
+>
+  <i
+    class="fas rating__star fa-star rating__star--selected"
+  />
+  <i
+    class="fas rating__star fa-star rating__star--selected"
+  />
+  <i
+    class="fas rating__star fa-star rating__star--selected"
+  />
+</div>
+`;
diff --git a/web-frontend/test/unit/database/components/rating.spec.js b/web-frontend/test/unit/database/components/rating.spec.js
new file mode 100644
index 000000000..2f0cdf9ee
--- /dev/null
+++ b/web-frontend/test/unit/database/components/rating.spec.js
@@ -0,0 +1,65 @@
+import { TestApp } from '@baserow/test/helpers/testApp'
+import Rating from '@baserow/modules/database/components/Rating'
+
+describe('Rating component', () => {
+  let testApp = null
+
+  beforeAll(() => {
+    testApp = new TestApp()
+  })
+
+  afterEach(() => {
+    testApp.afterEach()
+  })
+
+  const mountWebhookForm = (
+    props = { value: 3, maxValue: 5, readOnly: true },
+    listeners = {}
+  ) => {
+    return testApp.mount(Rating, { propsData: props, listeners })
+  }
+
+  const changeValue = async (wrapper, value) => {
+    const star = wrapper.find(`.rating :nth-child(${value})`)
+
+    await star.trigger('click')
+  }
+
+  test('Default rating component', async () => {
+    const wrapper = await mountWebhookForm()
+    expect(wrapper.element).toMatchSnapshot()
+  })
+
+  test('Customized rating component', async () => {
+    const wrapper = await mountWebhookForm({
+      value: 3,
+      maxValue: 5,
+      readOnly: false,
+      style: 'flag',
+      color: 'dark-blue',
+    })
+    expect(wrapper.element).toMatchSnapshot()
+  })
+
+  test('Test interactions with rating component', async () => {
+    const onUpdate = jest.fn()
+    const wrapper = await mountWebhookForm(
+      {
+        value: 3,
+        maxValue: 5,
+        readOnly: false,
+      },
+      { update: onUpdate }
+    )
+
+    changeValue(wrapper, 1)
+    expect(onUpdate).toHaveBeenCalledWith(1)
+
+    changeValue(wrapper, 5)
+    expect(onUpdate).toHaveBeenCalledWith(5)
+
+    // If we click on current value, should set value to 0
+    changeValue(wrapper, 3)
+    expect(onUpdate).toHaveBeenCalledWith(0)
+  })
+})
diff --git a/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap b/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap
new file mode 100644
index 000000000..a1349b982
--- /dev/null
+++ b/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap
@@ -0,0 +1,1353 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ViewFilterForm component Default view filter component 1`] = `
+<div>
+  <div
+    style="display: none;"
+  >
+    <div
+      class="filters__none"
+    >
+      <div
+        class="filters__none-title"
+      >
+        
+        viewFilterContext.noFilterTitle
+      
+      </div>
+       
+      <div
+        class="filters__none-description"
+      >
+        
+        viewFilterContext.noFilterText
+      
+      </div>
+    </div>
+  </div>
+    
+  <div
+    class="filters_footer"
+  >
+    <a
+      class="filters__add"
+    >
+      <i
+        class="fas fa-plus"
+      />
+      viewFilterContext.addFilter
+    </a>
+     
+    <!---->
+  </div>
+</div>
+`;
+
+exports[`ViewFilterForm component Full view filter component 1`] = `
+<div>
+  <div
+    style="display: none;"
+  >
+    <div
+      class="filters__none"
+    >
+      <div
+        class="filters__none-title"
+      >
+        
+        viewFilterContext.noFilterTitle
+      
+      </div>
+       
+      <div
+        class="filters__none-description"
+      >
+        
+        viewFilterContext.noFilterText
+      
+      </div>
+    </div>
+  </div>
+   
+  <div
+    class="filters__item"
+  >
+    <a
+      class="filters__remove"
+    >
+      <i
+        class="fas fa-times"
+      />
+    </a>
+     
+    <div
+      class="filters__operator"
+    >
+      <span>
+        viewFilterContext.where
+      </span>
+       
+      <!---->
+       
+      <!---->
+       
+      <!---->
+    </div>
+     
+    <div
+      class="filters__field"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        Name
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item active"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Name
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+             
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Stars
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Flag
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__type"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        viewFilter.is
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item active"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.is
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.isNot
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.contains
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.containsNot
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.lengthIsLowerThan
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.isEmpty
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.isNotEmpty
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__value"
+    >
+      <input
+        class="input filters__value-input"
+        type="text"
+      />
+    </div>
+  </div>
+  <div
+    class="filters__item"
+  >
+    <a
+      class="filters__remove"
+    >
+      <i
+        class="fas fa-times"
+      />
+    </a>
+     
+    <div
+      class="filters__operator"
+    >
+      <!---->
+       
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        viewFilterContext.and
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <!---->
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item active"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilterContext.and
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+             
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilterContext.or
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+       
+      <!---->
+       
+      <!---->
+    </div>
+     
+    <div
+      class="filters__field"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        Stars
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Name
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+             
+            <li
+              class="select__item active"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Stars
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Flag
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__type"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        viewFilter.is
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item active"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.is
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.isNot
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.higherThan
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.lowerThan
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__value"
+    >
+      <div
+        class="filters__value-rating"
+      >
+        <div
+          class="rating color--dark-orange editing"
+        >
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star"
+          />
+          <i
+            class="fas rating__star fa-star"
+          />
+          <i
+            class="fas rating__star fa-star"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+   
+  <div
+    class="filters_footer"
+  >
+    <a
+      class="filters__add"
+    >
+      <i
+        class="fas fa-plus"
+      />
+      viewFilterContext.addFilter
+    </a>
+     
+    <div>
+      <div
+        class="switch switch--has-content"
+      >
+        viewFilterContext.disableAllFilters
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`ViewFilterForm component Test rating filter 1`] = `
+<div>
+  <div
+    style="display: none;"
+  >
+    <div
+      class="filters__none"
+    >
+      <div
+        class="filters__none-title"
+      >
+        
+        viewFilterContext.noFilterTitle
+      
+      </div>
+       
+      <div
+        class="filters__none-description"
+      >
+        
+        viewFilterContext.noFilterText
+      
+      </div>
+    </div>
+  </div>
+   
+  <div
+    class="filters__item"
+  >
+    <a
+      class="filters__remove"
+    >
+      <i
+        class="fas fa-times"
+      />
+    </a>
+     
+    <div
+      class="filters__operator"
+    >
+      <span>
+        viewFilterContext.where
+      </span>
+       
+      <!---->
+       
+      <!---->
+       
+      <!---->
+    </div>
+     
+    <div
+      class="filters__field"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        Stars
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Name
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+             
+            <li
+              class="select__item active"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Stars
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Flag
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__type"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        viewFilter.is
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item active hover"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.is
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.isNot
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.higherThan
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.lowerThan
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__value"
+    >
+      <div
+        class="filters__value-rating"
+      >
+        <div
+          class="rating color--dark-orange editing"
+        >
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star"
+          />
+          <i
+            class="fas rating__star fa-star"
+          />
+          <i
+            class="fas rating__star fa-star"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+   
+  <div
+    class="filters_footer"
+  >
+    <a
+      class="filters__add"
+    >
+      <i
+        class="fas fa-plus"
+      />
+      viewFilterContext.addFilter
+    </a>
+     
+    <div>
+      <div
+        class="switch switch--has-content"
+      >
+        viewFilterContext.disableAllFilters
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`ViewFilterForm component Test rating filter 2`] = `
+<div>
+  <div
+    style="display: none;"
+  >
+    <div
+      class="filters__none"
+    >
+      <div
+        class="filters__none-title"
+      >
+        
+        viewFilterContext.noFilterTitle
+      
+      </div>
+       
+      <div
+        class="filters__none-description"
+      >
+        
+        viewFilterContext.noFilterText
+      
+      </div>
+    </div>
+  </div>
+   
+  <div
+    class="filters__item"
+  >
+    <a
+      class="filters__remove"
+    >
+      <i
+        class="fas fa-times"
+      />
+    </a>
+     
+    <div
+      class="filters__operator"
+    >
+      <span>
+        viewFilterContext.where
+      </span>
+       
+      <!---->
+       
+      <!---->
+       
+      <!---->
+    </div>
+     
+    <div
+      class="filters__field"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        Stars
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Name
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+             
+            <li
+              class="select__item active"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Stars
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        Flag
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__type"
+    >
+      <div
+        class="dropdown dropdown--floating dropdown--tiny"
+      >
+        <a
+          class="dropdown__selected"
+        >
+          <!---->
+          
+        viewFilter.is
+       
+          <i
+            class="dropdown__toggle-icon fas fa-caret-down"
+          />
+        </a>
+         
+        <div
+          class="dropdown__items hidden"
+        >
+          <div
+            class="select__search"
+          >
+            <i
+              class="select__search-icon fas fa-search"
+            />
+             
+            <input
+              class="select__search-input"
+              placeholder="action.search"
+              type="text"
+            />
+          </div>
+           
+          <ul
+            class="select__items"
+          >
+            <li
+              class="select__item active hover"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.is
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.isNot
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.higherThan
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+            <li
+              class="select__item"
+            >
+              <a
+                class="select__item-link"
+              >
+                <div
+                  class="select__item-name"
+                >
+                  <!---->
+                  
+        viewFilter.lowerThan
+      
+                </div>
+                 
+                <!---->
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+     
+    <div
+      class="filters__value"
+    >
+      <div
+        class="filters__value-rating"
+      >
+        <div
+          class="rating color--dark-orange editing"
+        >
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+          <i
+            class="fas rating__star fa-star rating__star--selected"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+   
+  <div
+    class="filters_footer"
+  >
+    <a
+      class="filters__add"
+    >
+      <i
+        class="fas fa-plus"
+      />
+      viewFilterContext.addFilter
+    </a>
+     
+    <div>
+      <div
+        class="switch switch--has-content"
+      >
+        viewFilterContext.disableAllFilters
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/web-frontend/test/unit/database/components/view/viewFilterForm.spec.js b/web-frontend/test/unit/database/components/view/viewFilterForm.spec.js
new file mode 100644
index 000000000..e127a8f6f
--- /dev/null
+++ b/web-frontend/test/unit/database/components/view/viewFilterForm.spec.js
@@ -0,0 +1,177 @@
+import { TestApp } from '@baserow/test/helpers/testApp'
+import ViewFilterForm from '@baserow/modules/database/components/view/ViewFilterForm'
+
+const primary = {
+  id: 1,
+  name: 'Name',
+  order: 0,
+  type: 'text',
+  primary: true,
+  text_default: '',
+  _: {
+    loading: false,
+  },
+}
+
+const fields = [
+  {
+    id: 2,
+    table_id: 196,
+    name: 'Stars',
+    order: 1,
+    type: 'rating',
+    primary: false,
+    max_value: 5,
+    color: 'dark-orange',
+    style: 'star',
+    _: {
+      loading: false,
+    },
+  },
+  {
+    id: 3,
+    table_id: 196,
+    name: 'Flag',
+    order: 2,
+    type: 'rating',
+    primary: false,
+    max_value: 10,
+    color: 'dark-red',
+    style: 'heart',
+    _: {
+      loading: false,
+    },
+  },
+]
+
+const view = {
+  id: 1,
+  name: 'Grid',
+  type: 'grid',
+  filter_type: 'AND',
+  filters: [
+    {
+      field: 1,
+      type: 'equal',
+      value: 'test',
+      preload_values: {},
+      _: { hover: false, loading: false },
+      id: 10,
+    },
+    {
+      field: 2,
+      type: 'equal',
+      value: 2,
+      preload_values: {},
+      _: { hover: false, loading: false },
+      id: 11,
+    },
+  ],
+  filters_disabled: false,
+  _: {
+    selected: true,
+    loading: false,
+  },
+}
+
+describe('ViewFilterForm component', () => {
+  let testApp = null
+  let mockServer = null
+
+  beforeAll(() => {
+    testApp = new TestApp()
+    mockServer = testApp.mockServer
+  })
+
+  afterEach(() => {
+    jest.useRealTimers()
+    testApp.afterEach()
+    mockServer.resetMockEndpoints()
+  })
+
+  const mountViewFilterForm = async (
+    props = {
+      primary: {},
+      fields: [],
+      view: { filters: {}, _: {} },
+      readOnly: false,
+    },
+    listeners = {}
+  ) => {
+    const wrapper = await testApp.mount(ViewFilterForm, {
+      propsData: props,
+      listeners,
+    })
+    return wrapper
+  }
+
+  test('Default view filter component', async () => {
+    const wrapper = await mountViewFilterForm()
+    expect(wrapper.element).toMatchSnapshot()
+  })
+
+  test('Full view filter component', async () => {
+    const wrapper = await mountViewFilterForm({
+      primary,
+      fields,
+      view,
+      readOnly: false,
+    })
+    expect(wrapper.element).toMatchSnapshot()
+  })
+
+  test('Test rating filter', async (done) => {
+    // We want to bypass some setTimeout
+    jest.useFakeTimers()
+    // Mock server filter update call
+    mockServer.updateViewFilter(11, 5)
+
+    // Add rating one filter
+    const viewClone = JSON.parse(JSON.stringify(view))
+    viewClone.filters = [
+      {
+        field: 2,
+        type: 'equal',
+        value: 2,
+        preload_values: {},
+        _: { hover: false, loading: false },
+        id: 11,
+      },
+    ]
+
+    const onChange = jest.fn(() => {
+      // The test is about to finish
+      expect(wrapper.emitted().changed).toBeTruthy()
+      // The Five star option should be selected
+      expect(wrapper.element).toMatchSnapshot()
+      done()
+    })
+
+    // Mounting the component
+    const wrapper = await mountViewFilterForm(
+      {
+        primary,
+        fields,
+        view: viewClone,
+        readOnly: false,
+      },
+      { changed: onChange }
+    )
+
+    // Open type dropdown
+    await wrapper.find('.filters__type .dropdown__selected').trigger('click')
+    expect(wrapper.element).toMatchSnapshot()
+
+    // Select five stars
+    const option = wrapper.find(
+      '.filters__value  .rating > .rating__star:nth-child(5)'
+    )
+
+    await option.trigger('click')
+    // Wait some timers
+    await jest.runAllTimers()
+
+    // Test finishes only when onChange callback is called
+    // Wait for mockServer to respond -> see onChange callback
+  })
+})
diff --git a/web-frontend/test/unit/database/fieldTypes.spec.js b/web-frontend/test/unit/database/fieldTypes.spec.js
index 7858c1375..59e3ebb92 100644
--- a/web-frontend/test/unit/database/fieldTypes.spec.js
+++ b/web-frontend/test/unit/database/fieldTypes.spec.js
@@ -38,6 +38,14 @@ const mockedFields = {
     number_negative: false,
     number_type: 'INTEGER',
   },
+  rating: {
+    id: 16,
+    name: 'rating',
+    order: 4,
+    primary: false,
+    table_id: 42,
+    type: 'rating',
+  },
   boolean: {
     id: 5,
     name: 'boolean',
diff --git a/web-frontend/test/unit/jest.config.js b/web-frontend/test/unit/jest.config.js
index 072e15caa..e905d61fe 100644
--- a/web-frontend/test/unit/jest.config.js
+++ b/web-frontend/test/unit/jest.config.js
@@ -5,4 +5,5 @@ module.exports = Object.assign({}, baseConfig, {
   testMatch: ['<rootDir>/test/unit/**/*.spec.js'],
   displayName: 'unit',
   setupFilesAfterEnv: ['./test/unit/jest.setup.js'],
+  snapshotSerializers: ['jest-serializer-vue'],
 })
diff --git a/web-frontend/yarn.lock b/web-frontend/yarn.lock
index a6f371e41..72b251401 100644
--- a/web-frontend/yarn.lock
+++ b/web-frontend/yarn.lock
@@ -6974,6 +6974,13 @@ jest-runtime@^26.6.3:
     strip-bom "^4.0.0"
     yargs "^15.4.1"
 
+jest-serializer-vue@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/jest-serializer-vue/-/jest-serializer-vue-2.0.2.tgz#b238ef286357ec6b480421bd47145050987d59b3"
+  integrity sha1-sjjvKGNX7GtIBCG9RxRQUJh9WbM=
+  dependencies:
+    pretty "2.0.0"
+
 jest-serializer@^26.6.2:
   version "26.6.2"
   resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1"
@@ -9649,7 +9656,7 @@ pretty-time@^1.1.0:
   resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e"
   integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==
 
-pretty@^2.0.0:
+pretty@2.0.0, pretty@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/pretty/-/pretty-2.0.0.tgz#adbc7960b7bbfe289a557dc5f737619a220d06a5"
   integrity sha1-rbx5YLe7/iiaVX3F9zdhmiINBqU=