1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-06 22:08:52 +00:00

Resolve "Collaborator Field Support in Form Views"

This commit is contained in:
Davide Silvestri 2025-02-24 07:46:11 +00:00
parent 591914fcc1
commit 2c7e0013d9
28 changed files with 282 additions and 58 deletions

View file

@ -89,7 +89,6 @@ from baserow.contrib.database.fields.exceptions import (
) )
from baserow.contrib.database.fields.handler import FieldHandler from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.job_types import DuplicateFieldJobType from baserow.contrib.database.fields.job_types import DuplicateFieldJobType
from baserow.contrib.database.fields.models import Field
from baserow.contrib.database.fields.operations import ( from baserow.contrib.database.fields.operations import (
CreateFieldOperationType, CreateFieldOperationType,
ListFieldsOperationType, ListFieldsOperationType,
@ -191,10 +190,9 @@ class FieldsView(APIView):
request, ["read", "create", "update"], table, False request, ["read", "create", "update"], table, False
) )
base_field_queryset = FieldHandler().get_base_fields_queryset()
fields = specific_iterator( fields = specific_iterator(
Field.objects.filter(table=table) base_field_queryset.filter(table=table),
.select_related("content_type")
.prefetch_related("select_options"),
per_content_type_queryset_hook=( per_content_type_queryset_hook=(
lambda field, queryset: field_type_registry.get_by_model( lambda field, queryset: field_type_registry.get_by_model(
field field

View file

@ -17,7 +17,10 @@ class TypeFormulaRequestSerializer(serializers.ModelSerializer):
class TypeFormulaResultSerializer(serializers.ModelSerializer): class TypeFormulaResultSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = FormulaField model = FormulaField
fields = FormulaFieldType.serializer_field_names fields = list(
set(FormulaFieldType.serializer_field_names)
- {"available_collaborators", "select_options"}
)
class BaserowFormulaSelectOptionsSerializer(serializers.ListField): class BaserowFormulaSelectOptionsSerializer(serializers.ListField):
@ -36,3 +39,21 @@ class BaserowFormulaSelectOptionsSerializer(serializers.ListField):
return [self.child.to_representation(item) for item in select_options] return [self.child.to_representation(item) for item in select_options]
else: else:
return [] return []
class BaserowFormulaCollaboratorsSerializer(serializers.ListField):
def get_attribute(self, instance):
return instance
def to_representation(self, field):
field_type = field_type_registry.get_by_model(field)
# Available collaborators are needed for view filters in the frontend,
# but let's avoid the potentially slow query if not required.
if field_type.can_represent_collaborators(field):
available_collaborators = field.table.database.workspace.users.all()
return [
self.child.to_representation(item) for item in available_collaborators
]
else:
return []

View file

@ -2394,14 +2394,11 @@ class LinkRowFieldType(
def enhance_field_queryset( def enhance_field_queryset(
self, queryset: QuerySet[Field], field: Field self, queryset: QuerySet[Field], field: Field
) -> QuerySet[Field]: ) -> QuerySet[Field]:
base_field_queryset = FieldHandler().get_base_fields_queryset()
return queryset.prefetch_related( return queryset.prefetch_related(
models.Prefetch( models.Prefetch(
"link_row_table__field_set", "link_row_table__field_set",
queryset=specific_queryset( queryset=specific_queryset(base_field_queryset.filter(primary=True)),
Field.objects.filter(primary=True)
.select_related("content_type")
.prefetch_related("select_options")
),
to_attr=LinkRowField.RELATED_PPRIMARY_FIELD_ATTR, to_attr=LinkRowField.RELATED_PPRIMARY_FIELD_ATTR,
) )
) )
@ -5317,6 +5314,9 @@ class FormulaFieldType(FormulaFieldTypeArrayFilterSupport, ReadOnlyFieldType):
def can_represent_select_options(self, field): def can_represent_select_options(self, field):
return self.to_baserow_formula_type(field.specific).can_represent_select_options return self.to_baserow_formula_type(field.specific).can_represent_select_options
def can_represent_collaborators(self, field):
return self.to_baserow_formula_type(field.specific).can_represent_collaborators
def get_permission_error_when_user_changes_field_to_depend_on_forbidden_field( def get_permission_error_when_user_changes_field_to_depend_on_forbidden_field(
self, user: AbstractUser, changed_field: Field, forbidden_field: Field self, user: AbstractUser, changed_field: Field, forbidden_field: Field
) -> Exception: ) -> Exception:
@ -5518,6 +5518,11 @@ class CountFieldType(FormulaFieldType):
target_field_name = target_field["name"] target_field_name = target_field["name"]
return {(target_field_name, through_field_name)} return {(target_field_name, through_field_name)}
def enhance_field_queryset(
self, queryset: QuerySet[Field], field: Field
) -> QuerySet[Field]:
return queryset.select_related("through_field")
class RollupFieldType(FormulaFieldType): class RollupFieldType(FormulaFieldType):
type = "rollup" type = "rollup"
@ -5722,6 +5727,11 @@ class RollupFieldType(FormulaFieldType):
target_field_name = target_field["name"] target_field_name = target_field["name"]
return {(target_field_name, via_field_name)} return {(target_field_name, via_field_name)}
def enhance_field_queryset(
self, queryset: QuerySet[Field], field: Field
) -> QuerySet[Field]:
return queryset.select_related("through_field", "target_field")
class LookupFieldType(FormulaFieldType): class LookupFieldType(FormulaFieldType):
type = "lookup" type = "lookup"
@ -6020,6 +6030,11 @@ class LookupFieldType(FormulaFieldType):
return {(target_field_name, via_field_name)} return {(target_field_name, via_field_name)}
def enhance_field_queryset(
self, queryset: QuerySet[Field], field: Field
) -> QuerySet[Field]:
return queryset.select_related("through_field", "target_field")
class MultipleCollaboratorsFieldType( class MultipleCollaboratorsFieldType(
CollationSortMixin, ManyToManyFieldTypeSerializeToInputValueMixin, FieldType CollationSortMixin, ManyToManyFieldTypeSerializeToInputValueMixin, FieldType
@ -6027,11 +6042,15 @@ class MultipleCollaboratorsFieldType(
type = "multiple_collaborators" type = "multiple_collaborators"
model_class = MultipleCollaboratorsField model_class = MultipleCollaboratorsField
can_get_unique_values = False can_get_unique_values = False
can_be_in_form_view = False
allowed_fields = ["notify_user_when_added"] allowed_fields = ["notify_user_when_added"]
serializer_field_names = ["notify_user_when_added"] serializer_field_names = ["available_collaborators", "notify_user_when_added"]
serializer_field_overrides = { serializer_field_overrides = {
"notify_user_when_added": serializers.BooleanField(required=False) "available_collaborators": serializers.ListField(
child=CollaboratorSerializer(),
read_only=True,
source="table.database.workspace.users.all",
),
"notify_user_when_added": serializers.BooleanField(required=False),
} }
is_many_to_many_field = True is_many_to_many_field = True
@ -6078,6 +6097,9 @@ class MultipleCollaboratorsFieldType(
} }
) )
def serialize_to_input_value(self, field: Field, value: any) -> any:
return [{"id": u.id, "name": u.first_name} for u in value.all()]
def prepare_value_for_db(self, instance, value): def prepare_value_for_db(self, instance, value):
if not isinstance( if not isinstance(
value, value,

View file

@ -17,7 +17,7 @@ from typing import (
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import connection from django.db import connection
from django.db.models import QuerySet from django.db.models import Prefetch, QuerySet
from django.db.utils import DatabaseError, DataError, ProgrammingError from django.db.utils import DatabaseError, DataError, ProgrammingError
from loguru import logger from loguru import logger
@ -53,7 +53,7 @@ from baserow.contrib.database.table.models import Table
from baserow.contrib.database.views.handler import ViewHandler from baserow.contrib.database.views.handler import ViewHandler
from baserow.core.db import specific_iterator from baserow.core.db import specific_iterator
from baserow.core.handler import CoreHandler from baserow.core.handler import CoreHandler
from baserow.core.models import TrashEntry from baserow.core.models import TrashEntry, User
from baserow.core.telemetry.utils import baserow_trace_methods from baserow.core.telemetry.utils import baserow_trace_methods
from baserow.core.trash.exceptions import RelatedTableTrashedException from baserow.core.trash.exceptions import RelatedTableTrashedException
from baserow.core.trash.handler import TrashHandler from baserow.core.trash.handler import TrashHandler
@ -239,6 +239,26 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
else: else:
return filtered_qs return filtered_qs
def get_base_fields_queryset(self) -> QuerySet[Field]:
"""
Returns a base queryset with proper select and prefetch related fields to use in
queries that need to fetch fields.
:return: A queryset with select and prefetch related fields set.
"""
return Field.objects.select_related(
"content_type", "table__database__workspace"
).prefetch_related(
Prefetch(
"table__database__workspace__users",
queryset=User.objects.filter(profile__to_be_deleted=False).order_by(
"first_name"
),
),
"select_options",
)
def get_fields( def get_fields(
self, self,
table: Table, table: Table,

View file

@ -1786,6 +1786,11 @@ class FieldType(
return False return False
def can_represent_collaborators(self, field):
"""Indicates whether the field can be used to represent collaborators."""
return False
def get_permission_error_when_user_changes_field_to_depend_on_forbidden_field( def get_permission_error_when_user_changes_field_to_depend_on_forbidden_field(
self, user: AbstractUser, changed_field: Field, forbidden_field: Field self, user: AbstractUser, changed_field: Field, forbidden_field: Field
) -> Exception: ) -> Exception:

View file

@ -277,6 +277,10 @@ class BaserowFormulaType(abc.ABC):
def can_represent_select_options(self) -> bool: def can_represent_select_options(self) -> bool:
return False return False
@property
def can_represent_collaborators(self) -> bool:
return False
@property @property
def item_is_in_nested_value_object_when_in_array(self) -> bool: def item_is_in_nested_value_object_when_in_array(self) -> bool:
return True return True

View file

@ -1339,12 +1339,17 @@ class BaserowFormulaArrayType(
def can_represent_select_options(self, field) -> bool: def can_represent_select_options(self, field) -> bool:
return self.sub_type.can_represent_select_options(field) return self.sub_type.can_represent_select_options(field)
def can_represent_collaborators(self, field):
return self.sub_type.can_represent_collaborators(field)
@classmethod @classmethod
def get_serializer_field_overrides(cls): def get_serializer_field_overrides(cls):
from baserow.contrib.database.api.fields.serializers import ( from baserow.contrib.database.api.fields.serializers import (
CollaboratorSerializer,
SelectOptionSerializer, SelectOptionSerializer,
) )
from baserow.contrib.database.api.formula.serializers import ( from baserow.contrib.database.api.formula.serializers import (
BaserowFormulaCollaboratorsSerializer,
BaserowFormulaSelectOptionsSerializer, BaserowFormulaSelectOptionsSerializer,
) )
@ -1354,7 +1359,13 @@ class BaserowFormulaArrayType(
required=False, required=False,
allow_null=True, allow_null=True,
read_only=True, read_only=True,
) ),
"available_collaborators": BaserowFormulaCollaboratorsSerializer(
child=CollaboratorSerializer(),
required=False,
allow_null=True,
read_only=True,
),
} }
def parse_filter_value(self, field, model_field, value): def parse_filter_value(self, field, model_field, value):
@ -1656,7 +1667,7 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType):
return "p_in = '';" return "p_in = '';"
@property @property
def can_represent_select_options(self) -> bool: def can_represent_collaborators(self) -> bool:
return True return True
@property @property
@ -1683,12 +1694,13 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType):
setattr( setattr(
field, field,
cache_key, cache_key,
list(field.table.database.workspace.users.order_by("id").all()), {usr.id: usr for usr in field.table.database.workspace.users.all()},
) )
user_ids = set((item["id"] for item in value)) # Replace the JSON object with the actual user object, so we have
workspace_users = getattr(field, cache_key) # access to the user's email.
value = [user for user in workspace_users if user.id in user_ids] users = getattr(field, cache_key)
value = [users[item["id"]] for item in value]
return field_type.get_export_value(value, field_object, rich_value=rich_value) return field_type.get_export_value(value, field_object, rich_value=rich_value)
@ -1764,6 +1776,28 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType):
return "first_name" return "first_name"
@classmethod
def get_serializer_field_overrides(cls):
from baserow.contrib.database.api.fields.serializers import (
CollaboratorSerializer,
)
from baserow.contrib.database.api.formula.serializers import (
BaserowFormulaCollaboratorsSerializer,
)
return {
"available_collaborators": BaserowFormulaCollaboratorsSerializer(
child=CollaboratorSerializer(),
allow_null=True,
required=False,
read_only=True,
)
}
@classmethod
def get_serializer_field_names(cls) -> List[str]:
return super().all_fields() + ["available_collaborators"]
BASEROW_FORMULA_TYPES = [ BASEROW_FORMULA_TYPES = [
BaserowFormulaInvalidType, BaserowFormulaInvalidType,

View file

@ -20,7 +20,7 @@ from typing import (
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS, connection, transaction from django.db import DEFAULT_DB_ALIAS, connection, transaction
from django.db.models import ForeignKey, ManyToManyField, Max, Model, QuerySet from django.db.models import ForeignKey, ManyToManyField, Max, Model, Prefetch, QuerySet
from django.db.models.functions import Collate from django.db.models.functions import Collate
from django.db.models.sql.query import LOOKUP_SEP from django.db.models.sql.query import LOOKUP_SEP
from django.db.transaction import Atomic, get_connection from django.db.transaction import Atomic, get_connection
@ -110,12 +110,14 @@ def specific_iterator(
if isinstance(select_related, bool): if isinstance(select_related, bool):
select_related_keys = [] select_related_keys = []
else: else:
select_related_keys = select_related.keys() select_related_keys = list(select_related.keys())
# Nested prefetch result in cached objects to avoid additional queries. If # Nested prefetch result in cached objects to avoid additional queries. If
# they're present, they must be added to the `select_related_keys` to make sure # they're present, they must be added to the `select_related_keys` to make sure
# they're correctly set on the specific objects. # they're correctly set on the specific objects.
for lookup in queryset_or_list._prefetch_related_lookups: for lookup in queryset_or_list._prefetch_related_lookups:
if isinstance(lookup, Prefetch):
lookup = lookup.prefetch_through
split_lookup = lookup.split(LOOKUP_SEP)[:-1] split_lookup = lookup.split(LOOKUP_SEP)[:-1]
if split_lookup and split_lookup[0] not in select_related_keys: if split_lookup and split_lookup[0] not in select_related_keys:
select_related_keys.append(split_lookup[0]) select_related_keys.append(split_lookup[0])

View file

@ -74,6 +74,7 @@
<mj-class <mj-class
name="notification-description" name="notification-description"
font-size="12px" font-size="12px"
line-height="18px"
color="#838387" color="#838387"
font-family="Inter,sans-serif" font-family="Inter,sans-serif"
/> />

View file

@ -206,7 +206,7 @@
<!-- htmlmin:ignore --> <!-- htmlmin:ignore -->
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Inter,sans-serif;font-size:12px;line-height:1;text-align:left;color:#838387;">{{ notification.description|linebreaksbr }}</div> <div style="font-family:Inter,sans-serif;font-size:12px;line-height:18px;text-align:left;color:#838387;">{{ notification.description|linebreaksbr }}</div>
</td> </td>
</tr> </tr>
<!-- htmlmin:ignore -->{% endif %} <!-- htmlmin:ignore -->{% endif %}
@ -225,7 +225,7 @@
<!-- htmlmin:ignore --> <!-- htmlmin:ignore -->
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Inter,sans-serif;font-size:12px;line-height:1;text-align:left;color:#838387;">{% blocktrans trimmed count counter=unlisted_notifications_count %} Plus {{ counter }} more notification. {% plural %} Plus {{ counter }} more notifications. {% endblocktrans %}</div> <div style="font-family:Inter,sans-serif;font-size:12px;line-height:18px;text-align:left;color:#838387;">{% blocktrans trimmed count counter=unlisted_notifications_count %} Plus {{ counter }} more notification. {% plural %} Plus {{ counter }} more notifications. {% endblocktrans %}</div>
</td> </td>
</tr> </tr>
<!-- htmlmin:ignore -->{% endif %} <!-- htmlmin:ignore -->{% endif %}

View file

@ -970,7 +970,6 @@ def test_can_type_a_valid_formula_field(data_fixture, api_client):
"number_prefix": "", "number_prefix": "",
"number_separator": "", "number_separator": "",
"number_suffix": "", "number_suffix": "",
"select_options": [],
} }

View file

@ -31,6 +31,10 @@ def test_multiple_collaborators_field_type_create(api_client, data_fixture):
assert response.status_code == HTTP_200_OK assert response.status_code == HTTP_200_OK
assert response_json["name"] == "Collaborator 1" assert response_json["name"] == "Collaborator 1"
assert response_json["type"] == "multiple_collaborators" assert response_json["type"] == "multiple_collaborators"
assert response_json["notify_user_when_added"] is False
assert response_json["available_collaborators"] == [
{"id": user.id, "name": user.first_name}
]
@pytest.mark.field_multiple_collaborators @pytest.mark.field_multiple_collaborators
@ -43,6 +47,7 @@ def test_multiple_collaborators_field_type_update(api_client, data_fixture):
first_name="Test1", first_name="Test1",
workspace=workspace, workspace=workspace,
) )
user_2 = data_fixture.create_user(workspace=workspace, first_name="Test2")
database = data_fixture.create_database_application( database = data_fixture.create_database_application(
user=user, name="Placeholder", workspace=workspace user=user, name="Placeholder", workspace=workspace
) )
@ -62,6 +67,10 @@ def test_multiple_collaborators_field_type_update(api_client, data_fixture):
assert response.status_code == HTTP_200_OK assert response.status_code == HTTP_200_OK
assert response_json["name"] == "New collaborator 1" assert response_json["name"] == "New collaborator 1"
assert response_json["type"] == "multiple_collaborators" assert response_json["type"] == "multiple_collaborators"
assert response_json["available_collaborators"] == [
{"id": user.id, "name": user.first_name},
{"id": user_2.id, "name": user_2.first_name},
]
@pytest.mark.field_multiple_collaborators @pytest.mark.field_multiple_collaborators

View file

@ -1361,12 +1361,25 @@ def test_form_view_multiple_collaborators_field_options(api_client, data_fixture
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
response_json = response.json() response_json = response.json()
assert response.status_code == HTTP_400_BAD_REQUEST assert response.status_code == HTTP_200_OK
assert response_json["error"] == "ERROR_FORM_VIEW_FIELD_TYPE_IS_NOT_SUPPORTED" assert response_json == {
assert ( "field_options": {
response_json["detail"] str(multiple_collaborators_field.id): {
== "The multiple_collaborators field type is not compatible with the form view." "name": "",
) "description": "",
"enabled": True,
"required": True,
"order": 32767,
"show_when_matching_conditions": False,
"condition_type": "AND",
"condition_groups": [],
"conditions": [],
"field_component": "default",
"include_all_select_options": True,
"allowed_select_options": [],
}
}
}
@pytest.mark.django_db @pytest.mark.django_db

View file

@ -2,7 +2,7 @@ from unittest.mock import MagicMock
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import connection from django.db import connection
from django.db.models import CharField, Value from django.db.models import CharField, Prefetch, Value
from django.db.models.expressions import ExpressionWrapper from django.db.models.expressions import ExpressionWrapper
from django.db.models.functions import Concat from django.db.models.functions import Concat
from django.test.utils import override_settings from django.test.utils import override_settings
@ -17,7 +17,12 @@ from baserow.contrib.database.fields.models import (
TextField, TextField,
) )
from baserow.contrib.database.rows.handler import RowHandler from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.views.models import GalleryView, GridView, View from baserow.contrib.database.views.models import (
GalleryView,
GridView,
View,
ViewFilter,
)
from baserow.core.db import ( from baserow.core.db import (
CombinedForeignKeyAndManyToManyMultipleFieldPrefetch, CombinedForeignKeyAndManyToManyMultipleFieldPrefetch,
LockedAtomicTransaction, LockedAtomicTransaction,
@ -192,8 +197,12 @@ def test_specific_iterator_with_select_related(data_fixture, django_assert_num_q
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize(
"prefetch_related",
["viewfilter_set", Prefetch("viewfilter_set", queryset=ViewFilter.objects.all())],
)
def test_specific_iterator_with_prefetch_related( def test_specific_iterator_with_prefetch_related(
data_fixture, django_assert_num_queries prefetch_related, data_fixture, django_assert_num_queries
): ):
grid_view = data_fixture.create_grid_view() grid_view = data_fixture.create_grid_view()
gallery_view = data_fixture.create_gallery_view() gallery_view = data_fixture.create_gallery_view()
@ -209,7 +218,7 @@ def test_specific_iterator_with_prefetch_related(
] ]
) )
.order_by("id") .order_by("id")
.prefetch_related("viewfilter_set") .prefetch_related(prefetch_related)
) )
with django_assert_num_queries(4): with django_assert_num_queries(4):

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Add support for collaborator fields in form views.",
"issue_number": 1554,
"bullet_points": [],
"created_at": "2025-02-12"
}

View file

@ -144,7 +144,9 @@
"singleSelectRadios": "Radios", "singleSelectRadios": "Radios",
"autonumber": "Autonumber", "autonumber": "Autonumber",
"password": "Password", "password": "Password",
"ai": "AI prompt" "ai": "AI prompt",
"multipleCollaboratorsDropdown": "Dropdown",
"multipleCollaboratorsCheckboxes": "Checkboxes"
}, },
"fieldErrors": { "fieldErrors": {
"invalidNumber": "Invalid number", "invalidNumber": "Invalid number",

View file

@ -59,6 +59,7 @@
.notification-panel__notification-content { .notification-panel__notification-content {
flex-grow: 1; flex-grow: 1;
max-width: calc(100% - 34px - 16px); // 34px for icon, 16px for status
strong { strong {
font-weight: 600; font-weight: 600;
@ -105,6 +106,13 @@
margin-bottom: 8px; margin-bottom: 8px;
} }
.notification-panel__notification-content-summary-item {
@extend %ellipsis;
display: block;
max-width: 100%;
}
.notification-panel__notification-status { .notification-panel__notification-status {
flex: 0 0 16px; flex: 0 0 16px;
text-align: right; text-align: right;

View file

@ -16,7 +16,7 @@ export default {
} }
}, },
search() { search() {
this.fetch(this.page, this.query) this.fetch(1, this.query)
}, },
hide() { hide() {
// Call the dropdown `hide` method because it resets the search. We're // Call the dropdown `hide` method because it resets the search. We're

View file

@ -125,7 +125,12 @@ export default {
if (replace) { if (replace) {
this.results = results this.results = results
} else { } else {
this.results.push(...results) const resultIds = new Set(this.results.map((res) => res.id))
this.results.push(
...results.filter((res) => {
return !resultIds.has(res.id)
})
)
} }
this.count = count this.count = count

View file

@ -65,9 +65,9 @@
></FieldCollaboratorDropdownItem> ></FieldCollaboratorDropdownItem>
<FieldCollaboratorDropdownItem <FieldCollaboratorDropdownItem
v-for="collaborator in results" v-for="collaborator in results"
:key="collaborator.user_id" :key="collaborator.id"
:name="collaborator.name" :name="collaborator.name"
:value="collaborator.user_id" :value="collaborator.id"
></FieldCollaboratorDropdownItem> ></FieldCollaboratorDropdownItem>
</ul> </ul>
<div v-if="isNotFound" class="select__description"> <div v-if="isNotFound" class="select__description">
@ -95,6 +95,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
pageSize: {
type: Number,
required: false,
default: 100, // override default pageSize of 20
},
}, },
computed: { computed: {
isNotFound() { isNotFound() {

View file

@ -17,7 +17,9 @@
<div class="notification-panel__notification-content-desc"> <div class="notification-panel__notification-content-desc">
<ul class="notification-panel__notification-content-summary"> <ul class="notification-panel__notification-content-summary">
<li v-for="(elem, index) in submittedValuesSummary" :key="index"> <li v-for="(elem, index) in submittedValuesSummary" :key="index">
{{ elem.field }}: {{ elem.value }} <span class="notification-panel__notification-content-summary-item"
>{{ elem.field }}: {{ elem.value }}</span
>
</li> </li>
</ul> </ul>
<div v-if="hiddenFieldsCount > 0"> <div v-if="hiddenFieldsCount > 0">

View file

@ -27,7 +27,7 @@ export default {
}, },
initialDisplayName() { initialDisplayName() {
const selected = this.workspaceCollaborators.find( const selected = this.workspaceCollaborators.find(
(c) => c.user_id === this.copy (c) => c.id === this.copy
) )
return selected?.name return selected?.name
}, },

View file

@ -0,0 +1,52 @@
<template>
<div class="control__elements">
<div
v-if="field.available_collaborators.length === 0"
class="control--messages"
>
<p>{{ $t('formViewField.noCollaboratorsAvailable') }}</p>
</div>
<div v-for="option in field.available_collaborators" :key="option.id">
<Checkbox
:checked="value.findIndex((o) => o.id === option.id) !== -1"
class="margin-bottom-1"
@input=";[touch(), toggleValue(option.id, value)]"
>{{ option.name }}</Checkbox
>
</div>
<div v-if="!required" class="margin-top-1">
<a @click=";[touch(), clear(value)]">clear value</a>
</div>
<div v-show="touched && !valid" class="error">
{{ error }}
</div>
</div>
</template>
<script>
import rowEditField from '@baserow/modules/database/mixins/rowEditField'
import collaboratorField from '@baserow/modules/database/mixins/collaboratorField'
export default {
name: 'FormViewFieldMultipleCollaboratorsCheckboxes',
mixins: [rowEditField, collaboratorField],
methods: {
toggleValue(id, oldValue) {
const index = oldValue.findIndex((option) => option.id === id)
if (index === -1) {
this.updateValue(id, oldValue)
} else {
this.removeValue(null, oldValue, id)
}
},
clear(oldValue) {
const newValue = []
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
this.$emit('update', newValue, oldValue)
}
},
},
}
</script>

View file

@ -138,6 +138,7 @@ import RowHistoryFieldPassword from '@baserow/modules/database/components/row/Ro
import FormViewFieldLinkRow from '@baserow/modules/database/components/view/form/FormViewFieldLinkRow' import FormViewFieldLinkRow from '@baserow/modules/database/components/view/form/FormViewFieldLinkRow'
import FormViewFieldMultipleLinkRow from '@baserow/modules/database/components/view/form/FormViewFieldMultipleLinkRow' import FormViewFieldMultipleLinkRow from '@baserow/modules/database/components/view/form/FormViewFieldMultipleLinkRow'
import FormViewFieldMultipleSelectCheckboxes from '@baserow/modules/database/components/view/form/FormViewFieldMultipleSelectCheckboxes' import FormViewFieldMultipleSelectCheckboxes from '@baserow/modules/database/components/view/form/FormViewFieldMultipleSelectCheckboxes'
import FormViewFieldMultipleCollaboratorsCheckboxes from '@baserow/modules/database/components/view/form/FormViewFieldMultipleCollaboratorsCheckboxes'
import FormViewFieldSingleSelectRadios from '@baserow/modules/database/components/view/form/FormViewFieldSingleSelectRadios' import FormViewFieldSingleSelectRadios from '@baserow/modules/database/components/view/form/FormViewFieldSingleSelectRadios'
import { import {
@ -4159,7 +4160,20 @@ export class MultipleCollaboratorsFieldType extends FieldType {
} }
getFormViewFieldComponents(field) { getFormViewFieldComponents(field) {
return {} const { i18n } = this.app
const components = super.getFormViewFieldComponents(field)
components[DEFAULT_FORM_VIEW_FIELD_COMPONENT_KEY].name = i18n.t(
'fieldType.multipleCollaboratorsDropdown'
)
components[DEFAULT_FORM_VIEW_FIELD_COMPONENT_KEY].properties = {
'allow-create-options': false,
}
components.checkboxes = {
name: i18n.t('fieldType.multipleCollaboratorsCheckboxes'),
component: FormViewFieldMultipleCollaboratorsCheckboxes,
properties: {},
}
return components
} }
getEmptyValue() { getEmptyValue() {

View file

@ -801,7 +801,8 @@
"addCondition": "Add condition", "addCondition": "Add condition",
"addConditionGroup": "Add condition group", "addConditionGroup": "Add condition group",
"showFieldAs": "Show field as", "showFieldAs": "Show field as",
"noSelectOptions": "There are no select options available." "noSelectOptions": "There are no select options available.",
"noCollaboratorsAvailable": "There are no collaborators available."
}, },
"duplicateFieldContext": { "duplicateFieldContext": {
"duplicate": "Duplicate field", "duplicate": "Duplicate field",

View file

@ -1,8 +1,7 @@
export default { export default {
computed: { computed: {
workspaceCollaborators() { workspaceCollaborators() {
const workspace = this.$store.getters['workspace/getSelected'] return this.field.available_collaborators
return workspace.users.filter((user) => user.to_be_deleted === false)
}, },
availableCollaborators() { availableCollaborators() {
// When converting from a CollaboratorField to another field it can happen // When converting from a CollaboratorField to another field it can happen
@ -15,7 +14,7 @@ export default {
const ids = new Set(this.value.map((item) => item.id)) const ids = new Set(this.value.map((item) => item.id))
const result = this.workspaceCollaborators.filter( const result = this.workspaceCollaborators.filter(
(item) => !ids.has(item.user_id) (item) => !ids.has(item.id)
) )
return result return result
}, },

View file

@ -25,13 +25,13 @@ export default {
const workspaceUser = const workspaceUser =
this.workspaceCollaborators.find( this.workspaceCollaborators.find(
(workspaceUser) => workspaceUser.user_id === newId (workspaceUser) => workspaceUser.id === newId
) || null ) || null
let newOption = null let newOption = null
if (workspaceUser) { if (workspaceUser) {
newOption = { newOption = {
id: workspaceUser.user_id, id: workspaceUser.id,
name: workspaceUser.name, name: workspaceUser.name,
} }
} }

View file

@ -2445,10 +2445,6 @@ export class MultipleCollaboratorsHasFilterType extends ViewFilterType {
] ]
} }
isAllowedInPublicViews() {
return false
}
matches(rowValue, filterValue, field, fieldType) { matches(rowValue, filterValue, field, fieldType) {
if (!isNumeric(filterValue)) { if (!isNumeric(filterValue)) {
return true return true
@ -2484,10 +2480,6 @@ export class MultipleCollaboratorsHasNotFilterType extends ViewFilterType {
] ]
} }
isAllowedInPublicViews() {
return false
}
matches(rowValue, filterValue, field, fieldType) { matches(rowValue, filterValue, field, fieldType) {
if (!isNumeric(filterValue)) { if (!isNumeric(filterValue)) {
return true return true