promote: mobile-runway to preview #3603

This commit is contained in:
sriram veeraghanta
2025-07-08 18:34:37 +05:30
committed by GitHub
7 changed files with 188 additions and 97 deletions

View File

@@ -12,8 +12,9 @@ from strawberry.types import Info
# Module imports
from plane.db.models import Page, ProjectMember, WorkspaceMember
from plane.graphql.permissions.workspace import WorkspaceBasePermission
from plane.graphql.permissions.workspace import WorkspacePermission
from plane.graphql.types.page import PageType
from plane.graphql.utils.roles import Roles
@sync_to_async
@@ -39,7 +40,11 @@ def get_project_member(slug: str, project: str, user_id: str):
@strawberry.type
class WorkspacePageMutation:
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[WorkspaceBasePermission()])]
extensions=[
PermissionExtension(
permissions=[WorkspacePermission(roles=[Roles.ADMIN, Roles.MEMBER])]
)
]
)
async def updateWorkspacePage(
self,

View File

@@ -1,35 +1,38 @@
# Strawberry imports
import strawberry
from strawberry.types import Info
from strawberry.scalars import JSON
from strawberry.permission import PermissionExtension
from strawberry.exceptions import GraphQLError
# Third-party imports
from asgiref.sync import sync_to_async
# Python imports
from typing import Optional
# Third-party imports
import strawberry
from asgiref.sync import sync_to_async
# Strawberry imports
from strawberry.exceptions import GraphQLError
from strawberry.permission import PermissionExtension
from strawberry.scalars import JSON
from strawberry.types import Info
# Module imports
from plane.db.models import (
IssueUserProperty,
Project,
ProjectMember,
State,
UserFavorite,
Workspace,
WorkspaceMember,
)
from plane.ee.models import ProjectFeature
from plane.graphql.permissions.project import (
ProjectAdminPermission,
ProjectBasePermission,
ProjectMemberPermission,
)
from plane.graphql.permissions.workspace import (
WorkspaceBasePermission,
WorkspacePermission,
)
from plane.graphql.permissions.project import (
ProjectMemberPermission,
ProjectBasePermission,
ProjectAdminPermission,
)
from plane.graphql.types.project import ProjectType
from plane.graphql.utils.roles import Roles
from plane.db.models import (
Workspace,
WorkspaceMember,
Project,
ProjectMember,
UserFavorite,
IssueUserProperty,
State,
)
@strawberry.type
@@ -52,9 +55,13 @@ class ProjectMutation:
cover_image: Optional[str] = None,
project_lead: Optional[strawberry.ID] = None,
logo_props: Optional[JSON] = {},
page_view: Optional[bool] = False,
page_view: Optional[bool] = True,
module_view: Optional[bool] = True,
cycle_view: Optional[bool] = True,
issue_views_view: Optional[bool] = True,
) -> ProjectType:
workspace = await sync_to_async(Workspace.objects.get)(slug=slug)
try:
project = await sync_to_async(Project.objects.create)(
name=name,
@@ -64,6 +71,9 @@ class ProjectMutation:
logo_props=logo_props,
cover_image=cover_image,
page_view=page_view,
module_view=module_view,
cycle_view=cycle_view,
issue_views_view=issue_views_view,
workspace=workspace,
)
except Exception:
@@ -143,6 +153,11 @@ class ProjectMutation:
]
)
# Project feature
_ = await sync_to_async(ProjectFeature.objects.create)(
workspace_id=workspace.id, project_id=project.id
)
return project
@strawberry.mutation(
@@ -158,6 +173,9 @@ class ProjectMutation:
network: Optional[int] = None,
logo_props: Optional[JSON] = None,
page_view: Optional[bool] = None,
module_view: Optional[bool] = None,
cycle_view: Optional[bool] = None,
issue_views_view: Optional[bool] = None,
cover_image: Optional[str] = None,
) -> ProjectType:
project = await sync_to_async(Project.objects.get)(id=id)
@@ -173,8 +191,12 @@ class ProjectMutation:
project.logo_props = logo_props
if page_view is not None:
project.page_view = page_view
if page_view is not None:
project.page_view = page_view
if module_view is not None:
project.module_view = module_view
if cycle_view is not None:
project.cycle_view = cycle_view
if issue_views_view is not None:
project.issue_views_view = issue_views_view
if cover_image is not None:
project.cover_image = cover_image
await sync_to_async(project.save)()

View File

