diff --git a/backend/src/baserow/contrib/database/api/fields/views.py b/backend/src/baserow/contrib/database/api/fields/views.py index af0030287..9a7ace1e1 100644 --- a/backend/src/baserow/contrib/database/api/fields/views.py +++ b/backend/src/baserow/contrib/database/api/fields/views.py @@ -89,7 +89,6 @@ from baserow.contrib.database.fields.exceptions import ( ) from baserow.contrib.database.fields.handler import FieldHandler from baserow.contrib.database.fields.job_types import DuplicateFieldJobType -from baserow.contrib.database.fields.models import Field from baserow.contrib.database.fields.operations import ( CreateFieldOperationType, ListFieldsOperationType, @@ -191,10 +190,9 @@ class FieldsView(APIView): request, ["read", "create", "update"], table, False ) + base_field_queryset = FieldHandler().get_base_fields_queryset() fields = specific_iterator( - Field.objects.filter(table=table) - .select_related("content_type") - .prefetch_related("select_options"), + base_field_queryset.filter(table=table), per_content_type_queryset_hook=( lambda field, queryset: field_type_registry.get_by_model( field diff --git a/backend/src/baserow/contrib/database/api/formula/serializers.py b/backend/src/baserow/contrib/database/api/formula/serializers.py index cdd72475c..f777091f3 100644 --- a/backend/src/baserow/contrib/database/api/formula/serializers.py +++ b/backend/src/baserow/contrib/database/api/formula/serializers.py @@ -17,7 +17,10 @@ class TypeFormulaRequestSerializer(serializers.ModelSerializer): class TypeFormulaResultSerializer(serializers.ModelSerializer): class Meta: model = FormulaField - fields = FormulaFieldType.serializer_field_names + fields = list( + set(FormulaFieldType.serializer_field_names) + - {"available_collaborators", "select_options"} + ) class BaserowFormulaSelectOptionsSerializer(serializers.ListField): @@ -36,3 +39,21 @@ class BaserowFormulaSelectOptionsSerializer(serializers.ListField): return [self.child.to_representation(item) for item in select_options] else: 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 [] diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py index bc3a34d68..7be858c65 100755 --- a/backend/src/baserow/contrib/database/fields/field_types.py +++ b/backend/src/baserow/contrib/database/fields/field_types.py @@ -2394,14 +2394,11 @@ class LinkRowFieldType( def enhance_field_queryset( self, queryset: QuerySet[Field], field: Field ) -> QuerySet[Field]: + base_field_queryset = FieldHandler().get_base_fields_queryset() return queryset.prefetch_related( models.Prefetch( "link_row_table__field_set", - queryset=specific_queryset( - Field.objects.filter(primary=True) - .select_related("content_type") - .prefetch_related("select_options") - ), + queryset=specific_queryset(base_field_queryset.filter(primary=True)), to_attr=LinkRowField.RELATED_PPRIMARY_FIELD_ATTR, ) ) @@ -5317,6 +5314,9 @@ class FormulaFieldType(FormulaFieldTypeArrayFilterSupport, ReadOnlyFieldType): def can_represent_select_options(self, field): 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( self, user: AbstractUser, changed_field: Field, forbidden_field: Field ) -> Exception: @@ -5518,6 +5518,11 @@ class CountFieldType(FormulaFieldType): target_field_name = target_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): type = "rollup" @@ -5722,6 +5727,11 @@ class RollupFieldType(FormulaFieldType): target_field_name = target_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): type = "lookup" @@ -6020,6 +6030,11 @@ class LookupFieldType(FormulaFieldType): 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( CollationSortMixin, ManyToManyFieldTypeSerializeToInputValueMixin, FieldType @@ -6027,11 +6042,15 @@ class MultipleCollaboratorsFieldType( type = "multiple_collaborators" model_class = MultipleCollaboratorsField can_get_unique_values = False - can_be_in_form_view = False 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 = { - "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 @@ -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): if not isinstance( value, diff --git a/backend/src/baserow/contrib/database/fields/handler.py b/backend/src/baserow/contrib/database/fields/handler.py index 92cbc141c..270e4dfcb 100644 --- a/backend/src/baserow/contrib/database/fields/handler.py +++ b/backend/src/baserow/contrib/database/fields/handler.py @@ -17,7 +17,7 @@ from typing import ( from django.conf import settings from django.contrib.auth.models import AbstractUser 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 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.core.db import specific_iterator 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.trash.exceptions import RelatedTableTrashedException from baserow.core.trash.handler import TrashHandler @@ -239,6 +239,26 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)): else: 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( self, table: Table, diff --git a/backend/src/baserow/contrib/database/fields/registries.py b/backend/src/baserow/contrib/database/fields/registries.py index 9690cb5c3..cbd19016d 100644 --- a/backend/src/baserow/contrib/database/fields/registries.py +++ b/backend/src/baserow/contrib/database/fields/registries.py @@ -1786,6 +1786,11 @@ class FieldType( 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( self, user: AbstractUser, changed_field: Field, forbidden_field: Field ) -> Exception: diff --git a/backend/src/baserow/contrib/database/formula/types/formula_type.py b/backend/src/baserow/contrib/database/formula/types/formula_type.py index d5f48527f..42e2cc936 100644 --- a/backend/src/baserow/contrib/database/formula/types/formula_type.py +++ b/backend/src/baserow/contrib/database/formula/types/formula_type.py @@ -277,6 +277,10 @@ class BaserowFormulaType(abc.ABC): def can_represent_select_options(self) -> bool: return False + @property + def can_represent_collaborators(self) -> bool: + return False + @property def item_is_in_nested_value_object_when_in_array(self) -> bool: return True diff --git a/backend/src/baserow/contrib/database/formula/types/formula_types.py b/backend/src/baserow/contrib/database/formula/types/formula_types.py index 57e8cf578..ba7bfcd92 100644 --- a/backend/src/baserow/contrib/database/formula/types/formula_types.py +++ b/backend/src/baserow/contrib/database/formula/types/formula_types.py @@ -1339,12 +1339,17 @@ class BaserowFormulaArrayType( def can_represent_select_options(self, field) -> bool: return self.sub_type.can_represent_select_options(field) + def can_represent_collaborators(self, field): + return self.sub_type.can_represent_collaborators(field) + @classmethod def get_serializer_field_overrides(cls): from baserow.contrib.database.api.fields.serializers import ( + CollaboratorSerializer, SelectOptionSerializer, ) from baserow.contrib.database.api.formula.serializers import ( + BaserowFormulaCollaboratorsSerializer, BaserowFormulaSelectOptionsSerializer, ) @@ -1354,7 +1359,13 @@ class BaserowFormulaArrayType( required=False, allow_null=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): @@ -1656,7 +1667,7 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType): return "p_in = '';" @property - def can_represent_select_options(self) -> bool: + def can_represent_collaborators(self) -> bool: return True @property @@ -1683,12 +1694,13 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType): setattr( field, 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)) - workspace_users = getattr(field, cache_key) - value = [user for user in workspace_users if user.id in user_ids] + # Replace the JSON object with the actual user object, so we have + # access to the user's email. + 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) @@ -1764,6 +1776,28 @@ class BaserowFormulaMultipleCollaboratorsType(BaserowJSONBObjectBaseType): 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 = [ BaserowFormulaInvalidType, diff --git a/backend/src/baserow/core/db.py b/backend/src/baserow/core/db.py index fbb673143..d44b083b4 100644 --- a/backend/src/baserow/core/db.py +++ b/backend/src/baserow/core/db.py @@ -20,7 +20,7 @@ from typing import ( from django.conf import settings from django.contrib.contenttypes.models import ContentType 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.sql.query import LOOKUP_SEP from django.db.transaction import Atomic, get_connection @@ -110,12 +110,14 @@ def specific_iterator( if isinstance(select_related, bool): select_related_keys = [] 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 # they're present, they must be added to the `select_related_keys` to make sure # they're correctly set on the specific objects. for lookup in queryset_or_list._prefetch_related_lookups: + if isinstance(lookup, Prefetch): + lookup = lookup.prefetch_through split_lookup = lookup.split(LOOKUP_SEP)[:-1] if split_lookup and split_lookup[0] not in select_related_keys: select_related_keys.append(split_lookup[0]) diff --git a/backend/src/baserow/core/templates/baserow/base.layout.eta b/backend/src/baserow/core/templates/baserow/base.layout.eta index aced29905..a7a3321d2 100644 --- a/backend/src/baserow/core/templates/baserow/base.layout.eta +++ b/backend/src/baserow/core/templates/baserow/base.layout.eta @@ -74,6 +74,7 @@ <mj-class name="notification-description" font-size="12px" + line-height="18px" color="#838387" font-family="Inter,sans-serif" /> diff --git a/backend/src/baserow/core/templates/baserow/core/notifications_summary.html b/backend/src/baserow/core/templates/baserow/core/notifications_summary.html index b9dc8981e..e26965f31 100644 --- a/backend/src/baserow/core/templates/baserow/core/notifications_summary.html +++ b/backend/src/baserow/core/templates/baserow/core/notifications_summary.html @@ -206,7 +206,7 @@ <!-- htmlmin:ignore --> <tr> <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> </tr> <!-- htmlmin:ignore -->{% endif %} @@ -225,7 +225,7 @@ <!-- htmlmin:ignore --> <tr> <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> </tr> <!-- htmlmin:ignore -->{% endif %} diff --git a/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py b/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py index 6974f7d94..f6fbeb25e 100644 --- a/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py +++ b/backend/tests/baserow/contrib/database/api/fields/test_formula_views.py @@ -970,7 +970,6 @@ def test_can_type_a_valid_formula_field(data_fixture, api_client): "number_prefix": "", "number_separator": "", "number_suffix": "", - "select_options": [], } diff --git a/backend/tests/baserow/contrib/database/api/fields/test_multiple_collaborators_views.py b/backend/tests/baserow/contrib/database/api/fields/test_multiple_collaborators_views.py index d878f4140..5e57d8aab 100644 --- a/backend/tests/baserow/contrib/database/api/fields/test_multiple_collaborators_views.py +++ b/backend/tests/baserow/contrib/database/api/fields/test_multiple_collaborators_views.py @@ -31,6 +31,10 @@ def test_multiple_collaborators_field_type_create(api_client, data_fixture): assert response.status_code == HTTP_200_OK assert response_json["name"] == "Collaborator 1" 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 @@ -43,6 +47,7 @@ def test_multiple_collaborators_field_type_update(api_client, data_fixture): first_name="Test1", workspace=workspace, ) + user_2 = data_fixture.create_user(workspace=workspace, first_name="Test2") database = data_fixture.create_database_application( 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_json["name"] == "New collaborator 1" 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 diff --git a/backend/tests/baserow/contrib/database/api/views/form/test_form_view_views.py b/backend/tests/baserow/contrib/database/api/views/form/test_form_view_views.py index 18e39485e..c06cfd3b1 100644 --- a/backend/tests/baserow/contrib/database/api/views/form/test_form_view_views.py +++ b/backend/tests/baserow/contrib/database/api/views/form/test_form_view_views.py @@ -1361,12 +1361,25 @@ def test_form_view_multiple_collaborators_field_options(api_client, data_fixture HTTP_AUTHORIZATION=f"JWT {token}", ) response_json = response.json() - assert response.status_code == HTTP_400_BAD_REQUEST - assert response_json["error"] == "ERROR_FORM_VIEW_FIELD_TYPE_IS_NOT_SUPPORTED" - assert ( - response_json["detail"] - == "The multiple_collaborators field type is not compatible with the form view." - ) + assert response.status_code == HTTP_200_OK + assert response_json == { + "field_options": { + str(multiple_collaborators_field.id): { + "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 diff --git a/backend/tests/baserow/core/test_core_db.py b/backend/tests/baserow/core/test_core_db.py index 07119f692..33c999257 100644 --- a/backend/tests/baserow/core/test_core_db.py +++ b/backend/tests/baserow/core/test_core_db.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock from django.contrib.contenttypes.models import ContentType 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.functions import Concat from django.test.utils import override_settings @@ -17,7 +17,12 @@ from baserow.contrib.database.fields.models import ( TextField, ) 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 ( CombinedForeignKeyAndManyToManyMultipleFieldPrefetch, LockedAtomicTransaction, @@ -192,8 +197,12 @@ def test_specific_iterator_with_select_related(data_fixture, django_assert_num_q @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( - data_fixture, django_assert_num_queries + prefetch_related, data_fixture, django_assert_num_queries ): grid_view = data_fixture.create_grid_view() gallery_view = data_fixture.create_gallery_view() @@ -209,7 +218,7 @@ def test_specific_iterator_with_prefetch_related( ] ) .order_by("id") - .prefetch_related("viewfilter_set") + .prefetch_related(prefetch_related) ) with django_assert_num_queries(4): diff --git a/changelog/entries/unreleased/feature/1554_collaborator_field_support_in_form_views.json b/changelog/entries/unreleased/feature/1554_collaborator_field_support_in_form_views.json new file mode 100644 index 000000000..05e7dd9b3 --- /dev/null +++ b/changelog/entries/unreleased/feature/1554_collaborator_field_support_in_form_views.json @@ -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" +} \ No newline at end of file diff --git a/web-frontend/locales/en.json b/web-frontend/locales/en.json index 98b25ca39..465950fa7 100644 --- a/web-frontend/locales/en.json +++ b/web-frontend/locales/en.json @@ -144,7 +144,9 @@ "singleSelectRadios": "Radios", "autonumber": "Autonumber", "password": "Password", - "ai": "AI prompt" + "ai": "AI prompt", + "multipleCollaboratorsDropdown": "Dropdown", + "multipleCollaboratorsCheckboxes": "Checkboxes" }, "fieldErrors": { "invalidNumber": "Invalid number", diff --git a/web-frontend/modules/core/assets/scss/components/notification_panel.scss b/web-frontend/modules/core/assets/scss/components/notification_panel.scss index 3ac4bc32a..a8abfe1b2 100644 --- a/web-frontend/modules/core/assets/scss/components/notification_panel.scss +++ b/web-frontend/modules/core/assets/scss/components/notification_panel.scss @@ -59,6 +59,7 @@ .notification-panel__notification-content { flex-grow: 1; + max-width: calc(100% - 34px - 16px); // 34px for icon, 16px for status strong { font-weight: 600; @@ -105,6 +106,13 @@ margin-bottom: 8px; } +.notification-panel__notification-content-summary-item { + @extend %ellipsis; + + display: block; + max-width: 100%; +} + .notification-panel__notification-status { flex: 0 0 16px; text-align: right; diff --git a/web-frontend/modules/core/mixins/inMemoryPaginatedDropdown.js b/web-frontend/modules/core/mixins/inMemoryPaginatedDropdown.js index 8734d8460..28227a89b 100644 --- a/web-frontend/modules/core/mixins/inMemoryPaginatedDropdown.js +++ b/web-frontend/modules/core/mixins/inMemoryPaginatedDropdown.js @@ -16,7 +16,7 @@ export default { } }, search() { - this.fetch(this.page, this.query) + this.fetch(1, this.query) }, hide() { // Call the dropdown `hide` method because it resets the search. We're diff --git a/web-frontend/modules/core/mixins/paginatedDropdown.js b/web-frontend/modules/core/mixins/paginatedDropdown.js index 7bb51e43d..09091ce5a 100644 --- a/web-frontend/modules/core/mixins/paginatedDropdown.js +++ b/web-frontend/modules/core/mixins/paginatedDropdown.js @@ -125,7 +125,12 @@ export default { if (replace) { this.results = results } 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 diff --git a/web-frontend/modules/database/components/field/FieldCollaboratorDropdown.vue b/web-frontend/modules/database/components/field/FieldCollaboratorDropdown.vue index aedf87f98..c99a1d761 100644 --- a/web-frontend/modules/database/components/field/FieldCollaboratorDropdown.vue +++ b/web-frontend/modules/database/components/field/FieldCollaboratorDropdown.vue @@ -65,9 +65,9 @@ ></FieldCollaboratorDropdownItem> <FieldCollaboratorDropdownItem v-for="collaborator in results" - :key="collaborator.user_id" + :key="collaborator.id" :name="collaborator.name" - :value="collaborator.user_id" + :value="collaborator.id" ></FieldCollaboratorDropdownItem> </ul> <div v-if="isNotFound" class="select__description"> @@ -95,6 +95,11 @@ export default { required: false, default: true, }, + pageSize: { + type: Number, + required: false, + default: 100, // override default pageSize of 20 + }, }, computed: { isNotFound() { diff --git a/web-frontend/modules/database/components/notifications/FormSubmittedNotification.vue b/web-frontend/modules/database/components/notifications/FormSubmittedNotification.vue index 3c78ce685..1bb24e0dc 100644 --- a/web-frontend/modules/database/components/notifications/FormSubmittedNotification.vue +++ b/web-frontend/modules/database/components/notifications/FormSubmittedNotification.vue @@ -17,7 +17,9 @@ <div class="notification-panel__notification-content-desc"> <ul class="notification-panel__notification-content-summary"> <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> </ul> <div v-if="hiddenFieldsCount > 0"> diff --git a/web-frontend/modules/database/components/view/ViewFilterTypeCollaborators.vue b/web-frontend/modules/database/components/view/ViewFilterTypeCollaborators.vue index db588bc98..ee633a439 100644 --- a/web-frontend/modules/database/components/view/ViewFilterTypeCollaborators.vue +++ b/web-frontend/modules/database/components/view/ViewFilterTypeCollaborators.vue @@ -27,7 +27,7 @@ export default { }, initialDisplayName() { const selected = this.workspaceCollaborators.find( - (c) => c.user_id === this.copy + (c) => c.id === this.copy ) return selected?.name }, diff --git a/web-frontend/modules/database/components/view/form/FormViewFieldMultipleCollaboratorsCheckboxes.vue b/web-frontend/modules/database/components/view/form/FormViewFieldMultipleCollaboratorsCheckboxes.vue new file mode 100644 index 000000000..e6d09716a --- /dev/null +++ b/web-frontend/modules/database/components/view/form/FormViewFieldMultipleCollaboratorsCheckboxes.vue @@ -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> diff --git a/web-frontend/modules/database/fieldTypes.js b/web-frontend/modules/database/fieldTypes.js index 46ff1f5c3..576d4cd08 100644 --- a/web-frontend/modules/database/fieldTypes.js +++ b/web-frontend/modules/database/fieldTypes.js @@ -138,6 +138,7 @@ import RowHistoryFieldPassword from '@baserow/modules/database/components/row/Ro import FormViewFieldLinkRow from '@baserow/modules/database/components/view/form/FormViewFieldLinkRow' import FormViewFieldMultipleLinkRow from '@baserow/modules/database/components/view/form/FormViewFieldMultipleLinkRow' 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 { @@ -4159,7 +4160,20 @@ export class MultipleCollaboratorsFieldType extends FieldType { } 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() { diff --git a/web-frontend/modules/database/locales/en.json b/web-frontend/modules/database/locales/en.json index b344db690..65fb0d147 100644 --- a/web-frontend/modules/database/locales/en.json +++ b/web-frontend/modules/database/locales/en.json @@ -801,7 +801,8 @@ "addCondition": "Add condition", "addConditionGroup": "Add condition group", "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": { "duplicate": "Duplicate field", diff --git a/web-frontend/modules/database/mixins/availableCollaborators.js b/web-frontend/modules/database/mixins/availableCollaborators.js index 279a44e3f..dd2f170b8 100644 --- a/web-frontend/modules/database/mixins/availableCollaborators.js +++ b/web-frontend/modules/database/mixins/availableCollaborators.js @@ -1,8 +1,7 @@ export default { computed: { workspaceCollaborators() { - const workspace = this.$store.getters['workspace/getSelected'] - return workspace.users.filter((user) => user.to_be_deleted === false) + return this.field.available_collaborators }, availableCollaborators() { // 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 result = this.workspaceCollaborators.filter( - (item) => !ids.has(item.user_id) + (item) => !ids.has(item.id) ) return result }, diff --git a/web-frontend/modules/database/mixins/collaboratorField.js b/web-frontend/modules/database/mixins/collaboratorField.js index 7a327e536..c64ed16bd 100644 --- a/web-frontend/modules/database/mixins/collaboratorField.js +++ b/web-frontend/modules/database/mixins/collaboratorField.js @@ -25,13 +25,13 @@ export default { const workspaceUser = this.workspaceCollaborators.find( - (workspaceUser) => workspaceUser.user_id === newId + (workspaceUser) => workspaceUser.id === newId ) || null let newOption = null if (workspaceUser) { newOption = { - id: workspaceUser.user_id, + id: workspaceUser.id, name: workspaceUser.name, } } diff --git a/web-frontend/modules/database/viewFilters.js b/web-frontend/modules/database/viewFilters.js index 0ba504e42..5d429c806 100644 --- a/web-frontend/modules/database/viewFilters.js +++ b/web-frontend/modules/database/viewFilters.js @@ -2445,10 +2445,6 @@ export class MultipleCollaboratorsHasFilterType extends ViewFilterType { ] } - isAllowedInPublicViews() { - return false - } - matches(rowValue, filterValue, field, fieldType) { if (!isNumeric(filterValue)) { return true @@ -2484,10 +2480,6 @@ export class MultipleCollaboratorsHasNotFilterType extends ViewFilterType { ] } - isAllowedInPublicViews() { - return false - } - matches(rowValue, filterValue, field, fieldType) { if (!isNumeric(filterValue)) { return true