mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 22:35:36 +00:00
Resolve "Allow more values for link row fields and file fields"
This commit is contained in:
parent
319cbda9bc
commit
6ad6bcb36b
8 changed files with 382 additions and 47 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database
changelog/entries/unreleased/feature
web-frontend/locales
|
@ -140,22 +140,50 @@ class LinkRowValueSerializer(serializers.Serializer):
|
|||
)
|
||||
|
||||
|
||||
class FileFieldRequestSerializer(serializers.Serializer):
|
||||
visible_name = serializers.CharField(
|
||||
required=False, help_text="A visually editable name for the field."
|
||||
)
|
||||
name = serializers.CharField(
|
||||
required=True,
|
||||
validators=[user_file_name_validator],
|
||||
help_text="Accepts the name of the already uploaded user file.",
|
||||
)
|
||||
class FileFieldRequestSerializer(serializers.ListField):
|
||||
"""
|
||||
A serializer field that accept a List or a CSV string that will be converted to
|
||||
an Array.
|
||||
"""
|
||||
|
||||
def validate(self, data):
|
||||
if "name" not in data:
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, (str, list)):
|
||||
raise serializers.ValidationError(
|
||||
{"name": "This field is required."}, code="required"
|
||||
"This value must be a list or a string.",
|
||||
code="not_a_list",
|
||||
)
|
||||
return data
|
||||
|
||||
if not data:
|
||||
return []
|
||||
|
||||
if isinstance(data, str):
|
||||
data = split_comma_separated_string(data)
|
||||
|
||||
if any([not isinstance(i, type(data[0])) for i in data]):
|
||||
raise serializers.ValidationError(
|
||||
"All list values must be same type.", code="not_same_type"
|
||||
)
|
||||
|
||||
if isinstance(data[0], str):
|
||||
[user_file_name_validator(name) for name in data] # noqa W0106
|
||||
data = [{"name": name} for name in data]
|
||||
|
||||
elif isinstance(data[0], dict):
|
||||
for val in data:
|
||||
if "name" not in val:
|
||||
raise serializers.ValidationError(
|
||||
"A name property is required for all values of the list.",
|
||||
code="required",
|
||||
)
|
||||
user_file_name_validator(val["name"])
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
"The provided value should be a list of valid string or objects "
|
||||
"containing a value property.",
|
||||
code="invalid",
|
||||
)
|
||||
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class FileFieldResponseSerializer(
|
||||
|
@ -174,6 +202,41 @@ class FileFieldResponseSerializer(
|
|||
return instance[name]
|
||||
|
||||
|
||||
class LinkRowRequestSerializer(serializers.ListField):
|
||||
"""
|
||||
A serializer field that accept a List or a CSV string that will be converted to
|
||||
an Array.
|
||||
"""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not data:
|
||||
return []
|
||||
|
||||
if isinstance(data, list):
|
||||
if any([not isinstance(i, type(data[0])) for i in data]):
|
||||
raise serializers.ValidationError(
|
||||
"All list values must be same type.", code="not_same_type"
|
||||
)
|
||||
if not isinstance(data[0], (str, int)):
|
||||
raise serializers.ValidationError(
|
||||
"The provided value must be a list of valid integer or string",
|
||||
code="invalid",
|
||||
)
|
||||
|
||||
elif isinstance(data, str):
|
||||
data = split_comma_separated_string(data)
|
||||
|
||||
elif isinstance(data, int):
|
||||
data = [data]
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
"This provided value must be a list, an integer or a string.",
|
||||
code="invalid",
|
||||
)
|
||||
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.NONE)
|
||||
class MustBeEmptyField(serializers.Field):
|
||||
def __init__(self, error_message, *args, **kwargs):
|
||||
|
|
|
@ -62,6 +62,7 @@ from baserow.contrib.database.api.fields.serializers import (
|
|||
FileFieldRequestSerializer,
|
||||
FileFieldResponseSerializer,
|
||||
IntegerOrStringField,
|
||||
LinkRowRequestSerializer,
|
||||
LinkRowValueSerializer,
|
||||
ListOrStringField,
|
||||
MustBeEmptyField,
|
||||
|
@ -2299,13 +2300,7 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
representing the related row ids.
|
||||
"""
|
||||
|
||||
return ListOrStringField(
|
||||
**{
|
||||
"child": IntegerOrStringField(min_value=0),
|
||||
"required": False,
|
||||
**kwargs,
|
||||
}
|
||||
)
|
||||
return LinkRowRequestSerializer(required=False, **kwargs)
|
||||
|
||||
def get_response_serializer_field(self, instance, **kwargs):
|
||||
"""
|
||||
|
@ -2324,7 +2319,8 @@ class LinkRowFieldType(ManyToManyFieldTypeSerializeToInputValueMixin, FieldType)
|
|||
"This field accepts an `array` containing the ids or the names of the "
|
||||
"related rows. "
|
||||
"A name is the value of the primary key of the related row. "
|
||||
"This field also accepts a string with names separated by a comma. "
|
||||
"This field also accepts a string with names separated by a comma or an "
|
||||
"array of row names. You can also provide a unique row Id."
|
||||
"The response contains a list of objects containing the `id` and "
|
||||
"the primary field's `value` as a string for display purposes."
|
||||
)
|
||||
|
@ -3127,14 +3123,9 @@ class FileFieldType(FieldType):
|
|||
return values_by_row
|
||||
|
||||
def get_serializer_field(self, instance, **kwargs):
|
||||
required = kwargs.get("required", False)
|
||||
return serializers.ListSerializer(
|
||||
**{
|
||||
"child": FileFieldRequestSerializer(),
|
||||
"required": required,
|
||||
"allow_null": not required,
|
||||
**kwargs,
|
||||
}
|
||||
required = kwargs.pop("required", False)
|
||||
return FileFieldRequestSerializer(
|
||||
required=required, allow_null=not required, **kwargs
|
||||
)
|
||||
|
||||
def get_response_serializer_field(self, instance, **kwargs):
|
||||
|
@ -3913,7 +3904,8 @@ class MultipleSelectFieldType(
|
|||
"when getting or listing the field. "
|
||||
"You can also send a list of option names in which case the option are "
|
||||
"searched by name. The first one that matches is used. "
|
||||
"This field also accepts a string with names separated by a comma. "
|
||||
"This field also accepts a string with names separated by a comma or an "
|
||||
"array of file names. "
|
||||
"The response represents chosen field, but also the value and "
|
||||
"color is exposed."
|
||||
)
|
||||
|
|
|
@ -555,9 +555,7 @@ def test_file_field_type(api_client, data_fixture):
|
|||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
assert (
|
||||
response_json["detail"][f"field_{field_id}"][0]["name"][0]["code"] == "invalid"
|
||||
)
|
||||
assert response_json["detail"][f"field_{field_id}"][0]["code"] == "invalid"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:rows:list", kwargs={"table_id": table.id}),
|
||||
|
|
|
@ -89,6 +89,193 @@ def test_batch_create_rows_file_field(api_client, data_fixture):
|
|||
assert len(getattr(rows[2], f"field_{file_field.id}")) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.field_file
|
||||
@pytest.mark.api_rows
|
||||
def test_batch_create_rows_file_field_with_csv_string(api_client, data_fixture):
|
||||
user, jwt_token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
file_field = data_fixture.create_file_field(table=table)
|
||||
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
|
||||
file1 = data_fixture.create_user_file(
|
||||
original_name="test.txt",
|
||||
is_image=True,
|
||||
)
|
||||
file2 = data_fixture.create_user_file(
|
||||
original_name="test2.txt",
|
||||
is_image=True,
|
||||
)
|
||||
file3 = data_fixture.create_user_file(
|
||||
original_name="test3.txt",
|
||||
is_image=True,
|
||||
)
|
||||
|
||||
# Test with only one field name
|
||||
request_body = {
|
||||
"items": [
|
||||
{
|
||||
f"field_{file_field.id}": file1.name,
|
||||
},
|
||||
]
|
||||
}
|
||||
response = api_client.post(
|
||||
url,
|
||||
request_body,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
model = table.get_model()
|
||||
rows = model.objects.all()
|
||||
|
||||
assert len(getattr(rows[0], f"field_{file_field.id}")) == 1
|
||||
|
||||
# Test with multiple field names
|
||||
request_body = {
|
||||
"items": [
|
||||
{
|
||||
f"field_{file_field.id}": f"{file1.name},{file2.name},{file3.name}",
|
||||
},
|
||||
]
|
||||
}
|
||||
response = api_client.post(
|
||||
url,
|
||||
request_body,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
rows = model.objects.all()
|
||||
|
||||
assert len(getattr(rows[1], f"field_{file_field.id}")) == 3
|
||||
|
||||
# Test with array of field names
|
||||
request_body = {
|
||||
"items": [
|
||||
{
|
||||
f"field_{file_field.id}": [file1.name, file2.name],
|
||||
},
|
||||
]
|
||||
}
|
||||
response = api_client.post(
|
||||
url,
|
||||
request_body,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
rows = model.objects.all()
|
||||
|
||||
assert len(getattr(rows[2], f"field_{file_field.id}")) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.field_file
|
||||
@pytest.mark.api_rows
|
||||
def test_batch_create_rows_file_field_mixed_types(api_client, data_fixture):
|
||||
user, jwt_token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
file_field = data_fixture.create_file_field(table=table)
|
||||
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
|
||||
file1 = data_fixture.create_user_file(
|
||||
original_name="test.txt",
|
||||
is_image=True,
|
||||
)
|
||||
file2 = data_fixture.create_user_file(
|
||||
original_name="test2.txt",
|
||||
is_image=True,
|
||||
)
|
||||
|
||||
request_body = {
|
||||
"items": [
|
||||
{
|
||||
f"field_{file_field.id}": [file1.name, {"name": file2.name}],
|
||||
},
|
||||
]
|
||||
}
|
||||
response = api_client.post(
|
||||
url,
|
||||
request_body,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
response_json = response.json()
|
||||
assert response_json == {
|
||||
"error": "ERROR_REQUEST_BODY_VALIDATION",
|
||||
"detail": {
|
||||
"items": {
|
||||
"0": {
|
||||
f"field_{file_field.id}": [
|
||||
{
|
||||
"error": "All list values must be same type.",
|
||||
"code": "not_same_type",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.field_file
|
||||
@pytest.mark.api_rows
|
||||
def test_batch_create_rows_file_field_invalid_values(api_client, data_fixture):
|
||||
user, jwt_token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
file_field = data_fixture.create_file_field(table=table)
|
||||
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
|
||||
file1 = data_fixture.create_user_file(
|
||||
original_name="test.txt",
|
||||
is_image=True,
|
||||
)
|
||||
file2 = data_fixture.create_user_file(
|
||||
original_name="test2.txt",
|
||||
is_image=True,
|
||||
)
|
||||
|
||||
request_body = {
|
||||
"items": [
|
||||
{
|
||||
f"field_{file_field.id}": [1, 2],
|
||||
},
|
||||
]
|
||||
}
|
||||
response = api_client.post(
|
||||
url,
|
||||
request_body,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
response_json = response.json()
|
||||
assert response_json == {
|
||||
"error": "ERROR_REQUEST_BODY_VALIDATION",
|
||||
"detail": {
|
||||
"items": {
|
||||
"0": {
|
||||
f"field_{file_field.id}": [
|
||||
{
|
||||
"error": "The provided value should be a list of valid "
|
||||
"string or objects containing a value property.",
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.field_file
|
||||
@pytest.mark.api_rows
|
||||
|
@ -120,9 +307,11 @@ def test_batch_create_rows_file_field_without_name_property(api_client, data_fix
|
|||
"0": {
|
||||
f"field_{file_field.id}": [
|
||||
{
|
||||
"name": [
|
||||
{"error": "This field is required.", "code": "required"}
|
||||
]
|
||||
"error": (
|
||||
"A name property is required for all values "
|
||||
"of the list."
|
||||
),
|
||||
"code": "required",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -83,7 +83,9 @@ def test_batch_create_rows_link_row_field(api_client, data_fixture):
|
|||
@pytest.mark.django_db
|
||||
@pytest.mark.field_link_row
|
||||
@pytest.mark.api_rows
|
||||
def test_batch_create_rows_link_row_field_with_text_values(api_client, data_fixture):
|
||||
def test_batch_create_rows_link_row_field_with_other_value_types(
|
||||
api_client, data_fixture
|
||||
):
|
||||
user, jwt_token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
linked_table = data_fixture.create_database_table(
|
||||
|
@ -114,6 +116,12 @@ def test_batch_create_rows_link_row_field_with_text_values(api_client, data_fixt
|
|||
{
|
||||
f"field_{link_field.id}": "",
|
||||
},
|
||||
{
|
||||
f"field_{link_field.id}": ["Row 3", "Row 2"],
|
||||
},
|
||||
{
|
||||
f"field_{link_field.id}": linked_row_2.id,
|
||||
},
|
||||
]
|
||||
}
|
||||
expected_response_body = {
|
||||
|
@ -136,6 +144,21 @@ def test_batch_create_rows_link_row_field_with_text_values(api_client, data_fixt
|
|||
f"field_{link_field.id}": [],
|
||||
"order": "3.00000000000000000000",
|
||||
},
|
||||
{
|
||||
f"id": 4,
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_2.id, "value": "Row 2"},
|
||||
{"id": linked_row_3.id, "value": "Row 3"},
|
||||
],
|
||||
"order": "4.00000000000000000000",
|
||||
},
|
||||
{
|
||||
f"id": 5,
|
||||
f"field_{link_field.id}": [
|
||||
{"id": linked_row_2.id, "value": "Row 2"},
|
||||
],
|
||||
"order": "5.00000000000000000000",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -204,6 +227,74 @@ def test_batch_create_rows_link_row_field_with_invalid_text_values(
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.field_link_row
|
||||
@pytest.mark.api_rows
|
||||
def test_batch_create_rows_link_row_field_with_invalid_values(api_client, data_fixture):
|
||||
user, jwt_token = data_fixture.create_user_and_token()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
linked_table = data_fixture.create_database_table(
|
||||
user=user, database=table.database
|
||||
)
|
||||
linked_field = data_fixture.create_text_field(
|
||||
primary=True,
|
||||
name="Primary",
|
||||
table=linked_table,
|
||||
)
|
||||
linked_model = linked_table.get_model()
|
||||
linked_row_1 = linked_model.objects.create(**{f"field_{linked_field.id}": "Row 1"})
|
||||
linked_row_2 = linked_model.objects.create(**{f"field_{linked_field.id}": "Row 2"})
|
||||
linked_row_3 = linked_model.objects.create(**{f"field_{linked_field.id}": "Row 3"})
|
||||
link_field = FieldHandler().create_field(
|
||||
user, table, "link_row", link_row_table=linked_table, name="Link"
|
||||
)
|
||||
model = table.get_model()
|
||||
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
|
||||
request_body = {
|
||||
"items": [
|
||||
{
|
||||
f"field_{link_field.id}": 1.2,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
response = api_client.post(
|
||||
url,
|
||||
request_body,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
assert response.json()["detail"]["items"]["0"][link_field.db_column][0] == {
|
||||
"code": "invalid",
|
||||
"error": "This provided value must be a list, an integer or a string.",
|
||||
}
|
||||
|
||||
request_body = {
|
||||
"items": [
|
||||
{
|
||||
f"field_{link_field.id}": [1.2],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
response = api_client.post(
|
||||
url,
|
||||
request_body,
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
assert response.json()["detail"]["items"]["0"][link_field.db_column][0] == {
|
||||
"code": "invalid",
|
||||
"error": "The provided value must be a list of valid integer or string",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.field_link_row
|
||||
@pytest.mark.api_rows
|
||||
|
|
|
@ -780,7 +780,7 @@ def test_link_row_field_type_api_row_views(api_client, data_fixture):
|
|||
response = api_client.post(
|
||||
reverse("api:database:rows:list", kwargs={"table_id": example_table.id}),
|
||||
{
|
||||
f"field_{link_row_field.id}": {},
|
||||
f"field_{link_row_field.id}": {"something": 42},
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
|
@ -788,9 +788,7 @@ def test_link_row_field_type_api_row_views(api_client, data_fixture):
|
|||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
assert (
|
||||
response_json["detail"][f"field_{link_row_field.id}"][0]["code"] == "not_a_list"
|
||||
)
|
||||
assert response_json["detail"][f"field_{link_row_field.id}"][0]["code"] == "invalid"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:rows:list", kwargs={"table_id": example_table.id}),
|
||||
|
@ -819,10 +817,7 @@ def test_link_row_field_type_api_row_views(api_client, data_fixture):
|
|||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
assert (
|
||||
response_json["detail"][f"field_{link_row_field.id}"]["0"][0]["code"]
|
||||
== "invalid"
|
||||
)
|
||||
assert response_json["detail"][f"field_{link_row_field.id}"][0]["code"] == "invalid"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:rows:list", kwargs={"table_id": example_table.id}),
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "The API now accepts a single integer or a CSV string or an array of strings as value for link row field type and an array of string or a CSV string for file field type.",
|
||||
"issue_number": 2570,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-04-26"
|
||||
}
|
|
@ -139,7 +139,7 @@
|
|||
"readOnly": "This is a read only field.",
|
||||
"text": "Accepts single line text.",
|
||||
"longText": "Accepts multi line text. If the rich text formatting is enabled, you can use markdown to format the text.",
|
||||
"linkRow": "Accepts an array containing the identifiers or main field text values of the related rows from table id {table}. All identifiers must be provided every time the relations are updated. If an empty array is provided all relations will be deleted. In case of a text value instead of an identifier, a row with the matching value for its main field will be searched. If more than one match is found, the first one in the order of the table is selected. You can send a string with names separated by comma in which case the string is converted into an array of row names.",
|
||||
"linkRow": "Accepts an array containing the identifiers or main field text values of the related rows from table id {table}. All identifiers must be provided every time the relations are updated. If an empty array is provided all relations will be deleted. In case of a text value instead of an identifier, a row with the matching value for its main field will be searched. If more than one match is found, the first one in the order of the table is selected. You can send a string with names separated by comma in which case the string is converted into an array of row names. You can also send only an ID of a row alone.",
|
||||
"number": "Accepts a number.",
|
||||
"numberPositive": "Accepts a positive number.",
|
||||
"decimal": "Accepts a decimal with {places} decimal places after the dot.",
|
||||
|
@ -157,7 +157,7 @@
|
|||
"duration": "Accepts a time-based duration as a string in the format {format} or as a number representing the total number of seconds.",
|
||||
"url": "Accepts a string that must be a URL.",
|
||||
"email": "Accepts a string that must be an email address.",
|
||||
"file": "Accepts an array of objects containing at least the name of the user file. You can use the \"File uploads\" endpoints to upload the file. The response of those calls can be provided directly as object here. The endpoints can be found in the left sidebar.",
|
||||
"file": "Accepts an array of objects containing at least the name of the user file. Alternatively, you can provide a comma-separated list of file names or an array of file names. You can use the \"File uploads\" endpoints to upload the file. The response of those calls can be provided directly as object here. The endpoints can be found in the left sidebar.",
|
||||
"singleSelect": "Accepts an integer or a text value representing the chosen select option id or option value. A null value means none is selected. In case of a text value, the first matching option is selected.",
|
||||
"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. You can send a string with names separated by comma as value in which case the string is converted into an array of option names.",
|
||||
"phoneNumber": "Accepts a phone number which has a maximum length of 100 characters consisting solely of digits, spaces and the following characters: Nx,._+*()#=;/- .",
|
||||
|
|
Loading…
Add table
Reference in a new issue