@@ -9,6 +9,7 @@ from strawberry.types import Info
# Module Imports
from plane.db.models import Issue, Project, ProjectMember
from plane.graphql.helpers.teamspace import project_member_filter_via_teamspaces_async
from plane.graphql.permissions.workspace import WorkspacePermission
from plane.graphql.types.issues.meta import IssueShortenedMetaInfo
from plane.graphql.utils.roles import Roles
@@ -72,6 +73,9 @@ class IssueShortenedMetaInfoQuery:
async def issue_shortened_meta_info(
self, info: Info, slug: str, work_item_identifier: str
) -> IssueShortenedMetaInfo:
user = info.context.user
user_id = str(user.id)
workspace_slug = slug
project_identifier = None
issue_sequence = None
@@ -87,15 +91,25 @@ class IssueShortenedMetaInfoQuery:
error_extensions = {"code": "ISSUE_NOT_FOUND", "statusCode": 404}
raise GraphQLError(message, extensions=error_extensions)
project_id = await get_project_id(workspace_slug, project_identifier)
is_project_member = await project_member_exists(
workspace_slug, project_identifier, info.context.user
)
if not is_project_member:
message = "User does not have permission to access this project."
error_extensions = {"code": "UNAUTHORIZED", "statusCode": 403}
raise GraphQLError(message, extensions=error_extensions)
project_id = await get_project_id(workspace_slug, project_identifier)
# validate teamspace membership
project_teamspace_filter = await project_member_filter_via_teamspaces_async(
user_id=user_id,
workspace_slug=workspace_slug,
)
teamspace_project_ids = project_teamspace_filter.teamspace_project_ids
if (
not teamspace_project_ids
or str(project_id) not in teamspace_project_ids
):
message = "User does not have permission to access this project."
error_extensions = {"code": "UNAUTHORIZED", "statusCode": 403}
raise GraphQLError(message, extensions=error_extensions)
issue_id, is_epic = await get_issue_id(
workspace_slug, project_id, issue_sequence

View File

@@ -64,16 +64,17 @@ class PageQuery:
)
# Get shared page ids for private and shared pages
if type in ["private", "shared"]:
shared_pages = await sync_to_async(list)(
PageUser.objects.filter(
workspace__slug=slug,
project__id=project,
).values_list("page_id", flat=True)
shared_pages_data = await sync_to_async(list)(
PageUser.objects.filter(workspace__slug=slug, project__id=project).values(
"page_id", "user_id"
)
shared_page_ids = list(shared_pages)
else:
shared_page_ids = []
)
shared_page_ids = list(set(str(item["page_id"]) for item in shared_pages_data))
pages_shared_with_user = [
str(item["page_id"])
for item in shared_pages_data
if str(item["user_id"]) == user_id
]
# Filter archived pages for all, public and shared pages
if type != "archived":
@@ -90,11 +91,15 @@ class PageQuery:
page_base_query = page_base_query.filter(access=0)
elif type == "private":
page_base_query = page_base_query.filter(
Q(access=1) & ~Q(id__in=shared_page_ids)
).filter(owned_by_id=user_id)
access=1, owned_by_id=user_id
).exclude(id__in=shared_page_ids)
elif type == "shared":
page_base_query = page_base_query.filter(
Q(access=1) & Q(id__in=shared_page_ids)
Q(access=1)
& (
Q(id__in=pages_shared_with_user)
| (Q(owned_by_id=user_id) & Q(id__in=shared_page_ids))
)
)
# Subquery for UserFavorite

View File

