diff --git a/backend/src/baserow/contrib/database/api/rows/views.py b/backend/src/baserow/contrib/database/api/rows/views.py index 7a1821a173..bad17ed884 100644 --- a/backend/src/baserow/contrib/database/api/rows/views.py +++ b/backend/src/baserow/contrib/database/api/rows/views.py @@ -558,7 +558,11 @@ def get(self, request, table_id, query_params): ), 401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -821,7 +825,11 @@ class RowView(APIView): ), 401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -830,6 +838,7 @@ class RowView(APIView): UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, TokenCannotIncludeRowMetadata: ERROR_CANNOT_INCLUDE_ROW_METADATA, FieldDataConstraintException: ERROR_FIELD_DATA_CONSTRAINT, @@ -956,7 +965,11 @@ def get(self, request, table_id, row_id, metadata, query_params: dict): ), 401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -965,6 +978,7 @@ def get(self, request, table_id, row_id, metadata, query_params: dict): UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, DeadlockException: ERROR_DATABASE_DEADLOCK, FieldDataConstraintException: ERROR_FIELD_DATA_CONSTRAINT, @@ -1088,7 +1102,11 @@ def patch( ["ERROR_USER_NOT_IN_GROUP", "ERROR_CANNOT_DELETE_ALREADY_DELETED_ITEM"] ), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -1097,6 +1115,7 @@ def patch( UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, CannotDeleteAlreadyDeletedItem: ERROR_CANNOT_DELETE_ALREADY_DELETED_ITEM, CannotDeleteRowsInTable: ERROR_CANNOT_DELETE_ROWS_IN_TABLE, @@ -1337,7 +1356,11 @@ class BatchRowsView(APIView): ), 401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -1347,6 +1370,7 @@ class BatchRowsView(APIView): TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, RowIdsNotUnique: ERROR_ROW_IDS_NOT_UNIQUE, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, CannotCreateRowsInTable: ERROR_CANNOT_CREATE_ROWS_IN_TABLE, DeadlockException: ERROR_DATABASE_DEADLOCK, @@ -1501,7 +1525,11 @@ def post(self, request: Request, table_id: int, query_params) -> Response: ), 401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -1511,6 +1539,7 @@ def post(self, request: Request, table_id: int, query_params) -> Response: TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, RowIdsNotUnique: ERROR_ROW_IDS_NOT_UNIQUE, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE, DeadlockException: ERROR_DATABASE_DEADLOCK, FieldDataConstraintException: ERROR_FIELD_DATA_CONSTRAINT, diff --git a/changelog/entries/unreleased/feature/4926_show_matching_restricted_view_filters.json b/changelog/entries/unreleased/feature/4926_show_matching_restricted_view_filters.json new file mode 100644 index 0000000000..84b5e180cb --- /dev/null +++ b/changelog/entries/unreleased/feature/4926_show_matching_restricted_view_filters.json @@ -0,0 +1,9 @@ +{ + "type": "feature", + "message": "Show matching restricted view filters and allow setting default values", + "issue_origin": "github", + "issue_number": 4926, + "domain": "database", + "bullet_points": [], + "created_at": "2026-04-08" +} diff --git a/changelog/entries/unreleased/refactor/replace_moveToBody_mixin_with_builtin_teleport_component.json b/changelog/entries/unreleased/refactor/replace_moveToBody_mixin_with_builtin_teleport_component.json new file mode 100644 index 0000000000..4a201a5dc5 --- /dev/null +++ b/changelog/entries/unreleased/refactor/replace_moveToBody_mixin_with_builtin_teleport_component.json @@ -0,0 +1,9 @@ +{ + "type": "refactor", + "message": "Replace custom moveToBody mixin with Vue 3 built-in Teleport component.", + "issue_origin": "github", + "issue_number": null, + "domain": "core", + "bullet_points": [], + "created_at": "2026-04-08" +} diff --git a/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/all.scss b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/all.scss index 533258413e..6239ef9ae8 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/all.scss +++ b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/all.scss @@ -25,3 +25,4 @@ @import 'assistant_onboarding'; @import 'date_dependency'; @import 'data_scanner'; +@import 'restricted_view_filter_context'; diff --git a/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/restricted_view_filter_context.scss b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/restricted_view_filter_context.scss new file mode 100644 index 0000000000..cf5d3c71a8 --- /dev/null +++ b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/restricted_view_filter_context.scss @@ -0,0 +1,95 @@ +.restricted-view-filter-context { + .expandable__content--card { + padding: 0; + } +} + +.restricted-view-filter-context__head { + display: flex; + align-items: flex-start; + gap: 10px; +} + +.restricted-view-filter-context__icon { + flex: 0 0 20px; + font-size: 16px; + position: relative; + top: 1px; + + &--warning { + color: $palette-yellow-800; + } + + &--success { + color: $palette-green-700; + } +} + +.restricted-view-filter-context__text { + flex: 1 1 auto; + min-width: 0; +} + +.restricted-view-filter-context__title { + font-weight: 500; + font-size: 13px; + line-height: 20px; + margin-bottom: 4px; +} + +.restricted-view-filter-context__subtitle { + font-size: 12px; + color: $palette-neutral-900; + margin-top: 2px; + line-height: 16px; +} + +.restricted-view-filter-context__toggle { + flex: 0 0 auto; + font-size: 14px; + color: $color-neutral-500; + position: relative; + top: 3px; +} + +.restricted-view-filter-context__body { + padding: 20px 16px 20px 46px; +} + +.restricted-view-filter-context__field { + display: flex; + align-items: center; + width: 100%; + + &:not(:last-child) { + margin-bottom: 24px; + } +} + +.restricted-view-filter-context__field-label { + @extend %ellipsis; + + width: 110px; + flex: 0 0 110px; + margin-right: 20px; + font-size: 13px; + font-weight: 500; + line-height: 20px; + color: $palette-neutral-1200; +} + +.restricted-view-filter-context__field-icon { + width: 16px; + color: $palette-neutral-600; + font-size: 14px; +} + +.restricted-view-filter-context__field-input { + width: 100%; +} + +.restricted-view-filter-context__remove { + display: inline-block; + margin-top: 4px; + font-size: 12px; +} diff --git a/enterprise/web-frontend/modules/baserow_enterprise/components/views/RestrictedViewFilterContext.vue b/enterprise/web-frontend/modules/baserow_enterprise/components/views/RestrictedViewFilterContext.vue new file mode 100644 index 0000000000..f3724cc93b --- /dev/null +++ b/enterprise/web-frontend/modules/baserow_enterprise/components/views/RestrictedViewFilterContext.vue @@ -0,0 +1,454 @@ + + + diff --git a/enterprise/web-frontend/modules/baserow_enterprise/locales/en.json b/enterprise/web-frontend/modules/baserow_enterprise/locales/en.json index 958d50c02d..7726f6fce8 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/locales/en.json +++ b/enterprise/web-frontend/modules/baserow_enterprise/locales/en.json @@ -801,6 +801,14 @@ "restricted": "Restricted", "restrictedDescription": "Editors and lower can only see the filtered data and visible fields. It's possible to manage the members." }, + "restrictedViewFilterContext": { + "matchTitle": "Default view values match the filters", + "matchSubtitle": "Editors will see the rows they create.", + "mismatchTitle": "Default values don't match the filters", + "mismatchSubtitle": "Editors will be able to create rows but won't see them.", + "removeDefaultValue": "Remove default value", + "removeDefaultValueHelper": "This field has a default value but no filter. You can remove it here, or manage all default values by clicking the three dots next to the view and then on \"Default row values\"." + }, "databaseStep": { "ai": "Kuma AI" }, diff --git a/enterprise/web-frontend/modules/baserow_enterprise/viewOwnershipTypes.js b/enterprise/web-frontend/modules/baserow_enterprise/viewOwnershipTypes.js index 3a3a67bfe1..dcc9b77f93 100644 --- a/enterprise/web-frontend/modules/baserow_enterprise/viewOwnershipTypes.js +++ b/enterprise/web-frontend/modules/baserow_enterprise/viewOwnershipTypes.js @@ -3,6 +3,7 @@ import EnterpriseFeatures from '@baserow_enterprise/features' import PaidFeaturesModal from '@baserow_premium/components/PaidFeaturesModal' import { RBACPaidFeature } from '@baserow_enterprise/paidFeatures' import { FormViewType } from '@baserow/modules/database/viewTypes.js' +import RestrictedViewFilterContext from '@baserow_enterprise/components/views/RestrictedViewFilterContext' export class RestrictedViewOwnershipType extends ViewOwnershipType { static getType() { @@ -83,6 +84,19 @@ export class RestrictedViewOwnershipType extends ViewOwnershipType { return !canListFields } + getFilterContextComponent(view, database) { + if ( + !this.app.$hasPermission( + 'database.table.view.update_default_values', + view, + database.workspace.id + ) + ) { + return null + } + return RestrictedViewFilterContext + } + _canUpdateFieldOptions(view, database) { return this.app.$hasPermission( 'database.table.view.update_field_options', diff --git a/premium/backend/src/baserow_premium/api/row_comments/views.py b/premium/backend/src/baserow_premium/api/row_comments/views.py index 01da3c0ef8..c903e240b7 100755 --- a/premium/backend/src/baserow_premium/api/row_comments/views.py +++ b/premium/backend/src/baserow_premium/api/row_comments/views.py @@ -18,8 +18,10 @@ from baserow.api.serializers import get_example_pagination_serializer_class from baserow.contrib.database.api.rows.errors import ERROR_ROW_DOES_NOT_EXIST from baserow.contrib.database.api.tables.errors import ERROR_TABLE_DOES_NOT_EXIST +from baserow.contrib.database.api.views.errors import ERROR_VIEW_DOES_NOT_EXIST from baserow.contrib.database.rows.exceptions import RowDoesNotExist from baserow.contrib.database.table.exceptions import TableDoesNotExist +from baserow.contrib.database.views.exceptions import ViewDoesNotExist from baserow.core.action.registries import action_type_registry from baserow.core.exceptions import UserNotInWorkspace from baserow_premium.api.row_comments.errors import ( @@ -117,6 +119,7 @@ class RowCommentsView(APIView): [ "ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", ] ), }, @@ -125,6 +128,7 @@ class RowCommentsView(APIView): { TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, } ) @@ -176,7 +180,11 @@ def get(self, request, table_id, row_id, query_params): ["ERROR_USER_NOT_IN_GROUP", "ERROR_INVALID_COMMENT_MENTION"] ), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -184,6 +192,7 @@ def get(self, request, table_id, row_id, query_params): { TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, InvalidRowCommentMentionException: ERROR_INVALID_COMMENT_MENTION, } @@ -233,7 +242,11 @@ class RowCommentView(APIView): ), 401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_COMMENT_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_COMMENT_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -241,6 +254,7 @@ class RowCommentView(APIView): { TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowCommentDoesNotExist: ERROR_ROW_COMMENT_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, UserNotRowCommentAuthorException: ERROR_USER_NOT_COMMENT_AUTHOR, InvalidRowCommentMentionException: ERROR_INVALID_COMMENT_MENTION, @@ -285,7 +299,11 @@ def patch(self, request, table_id, comment_id, data, query_params): ), 401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]), 404: get_error_schema( - ["ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_COMMENT_DOES_NOT_EXIST"] + [ + "ERROR_TABLE_DOES_NOT_EXIST", + "ERROR_ROW_COMMENT_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", + ] ), }, ) @@ -293,6 +311,7 @@ def patch(self, request, table_id, comment_id, data, query_params): { TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowCommentDoesNotExist: ERROR_ROW_COMMENT_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, UserNotRowCommentAuthorException: ERROR_USER_NOT_COMMENT_AUTHOR, } @@ -340,6 +359,7 @@ class RowCommentsNotificationModeView(APIView): [ "ERROR_TABLE_DOES_NOT_EXIST", "ERROR_ROW_DOES_NOT_EXIST", + "ERROR_VIEW_DOES_NOT_EXIST", ] ), }, @@ -348,6 +368,7 @@ class RowCommentsNotificationModeView(APIView): { TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST, RowDoesNotExist: ERROR_ROW_DOES_NOT_EXIST, + ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST, UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP, } ) diff --git a/premium/web-frontend/modules/baserow_premium/components/row_comments/RowComment.vue b/premium/web-frontend/modules/baserow_premium/components/row_comments/RowComment.vue index 44cc71a924..a0a993bdfc 100644 --- a/premium/web-frontend/modules/baserow_premium/components/row_comments/RowComment.vue +++ b/premium/web-frontend/modules/baserow_premium/components/row_comments/RowComment.vue @@ -215,9 +215,10 @@ export default { this.editing = true this.onClickOutsideHandler = (evt) => { + const contextRoot = this.$refs.commentContext.getTeleportedElement() if ( !this.$el.contains(evt.target) && - !this.$refs.commentContext.$el.contains(evt.target) && + (!contextRoot || !contextRoot.contains(evt.target)) && !evt.composedPath().includes(this.$el) ) { this.stopEdit() diff --git a/premium/web-frontend/modules/baserow_premium/components/views/grid/fields/GridViewFieldAI.vue b/premium/web-frontend/modules/baserow_premium/components/views/grid/fields/GridViewFieldAI.vue index d76c9afb51..21d93b157a 100644 --- a/premium/web-frontend/modules/baserow_premium/components/views/grid/fields/GridViewFieldAI.vue +++ b/premium/web-frontend/modules/baserow_premium/components/views/grid/fields/GridViewFieldAI.vue @@ -69,7 +69,7 @@ diff --git a/web-frontend/modules/core/components/Modal.vue b/web-frontend/modules/core/components/Modal.vue index dcba5b53ee..edfbb0d83a 100644 --- a/web-frontend/modules/core/components/Modal.vue +++ b/web-frontend/modules/core/components/Modal.vue @@ -1,95 +1,98 @@