mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-17 02:17:49 +00:00
Zapier actions and triggers integration
This commit is contained in:
parent
402084a0cd
commit
fc340ae8a2
39 changed files with 9993 additions and 45 deletions
.gitlab-ci.ymlchangelog.md
backend
src/baserow/contrib/database
tests/baserow/contrib/database
api
fields
tokens
views
table
view
ws/public
integrations/zapier
.gitignoreREADME.mdauthentication.jsindex.jspackage.json
src
test
yarn.lockpremium/backend/tests/baserow_premium_tests/api/views/views
web-frontend/modules
|
@ -521,6 +521,17 @@ web-frontend-test:
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
coverage: '/Lines\s*:\s*(\d+.?\d*)%/'
|
coverage: '/Lines\s*:\s*(\d+.?\d*)%/'
|
||||||
|
|
||||||
|
zapier-integration-test:
|
||||||
|
extends:
|
||||||
|
- .docker-image-test-stage
|
||||||
|
- .skippable-job
|
||||||
|
variables:
|
||||||
|
RUN_WHEN_CHANGES_MADE_IN: "integrations/zapier"
|
||||||
|
script:
|
||||||
|
- cd integrations/zapier
|
||||||
|
- yarn install
|
||||||
|
- yarn run zapier test
|
||||||
|
|
||||||
# If pipeline not triggered by tag:
|
# If pipeline not triggered by tag:
|
||||||
# - Build and store non-dev images in CI repo under the `ci-tested` tag so we know
|
# - Build and store non-dev images in CI repo under the `ci-tested` tag so we know
|
||||||
# those images have passed the tests.
|
# those images have passed the tests.
|
||||||
|
|
|
@ -13,10 +13,14 @@ from baserow.contrib.database.fields.registries import field_type_registry
|
||||||
|
|
||||||
class FieldSerializer(serializers.ModelSerializer):
|
class FieldSerializer(serializers.ModelSerializer):
|
||||||
type = serializers.SerializerMethodField(help_text="The type of the related field.")
|
type = serializers.SerializerMethodField(help_text="The type of the related field.")
|
||||||
|
read_only = serializers.SerializerMethodField(
|
||||||
|
help_text="Indicates whether the field is a read only field. If true, "
|
||||||
|
"it's not possible to update the cell value."
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Field
|
model = Field
|
||||||
fields = ("id", "table_id", "name", "order", "type", "primary")
|
fields = ("id", "table_id", "name", "order", "type", "primary", "read_only")
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"id": {"read_only": True},
|
"id": {"read_only": True},
|
||||||
"table_id": {"read_only": True},
|
"table_id": {"read_only": True},
|
||||||
|
@ -26,6 +30,10 @@ class FieldSerializer(serializers.ModelSerializer):
|
||||||
def get_type(self, instance):
|
def get_type(self, instance):
|
||||||
return field_type_registry.get_by_model(instance.specific_class).type
|
return field_type_registry.get_by_model(instance.specific_class).type
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.BOOL)
|
||||||
|
def get_read_only(self, instance):
|
||||||
|
return field_type_registry.get_by_model(instance.specific_class).read_only
|
||||||
|
|
||||||
|
|
||||||
class RelatedFieldsSerializer(serializers.Serializer):
|
class RelatedFieldsSerializer(serializers.Serializer):
|
||||||
related_fields = serializers.SerializerMethodField(
|
related_fields = serializers.SerializerMethodField(
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from .views import TokensView, TokenView
|
from .views import TokenCheckView, TokensView, TokenView
|
||||||
|
|
||||||
app_name = "baserow.contrib.database.api.tokens"
|
app_name = "baserow.contrib.database.api.tokens"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
re_path(r"check/$", TokenCheckView.as_view(), name="check"),
|
||||||
re_path(r"(?P<token_id>[0-9]+)/$", TokenView.as_view(), name="item"),
|
re_path(r"(?P<token_id>[0-9]+)/$", TokenView.as_view(), name="item"),
|
||||||
re_path(r"$", TokensView.as_view(), name="list"),
|
re_path(r"$", TokensView.as_view(), name="list"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,6 +21,7 @@ from baserow.contrib.database.tokens.models import Token
|
||||||
from baserow.core.exceptions import UserNotInGroup
|
from baserow.core.exceptions import UserNotInGroup
|
||||||
from baserow.core.handler import CoreHandler
|
from baserow.core.handler import CoreHandler
|
||||||
|
|
||||||
|
from .authentications import TokenAuthentication
|
||||||
from .errors import ERROR_TOKEN_DOES_NOT_EXIST
|
from .errors import ERROR_TOKEN_DOES_NOT_EXIST
|
||||||
from .serializers import TokenCreateSerializer, TokenSerializer, TokenUpdateSerializer
|
from .serializers import TokenCreateSerializer, TokenSerializer, TokenUpdateSerializer
|
||||||
|
|
||||||
|
@ -210,3 +211,24 @@ class TokenView(APIView):
|
||||||
token = TokenHandler().get_token(request.user, token_id)
|
token = TokenHandler().get_token(request.user, token_id)
|
||||||
TokenHandler().delete_token(request.user, token)
|
TokenHandler().delete_token(request.user, token)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenCheckView(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication,)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Database tokens"],
|
||||||
|
operation_id="check_database_token",
|
||||||
|
description=(
|
||||||
|
"This endpoint check be used to check if the provided personal API token "
|
||||||
|
"is valid. If returns a `200` response if so and a `403` is not. This can "
|
||||||
|
"be used by integrations like Zapier or n8n to test if a token is valid."
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
200: None,
|
||||||
|
403: get_error_schema(["ERROR_TOKEN_DOES_NOT_EXIST"]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
return Response({"token": "OK"})
|
||||||
|
|
|
@ -17,6 +17,7 @@ from baserow.contrib.database.fields.field_filters import (
|
||||||
FilterBuilder,
|
FilterBuilder,
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.fields.field_sortings import AnnotatedOrder
|
from baserow.contrib.database.fields.field_sortings import AnnotatedOrder
|
||||||
|
from baserow.contrib.database.fields.models import CreatedOnField, LastModifiedField
|
||||||
from baserow.contrib.database.fields.registries import field_type_registry
|
from baserow.contrib.database.fields.registries import field_type_registry
|
||||||
from baserow.contrib.database.table.cache import (
|
from baserow.contrib.database.table.cache import (
|
||||||
get_cached_model_field_attrs,
|
get_cached_model_field_attrs,
|
||||||
|
@ -34,7 +35,9 @@ from baserow.core.mixins import (
|
||||||
)
|
)
|
||||||
from baserow.core.utils import split_comma_separated_string
|
from baserow.core.utils import split_comma_separated_string
|
||||||
|
|
||||||
deconstruct_filter_key_regex = re.compile(r"filter__field_([0-9]+)__([a-zA-Z0-9_]*)$")
|
deconstruct_filter_key_regex = re.compile(
|
||||||
|
r"filter__field_([0-9]+|created_on|updated_on)__([a-zA-Z0-9_]*)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TableModelQuerySet(models.QuerySet):
|
class TableModelQuerySet(models.QuerySet):
|
||||||
|
@ -242,6 +245,12 @@ class TableModelQuerySet(models.QuerySet):
|
||||||
'filter__field_{id}__{view_filter_type}': {value}.
|
'filter__field_{id}__{view_filter_type}': {value}.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
In addition to that, it's also possible to directly filter on the
|
||||||
|
`created_on` and `updated_on` fields, even if the CreatedOn and LastModified
|
||||||
|
fields are not created. This can be done by providing
|
||||||
|
`filter__field_created_on__{view_filter_type}` or
|
||||||
|
`filter__field_updated_on__{view_filter_type}` as keys in the `filter_object`.
|
||||||
|
|
||||||
:param filter_object: The object containing the field and filter type as key
|
:param filter_object: The object containing the field and filter type as key
|
||||||
and the filter value as value.
|
and the filter value as value.
|
||||||
:type filter_object: object
|
:type filter_object: object
|
||||||
|
@ -273,21 +282,32 @@ class TableModelQuerySet(models.QuerySet):
|
||||||
if not matches:
|
if not matches:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
field_id = int(matches[1])
|
fixed_field_instance_mapping = {
|
||||||
|
"created_on": CreatedOnField(),
|
||||||
|
"updated_on": LastModifiedField(),
|
||||||
|
}
|
||||||
|
|
||||||
if field_id not in self.model._field_objects or (
|
if matches[1] in fixed_field_instance_mapping.keys():
|
||||||
only_filter_by_field_ids is not None
|
field_name = matches[1]
|
||||||
and field_id not in only_filter_by_field_ids
|
field_instance = fixed_field_instance_mapping.get(field_name)
|
||||||
):
|
else:
|
||||||
raise FilterFieldNotFound(field_id, f"Field {field_id} does not exist.")
|
field_id = int(matches[1])
|
||||||
|
|
||||||
|
if field_id not in self.model._field_objects or (
|
||||||
|
only_filter_by_field_ids is not None
|
||||||
|
and field_id not in only_filter_by_field_ids
|
||||||
|
):
|
||||||
|
raise FilterFieldNotFound(
|
||||||
|
field_id, f"Field {field_id} does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
field_object = self.model._field_objects[field_id]
|
||||||
|
field_instance = field_object["field"]
|
||||||
|
field_name = field_object["name"]
|
||||||
|
field_type = field_object["type"].type
|
||||||
|
|
||||||
field_object = self.model._field_objects[field_id]
|
|
||||||
field_instance = field_object["field"]
|
|
||||||
field_name = field_object["name"]
|
|
||||||
field_type = field_object["type"].type
|
|
||||||
model_field = self.model._meta.get_field(field_name)
|
model_field = self.model._meta.get_field(field_name)
|
||||||
view_filter_type = view_filter_type_registry.get(matches[2])
|
view_filter_type = view_filter_type_registry.get(matches[2])
|
||||||
|
|
||||||
if not view_filter_type.field_is_compatible(field_instance):
|
if not view_filter_type.field_is_compatible(field_instance):
|
||||||
raise ViewFilterTypeNotAllowedForField(
|
raise ViewFilterTypeNotAllowedForField(
|
||||||
matches[2],
|
matches[2],
|
||||||
|
@ -300,7 +320,7 @@ class TableModelQuerySet(models.QuerySet):
|
||||||
for value in values:
|
for value in values:
|
||||||
filter_builder.filter(
|
filter_builder.filter(
|
||||||
view_filter_type.get_filter(
|
view_filter_type.get_filter(
|
||||||
field_name, value, model_field, field_object["field"]
|
field_name, value, model_field, field_instance
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from math import ceil, floor
|
from math import ceil, floor
|
||||||
from typing import Dict
|
from typing import Dict, Union
|
||||||
|
|
||||||
from django.contrib.postgres.aggregates.general import ArrayAgg
|
from django.contrib.postgres.aggregates.general import ArrayAgg
|
||||||
from django.db.models import DateTimeField, IntegerField, Q
|
from django.db.models import DateTimeField, IntegerField, Q
|
||||||
|
@ -310,7 +310,7 @@ class DateEqualViewFilterType(ViewFilterType):
|
||||||
utc = timezone("UTC")
|
utc = timezone("UTC")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
datetime = parser.isoparse(value).astimezone(utc)
|
parsed_datetime = parser.isoparse(value).astimezone(utc)
|
||||||
except (ParserError, ValueError):
|
except (ParserError, ValueError):
|
||||||
return Q()
|
return Q()
|
||||||
|
|
||||||
|
@ -326,9 +326,9 @@ class DateEqualViewFilterType(ViewFilterType):
|
||||||
|
|
||||||
def query_dict(query_field_name):
|
def query_dict(query_field_name):
|
||||||
return {
|
return {
|
||||||
f"{query_field_name}__year": datetime.year,
|
f"{query_field_name}__year": parsed_datetime.year,
|
||||||
f"{query_field_name}__month": datetime.month,
|
f"{query_field_name}__month": parsed_datetime.month,
|
||||||
f"{query_field_name}__day": datetime.day,
|
f"{query_field_name}__day": parsed_datetime.day,
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_timezone:
|
if has_timezone:
|
||||||
|
@ -343,7 +343,7 @@ class DateEqualViewFilterType(ViewFilterType):
|
||||||
else:
|
else:
|
||||||
return Q(**query_dict(field_name))
|
return Q(**query_dict(field_name))
|
||||||
else:
|
else:
|
||||||
return Q(**{field_name: datetime})
|
return Q(**{field_name: parsed_datetime})
|
||||||
|
|
||||||
|
|
||||||
class BaseDateFieldLookupFilterType(ViewFilterType):
|
class BaseDateFieldLookupFilterType(ViewFilterType):
|
||||||
|
@ -375,7 +375,7 @@ class BaseDateFieldLookupFilterType(ViewFilterType):
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_date(value: str) -> datetime.date:
|
def parse_date(value: str) -> Union[datetime.date, datetime]:
|
||||||
"""
|
"""
|
||||||
Parses the provided value string and converts it to a date object.
|
Parses the provided value string and converts it to a date object.
|
||||||
Raises an error if the provided value is an empty string or cannot be parsed
|
Raises an error if the provided value is an empty string or cannot be parsed
|
||||||
|
@ -387,19 +387,33 @@ class BaseDateFieldLookupFilterType(ViewFilterType):
|
||||||
if value == "":
|
if value == "":
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
|
utc = timezone("UTC")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed_date = parser.isoparse(value).date()
|
parsed_datetime = parser.isoparse(value).astimezone(utc)
|
||||||
return parsed_date
|
return parsed_datetime
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_date(value: str) -> bool:
|
||||||
|
try:
|
||||||
|
datetime.strptime(value, "%Y-%m-%d")
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
def get_filter(self, field_name, value, model_field, field):
|
def get_filter(self, field_name, value, model_field, field):
|
||||||
# in order to only compare the date part of a datetime field
|
# in order to only compare the date part of a datetime field
|
||||||
# we need to verify that we are in fact dealing with a datetime field
|
# we need to verify that we are in fact dealing with a datetime field
|
||||||
# if so the django query lookup '__date' gets appended to the field_name
|
# if so the django query lookup '__date' gets appended to the field_name
|
||||||
# otherwise (i.e. it is a date field) nothing gets appended
|
# otherwise (i.e. it is a date field) nothing gets appended
|
||||||
query_date_lookup = self.query_date_lookup
|
query_date_lookup = self.query_date_lookup
|
||||||
if isinstance(model_field, DateTimeField) and not query_date_lookup:
|
if (
|
||||||
|
isinstance(model_field, DateTimeField)
|
||||||
|
and self.is_date(value)
|
||||||
|
and not query_date_lookup
|
||||||
|
):
|
||||||
query_date_lookup = "__date"
|
query_date_lookup = "__date"
|
||||||
try:
|
try:
|
||||||
parsed_date = self.parse_date(value)
|
parsed_date = self.parse_date(value)
|
||||||
|
@ -433,14 +447,6 @@ class DateBeforeViewFilterType(BaseDateFieldLookupFilterType):
|
||||||
|
|
||||||
type = "date_before"
|
type = "date_before"
|
||||||
query_field_lookup = "__lt"
|
query_field_lookup = "__lt"
|
||||||
compatible_field_types = [
|
|
||||||
DateFieldType.type,
|
|
||||||
LastModifiedFieldType.type,
|
|
||||||
CreatedOnFieldType.type,
|
|
||||||
FormulaFieldType.compatible_with_formula_types(
|
|
||||||
BaserowFormulaDateType.type,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class DateAfterViewFilterType(BaseDateFieldLookupFilterType):
|
class DateAfterViewFilterType(BaseDateFieldLookupFilterType):
|
||||||
|
|
|
@ -52,6 +52,7 @@ def test_list_fields(api_client, data_fixture):
|
||||||
assert response_json[0]["type"] == "text"
|
assert response_json[0]["type"] == "text"
|
||||||
assert response_json[0]["primary"]
|
assert response_json[0]["primary"]
|
||||||
assert response_json[0]["text_default"] == field_1.text_default
|
assert response_json[0]["text_default"] == field_1.text_default
|
||||||
|
assert response_json[0]["read_only"] is False
|
||||||
|
|
||||||
assert response_json[1]["id"] == field_3.id
|
assert response_json[1]["id"] == field_3.id
|
||||||
assert response_json[1]["type"] == "number"
|
assert response_json[1]["type"] == "number"
|
||||||
|
@ -128,6 +129,27 @@ def test_list_fields(api_client, data_fixture):
|
||||||
assert response.json()["error"] == "ERROR_TABLE_DOES_NOT_EXIST"
|
assert response.json()["error"] == "ERROR_TABLE_DOES_NOT_EXIST"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_read_only_fields(api_client, data_fixture):
|
||||||
|
user, jwt_token = data_fixture.create_user_and_token(
|
||||||
|
email="test@test.nl", password="password", first_name="Test1"
|
||||||
|
)
|
||||||
|
table_1 = data_fixture.create_database_table(user=user)
|
||||||
|
field_1 = data_fixture.create_created_on_field(table=table_1, order=1)
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
reverse("api:database:fields:list", kwargs={"table_id": table_1.id}),
|
||||||
|
**{"HTTP_AUTHORIZATION": f"JWT {jwt_token}"},
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
assert len(response_json) == 1
|
||||||
|
assert response_json[0]["id"] == field_1.id
|
||||||
|
assert response_json[0]["type"] == "created_on"
|
||||||
|
assert response_json[0]["read_only"] is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_create_field(api_client, data_fixture):
|
def test_create_field(api_client, data_fixture):
|
||||||
user, jwt_token = data_fixture.create_user_and_token()
|
user, jwt_token = data_fixture.create_user_and_token()
|
||||||
|
|
|
@ -7,6 +7,7 @@ from rest_framework.status import (
|
||||||
HTTP_204_NO_CONTENT,
|
HTTP_204_NO_CONTENT,
|
||||||
HTTP_400_BAD_REQUEST,
|
HTTP_400_BAD_REQUEST,
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_403_FORBIDDEN,
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -615,3 +616,25 @@ def test_trashing_table_hides_restores_tokens(api_client, data_fixture):
|
||||||
["database", database_1.id],
|
["database", database_1.id],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_check_token(api_client, data_fixture):
|
||||||
|
user = data_fixture.create_user()
|
||||||
|
group = data_fixture.create_group(user=user)
|
||||||
|
|
||||||
|
url = reverse("api:database:tokens:check")
|
||||||
|
response = api_client.get(url, format="json")
|
||||||
|
assert response.status_code == HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
url = reverse("api:database:tokens:check")
|
||||||
|
response = api_client.get(url, format="json", HTTP_AUTHORIZATION="Token WRONG")
|
||||||
|
assert response.status_code == HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
token = TokenHandler().create_token(user, group, "Good")
|
||||||
|
url = reverse("api:database:tokens:check")
|
||||||
|
response = api_client.get(
|
||||||
|
url, format="json", HTTP_AUTHORIZATION=f"Token {token.key}"
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
assert response.json() == {"token": "OK"}
|
||||||
|
|
|
@ -360,6 +360,7 @@ def test_get_public_gallery_view(api_client, data_fixture):
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"text_default": "",
|
"text_default": "",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
"read_only": False,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view": {
|
"view": {
|
||||||
|
|
|
@ -1774,6 +1774,7 @@ def test_get_public_grid_view(api_client, data_fixture):
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"text_default": "",
|
"text_default": "",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
"read_only": False,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view": {
|
"view": {
|
||||||
|
|
|
@ -672,6 +672,68 @@ def test_filter_by_fields_object_queryset(data_fixture):
|
||||||
assert results[0].id == row_4.id
|
assert results[0].id == row_4.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_filter_by_fields_object_with_created_on_queryset(data_fixture):
|
||||||
|
table = data_fixture.create_database_table(name="Cars")
|
||||||
|
|
||||||
|
model = table.get_model()
|
||||||
|
|
||||||
|
row_1 = model.objects.create()
|
||||||
|
row_1.created_on = datetime(2021, 1, 1, 12, 0, 0, tzinfo=utc)
|
||||||
|
row_1.save()
|
||||||
|
|
||||||
|
row_2 = model.objects.create()
|
||||||
|
row_2.created_on = datetime(2021, 1, 2, 12, 0, 0, tzinfo=utc)
|
||||||
|
row_2.save()
|
||||||
|
|
||||||
|
row_3 = model.objects.create()
|
||||||
|
row_3.created_on = datetime(2021, 1, 3, 12, 0, 0, tzinfo=utc)
|
||||||
|
row_3.save()
|
||||||
|
|
||||||
|
print(row_1.created_on)
|
||||||
|
print(row_2.created_on)
|
||||||
|
print(row_3.created_on)
|
||||||
|
|
||||||
|
results = model.objects.all().filter_by_fields_object(
|
||||||
|
filter_object={
|
||||||
|
f"filter__field_created_on__date_after": "2021-01-02 13:00",
|
||||||
|
},
|
||||||
|
filter_type="AND",
|
||||||
|
)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0].id == row_3.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_filter_by_fields_object_with_updated_on_queryset(data_fixture):
|
||||||
|
table = data_fixture.create_database_table(name="Cars")
|
||||||
|
|
||||||
|
model = table.get_model()
|
||||||
|
|
||||||
|
row_1 = model.objects.create()
|
||||||
|
row_2 = model.objects.create()
|
||||||
|
row_3 = model.objects.create()
|
||||||
|
|
||||||
|
model.objects.filter(id=row_1.id).update(
|
||||||
|
updated_on=datetime(2021, 1, 1, 12, 0, 0, tzinfo=utc)
|
||||||
|
)
|
||||||
|
model.objects.filter(id=row_2.id).update(
|
||||||
|
updated_on=datetime(2021, 1, 2, 12, 0, 0, tzinfo=utc)
|
||||||
|
)
|
||||||
|
model.objects.filter(id=row_3.id).update(
|
||||||
|
updated_on=datetime(2021, 1, 3, 12, 0, 0, tzinfo=utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = model.objects.all().filter_by_fields_object(
|
||||||
|
filter_object={
|
||||||
|
f"filter__field_updated_on__date_before": "2021-01-02 12:00",
|
||||||
|
},
|
||||||
|
filter_type="AND",
|
||||||
|
)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0].id == row_1.id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_table_model_fields_requiring_refresh_on_insert(data_fixture):
|
def test_table_model_fields_requiring_refresh_on_insert(data_fixture):
|
||||||
table = data_fixture.create_database_table(name="Cars")
|
table = data_fixture.create_database_table(name="Cars")
|
||||||
|
|
|
@ -2774,6 +2774,21 @@ def test_date_before_filter_type(data_fixture):
|
||||||
assert len(ids) == 1
|
assert len(ids) == 1
|
||||||
assert row.id in ids
|
assert row.id in ids
|
||||||
|
|
||||||
|
view_filter.field = date_time_field
|
||||||
|
view_filter.value = "2021-07-06 01:20"
|
||||||
|
view_filter.save()
|
||||||
|
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
||||||
|
assert len(ids) == 1
|
||||||
|
assert row.id in ids
|
||||||
|
|
||||||
|
view_filter.field = date_time_field
|
||||||
|
view_filter.value = "2021-07-06 01:40"
|
||||||
|
view_filter.save()
|
||||||
|
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
||||||
|
assert len(ids) == 2
|
||||||
|
assert row.id in ids
|
||||||
|
assert row_2.id in ids
|
||||||
|
|
||||||
view_filter.value = ""
|
view_filter.value = ""
|
||||||
view_filter.save()
|
view_filter.save()
|
||||||
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
||||||
|
@ -2833,6 +2848,14 @@ def test_date_after_filter_type(data_fixture):
|
||||||
assert len(ids) == 1
|
assert len(ids) == 1
|
||||||
assert row_4.id in ids
|
assert row_4.id in ids
|
||||||
|
|
||||||
|
view_filter.field = date_time_field
|
||||||
|
view_filter.value = "2021-07-05"
|
||||||
|
view_filter.save()
|
||||||
|
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
||||||
|
assert len(ids) == 2
|
||||||
|
assert row_2.id in ids
|
||||||
|
assert row_4.id in ids
|
||||||
|
|
||||||
view_filter.field = date_time_field
|
view_filter.field = date_time_field
|
||||||
view_filter.value = "2021-07-06"
|
view_filter.value = "2021-07-06"
|
||||||
view_filter.save()
|
view_filter.save()
|
||||||
|
@ -2840,6 +2863,21 @@ def test_date_after_filter_type(data_fixture):
|
||||||
assert len(ids) == 1
|
assert len(ids) == 1
|
||||||
assert row_4.id in ids
|
assert row_4.id in ids
|
||||||
|
|
||||||
|
view_filter.field = date_time_field
|
||||||
|
view_filter.value = "2021-07-06 01:40"
|
||||||
|
view_filter.save()
|
||||||
|
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
||||||
|
assert len(ids) == 2
|
||||||
|
assert row_2.id in ids
|
||||||
|
assert row_4.id in ids
|
||||||
|
|
||||||
|
view_filter.field = date_time_field
|
||||||
|
view_filter.value = "2021-07-06 02:41"
|
||||||
|
view_filter.save()
|
||||||
|
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
||||||
|
assert len(ids) == 1
|
||||||
|
assert row_4.id in ids
|
||||||
|
|
||||||
view_filter.value = ""
|
view_filter.value = ""
|
||||||
view_filter.save()
|
view_filter.save()
|
||||||
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
ids = [r.id for r in handler.apply_filters(grid_view, model.objects.all()).all()]
|
||||||
|
|
|
@ -217,6 +217,7 @@ def test_when_field_unhidden_in_public_view_force_refresh_sent(
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"text_default": "",
|
"text_default": "",
|
||||||
|
"read_only": False,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view": view_serialized["view"],
|
"view": view_serialized["view"],
|
||||||
|
@ -291,6 +292,7 @@ def test_when_only_field_options_updated_in_public_grid_view_force_refresh_sent(
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"text_default": "",
|
"text_default": "",
|
||||||
|
"read_only": False,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view": view_serialized["view"],
|
"view": view_serialized["view"],
|
||||||
|
|
|
@ -15,6 +15,11 @@ For example:
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
|
* Added Zapier integration code. [#816](https://gitlab.com/bramw/baserow/-/issues/816)
|
||||||
|
* Made it possible to filter on the `created_on` and `updated_on` columns, even though
|
||||||
|
they're not exposed via fields.
|
||||||
|
* Expose `read_only` in the list fields endpoint.
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
### Refactors
|
### Refactors
|
||||||
|
|
66
integrations/zapier/.gitignore
vendored
Normal file
66
integrations/zapier/.gitignore
vendored
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# environment variables file
|
||||||
|
.env
|
||||||
|
.environment
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
zapier.test.js
|
||||||
|
|
||||||
|
.zapierapprc
|
24
integrations/zapier/README.md
Normal file
24
integrations/zapier/README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# zapier
|
||||||
|
|
||||||
|
This Zapier integration project is generated by the `zapier init` CLI command.
|
||||||
|
|
||||||
|
These are what you normally do next:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install # or you can use yarn
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
zapier test
|
||||||
|
|
||||||
|
# Register the integration on Zapier if you haven't
|
||||||
|
zapier register "App Title"
|
||||||
|
|
||||||
|
# Or you can link to an existing integration on Zapier
|
||||||
|
zapier link
|
||||||
|
|
||||||
|
# Push it to Zapier
|
||||||
|
zapier push
|
||||||
|
```
|
||||||
|
|
||||||
|
Find out more on the latest docs: https://github.com/zapier/zapier-platform/blob/master/packages/cli/README.md.
|
33
integrations/zapier/authentication.js
Normal file
33
integrations/zapier/authentication.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
module.exports = {
|
||||||
|
type: 'custom',
|
||||||
|
test: {
|
||||||
|
url: `{{bundle.authData.apiURL}}/api/database/tokens/check/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Authorization': 'Token {{bundle.authData.apiToken}}' },
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
computed: false,
|
||||||
|
key: 'apiToken',
|
||||||
|
required: true,
|
||||||
|
label: 'Baserow API token',
|
||||||
|
type: 'string',
|
||||||
|
helpText:
|
||||||
|
'Please enter your Baserow API token. Can be found by clicking on your ' +
|
||||||
|
'account in the top left corner -> Settings -> API tokens.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
computed: false,
|
||||||
|
key: 'apiURL',
|
||||||
|
required: false,
|
||||||
|
label: 'Baserow API URL',
|
||||||
|
default: 'https://api.baserow.io',
|
||||||
|
type: 'string',
|
||||||
|
helpText:
|
||||||
|
'Please enter your Baserow API URL. If you are using baserow.io, you ' +
|
||||||
|
'can leave the default one.'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connectionLabel: 'Baserow API authentication',
|
||||||
|
customConfig: {}
|
||||||
|
}
|
33
integrations/zapier/index.js
Normal file
33
integrations/zapier/index.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const authentication = require('./authentication.js')
|
||||||
|
|
||||||
|
const deleteRowCreate = require('./src/creates/delete-row.js')
|
||||||
|
const newRowCreate = require('./src/creates/new-row.js')
|
||||||
|
const updateRowCreate = require('./src/creates/update-row.js')
|
||||||
|
|
||||||
|
const getSingleRowSearch = require('./src/searches/get-single-row.js')
|
||||||
|
const listRowsSearch = require('./src/searches/list-rows.js')
|
||||||
|
|
||||||
|
const rowCreatedTrigger = require('./src/triggers/row-created.js')
|
||||||
|
const rowUpdatedTrigger = require('./src/triggers/row-updated.js')
|
||||||
|
const rowUpdatedOrCreatedTrigger =require('./src/triggers/row-updated-or-created.js')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version: require('./package.json').version,
|
||||||
|
platformVersion: require('zapier-platform-core').version,
|
||||||
|
authentication: authentication,
|
||||||
|
triggers: {
|
||||||
|
[rowCreatedTrigger.key]: rowCreatedTrigger,
|
||||||
|
[rowUpdatedTrigger.key]: rowUpdatedTrigger,
|
||||||
|
[rowUpdatedOrCreatedTrigger.key]: rowUpdatedOrCreatedTrigger
|
||||||
|
},
|
||||||
|
searches: {
|
||||||
|
[getSingleRowSearch.key]: getSingleRowSearch,
|
||||||
|
[listRowsSearch.key]: listRowsSearch
|
||||||
|
},
|
||||||
|
creates: {
|
||||||
|
[newRowCreate.key]: newRowCreate,
|
||||||
|
[deleteRowCreate.key]: deleteRowCreate,
|
||||||
|
[updateRowCreate.key]: updateRowCreate,
|
||||||
|
},
|
||||||
|
resources: {},
|
||||||
|
}
|
17
integrations/zapier/package.json
Normal file
17
integrations/zapier/package.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "baserow-zapier",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --testTimeout 10000"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zapier-platform-core": "11.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"zapier-platform-cli": "^12.0.3"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
6
integrations/zapier/src/constants.js
Normal file
6
integrations/zapier/src/constants.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// Must be in sync with the backend field types.
|
||||||
|
const unsupportedBaserowFieldTypes = ['file']
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
unsupportedBaserowFieldTypes,
|
||||||
|
}
|
49
integrations/zapier/src/creates/delete-row.js
Normal file
49
integrations/zapier/src/creates/delete-row.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
|
||||||
|
const deleteRowInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter the table ID where the row must be deleted in. You can ' +
|
||||||
|
'find the ID by clicking on the three dots next to the table. It\'s the number ' +
|
||||||
|
'between brackets.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rowID',
|
||||||
|
label: 'Row ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please the row ID that must be deleted.'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const DeleteRow = async (z, bundle) => {
|
||||||
|
const rowDeleteRequest = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/${bundle.inputData.rowID}/`,
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return rowDeleteRequest.status === 204
|
||||||
|
? { message: `Row ${bundle.inputData.rowID} deleted successfully.` }
|
||||||
|
: { message: 'A problem occurred during DELETE operation. The row was not deleted.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'deleteRow',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'Delete Row',
|
||||||
|
description: 'Deletes an existing row.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
perform: DeleteRow,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: deleteRowInputFields
|
||||||
|
}
|
||||||
|
}
|
51
integrations/zapier/src/creates/new-row.js
Normal file
51
integrations/zapier/src/creates/new-row.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
const {
|
||||||
|
getRowInputValues,
|
||||||
|
prepareInputDataForBaserow
|
||||||
|
} = require('../helpers')
|
||||||
|
|
||||||
|
const rowInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
altersDynamicFields: true,
|
||||||
|
helpText: 'Please enter the table ID where the row must be created in. You can ' +
|
||||||
|
'find the ID by clicking on the three dots next to the table. It\'s the number ' +
|
||||||
|
'between brackets.'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const createRow = async (z, bundle) => {
|
||||||
|
const rowData = await prepareInputDataForBaserow(z, bundle)
|
||||||
|
const rowPostRequest = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/`,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
'user_field_names': 'true',
|
||||||
|
},
|
||||||
|
body: rowData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return rowPostRequest.json
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'newRow',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'Create Row',
|
||||||
|
description: 'Creates a new row.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
perform: createRow,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: [...rowInputFields, getRowInputValues]
|
||||||
|
}
|
||||||
|
}
|
58
integrations/zapier/src/creates/update-row.js
Normal file
58
integrations/zapier/src/creates/update-row.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
const {
|
||||||
|
getRowInputValues,
|
||||||
|
prepareInputDataForBaserow
|
||||||
|
} = require('../helpers')
|
||||||
|
|
||||||
|
const updateRowInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
altersDynamicFields: true,
|
||||||
|
helpText: 'Please enter the table ID where the row must be updated in. You can ' +
|
||||||
|
'find the ID by clicking on the three dots next to the table. It\'s the ' +
|
||||||
|
'number between brackets.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rowID',
|
||||||
|
label: 'Row ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter the row ID that must be updated.'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const updateRow = async (z, bundle) => {
|
||||||
|
const rowData = await prepareInputDataForBaserow(z, bundle)
|
||||||
|
const rowPatchRequest = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/${bundle.inputData.rowID}/`,
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
'user_field_names': 'true',
|
||||||
|
},
|
||||||
|
body: rowData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return rowPatchRequest.json
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'updateRow',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'Update Row',
|
||||||
|
description: 'Updates an existing row.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
perform: updateRow,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: [...updateRowInputFields, getRowInputValues]
|
||||||
|
}
|
||||||
|
}
|
153
integrations/zapier/src/helpers.js
Normal file
153
integrations/zapier/src/helpers.js
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
const { unsupportedBaserowFieldTypes } = require('./constants')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the fields of a table and converts them to an array with valid Zapier
|
||||||
|
* field objects.
|
||||||
|
*/
|
||||||
|
const getRowInputValues = async (z, bundle) => {
|
||||||
|
if (!bundle.inputData.tableID) {
|
||||||
|
throw new Error('The `tableID` must be provided.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsGetRequest = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/fields/table/${bundle.inputData.tableID}/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return fieldsGetRequest.json.map(v => {
|
||||||
|
return mapBaserowFieldTypesToZapierTypes(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the fields and converts the input data to Baserow row compatible data.
|
||||||
|
*/
|
||||||
|
const prepareInputDataForBaserow = async (z, bundle) => {
|
||||||
|
if (!bundle.inputData.tableID) {
|
||||||
|
throw new Error('The `tableID` must be provided.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsGetRequest = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/fields/table/${bundle.inputData.tableID}/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let rowData = { id: bundle.inputData.rowID }
|
||||||
|
fieldsGetRequest
|
||||||
|
.json
|
||||||
|
.filter(
|
||||||
|
(baserowField) =>
|
||||||
|
baserowField.read_only
|
||||||
|
|| !unsupportedBaserowFieldTypes.includes(baserowField.type)
|
||||||
|
)
|
||||||
|
.filter((baserowField) => bundle.inputData.hasOwnProperty(baserowField.name))
|
||||||
|
.forEach(baserowField => {
|
||||||
|
let value = bundle.inputData[baserowField.name]
|
||||||
|
|
||||||
|
if (baserowField.type === 'multiple_collaborators') {
|
||||||
|
value = value.map(id => {
|
||||||
|
return { id }}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData[baserowField.name] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
return rowData
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the provided Baserow field type object to a Zapier compatible object.
|
||||||
|
*/
|
||||||
|
const mapBaserowFieldTypesToZapierTypes = (baserowField) => {
|
||||||
|
const zapType = {
|
||||||
|
key: baserowField.name,
|
||||||
|
label: baserowField.name,
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'long_text') {
|
||||||
|
zapType.type = 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'boolean') {
|
||||||
|
zapType.type = 'boolean'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'number') {
|
||||||
|
zapType.type = 'integer'
|
||||||
|
|
||||||
|
if (baserowField.number_decimal_places > 0) {
|
||||||
|
zapType.type = 'float'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'boolean') {
|
||||||
|
zapType.type = 'boolean'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'rating') {
|
||||||
|
zapType.type = 'integer'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['single_select', 'multiple_select'].includes(baserowField.type)) {
|
||||||
|
const choices = {}
|
||||||
|
baserowField.select_options.forEach(el => {
|
||||||
|
choices[`${el.id}`] = el.value
|
||||||
|
})
|
||||||
|
zapType.type = 'string'
|
||||||
|
zapType.choices = choices
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'multiple_select') {
|
||||||
|
zapType.list = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'link_row') {
|
||||||
|
zapType.type = 'integer'
|
||||||
|
zapType.helpText = 'Provide row ids that you want to link to.'
|
||||||
|
zapType.list = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'multiple_collaborators') {
|
||||||
|
zapType.type = 'integer'
|
||||||
|
zapType.helpText = 'Provide user ids that you want to link to.'
|
||||||
|
zapType.list = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'date' && !baserowField.date_include_time) {
|
||||||
|
zapType.type = 'date'
|
||||||
|
zapType.helpText =
|
||||||
|
'the date fields accepts a date in ISO format (e.g. 2020-01-01)'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baserowField.type === 'date' && baserowField.date_include_time) {
|
||||||
|
zapType.type = 'datetime'
|
||||||
|
zapType.helpText =
|
||||||
|
'the date fields accepts date and time in ISO format (e.g. 2020-01-01 12:00)'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
baserowField.read_only
|
||||||
|
|| unsupportedBaserowFieldTypes.includes(baserowField.type)
|
||||||
|
) {
|
||||||
|
// Read only and the file field are not supported.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return zapType
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getRowInputValues,
|
||||||
|
prepareInputDataForBaserow,
|
||||||
|
mapBaserowFieldTypesToZapierTypes,
|
||||||
|
}
|
8
integrations/zapier/src/samples/row.js
Normal file
8
integrations/zapier/src/samples/row.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const rowSample = {
|
||||||
|
id: 0,
|
||||||
|
order: '1.00000000000000000000',
|
||||||
|
Name: 'string',
|
||||||
|
Notes: 'string',
|
||||||
|
Active: true
|
||||||
|
}
|
||||||
|
module.exports = { rowSample }
|
50
integrations/zapier/src/searches/get-single-row.js
Normal file
50
integrations/zapier/src/searches/get-single-row.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
|
||||||
|
const getSingleRowInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter the table ID where you want to get the row from. You can ' +
|
||||||
|
'find the ID by clicking on the three dots next to the table. It\'s the number ' +
|
||||||
|
'between brackets.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rowID',
|
||||||
|
label: 'Row ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter the ID of the row that you want to get.'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const getSingleRow = async (z, bundle) => {
|
||||||
|
const rowGetRequest = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/${bundle.inputData.rowID}/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
'user_field_names': 'true',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return [rowGetRequest.json]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'getSingleRow',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'Get Single Row',
|
||||||
|
description: 'Finds a single row in a given table.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
perform: getSingleRow,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: getSingleRowInputFields
|
||||||
|
}
|
||||||
|
}
|
83
integrations/zapier/src/searches/list-rows.js
Normal file
83
integrations/zapier/src/searches/list-rows.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
|
||||||
|
const listRowsInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter the table ID where you want to get the rows from. You ' +
|
||||||
|
'can find the ID by clicking on the three dots next to the table. It\'s the ' +
|
||||||
|
'number between brackets.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'page',
|
||||||
|
label: 'page',
|
||||||
|
helpText: 'Defines which page of rows should be returned.',
|
||||||
|
type: 'string',
|
||||||
|
default: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'size',
|
||||||
|
label: 'size',
|
||||||
|
helpText: 'Defines how many rows should be returned per page.',
|
||||||
|
type: 'string',
|
||||||
|
default: '100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
label: 'search',
|
||||||
|
helpText:
|
||||||
|
'If provided only rows with cell data that matches the search query ' +
|
||||||
|
'are going to be returned.',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const listRows = async (z, bundle) => {
|
||||||
|
let params = {
|
||||||
|
'size': bundle.inputData.size,
|
||||||
|
'page': bundle.inputData.page,
|
||||||
|
'user_field_names': 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bundle.inputData.search) {
|
||||||
|
params['search'] = bundle.inputData.search
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowGetRequest = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
params
|
||||||
|
})
|
||||||
|
|
||||||
|
// Modify array to be an single object, so it will display as 'row1-Name'.
|
||||||
|
let data = {}
|
||||||
|
rowGetRequest.json.results.forEach((row, index) => {
|
||||||
|
for (const [key, value] of Object.entries(row)) {
|
||||||
|
data[`row${index + 1}-${key}`] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// The search actions needs to be array of object with only one object. Other
|
||||||
|
// are not displayed in the UI.
|
||||||
|
return [data]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'listRows',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'List Rows',
|
||||||
|
description: 'Finds a page of rows in a given table.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
perform: listRows,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: listRowsInputFields
|
||||||
|
}
|
||||||
|
}
|
73
integrations/zapier/src/triggers/row-created.js
Normal file
73
integrations/zapier/src/triggers/row-created.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
|
||||||
|
const rowInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter your Baserow table ID. You can find the ID by clicking' +
|
||||||
|
' on the three dots next to the table. It\'s the number between brackets.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const getCreatedRows = async (z, bundle) => {
|
||||||
|
const nowDate = new Date()
|
||||||
|
let fromDate = new Date()
|
||||||
|
// This is the recommended way of doing it according to Zapier support.
|
||||||
|
fromDate.setHours(fromDate.getHours() - 2)
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
const size = 200
|
||||||
|
let page = 1
|
||||||
|
let pages = null
|
||||||
|
while (page <= pages || pages === null) {
|
||||||
|
const request = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
size: size,
|
||||||
|
page: page,
|
||||||
|
'user_field_names': 'true',
|
||||||
|
'filter_type': 'AND',
|
||||||
|
'filter__field_created_on__date_before': nowDate.toISOString(),
|
||||||
|
'filter__field_created_on__date_after': fromDate.toISOString()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pages === null) {
|
||||||
|
// Calculate the amount of pages based on the total count of the backend.
|
||||||
|
pages = Math.ceil(request.json.count / size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the rows to one big array.
|
||||||
|
rows.push(...request.json.results)
|
||||||
|
|
||||||
|
// Increase the page because we have already fetched it.
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zapier figures out the duplicates.
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'rowCreated',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'Row Created',
|
||||||
|
description: 'Trigger when new row is created.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
type: 'polling',
|
||||||
|
perform: getCreatedRows,
|
||||||
|
canPaginate: false,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: rowInputFields
|
||||||
|
}
|
||||||
|
}
|
73
integrations/zapier/src/triggers/row-updated-or-created.js
Normal file
73
integrations/zapier/src/triggers/row-updated-or-created.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
|
||||||
|
const rowInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter your Baserow table ID. You can find the ID by clicking' +
|
||||||
|
' on the three dots next to the table. It\'s the number between brackets.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const getCreatedOrUpdatedRows = async (z, bundle) => {
|
||||||
|
const nowDate = new Date()
|
||||||
|
let fromDate = new Date()
|
||||||
|
// This is the recommended way of doing it according to Zapier support.
|
||||||
|
fromDate.setHours(fromDate.getHours() - 2)
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
const size = 200
|
||||||
|
let page = 1
|
||||||
|
let pages = null
|
||||||
|
while (page <= pages || pages === null) {
|
||||||
|
const request = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
size: size,
|
||||||
|
page: page,
|
||||||
|
'user_field_names': 'true',
|
||||||
|
'filter_type': 'AND',
|
||||||
|
'filter__field_updated_on__date_before': nowDate.toISOString(),
|
||||||
|
'filter__field_updated_on__date_after': fromDate.toISOString()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pages === null) {
|
||||||
|
// Calculate the amount of pages based on the total count of the backend.
|
||||||
|
pages = Math.ceil(request.json.count / size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the rows to one big array.
|
||||||
|
rows.push(...request.json.results)
|
||||||
|
|
||||||
|
// Increase the page because we have already fetched it.
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zapier figures out the duplicates.
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'rowCreatedOrUpdated',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'Row created or updated',
|
||||||
|
description: 'Trigger when a new row is created or an existing one is updated.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
type: 'polling',
|
||||||
|
perform: getCreatedOrUpdatedRows,
|
||||||
|
canPaginate: false,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: rowInputFields
|
||||||
|
}
|
||||||
|
}
|
79
integrations/zapier/src/triggers/row-updated.js
Normal file
79
integrations/zapier/src/triggers/row-updated.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
const { rowSample } = require('../samples/row')
|
||||||
|
|
||||||
|
const rowInputFields = [
|
||||||
|
{
|
||||||
|
key: 'tableID',
|
||||||
|
label: 'Table ID',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
helpText: 'Please enter your Baserow table ID. You can find the ID by clicking' +
|
||||||
|
' on the three dots next to the table. It\'s the number between brackets.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const getUpdatedRows = async (z, bundle) => {
|
||||||
|
const nowDate = new Date()
|
||||||
|
let fromDate = new Date()
|
||||||
|
// This is the recommended way of doing it according to Zapier support.
|
||||||
|
fromDate.setHours(fromDate.getHours() - 2)
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
const size = 200
|
||||||
|
let page = 1
|
||||||
|
let pages = null
|
||||||
|
while (page <= pages || pages === null) {
|
||||||
|
const request = await z.request({
|
||||||
|
url: `${bundle.authData.apiURL}/api/database/rows/table/${bundle.inputData.tableID}/`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Token ${bundle.authData.apiToken}`,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
size: size,
|
||||||
|
page: page,
|
||||||
|
'user_field_names': 'true',
|
||||||
|
'filter_type': 'AND',
|
||||||
|
'filter__field_updated_on__date_before': nowDate.toISOString(),
|
||||||
|
'filter__field_updated_on__date_after': fromDate.toISOString(),
|
||||||
|
// This is not a bulletproof solution that only returns the updated rows
|
||||||
|
// because if the row is newly created and changed an hour later, it will
|
||||||
|
// return as updated here. I don't have any other ideas that can easily be
|
||||||
|
// implemented, so I think this is better than not having a row updated
|
||||||
|
// trigger at all.
|
||||||
|
'filter__field_created_on__date_before': fromDate.toISOString()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pages === null) {
|
||||||
|
// Calculate the amount of pages based on the count of the backend.
|
||||||
|
pages = Math.ceil(request.json.count / size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the rows to one big array.
|
||||||
|
rows.push(...request.json.results)
|
||||||
|
|
||||||
|
// Increase the page because we have already fetched it.
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zapier figures out the duplicates.
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
key: 'rowUpdated',
|
||||||
|
noun: 'Row',
|
||||||
|
display: {
|
||||||
|
label: 'Row updated',
|
||||||
|
description: 'Trigger when an existing row is updated.'
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
type: 'polling',
|
||||||
|
perform: getUpdatedRows,
|
||||||
|
canPaginate: false,
|
||||||
|
sample: rowSample,
|
||||||
|
inputFields: rowInputFields
|
||||||
|
}
|
||||||
|
}
|
343
integrations/zapier/test/helpers.spec.js
Normal file
343
integrations/zapier/test/helpers.spec.js
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
const { mapBaserowFieldTypesToZapierTypes } = require('../src/helpers')
|
||||||
|
|
||||||
|
describe('helpers', () => {
|
||||||
|
describe('mapBaserowFieldTypesToZapierTypes ', () => {
|
||||||
|
it('text field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'text',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
text_default: ''
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'string'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('long_text field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'long_text',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'text'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('url field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'url',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'string'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('email field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'email',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'string'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('number field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'number',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
number_decimal_places: 0,
|
||||||
|
number_negative: false,
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'integer'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('number field with decimal places', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'number',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
number_decimal_places: 1,
|
||||||
|
number_negative: false,
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'float'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rating field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'rating',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
max_value: 5,
|
||||||
|
color: 'red',
|
||||||
|
style: 'star'
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'integer'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('boolean field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'boolean'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('date field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'date',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
date_format: 'ISO',
|
||||||
|
date_include_time: false,
|
||||||
|
date_time_format: '12'
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'date'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('date field with time', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'date',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
date_format: 'ISO',
|
||||||
|
date_include_time: true,
|
||||||
|
date_time_format: '24'
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'datetime'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('last_modified field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'last_modified',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: true,
|
||||||
|
date_format: 'ISO',
|
||||||
|
date_include_time: true,
|
||||||
|
date_time_format: '24'
|
||||||
|
})).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('created_on field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'created_on',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: true,
|
||||||
|
date_format: 'EU',
|
||||||
|
date_include_time: false,
|
||||||
|
date_time_format: '12'
|
||||||
|
})).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('link_row field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'link_row',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
link_row_table_id: 1,
|
||||||
|
link_row_related_field_id: 2,
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'integer',
|
||||||
|
helpText: `Provide row ids that you want to link to.`,
|
||||||
|
list: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('file field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'file',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
})).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('single_select field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'single_select',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
select_options: [
|
||||||
|
{id: 1, value: 'test', color: 'red'},
|
||||||
|
{id: 2, value: 'value', color: 'green'}
|
||||||
|
]
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'string',
|
||||||
|
choices: {
|
||||||
|
'1': 'test',
|
||||||
|
'2': 'value',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple_select field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'multiple_select',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
select_options: [
|
||||||
|
{id: 1, value: 'test', color: 'red'},
|
||||||
|
{id: 2, value: 'value', color: 'green'}
|
||||||
|
]
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'string',
|
||||||
|
choices: {
|
||||||
|
'1': 'test',
|
||||||
|
'2': 'value',
|
||||||
|
},
|
||||||
|
list: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('phone_number field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'phone_number',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'string'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formula field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'formula',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: true
|
||||||
|
})).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lookup field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'lookup',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: true,
|
||||||
|
})).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple_collaborators field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'multiple_collaborators',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false,
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'integer',
|
||||||
|
helpText: `Provide user ids that you want to link to.`,
|
||||||
|
list: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('unknown field', () => {
|
||||||
|
expect(mapBaserowFieldTypesToZapierTypes({
|
||||||
|
id: 1,
|
||||||
|
type: 'unknown_field_type',
|
||||||
|
name: 'Name',
|
||||||
|
order: 0,
|
||||||
|
primary: false,
|
||||||
|
read_only: false
|
||||||
|
})).toMatchObject({
|
||||||
|
key: 'Name',
|
||||||
|
label: 'Name',
|
||||||
|
type: 'string'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
8517
integrations/zapier/yarn.lock
Normal file
8517
integrations/zapier/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1085,6 +1085,7 @@ def test_get_public_kanban_without_with_single_select_and_cover(
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"text_default": "",
|
"text_default": "",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
"read_only": False,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"view": {
|
"view": {
|
||||||
|
@ -1153,6 +1154,7 @@ def test_get_public_kanban_view_with_single_select_and_cover(
|
||||||
"select_options": [],
|
"select_options": [],
|
||||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||||
"type": "single_select",
|
"type": "single_select",
|
||||||
|
"read_only": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": cover_field.id,
|
"id": cover_field.id,
|
||||||
|
@ -1161,6 +1163,7 @@ def test_get_public_kanban_view_with_single_select_and_cover(
|
||||||
"primary": cover_field.primary,
|
"primary": cover_field.primary,
|
||||||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||||
"type": "file",
|
"type": "file",
|
||||||
|
"read_only": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": public_field.id,
|
"id": public_field.id,
|
||||||
|
@ -1170,6 +1173,7 @@ def test_get_public_kanban_view_with_single_select_and_cover(
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"text_default": "",
|
"text_default": "",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
"read_only": False,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"view": {
|
"view": {
|
||||||
|
|
|
@ -31,7 +31,9 @@
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
</a>
|
</a>
|
||||||
<Context ref="context">
|
<Context ref="context">
|
||||||
<div class="context__menu-title">{{ application.name }}</div>
|
<div class="context__menu-title">
|
||||||
|
{{ application.name }} ({{ application.id }})
|
||||||
|
</div>
|
||||||
<ul class="context__menu">
|
<ul class="context__menu">
|
||||||
<slot name="context"></slot>
|
<slot name="context"></slot>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -30,6 +30,9 @@
|
||||||
<APIDocsParameter name="type" :optional="false" type="string">
|
<APIDocsParameter name="type" :optional="false" type="string">
|
||||||
{{ $t('apiDocsTableListFields.type') }}
|
{{ $t('apiDocsTableListFields.type') }}
|
||||||
</APIDocsParameter>
|
</APIDocsParameter>
|
||||||
|
<APIDocsParameter name="read_only" :optional="false" type="boolean">
|
||||||
|
{{ $t('apiDocsTableListFields.readOnly') }}
|
||||||
|
</APIDocsParameter>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="api-docs__content">
|
<p class="api-docs__content">
|
||||||
{{ $t('apiDocsTableListFields.extraProps') }}
|
{{ $t('apiDocsTableListFields.extraProps') }}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
</a>
|
</a>
|
||||||
<Context ref="context">
|
<Context ref="context">
|
||||||
<div class="context__menu-title">{{ table.name }}</div>
|
<div class="context__menu-title">{{ table.name }} ({{ table.id }})</div>
|
||||||
<ul class="context__menu">
|
<ul class="context__menu">
|
||||||
<li>
|
<li>
|
||||||
<a @click="exportTable()">
|
<a @click="exportTable()">
|
||||||
|
|
|
@ -409,14 +409,10 @@ export class FieldType extends Registerable {
|
||||||
* Generate a field sample for the given field that is displayed in auto-doc.
|
* Generate a field sample for the given field that is displayed in auto-doc.
|
||||||
* @returns a sample for this field.
|
* @returns a sample for this field.
|
||||||
*/
|
*/
|
||||||
getDocsFieldResponseExample({
|
getDocsFieldResponseExample(
|
||||||
id,
|
{ id, table_id: tableId, name, order, type, primary },
|
||||||
table_id: tableId,
|
readOnly
|
||||||
name,
|
) {
|
||||||
order,
|
|
||||||
type,
|
|
||||||
primary,
|
|
||||||
}) {
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
table_id: tableId,
|
table_id: tableId,
|
||||||
|
@ -424,6 +420,7 @@ export class FieldType extends Registerable {
|
||||||
order,
|
order,
|
||||||
type,
|
type,
|
||||||
primary,
|
primary,
|
||||||
|
read_only: readOnly,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,8 @@
|
||||||
"order": "Field order in table. 0 for the first field.",
|
"order": "Field order in table. 0 for the first field.",
|
||||||
"primary": "Indicates if the field is a primary field. If `true` the field cannot be deleted and the value should represent the whole row.",
|
"primary": "Indicates if the field is a primary field. If `true` the field cannot be deleted and the value should represent the whole row.",
|
||||||
"type": "Type defined for this field.",
|
"type": "Type defined for this field.",
|
||||||
"extraProps": "Some extra properties are not described here because they are type specific."
|
"extraProps": "Some extra properties are not described here because they are type specific.",
|
||||||
|
"readOnly": "Indicates whether the field is a read only field. If true, it's not possible to update the cell value."
|
||||||
},
|
},
|
||||||
"apiDocsTableDeleteRow": {
|
"apiDocsTableDeleteRow": {
|
||||||
"description": "Deletes an existing {name} row.",
|
"description": "Deletes an existing {name} row.",
|
||||||
|
|
|
@ -275,7 +275,10 @@ export default {
|
||||||
description: fieldType.getDocsDescription(field),
|
description: fieldType.getDocsDescription(field),
|
||||||
requestExample: fieldType.getDocsRequestExample(field),
|
requestExample: fieldType.getDocsRequestExample(field),
|
||||||
responseExample: fieldType.getDocsResponseExample(field),
|
responseExample: fieldType.getDocsResponseExample(field),
|
||||||
fieldResponseExample: fieldType.getDocsFieldResponseExample(field),
|
fieldResponseExample: fieldType.getDocsFieldResponseExample(
|
||||||
|
field,
|
||||||
|
fieldType.isReadOnly
|
||||||
|
),
|
||||||
isReadOnly: fieldType.isReadOnly,
|
isReadOnly: fieldType.isReadOnly,
|
||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
|
|
Loading…
Add table
Reference in a new issue