-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[WEB-5170] feat: navigation revamp #8162
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
Changes from all commits
1af42a7
4b738e1
9b2c66a
ac353f3
e77baef
90afb26
d452baf
1c30b89
2a94eb8
2162a67
54a9109
32f3bd5
ed054a8
dca4a99
08f5bb3
36e145c
a2761a0
45d68b9
1ef6ebe
2733d2e
f56f9a9
e103462
56b327d
92867ba
c93dbc2
b454230
d775be7
c64d3fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -300,3 +300,37 @@ def get(self, request, slug): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_members = {str(member["project_id"]): member["role"] for member in project_members} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response(project_members, status=status.HTTP_200_OK) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class ProjectMemberPreferenceEndpoint(BaseAPIView): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_project_member(self, slug, project_id, member_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ProjectMember.objects.get( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_id=project_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| member_id=member_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspace__slug=slug, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def patch(self, request, slug, project_id, member_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_member = self.get_project_member(slug, project_id, member_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_preferences = project_member.preferences or {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_preferences["navigation"] = request.data["navigation"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_member.preferences = current_preferences | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_member.save(update_fields=["preferences"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response({"preferences": project_member.preferences}, status=status.HTTP_200_OK) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get(self, request, slug, project_id, member_id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project_member = self.get_project_member(slug, project_id, member_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "preferences": project_member.preferences, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "project_id": project_member.project_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "member_id": project_member.member_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "workspace_id": project_member.workspace_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response(response, status=status.HTTP_200_OK) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+305
to
+336
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ProjectMemberPreferenceEndpoint lacks per‑member authorization and input validation As written:
Given this endpoint is meant for per‑member navigation preferences, you should:
One way to address this: class ProjectMemberPreferenceEndpoint(BaseAPIView):
- def get_project_member(self, slug, project_id, member_id):
- return ProjectMember.objects.get(
- project_id=project_id,
- member_id=member_id,
- workspace__slug=slug,
- )
+ def get_project_member(self, slug, project_id, member_id):
+ return ProjectMember.objects.get(
+ project_id=project_id,
+ member_id=member_id,
+ workspace__slug=slug,
+ is_active=True,
+ )
+
+ def _is_workspace_admin(self, request, slug):
+ return WorkspaceMember.objects.filter(
+ workspace__slug=slug,
+ member=request.user,
+ is_active=True,
+ role=ROLE.ADMIN.value,
+ ).exists()
@@
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id, member_id):
project_member = self.get_project_member(slug, project_id, member_id)
- current_preferences = project_member.preferences or {}
- current_preferences["navigation"] = request.data["navigation"]
+ # Only the member themselves or a workspace admin can update these prefs
+ if request.user.id != project_member.member_id and not self._is_workspace_admin(request, slug):
+ return Response(
+ {"error": "You are not allowed to update this member's preferences."},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+
+ navigation = request.data.get("navigation")
+ if navigation is None:
+ return Response(
+ {"error": "Missing 'navigation' payload."},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ current_preferences = project_member.preferences or {}
+ current_preferences["navigation"] = navigation
@@
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, member_id):
project_member = self.get_project_member(slug, project_id, member_id)
+
+ if request.user.id != project_member.member_id and not self._is_workspace_admin(request, slug):
+ return Response(
+ {"error": "You are not allowed to view this member's preferences."},
+ status=status.HTTP_403_FORBIDDEN,
+ )This keeps the endpoint behavior but closes the authorization hole and handles bad payloads gracefully. Adjust the exact error messages/roles if you intend admins to be able to manage other members’ navigation settings differently. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,15 +65,23 @@ def get(self, request, slug): | |
| ) | ||
|
|
||
| @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") | ||
| def patch(self, request, slug, key): | ||
| preference = WorkspaceUserPreference.objects.filter(key=key, workspace__slug=slug, user=request.user).first() | ||
| def patch(self, request, slug): | ||
| for data in request.data: | ||
| key = data.pop("key", None) | ||
| if not key: | ||
| continue | ||
|
|
||
| if preference: | ||
| serializer = WorkspaceUserPreferenceSerializer(preference, data=request.data, partial=True) | ||
| preference = WorkspaceUserPreference.objects.filter(key=key, workspace__slug=slug).first() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Missing user filter in workspace preference queryThe |
||
|
|
||
| if serializer.is_valid(): | ||
| serializer.save() | ||
| return Response(serializer.data, status=status.HTTP_200_OK) | ||
| return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||
| if not preference: | ||
| continue | ||
|
|
||
| return Response({"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND) | ||
| if "is_pinned" in data: | ||
| preference.is_pinned = data["is_pinned"] | ||
|
|
||
| if "sort_order" in data: | ||
| preference.sort_order = data["sort_order"] | ||
|
|
||
| preference.save(update_fields=["is_pinned", "sort_order"]) | ||
|
|
||
| return Response({"message": "Successfully updated"}, status=status.HTTP_200_OK) | ||
|
Comment on lines
67
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PATCH updates the wrong user’s preferences (missing Current logic:
This means:
You can fix this and simplify the loop by scoping to @@
- @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
- def patch(self, request, slug):
- for data in request.data:
- key = data.pop("key", None)
- if not key:
- continue
-
- preference = WorkspaceUserPreference.objects.filter(key=key, workspace__slug=slug).first()
-
- if not preference:
- continue
-
- if "is_pinned" in data:
- preference.is_pinned = data["is_pinned"]
-
- if "sort_order" in data:
- preference.sort_order = data["sort_order"]
-
- preference.save(update_fields=["is_pinned", "sort_order"])
-
- return Response({"message": "Successfully updated"}, status=status.HTTP_200_OK)
+ @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
+ def patch(self, request, slug):
+ for item in request.data:
+ key = item.get("key")
+ if not key:
+ continue
+
+ preference = WorkspaceUserPreference.objects.filter(
+ key=key,
+ workspace__slug=slug,
+ user=request.user,
+ ).first()
+
+ if not preference:
+ continue
+
+ if "is_pinned" in item:
+ preference.is_pinned = item["is_pinned"]
+
+ if "sort_order" in item:
+ preference.sort_order = item["sort_order"]
+
+ preference.save(update_fields=["is_pinned", "sort_order"])
+
+ return Response({"message": "Successfully updated"}, status=status.HTTP_200_OK)This keeps the bulk‑update behavior but guarantees you only touch the current user’s preferences. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,61 +1,43 @@ | ||
| import { observer } from "mobx-react"; | ||
| import { useParams } from "next/navigation"; | ||
| // plane imports | ||
| import { EProjectFeatureKey } from "@plane/constants"; | ||
| import { Breadcrumbs, Header } from "@plane/ui"; | ||
| // components | ||
| import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; | ||
| import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions"; | ||
| // hooks | ||
| import { Header, Row } from "@plane/ui"; | ||
| import { AppHeader } from "@/components/core/app-header"; | ||
| import { TabNavigationRoot } from "@/components/navigation"; | ||
| import { useIssueDetail } from "@/hooks/store/use-issue-detail"; | ||
| import { useProject } from "@/hooks/store/use-project"; | ||
| import { useAppRouter } from "@/hooks/use-app-router"; | ||
| import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common"; | ||
| // local components | ||
| import { WorkItemDetailsHeader } from "./work-item-header"; | ||
|
|
||
| export const ProjectIssueDetailsHeader = observer(function ProjectIssueDetailsHeader() { | ||
| export const ProjectWorkItemDetailsHeader = observer(function ProjectWorkItemDetailsHeader() { | ||
| // 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; | ||
|
|
||
| if (!workspaceSlug || !projectId || !issueId) return null; | ||
| const issueDetails = issueId ? getIssueById(issueId?.toString()) : undefined; | ||
|
|
||
| return ( | ||
| <Header> | ||
| <Header.LeftItem> | ||
| <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> | ||
| <CommonProjectBreadcrumbs | ||
| workspaceSlug={workspaceSlug?.toString()} | ||
| projectId={projectId?.toString()} | ||
| featureKey={EProjectFeatureKey.WORK_ITEMS} | ||
| /> | ||
| <Breadcrumbs.Item | ||
| component={ | ||
| <BreadcrumbLink | ||
| label={projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""} | ||
| /> | ||
| } | ||
| /> | ||
| </Breadcrumbs> | ||
| </Header.LeftItem> | ||
| <Header.RightItem> | ||
| {projectId && issueId && ( | ||
| <IssueDetailQuickActions | ||
| workspaceSlug={workspaceSlug?.toString()} | ||
| projectId={projectId?.toString()} | ||
| issueId={issueId?.toString()} | ||
| /> | ||
| )} | ||
| </Header.RightItem> | ||
| </Header> | ||
| <> | ||
| <div className="z-20"> | ||
| <Row className="h-header flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100"> | ||
| <div className="flex items-center gap-2 divide-x divide-custom-border-100 h-full w-full"> | ||
| <div className="flex items-center h-full w-full flex-1"> | ||
| <Header className="h-full"> | ||
| <Header.LeftItem className="h-full max-w-full"> | ||
| <TabNavigationRoot | ||
| workspaceSlug={workspaceSlug} | ||
| projectId={issueDetails?.project_id?.toString() ?? ""} | ||
| /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Empty projectId passed to TabNavigationRoot componentWhen |
||
| </Header.LeftItem> | ||
| </Header> | ||
| </div> | ||
| </div> | ||
| </Row> | ||
| </div> | ||
| <AppHeader header={<WorkItemDetailsHeader />} /> | ||
| </> | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| "use client"; | ||
|
|
||
| import React from "react"; | ||
| import { observer } from "mobx-react"; | ||
| import { useParams } from "next/navigation"; | ||
| // plane ui | ||
| import { WorkItemsIcon } from "@plane/propel/icons"; | ||
| import { Breadcrumbs, Header } from "@plane/ui"; | ||
| // components | ||
| import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; | ||
| import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions"; | ||
| // hooks | ||
| import { useIssueDetail } from "@/hooks/store/use-issue-detail"; | ||
| import { useProject } from "@/hooks/store/use-project"; | ||
| import { useAppRouter } from "@/hooks/use-app-router"; | ||
|
|
||
| export const WorkItemDetailsHeader = 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; | ||
|
|
||
| if (!workspaceSlug || !projectId || !issueId) return null; | ||
| return ( | ||
| <Header> | ||
| <Header.LeftItem> | ||
| <Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}> | ||
| <Breadcrumbs.Item | ||
| component={ | ||
| <BreadcrumbLink | ||
| label="Work Items" | ||
| href={`/${workspaceSlug}/projects/${projectId}/issues/`} | ||
| icon={<WorkItemsIcon className="h-4 w-4 text-custom-text-300" />} | ||
| /> | ||
| } | ||
| /> | ||
| <Breadcrumbs.Item | ||
| component={ | ||
| <BreadcrumbLink | ||
| label={projectDetails && issueDetails ? `${projectDetails.identifier}-${issueDetails.sequence_id}` : ""} | ||
| /> | ||
| } | ||
| /> | ||
| </Breadcrumbs> | ||
| </Header.LeftItem> | ||
| <Header.RightItem> | ||
| {projectId && issueId && ( | ||
| <IssueDetailQuickActions | ||
| workspaceSlug={workspaceSlug?.toString()} | ||
| projectId={projectId?.toString()} | ||
| issueId={issueId?.toString()} | ||
| /> | ||
| )} | ||
| </Header.RightItem> | ||
| </Header> | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Missing validation for navigation field in request
The
patchmethod directly accessesrequest.data["navigation"]without checking if the key exists, which will raise aKeyErrorif the client doesn't send thenavigationfield. This causes the API to return a 500 error instead of a proper validation error. The code should validate thatnavigationexists inrequest.databefore accessing it.