Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-3268] feat: url pattern #6546

Open
wants to merge 13 commits into
base: preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apiserver/plane/app/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
IssueDescriptionVersionEndpoint,
IssueMetaEndpoint,
IssueDetailIdentifierEndpoint,
)

urlpatterns = [
Expand Down Expand Up @@ -278,4 +280,14 @@
IssueDescriptionVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/meta/",
IssueMetaEndpoint.as_view(),
name="issue-meta",
),
path(
"workspaces/<str:slug>/work-items/<str:project_identifier>-<str:issue_identifier>/",
IssueDetailIdentifierEndpoint.as_view(),
name="issue-detail-identifier",
),
]
2 changes: 2 additions & 0 deletions apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
IssuePaginatedViewSet,
IssueDetailEndpoint,
IssueBulkUpdateDateEndpoint,
IssueMetaEndpoint,
IssueDetailIdentifierEndpoint,
)

from .issue.activity import IssueActivityEndpoint
Expand Down
175 changes: 175 additions & 0 deletions apiserver/plane/app/views/issue/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,3 +1096,178 @@ def post(self, request, slug, project_id):
return Response(
{"message": "Issues updated successfully"}, status=status.HTTP_200_OK
)


class IssueMetaEndpoint(BaseAPIView):

@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT")
def get(self, request, slug, project_id, issue_id):
issue = Issue.objects.only("sequence_id", "project__identifier").get(
id=issue_id, project_id=project_id, workspace__slug=slug
)
return Response(
{
"sequence_id": issue.sequence_id,
"project_identifier": issue.project.identifier,
},
status=status.HTTP_200_OK,
)


class IssueDetailIdentifierEndpoint(BaseAPIView):

def get(self, request, slug, project_identifier, issue_identifier):

# Fetch the project
project = Project.objects.get(
identifier__iexact=project_identifier,
workspace__slug=slug,
)

# Check if the user is a member of the project
if not ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project.id,
member=request.user,
is_active=True,
).exists():
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_403_FORBIDDEN,
)

# Fetch the issue
issue = (
Issue.objects.filter(project_id=project.id)
.filter(workspace__slug=slug)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
:1
]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(sequence_id=issue_identifier)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("issue", "actor"),
)
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related("created_by"),
)
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project.id,
issue__sequence_id=issue_identifier,
subscriber=request.user,
)
)
)
).first()

# Check if the issue exists
if not issue:
return Response(
{"error": "The required object does not exist."},
status=status.HTTP_404_NOT_FOUND,
)

"""
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the issue
"""

if (
ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project.id,
member=request.user,
role=5,
is_active=True,
).exists()
and not project.guest_view_all_features
and not issue.created_by == request.user
):
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_400_BAD_REQUEST,
)

recent_visited_task.delay(
slug=slug,
entity_name="issue",
entity_identifier=str(issue.id),
user_id=str(request.user.id),
project_id=str(project.id),
)

# Serialize the issue
serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
92 changes: 92 additions & 0 deletions web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client";

import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Briefcase } from "lucide-react";
// ui
import { Breadcrumbs, LayersIcon, Header, Logo } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { IssueDetailQuickActions } from "@/components/issues";
// hooks
import { useIssueDetail, useProject } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";

export const ProjectIssueDetailsHeader = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, workItem } = useParams();
// store hooks
const { getProjectById, loader } = useProject();
const {
issue: { getIssueById, getIssueIdByIdentifier },
} = useIssueDetail();
// derived values
const issueId = getIssueIdByIdentifier(workItem?.toString());
const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined;
const projectId = issueDetails ? issueDetails?.project_id : undefined;
const projectDetails = projectId ? getProjectById(projectId?.toString()) : undefined;

return (
<Header>
<Header.LeftItem>
<div>
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={projectDetails?.name ?? "Project"}
icon={
projectDetails ? (
projectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={projectDetails?.logo_props} size={16} />
</span>
)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
</span>
)
}
/>
}
/>

<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/issues`}
label="Issues"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>

<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={
projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""
}
/>
}
/>
</Breadcrumbs>
</div>
</Header.LeftItem>
<Header.RightItem>
{projectId && issueId && (
<IssueDetailQuickActions
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
issueId={issueId?.toString()}
/>
)}
</Header.RightItem>
</Header>
);
});
Loading
Loading