mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-10 23:50:12 +00:00
Count field
This commit is contained in:
parent
cd177703ea
commit
4751dc5dc6
25 changed files with 853 additions and 13 deletions
backend
src/baserow
config/settings
contrib/database
test_utils
tests/baserow/contrib/database
changelog/entries/unreleased/feature
premium/backend/tests/baserow_premium_tests/export
web-frontend
locales
modules/database
test/unit/database
|
@ -541,6 +541,7 @@ SPECTACULAR_SETTINGS = {
|
|||
"multiple_select",
|
||||
"phone_number",
|
||||
"formula",
|
||||
"count",
|
||||
"lookup",
|
||||
],
|
||||
"ViewFilterTypesEnum": [
|
||||
|
|
|
@ -12,6 +12,7 @@ from baserow.contrib.database.export_serialized import DatabaseExportSerializedS
|
|||
from baserow.contrib.database.fields.models import (
|
||||
NUMBER_MAX_DECIMAL_PLACES,
|
||||
BooleanField,
|
||||
CountField,
|
||||
CreatedOnField,
|
||||
DateField,
|
||||
EmailField,
|
||||
|
@ -377,3 +378,21 @@ class PhoneAirtableColumnType(AirtableColumnType):
|
|||
return value
|
||||
except ValidationError:
|
||||
return ""
|
||||
|
||||
|
||||
class CountAirtableColumnType(AirtableColumnType):
|
||||
type = "count"
|
||||
|
||||
def to_baserow_field(self, raw_airtable_table, raw_airtable_column):
|
||||
type_options = raw_airtable_column.get("typeOptions", {})
|
||||
return CountField(through_field_id=type_options.get("relationColumnId"))
|
||||
|
||||
def to_baserow_export_serialized_value(
|
||||
self,
|
||||
row_id_mapping,
|
||||
raw_airtable_column,
|
||||
baserow_field,
|
||||
value,
|
||||
files_to_download,
|
||||
):
|
||||
return None
|
||||
|
|
|
@ -98,6 +98,12 @@ ERROR_FIELD_CIRCULAR_REFERENCE = (
|
|||
HTTP_400_BAD_REQUEST,
|
||||
"Fields cannot reference each other resulting in a circular chain of references.",
|
||||
)
|
||||
ERROR_INVALID_COUNT_THROUGH_FIELD = (
|
||||
"ERROR_INVALID_COUNT_THROUGH_FIELD",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"The provided through field does not exist, is in a different table or is not a "
|
||||
"link row field.",
|
||||
)
|
||||
ERROR_INVALID_LOOKUP_THROUGH_FIELD = (
|
||||
"ERROR_INVALID_LOOKUP_THROUGH_FIELD",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
|
|
|
@ -187,6 +187,7 @@ class DatabaseConfig(AppConfig):
|
|||
|
||||
from .fields.field_types import (
|
||||
BooleanFieldType,
|
||||
CountFieldType,
|
||||
CreatedOnFieldType,
|
||||
DateFieldType,
|
||||
EmailFieldType,
|
||||
|
@ -222,6 +223,7 @@ class DatabaseConfig(AppConfig):
|
|||
field_type_registry.register(MultipleSelectFieldType())
|
||||
field_type_registry.register(PhoneNumberFieldType())
|
||||
field_type_registry.register(FormulaFieldType())
|
||||
field_type_registry.register(CountFieldType())
|
||||
field_type_registry.register(LookupFieldType())
|
||||
field_type_registry.register(MultipleCollaboratorsFieldType())
|
||||
|
||||
|
@ -436,6 +438,7 @@ class DatabaseConfig(AppConfig):
|
|||
|
||||
from .airtable.airtable_column_types import (
|
||||
CheckboxAirtableColumnType,
|
||||
CountAirtableColumnType,
|
||||
DateAirtableColumnType,
|
||||
ForeignKeyAirtableColumnType,
|
||||
FormulaAirtableColumnType,
|
||||
|
@ -463,6 +466,7 @@ class DatabaseConfig(AppConfig):
|
|||
airtable_column_type_registry.register(MultilineTextAirtableColumnType())
|
||||
airtable_column_type_registry.register(MultipleAttachmentAirtableColumnType())
|
||||
airtable_column_type_registry.register(RichTextTextAirtableColumnType())
|
||||
airtable_column_type_registry.register(CountAirtableColumnType())
|
||||
|
||||
from baserow.contrib.database.table.usage_types import (
|
||||
TableWorkspaceStorageUsageItemType,
|
||||
|
|
|
@ -196,6 +196,13 @@ class AllProvidedCollaboratorIdsMustBeValidUsers(ValidationError):
|
|||
)
|
||||
|
||||
|
||||
class InvalidCountThroughField(Exception):
|
||||
"""
|
||||
Raised when a count field is attempted to be created or updated with a through
|
||||
field that does not exist, is in a different table or is not a link row field.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidLookupThroughField(Exception):
|
||||
"""
|
||||
Raised when a a lookup field is attempted to be created or updated with a through
|
||||
|
|
|
@ -183,6 +183,12 @@ def construct_all_possible_field_kwargs(
|
|||
},
|
||||
{"name": "formula_link_url_only", "formula": "link('https://google.com')"},
|
||||
],
|
||||
"count": [
|
||||
{
|
||||
"name": "count",
|
||||
"through_field_name": "link_row",
|
||||
}
|
||||
],
|
||||
"lookup": [
|
||||
{
|
||||
"name": "lookup",
|
||||
|
|
|
@ -30,6 +30,7 @@ from rest_framework import serializers
|
|||
from baserow.contrib.database.api.fields.errors import (
|
||||
ERROR_DATE_FORCE_TIMEZONE_OFFSET_ERROR,
|
||||
ERROR_INCOMPATIBLE_PRIMARY_FIELD_TYPE,
|
||||
ERROR_INVALID_COUNT_THROUGH_FIELD,
|
||||
ERROR_INVALID_LOOKUP_TARGET_FIELD,
|
||||
ERROR_INVALID_LOOKUP_THROUGH_FIELD,
|
||||
ERROR_LINK_ROW_TABLE_NOT_IN_SAME_DATABASE,
|
||||
|
@ -87,6 +88,7 @@ from .exceptions import (
|
|||
DateForceTimezoneOffsetValueError,
|
||||
FieldDoesNotExist,
|
||||
IncompatiblePrimaryFieldTypeError,
|
||||
InvalidCountThroughField,
|
||||
InvalidLookupTargetField,
|
||||
InvalidLookupThroughField,
|
||||
LinkRowTableNotInSameDatabase,
|
||||
|
@ -110,6 +112,7 @@ from .handler import FieldHandler
|
|||
from .models import (
|
||||
AbstractSelectOption,
|
||||
BooleanField,
|
||||
CountField,
|
||||
CreatedOnField,
|
||||
DateField,
|
||||
EmailField,
|
||||
|
@ -3547,6 +3550,109 @@ class FormulaFieldType(ReadOnlyFieldType):
|
|||
return self.to_baserow_formula_type(field.specific).can_represent_date
|
||||
|
||||
|
||||
class CountFieldType(FormulaFieldType):
|
||||
type = "count"
|
||||
model_class = CountField
|
||||
api_exceptions_map = {
|
||||
**FormulaFieldType.api_exceptions_map,
|
||||
InvalidCountThroughField: ERROR_INVALID_COUNT_THROUGH_FIELD,
|
||||
}
|
||||
can_get_unique_values = False
|
||||
allowed_fields = BASEROW_FORMULA_TYPE_ALLOWED_FIELDS + [
|
||||
"through_field_id",
|
||||
]
|
||||
serializer_field_names = BASEROW_FORMULA_TYPE_ALLOWED_FIELDS + [
|
||||
"through_field_id",
|
||||
"formula_type",
|
||||
]
|
||||
serializer_field_overrides = {
|
||||
"through_field_id": serializers.IntegerField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
source="through_field.id",
|
||||
help_text="The id of the link row field to lookup values for. Will override"
|
||||
" the `through_field_name` parameter if both are provided, however only "
|
||||
"one is required.",
|
||||
),
|
||||
"nullable": serializers.BooleanField(required=False, read_only=True),
|
||||
}
|
||||
|
||||
def before_create(
|
||||
self, table, primary, allowed_field_values, order, user, field_kwargs
|
||||
):
|
||||
self._validate_through_field_values(
|
||||
table,
|
||||
allowed_field_values,
|
||||
field_kwargs,
|
||||
)
|
||||
|
||||
def get_fields_needing_periodic_update(self) -> Optional[QuerySet]:
|
||||
return None
|
||||
|
||||
def before_update(self, from_field, to_field_values, user, kwargs):
|
||||
if isinstance(from_field, CountField):
|
||||
through_field_id = (
|
||||
from_field.through_field.id
|
||||
if from_field.through_field is not None
|
||||
else None
|
||||
)
|
||||
self._validate_through_field_values(
|
||||
from_field.table,
|
||||
to_field_values,
|
||||
kwargs,
|
||||
through_field_id,
|
||||
)
|
||||
else:
|
||||
self._validate_through_field_values(
|
||||
from_field.table, to_field_values, kwargs
|
||||
)
|
||||
|
||||
def _validate_through_field_values(
|
||||
self,
|
||||
table,
|
||||
values,
|
||||
all_kwargs,
|
||||
default_through_field_id=None,
|
||||
):
|
||||
through_field_id = values.get("through_field_id", default_through_field_id)
|
||||
through_field_name = all_kwargs.get("through_field_name", None)
|
||||
|
||||
# If the `through_field_name` is provided in the kwargs when creating or
|
||||
# updating a field, then we want to find the `link_row` field by its name.
|
||||
if through_field_name is not None:
|
||||
try:
|
||||
through_field_id = table.field_set.get(name=through_field_name).id
|
||||
except Field.DoesNotExist:
|
||||
raise InvalidCountThroughField()
|
||||
|
||||
try:
|
||||
through_field = FieldHandler().get_field(through_field_id, LinkRowField)
|
||||
except FieldDoesNotExist:
|
||||
# Occurs when the through_field_id points at a non LinkRowField
|
||||
raise InvalidCountThroughField()
|
||||
|
||||
if through_field.table != table:
|
||||
raise InvalidCountThroughField()
|
||||
|
||||
values["through_field_id"] = through_field.id
|
||||
# There is never a need to allow decimal places on the count field.
|
||||
# Therefore, we reset it to 0 to make sure when a formula converts to count,
|
||||
# it will have the right value.
|
||||
values["number_decimal_places"] = 0
|
||||
|
||||
def import_serialized(
|
||||
self,
|
||||
table: "Table",
|
||||
serialized_values: Dict[str, Any],
|
||||
id_mapping: Dict[str, Any],
|
||||
) -> "Field":
|
||||
serialized_copy = serialized_values.copy()
|
||||
serialized_copy["through_field_id"] = id_mapping["database_fields"][
|
||||
serialized_values["through_field_id"]
|
||||
]
|
||||
return super().import_serialized(table, serialized_copy, id_mapping)
|
||||
|
||||
|
||||
class LookupFieldType(FormulaFieldType):
|
||||
type = "lookup"
|
||||
model_class = LookupField
|
||||
|
|
|
@ -521,6 +521,35 @@ class FormulaField(Field):
|
|||
)
|
||||
|
||||
|
||||
class CountField(FormulaField):
|
||||
through_field = models.ForeignKey(
|
||||
Field,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="count_fields_used_by",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from baserow.contrib.database.formula.ast.function_defs import BaserowCount
|
||||
from baserow.contrib.database.formula.ast.tree import BaserowFieldReference
|
||||
|
||||
field_reference = BaserowFieldReference(
|
||||
getattr(self.through_field, "name", ""), None, None
|
||||
)
|
||||
self.formula = f"{BaserowCount.type}({field_reference})"
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"CountField(\n"
|
||||
+ f"formula={self.formula},\n"
|
||||
+ f"through_field_id={self.through_field_id},\n"
|
||||
+ f"error={self.error},\n"
|
||||
+ ")"
|
||||
)
|
||||
|
||||
|
||||
class LookupField(FormulaField):
|
||||
through_field = models.ForeignKey(
|
||||
Field,
|
||||
|
|
|
@ -56,9 +56,11 @@ def fill_table_fields(limit, table):
|
|||
field_handler = FieldHandler()
|
||||
all_kwargs_per_type = construct_all_possible_field_kwargs(None, None, None, None)
|
||||
first_user = table.database.workspace.users.first()
|
||||
# Keep all fields but link_row and lookup
|
||||
# Keep all fields but link_row, count and lookup
|
||||
allowed_field_list = [
|
||||
f for f in all_kwargs_per_type.items() if f[0] not in ["link_row", "lookup"]
|
||||
f
|
||||
for f in all_kwargs_per_type.items()
|
||||
if f[0] not in ["link_row", "count", "lookup"]
|
||||
]
|
||||
for _ in range(limit):
|
||||
# This is a helper cli command, randomness is not being used for any security
|
||||
|
@ -85,7 +87,7 @@ def create_field_for_every_type(table):
|
|||
first_user = table.database.workspace.users.first()
|
||||
i = 0
|
||||
for field_type_name, all_possible_kwargs in all_kwargs_per_type.items():
|
||||
if field_type_name in ["link_row", "lookup"]:
|
||||
if field_type_name in ["link_row", "count", "lookup"]:
|
||||
continue
|
||||
for kwargs in all_possible_kwargs:
|
||||
kwargs.pop("primary", None)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 3.2.18 on 2023-05-25 12:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("database", "0114_alter_airtableimportjob_airtable_share_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CountField",
|
||||
fields=[
|
||||
(
|
||||
"formulafield_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="database.formulafield",
|
||||
),
|
||||
),
|
||||
(
|
||||
"through_field",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="count_fields_used_by",
|
||||
to="database.field",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("database.formulafield",),
|
||||
),
|
||||
]
|
|
@ -225,7 +225,9 @@ def setup_interesting_test_table(
|
|||
sha256_hash="name",
|
||||
)
|
||||
|
||||
missing_fields = set(name_to_field_id.keys()) - set(values.keys()) - {"lookup"}
|
||||
missing_fields = (
|
||||
set(name_to_field_id.keys()) - set(values.keys()) - {"lookup", "count"}
|
||||
)
|
||||
assert missing_fields == set(), (
|
||||
"Please update the dictionary above with interesting test values for your new "
|
||||
f"field type. In the values dict you are missing the fields {missing_fields}."
|
||||
|
|
|
@ -3,6 +3,7 @@ import responses
|
|||
|
||||
from baserow.contrib.database.airtable.airtable_column_types import (
|
||||
CheckboxAirtableColumnType,
|
||||
CountAirtableColumnType,
|
||||
DateAirtableColumnType,
|
||||
ForeignKeyAirtableColumnType,
|
||||
FormulaAirtableColumnType,
|
||||
|
@ -19,6 +20,7 @@ from baserow.contrib.database.airtable.airtable_column_types import (
|
|||
from baserow.contrib.database.airtable.registry import airtable_column_type_registry
|
||||
from baserow.contrib.database.fields.models import (
|
||||
BooleanField,
|
||||
CountField,
|
||||
CreatedOnField,
|
||||
DateField,
|
||||
EmailField,
|
||||
|
@ -1090,3 +1092,35 @@ def test_airtable_import_url_column(data_fixture, api_client):
|
|||
)
|
||||
== "https://test.nl"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@responses.activate
|
||||
def test_airtable_import_count_column(data_fixture, api_client):
|
||||
airtable_field = {
|
||||
"id": "fldG9y88Zw7q7u4Z7i4",
|
||||
"name": "Count",
|
||||
"type": "count",
|
||||
"typeOptions": {
|
||||
"relationColumnId": "fldABC88Zw7q7u4Z7i4",
|
||||
"dependencies": [],
|
||||
"resultType": "number",
|
||||
"resultIsArray": False,
|
||||
},
|
||||
}
|
||||
(
|
||||
baserow_field,
|
||||
airtable_column_type,
|
||||
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||
{}, airtable_field
|
||||
)
|
||||
assert isinstance(baserow_field, CountField)
|
||||
assert isinstance(airtable_column_type, CountAirtableColumnType)
|
||||
|
||||
assert baserow_field.through_field_id == "fldABC88Zw7q7u4Z7i4"
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
{}, airtable_field, baserow_field, "1", {}
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
|
|
@ -207,7 +207,7 @@ def test_to_baserow_database_export():
|
|||
assert baserow_database_export["tables"][1]["id"] == "tbl7glLIGtH8C8zGCzb"
|
||||
assert baserow_database_export["tables"][1]["name"] == "Data"
|
||||
assert baserow_database_export["tables"][1]["order"] == 1
|
||||
assert len(baserow_database_export["tables"][1]["fields"]) == 24
|
||||
assert len(baserow_database_export["tables"][1]["fields"]) == 25
|
||||
|
||||
# We don't have to check all the fields and rows, just a single one, because we have
|
||||
# separate tests for mapping the Airtable fields and values to Baserow.
|
||||
|
|
|
@ -334,6 +334,7 @@ def test_get_row_serializer_with_user_field_names(data_fixture):
|
|||
"value": "A",
|
||||
},
|
||||
"formula_text": "test FORMULA",
|
||||
"count": "3",
|
||||
"lookup": [
|
||||
{"id": 1, "value": "linked_row_1"},
|
||||
{"id": 2, "value": "linked_row_2"},
|
||||
|
|
|
@ -230,11 +230,11 @@ def test_can_export_every_interesting_different_field_to_csv(
|
|||
"file_link_row,file,single_select,multiple_select,multiple_collaborators,"
|
||||
"phone_number,formula_text,formula_int,formula_bool,formula_decimal,formula_dateinterval,"
|
||||
"formula_date,formula_singleselect,formula_email,formula_link_with_label,"
|
||||
"formula_link_url_only,lookup\r\n"
|
||||
"formula_link_url_only,count,lookup\r\n"
|
||||
"1,,,,,,,,,0,False,,,,,,,01/02/2021 12:00,01/02/2021,02/01/2021 12:00,02/01/2021,"
|
||||
"02/01/2021 13:00,01/02/2021 12:00,01/02/2021,02/01/2021 12:00,02/01/2021,"
|
||||
"02/01/2021 13:00,,,,,,,,,,,test FORMULA,1,True,33.3333333333,1 day,"
|
||||
"2020-01-01,,,label (https://google.com),https://google.com,\r\n"
|
||||
"2020-01-01,,,label (https://google.com),https://google.com,0,\r\n"
|
||||
"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/2020 02:23,"
|
||||
"01/02/2020 02:23,01/02/2021 12:00,01/02/2021,02/01/2021 12:00,02/01/2021,"
|
||||
|
@ -246,7 +246,7 @@ def test_can_export_every_interesting_different_field_to_csv(
|
|||
'b.txt (http://localhost:8000/media/user_files/other_name.txt)",A,"D,C,E",'
|
||||
'"user2@example.com,user3@example.com",+4412345678,test FORMULA,1,True,33.3333333333,'
|
||||
"1 day,2020-01-01,A,test@example.com,label (https://google.com),https://google.com,"
|
||||
'"linked_row_1,linked_row_2,"\r\n'
|
||||
'3,"linked_row_1,linked_row_2,"\r\n'
|
||||
)
|
||||
|
||||
assert contents == expected
|
||||
|
|
|
@ -0,0 +1,427 @@
|
|||
from io import BytesIO
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST
|
||||
|
||||
from baserow.contrib.database.fields.exceptions import InvalidCountThroughField
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.formula import BaserowFormulaNumberType
|
||||
from baserow.contrib.database.rows.handler import RowHandler
|
||||
from baserow.core.handler import CoreHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_count_through_field_with_invalid_linkrowfield(
|
||||
data_fixture, api_client, django_assert_num_queries
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table, primary=True)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table2, primary=True)
|
||||
linkrowfield = FieldHandler().create_field(
|
||||
user,
|
||||
table2,
|
||||
"link_row",
|
||||
name="linkrowfield",
|
||||
link_row_table=table2,
|
||||
)
|
||||
with pytest.raises(InvalidCountThroughField):
|
||||
FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"count",
|
||||
name="count_field",
|
||||
through_field_id=linkrowfield.id,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_count_through_field_with_invalid_linkrowfield_via_api(
|
||||
data_fixture, api_client, django_assert_num_queries
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table, primary=True)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table2, primary=True)
|
||||
linkrowfield = FieldHandler().create_field(
|
||||
user,
|
||||
table2,
|
||||
"link_row",
|
||||
name="linkrowfield",
|
||||
link_row_table=table2,
|
||||
)
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:fields:list", kwargs={"table_id": table.id}),
|
||||
{"name": "Test 1", "type": "count", "link_row_table_id": linkrowfield.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_INVALID_COUNT_THROUGH_FIELD"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_count_through_field_name(
|
||||
data_fixture, api_client, django_assert_num_queries
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table, primary=True)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table2, primary=True)
|
||||
linkrowfield = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"link_row",
|
||||
name="linkrowfield",
|
||||
link_row_table=table2,
|
||||
)
|
||||
field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"count",
|
||||
name="count_field",
|
||||
through_field_name=linkrowfield.name,
|
||||
)
|
||||
assert field.through_field_id == linkrowfield.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_count_through_field_name(
|
||||
data_fixture, api_client, django_assert_num_queries
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table, primary=True)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table2, primary=True)
|
||||
linkrowfield_1 = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"link_row",
|
||||
name="linkrowfield_1",
|
||||
link_row_table=table2,
|
||||
)
|
||||
linkrowfield_2 = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"link_row",
|
||||
name="linkrowfield_2",
|
||||
link_row_table=table2,
|
||||
)
|
||||
field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"count",
|
||||
name="count_field",
|
||||
through_field_name=linkrowfield_1.name,
|
||||
)
|
||||
field = FieldHandler().update_field(
|
||||
user,
|
||||
field,
|
||||
through_field_name=linkrowfield_2.name,
|
||||
)
|
||||
assert field.through_field_id == linkrowfield_2.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_update_count_field_value(
|
||||
data_fixture, api_client, django_assert_num_queries
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
table_primary_field = data_fixture.create_text_field(
|
||||
name="p", table=table, primary=True
|
||||
)
|
||||
data_fixture.create_text_field(name="primaryfield", table=table2, primary=True)
|
||||
|
||||
linkrowfield = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"link_row",
|
||||
name="linkrowfield",
|
||||
link_row_table=table2,
|
||||
)
|
||||
|
||||
table2_model = table2.get_model(attribute_names=True)
|
||||
a = table2_model.objects.create(primaryfield="primary a")
|
||||
b = table2_model.objects.create(primaryfield="primary b")
|
||||
|
||||
table_model = table.get_model(attribute_names=True)
|
||||
|
||||
table_row = table_model.objects.create()
|
||||
table_row.linkrowfield.add(a.id)
|
||||
table_row.linkrowfield.add(b.id)
|
||||
table_row.save()
|
||||
|
||||
count_field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"count",
|
||||
name="count_field",
|
||||
through_field_id=linkrowfield.id,
|
||||
)
|
||||
response = api_client.get(
|
||||
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.json() == {
|
||||
"count": 1,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [
|
||||
{
|
||||
f"field_{table_primary_field.id}": None,
|
||||
f"field_{linkrowfield.id}": [
|
||||
{"id": a.id, "value": "primary a"},
|
||||
{"id": b.id, "value": "primary b"},
|
||||
],
|
||||
f"field_{count_field.id}": "2",
|
||||
"id": table_row.id,
|
||||
"order": "1.00000000000000000000",
|
||||
}
|
||||
],
|
||||
}
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_batch_create_count_field_value(
|
||||
data_fixture, api_client, django_assert_num_queries
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
table_primary_field = data_fixture.create_text_field(
|
||||
name="tableprimary", table=table, primary=True
|
||||
)
|
||||
data_fixture.create_text_field(name="table2primary", table=table2, primary=True)
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"link_row",
|
||||
name="linkrowfield",
|
||||
link_row_table=table2,
|
||||
)
|
||||
count_field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"count",
|
||||
name="count_field",
|
||||
through_field_id=link_row_field.id,
|
||||
)
|
||||
|
||||
table2_model = table2.get_model(attribute_names=True)
|
||||
table2_row_1 = table2_model.objects.create(table2primary="row A")
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:database:rows:batch",
|
||||
kwargs={"table_id": table.id},
|
||||
),
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [table2_row_1.id],
|
||||
}
|
||||
]
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{count_field.id}": "1",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_batch_update_count_field_value(
|
||||
data_fixture, api_client, django_assert_num_queries
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
table_primary_field = data_fixture.create_text_field(
|
||||
name="tableprimary", table=table, primary=True
|
||||
)
|
||||
data_fixture.create_text_field(name="table2primary", table=table2, primary=True)
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"link_row",
|
||||
name="linkrowfield",
|
||||
link_row_table=table2,
|
||||
)
|
||||
count_field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"count",
|
||||
name="count_field",
|
||||
through_field_id=link_row_field.id,
|
||||
)
|
||||
|
||||
table1_model = table.get_model(attribute_names=True)
|
||||
table1_row_1 = table1_model.objects.create(tableprimary="row 1")
|
||||
|
||||
table2_model = table2.get_model(attribute_names=True)
|
||||
table2_row_1 = table2_model.objects.create(table2primary="row A")
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:rows:batch",
|
||||
kwargs={"table_id": table.id},
|
||||
),
|
||||
{
|
||||
"items": [
|
||||
{"id": table1_row_1.id, f"field_{link_row_field.id}": [table2_row_1.id]}
|
||||
]
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"order": "1.00000000000000000000",
|
||||
f"field_{table_primary_field.id}": "row 1",
|
||||
f"field_{link_row_field.id}": [{"id": 1, "value": "row A"}],
|
||||
f"field_{count_field.id}": "1",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_import_export_tables_with_count_fields(
|
||||
data_fixture, django_assert_num_queries
|
||||
):
|
||||
user = data_fixture.create_user()
|
||||
imported_workspace = data_fixture.create_workspace(user=user)
|
||||
database = data_fixture.create_database_application(user=user, name="Placeholder")
|
||||
table = data_fixture.create_database_table(
|
||||
name="Example", database=database, order=0
|
||||
)
|
||||
customers_table = data_fixture.create_database_table(
|
||||
name="Customers", database=database, order=1
|
||||
)
|
||||
customer_name = data_fixture.create_text_field(table=customers_table, primary=True)
|
||||
customer_age = data_fixture.create_number_field(table=customers_table)
|
||||
field_handler = FieldHandler()
|
||||
core_handler = CoreHandler()
|
||||
link_row_field = field_handler.create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
name="Link Row",
|
||||
type_name="link_row",
|
||||
link_row_table=customers_table,
|
||||
)
|
||||
|
||||
row_handler = RowHandler()
|
||||
c_row = row_handler.create_row(
|
||||
user=user,
|
||||
table=customers_table,
|
||||
values={
|
||||
f"field_{customer_name.id}": "mary",
|
||||
f"field_{customer_age.id}": 65,
|
||||
},
|
||||
)
|
||||
c_row_2 = row_handler.create_row(
|
||||
user=user,
|
||||
table=customers_table,
|
||||
values={
|
||||
f"field_{customer_name.id}": "bob",
|
||||
f"field_{customer_age.id}": 67,
|
||||
},
|
||||
)
|
||||
row = row_handler.create_row(
|
||||
user=user,
|
||||
table=table,
|
||||
values={f"field_{link_row_field.id}": [c_row.id, c_row_2.id]},
|
||||
)
|
||||
|
||||
count_field = field_handler.create_field(
|
||||
user=user,
|
||||
table=table,
|
||||
name="count",
|
||||
type_name="count",
|
||||
through_field_id=link_row_field.id,
|
||||
)
|
||||
|
||||
exported_applications = core_handler.export_workspace_applications(
|
||||
database.workspace, BytesIO()
|
||||
)
|
||||
imported_applications, id_mapping = core_handler.import_applications_to_workspace(
|
||||
imported_workspace, exported_applications, BytesIO(), None
|
||||
)
|
||||
imported_database = imported_applications[0]
|
||||
imported_tables = imported_database.table_set.all()
|
||||
imported_table = imported_tables[0]
|
||||
assert imported_table.name == table.name
|
||||
|
||||
imported_count_field = imported_table.field_set.get(name=count_field.name).specific
|
||||
imported_through_field = imported_table.field_set.get(
|
||||
name=link_row_field.name
|
||||
).specific
|
||||
assert imported_count_field.formula == count_field.formula
|
||||
assert imported_count_field.formula_type == BaserowFormulaNumberType.type
|
||||
assert imported_count_field.through_field.name == link_row_field.name
|
||||
assert imported_count_field.through_field_id == imported_through_field.id
|
||||
|
||||
imported_table_model = imported_table.get_model(attribute_names=True)
|
||||
imported_rows = imported_table_model.objects.all()
|
||||
assert imported_rows.count() == 1
|
||||
imported_row = imported_rows.first()
|
||||
assert imported_row.id == row.id
|
||||
assert imported_row.count == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_convert_count_to_text_field_via_api(data_fixture, api_client):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
table2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||
data_fixture.create_text_field(name="tableprimary", table=table, primary=True)
|
||||
data_fixture.create_text_field(name="tableprimary", table=table2, primary=True)
|
||||
link_row_field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"link_row",
|
||||
name="linkrowfield",
|
||||
link_row_table=table2,
|
||||
)
|
||||
count_field = FieldHandler().create_field(
|
||||
user,
|
||||
table,
|
||||
"count",
|
||||
name="count_field",
|
||||
through_field_id=link_row_field.id,
|
||||
)
|
||||
# text_field = FieldHandler().update_field(user, count_field, 'text')
|
||||
|
||||
response = api_client.patch(
|
||||
reverse("api:database:fields:item", kwargs={"field_id": count_field.id}),
|
||||
{"name": "Test 1", "type": "text"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
|
@ -183,7 +183,7 @@ def test_view_unique_count_aggregation_for_interesting_table(data_fixture):
|
|||
user, grid_view, aggregation_query, model=model, with_total=True
|
||||
)
|
||||
|
||||
assert len(result.keys()) == 33
|
||||
assert len(result.keys()) == 34
|
||||
|
||||
for field_obj in model._field_objects.values():
|
||||
field = field_obj["field"]
|
||||
|
@ -198,6 +198,7 @@ def test_view_unique_count_aggregation_for_interesting_table(data_fixture):
|
|||
"email",
|
||||
"rating",
|
||||
"phone_number",
|
||||
"count",
|
||||
]
|
||||
or field_type == "formula"
|
||||
and field.formula_type == "char"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Introduced count field",
|
||||
"issue_number": 224,
|
||||
"bullet_points": [],
|
||||
"created_at": "2023-05-25"
|
||||
}
|
|
@ -89,6 +89,7 @@ def test_can_export_every_interesting_different_field_to_json(
|
|||
"formula_link_url_only": {
|
||||
"url": "https://google.com"
|
||||
},
|
||||
"count": 0,
|
||||
"lookup": []
|
||||
},
|
||||
{
|
||||
|
@ -181,6 +182,7 @@ def test_can_export_every_interesting_different_field_to_json(
|
|||
"formula_link_url_only": {
|
||||
"url": "https://google.com"
|
||||
},
|
||||
"count": 3,
|
||||
"lookup": [
|
||||
"linked_row_1",
|
||||
"linked_row_2",
|
||||
|
@ -318,6 +320,7 @@ def test_can_export_every_interesting_different_field_to_xml(
|
|||
<formula-link-url-only>
|
||||
<url>https://google.com</url>
|
||||
</formula-link-url-only>
|
||||
<count>0</count>
|
||||
<lookup/>
|
||||
</row>
|
||||
<row>
|
||||
|
@ -410,6 +413,7 @@ def test_can_export_every_interesting_different_field_to_xml(
|
|||
<formula-link-url-only>
|
||||
<url>https://google.com</url>
|
||||
</formula-link-url-only>
|
||||
<count>3</count>
|
||||
<lookup>
|
||||
<item>linked_row_1</item>
|
||||
<item>linked_row_2</item>
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
"multipleSelect": "Multiple select",
|
||||
"phoneNumber": "Phone number",
|
||||
"formula": "Formula",
|
||||
"count": "Count",
|
||||
"lookup": "Lookup",
|
||||
"multipleCollaborators": "Collaborators"
|
||||
},
|
||||
|
@ -127,7 +128,8 @@
|
|||
"multipleSelect": "Accepts an array of mixed integers or text values each representing the chosen select option id or value. In case of a text value, the first matching option is selected.",
|
||||
"phoneNumber": "Accepts a phone number which has a maximum length of 100 characters consisting solely of digits, spaces and the following characters: Nx,._+*()#=;/- .",
|
||||
"formula": "A read-only field defined by a formula written in the Baserow formula language.",
|
||||
"lookup": "A read-only field connected to a link row field which returns an array of values and row ids from the chosen lookup field in the linked table.",
|
||||
"count": "A read-only field connected to a link to table field which returns the number of relations.",
|
||||
"lookup": "A read-only field connected to a link to table field which returns an array of values and row ids from the chosen lookup field in the linked table.",
|
||||
"multipleCollaborators": "Accepts an array of objects where each object contains a user's id."
|
||||
},
|
||||
"viewFilter": {
|
||||
|
@ -334,7 +336,7 @@
|
|||
"datetimeFormatTzDescription": "Converts the date to text given a way of formatting the date in the specified timezone.",
|
||||
"toNumberDescription": "Converts the input to a number if possible.",
|
||||
"fieldDescription": "Returns the field named by the single text argument.",
|
||||
"lookupDescription": "Looks up the values from a field in another table for rows in a link row field. The first argument should be the name of a link row field in the current table and the second should be the name of a field in the linked table.",
|
||||
"lookupDescription": "Looks up the values from a field in another table for rows in a link to table field. The first argument should be the name of a link to table field in the current table and the second should be the name of a field in the linked table.",
|
||||
"isBlankDescription": "Returns true if the argument is null, empty or blank, false otherwise.",
|
||||
"isNullDescription": "Returns true if the argument is null, false otherwise.",
|
||||
"tDescription": "Returns the arguments value if it is text, but otherwise ''.",
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="control">
|
||||
<Alert v-if="linkRowFieldsInThisTable.length === 0" minimal type="error">
|
||||
{{ $t('fieldCountSubForm.noTable') }}
|
||||
</Alert>
|
||||
<div v-if="linkRowFieldsInThisTable.length > 0">
|
||||
<label class="control__label control__label--small">
|
||||
{{ $t('fieldCountSubForm.selectThroughFieldLabel') }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<div class="control">
|
||||
<Dropdown
|
||||
v-model="values.through_field_id"
|
||||
:class="{ 'dropdown--error': $v.values.through_field_id.$error }"
|
||||
@hide="$v.values.through_field_id.$touch()"
|
||||
>
|
||||
<DropdownItem
|
||||
v-for="field in linkRowFieldsInThisTable"
|
||||
:key="field.id"
|
||||
:disabled="field.disabled"
|
||||
:name="field.name"
|
||||
:value="field.id"
|
||||
:icon="field.icon"
|
||||
></DropdownItem>
|
||||
</Dropdown>
|
||||
<div v-if="$v.values.through_field_id.$error" class="error">
|
||||
{{ $t('error.requiredField') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
|
||||
|
||||
export default {
|
||||
name: 'FieldCountSubForm',
|
||||
mixins: [form, fieldSubForm],
|
||||
data() {
|
||||
return {
|
||||
allowedValues: ['through_field_id'],
|
||||
values: {
|
||||
through_field_id: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
linkRowFieldsInThisTable() {
|
||||
const fields = this.$store.getters['field/getAll']
|
||||
return fields
|
||||
.filter((f) => f.type === 'link_row')
|
||||
.map((f) => {
|
||||
const fieldType = this.$registry.get('field', f.type)
|
||||
f.icon = fieldType.getIconClass()
|
||||
f.disabled = !this.tableIdsAccessible.includes(f.link_row_table_id)
|
||||
return f
|
||||
})
|
||||
},
|
||||
allTables() {
|
||||
const databaseType = DatabaseApplicationType.getType()
|
||||
return this.$store.getters['application/getAll'].reduce(
|
||||
(tables, application) => {
|
||||
if (application.type === databaseType) {
|
||||
return tables.concat(application.tables || [])
|
||||
}
|
||||
return tables
|
||||
},
|
||||
[]
|
||||
)
|
||||
},
|
||||
tableIdsAccessible() {
|
||||
return this.allTables.map((table) => table.id)
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
values: {
|
||||
through_field_id: { required },
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isValid() {
|
||||
return (
|
||||
form.methods.isValid().call(this) &&
|
||||
this.linkRowFieldsInThisTable.length > 0
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -95,6 +95,7 @@ import {
|
|||
import GridViewFieldFormula from '@baserow/modules/database/components/view/grid/fields/GridViewFieldFormula'
|
||||
import FieldFormulaSubForm from '@baserow/modules/database/components/field/FieldFormulaSubForm'
|
||||
import FieldLookupSubForm from '@baserow/modules/database/components/field/FieldLookupSubForm'
|
||||
import FieldCountSubForm from '@baserow/modules/database/components/field/FieldCountSubForm'
|
||||
import RowEditFieldFormula from '@baserow/modules/database/components/row/RowEditFieldFormula'
|
||||
import ViewService from '@baserow/modules/database/services/view'
|
||||
import FormService from '@baserow/modules/database/services/view/form'
|
||||
|
@ -2720,6 +2721,33 @@ export class FormulaFieldType extends FieldType {
|
|||
}
|
||||
}
|
||||
|
||||
export class CountFieldType extends FormulaFieldType {
|
||||
static getType() {
|
||||
return 'count'
|
||||
}
|
||||
|
||||
getIconClass() {
|
||||
return 'calculator'
|
||||
}
|
||||
|
||||
getName() {
|
||||
const { i18n } = this.app
|
||||
return i18n.t('fieldType.count')
|
||||
}
|
||||
|
||||
getDocsDescription(field) {
|
||||
return this.app.i18n.t('fieldDocs.count')
|
||||
}
|
||||
|
||||
getFormComponent() {
|
||||
return FieldCountSubForm
|
||||
}
|
||||
|
||||
shouldFetchFieldSelectOptions() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export class LookupFieldType extends FormulaFieldType {
|
||||
static getType() {
|
||||
return 'lookup'
|
||||
|
|
|
@ -271,9 +271,13 @@
|
|||
"nameNotAllowed": "This field name is not allowed.",
|
||||
"nameTooLong": "This field name is too long."
|
||||
},
|
||||
"fieldCountSubForm": {
|
||||
"noTable": "You need at least one link to table field to create a count field.",
|
||||
"selectThroughFieldLabel": "Select a link to table field"
|
||||
},
|
||||
"fieldLookupSubForm": {
|
||||
"noTable": "You need at least one link row field to create a lookup field.",
|
||||
"selectThroughFieldLabel": "Select a link row field",
|
||||
"noTable": "You need at least one link to table field to create a lookup field.",
|
||||
"selectThroughFieldLabel": "Select a link to table field",
|
||||
"selectTargetFieldLabel": "Select a field to lookup"
|
||||
},
|
||||
"fieldFormulaNumberSubForm": {
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
PhoneNumberFieldType,
|
||||
CreatedOnFieldType,
|
||||
FormulaFieldType,
|
||||
CountFieldType,
|
||||
LookupFieldType,
|
||||
MultipleCollaboratorsFieldType,
|
||||
} from '@baserow/modules/database/fieldTypes'
|
||||
|
@ -395,6 +396,7 @@ export default (context) => {
|
|||
app.$registry.register('field', new MultipleSelectFieldType(context))
|
||||
app.$registry.register('field', new PhoneNumberFieldType(context))
|
||||
app.$registry.register('field', new FormulaFieldType(context))
|
||||
app.$registry.register('field', new CountFieldType(context))
|
||||
app.$registry.register('field', new LookupFieldType(context))
|
||||
app.$registry.register('field', new MultipleCollaboratorsFieldType(context))
|
||||
|
||||
|
|
|
@ -174,6 +174,14 @@ const mockedFields = {
|
|||
table_id: 42,
|
||||
type: 'multiple_collaborators',
|
||||
},
|
||||
count: {
|
||||
id: 18,
|
||||
name: 'count',
|
||||
order: 18,
|
||||
primary: false,
|
||||
table_id: 42,
|
||||
type: 'count',
|
||||
},
|
||||
}
|
||||
|
||||
const valuesToCall = [null, undefined]
|
||||
|
|
Loading…
Add table
Reference in a new issue