From 3003a47ad6383938ecf2362543414051fafc3f3f Mon Sep 17 00:00:00 2001 From: "m.zeynali" Date: Wed, 26 Aug 2026 10:14:10 +0330 Subject: [PATCH] feat: allow private project views for the owning user Make view access writable via the API, add a Public/Private selector to the project view form, and return 404 when a private view is not visible to the requester. --- apps/api/plane/app/serializers/view.py | 1 - apps/api/plane/app/views/view/base.py | 3 ++ apps/web/core/components/views/form.tsx | 48 ++++++++++++++++++------- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/apps/api/plane/app/serializers/view.py b/apps/api/plane/app/serializers/view.py index 72f72ff71b2..28a9aee0f95 100644 --- a/apps/api/plane/app/serializers/view.py +++ b/apps/api/plane/app/serializers/view.py @@ -64,7 +64,6 @@ class Meta: "project", "query", "owned_by", - "access", "is_locked", ] diff --git a/apps/api/plane/app/views/view/base.py b/apps/api/plane/app/views/view/base.py index 3aad55abcc6..fd47d0291fb 100644 --- a/apps/api/plane/app/views/view/base.py +++ b/apps/api/plane/app/views/view/base.py @@ -314,6 +314,9 @@ def list(self, request, slug, project_id): @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def retrieve(self, request, slug, project_id, pk): issue_view = self.get_queryset().filter(pk=pk, project_id=project_id).first() + if issue_view is None: + return Response({"error": "View not found"}, status=status.HTTP_404_NOT_FOUND) + project = Project.objects.get(id=project_id) """ if the role is guest and guest_view_all_features is false and owned by is not diff --git a/apps/web/core/components/views/form.tsx b/apps/web/core/components/views/form.tsx index 10ad919d925..9974e27ec31 100644 --- a/apps/web/core/components/views/form.tsx +++ b/apps/web/core/components/views/form.tsx @@ -24,8 +24,11 @@ import { EViewAccess, EIssuesStoreType } from "@plane/types"; import { Input, TextArea } from "@plane/ui"; import { getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex } from "@plane/utils"; // components +import { AccessField } from "@/components/common/access-field"; import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters"; import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; +// helpers +import { VIEW_ACCESS_SPECIFIERS } from "@/helpers/views.helper"; // hooks import { useProject } from "@/hooks/store/use-project"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -79,6 +82,8 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) { // derived values const projectDetails = getProjectById(projectId); const logoValue = watch("logo_props"); + const accessValue = watch("access"); + const i18nAccessLabel = VIEW_ACCESS_SPECIFIERS.find((access) => access.key === accessValue)?.i18n_label; const workItemFilters: IIssueFilters = { richFilters: getValues("rich_filters"), displayFilters: getValues("display_filters"), @@ -278,19 +283,36 @@ export const ProjectViewForm = observer(function ProjectViewForm(props: Props) { -
- - +
+
+ ( + + )} + /> +
{t(i18nAccessLabel || "")}
+
+
+ + +
);