@@ -58,17 +58,18 @@ class UserPageQuery:
.filter(project_teamspace_filter_query)
)
# Get shared page ids
if type in ["private", "shared"]:
shared_pages = await sync_to_async(list)(
PageUser.objects.filter(
workspace__slug=slug,
project_id__isnull=False,
).values_list("page_id", flat=True)
)
shared_page_ids = list(shared_pages)
else:
shared_page_ids = []
shared_pages_data = await sync_to_async(list)(
PageUser.objects.filter(
workspace__slug=slug,
project_id__isnull=False,
).values("page_id", "user_id")
)
shared_page_ids = list(set(str(item["page_id"]) for item in shared_pages_data))
pages_shared_with_user = [
str(item["page_id"])
for item in shared_pages_data
if str(item["user_id"]) == user_id
]
# Filter archived pages
if type != "archived":
@@ -85,11 +86,15 @@ class UserPageQuery:
page_base_query = page_base_query.filter(access=0)
elif type == "private":
page_base_query = page_base_query.filter(
Q(access=1) & ~Q(id__in=shared_page_ids)
).filter(owned_by_id=user_id)
access=1, owned_by_id=user_id
).exclude(id__in=shared_page_ids)
elif type == "shared":
page_base_query = page_base_query.filter(
Q(access=1) & Q(id__in=shared_page_ids)
Q(access=1)
& (
Q(id__in=pages_shared_with_user)
| (Q(owned_by_id=user_id) & Q(id__in=shared_page_ids))
)
)
subquery = UserFavorite.objects.filter(

View File

@@ -25,13 +25,18 @@ from plane.graphql.types.page import PageType
from plane.graphql.types.paginator import PaginatorResponse
from plane.graphql.utils.feature_flag import validate_feature_flag
from plane.graphql.utils.paginator import paginate
from plane.graphql.utils.roles import Roles
# workspace level queries
@strawberry.type
class WorkspacePageQuery:
@strawberry.field(
extensions=[PermissionExtension(permissions=[WorkspacePermission()])]
extensions=[
PermissionExtension(
permissions=[WorkspacePermission(roles=[Roles.ADMIN, Roles.MEMBER])]
)
]
)
async def workspacePages(
self,
@@ -61,16 +66,15 @@ class WorkspacePageQuery:
.filter(moved_to_page__isnull=True)
)
# Get shared page ids
if type in ["private", "shared"]:
shared_pages = await sync_to_async(list)(
PageUser.objects.filter(
workspace__slug=slug,
).values_list("page_id", flat=True)
)
shared_page_ids = list(shared_pages)
else:
shared_page_ids = []
shared_pages_data = await sync_to_async(list)(
PageUser.objects.filter(workspace__slug=slug).values("page_id", "user_id")
)
shared_page_ids = list(set(str(item["page_id"]) for item in shared_pages_data))
pages_shared_with_user = [
str(item["page_id"])
for item in shared_pages_data
if str(item["user_id"]) == user_id
]
# Filter archived pages
if type != "archived":
@@ -87,11 +91,15 @@ class WorkspacePageQuery:
page_base_query = page_base_query.filter(access=0)
elif type == "private":
page_base_query = page_base_query.filter(
Q(access=1) & ~Q(id__in=shared_page_ids)
).filter(owned_by_id=user_id)
access=1, owned_by_id=user_id
).exclude(id__in=shared_page_ids)
elif type == "shared":
page_base_query = page_base_query.filter(
Q(access=1) & Q(id__in=shared_page_ids)
Q(access=1)
& (
Q(id__in=pages_shared_with_user)
| (Q(owned_by_id=user_id) & Q(id__in=shared_page_ids))
)
)
# Build subquery for UserFavorite
@@ -103,9 +111,10 @@ class WorkspacePageQuery:
)
pages = await sync_to_async(list)(
page_base_query.select_related("workspace", "owned_by")
page_base_query.distinct()
.select_related("workspace", "owned_by")
.prefetch_related("projects")
.annotate(is_favorite=Exists(subquery))
.order_by("-created_at")
)
return paginate(results_object=pages, cursor=cursor)
@@ -113,7 +122,10 @@ class WorkspacePageQuery:
@strawberry.field(
extensions=[
PermissionExtension(
permissions=[WorkspacePermission(), WorkspaceSharedPagePermission()]
permissions=[
WorkspacePermission(roles=[Roles.ADMIN, Roles.MEMBER]),
WorkspaceSharedPagePermission(),
]
)
]
)

View File

@@ -5,10 +5,11 @@ from typing import Optional
# Third-party library imports
import strawberry
import strawberry_django
from asgiref.sync import sync_to_async
# Strawberry imports
from asgiref.sync import sync_to_async
from strawberry.scalars import JSON
from strawberry.types import Info
# Module imports
from plane.db.models import Page
@@ -17,6 +18,25 @@ from plane.graphql.types.project import ProjectLiteType
from plane.graphql.utils.timezone import user_timezone_converter
@sync_to_async
def shared_page_access(user_id: str, workspace_slug: str, page_id: str, owner_id: str):
shared_pages_data = PageUser.objects.filter(
workspace__slug=workspace_slug, page_id=page_id
).values("user_id", "access")
shared_pages_data = list(shared_pages_data)
page_access = 0
if owner_id == user_id:
page_access = 2
else:
for item in shared_pages_data:
if str(item["user_id"]) == user_id:
page_access = item["access"]
break
return (len(shared_pages_data) > 0, page_access)
@strawberry_django.type(Page)
class PageType:
id: strawberry.ID
@@ -79,38 +99,46 @@ class PageType:
return converted_date
@strawberry.field
async def is_shared(self) -> bool:
async def is_shared(self, info: Info) -> bool:
if self.access == 0:
return False
user = info.context.user
user_id = str(user.id)
workspace_slug = self.workspace.slug
page_id = self.id
shared_page = await sync_to_async(
lambda: PageUser.objects.filter(
workspace__slug=workspace_slug,
page_id=page_id,
).first()
)()
if shared_page:
page_id = str(self.id)
shared_page_count, _ = await shared_page_access(
workspace_slug=workspace_slug,
page_id=page_id,
user_id=user_id,
owner_id=str(self.owned_by_id),
)
if shared_page_count > 0:
return True
return False
@strawberry.field
async def is_shared_access(self) -> int:
async def is_shared_access(self, info: Info) -> int:
if self.access == 0:
return 0
user = info.context.user
user_id = str(user.id)
workspace_slug = self.workspace.slug
page_id = self.id
shared_page = await sync_to_async(
lambda: PageUser.objects.filter(
workspace__slug=workspace_slug,
page_id=page_id,
).first()
)()
if shared_page:
shared_page_access = shared_page.access
return shared_page_access
page_id = str(self.id)
shared_page_count, page_access = await shared_page_access(
workspace_slug=workspace_slug,
page_id=page_id,
user_id=user_id,
owner_id=str(self.owned_by_id),
)
if shared_page_count > 0:
return page_access
return 0
# @strawberry.field