From 0ec1b91d3931519a2258688ed4009331d5d88954 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 4 Feb 2025 21:08:10 +0530 Subject: [PATCH 01/20] feat: meta endpoint for issue --- apiserver/plane/app/urls/issue.py | 6 ++++++ apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/issue/base.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index f09370d7d16..86bbebb2860 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -26,6 +26,7 @@ IssueBulkUpdateDateEndpoint, IssueVersionEndpoint, IssueDescriptionVersionEndpoint, + IssueMetaEndpoint, ) urlpatterns = [ @@ -278,4 +279,9 @@ IssueDescriptionVersionEndpoint.as_view(), name="page-versions", ), + path( + "workspaces//projects//issues//meta/", + IssueMetaEndpoint.as_view(), + name="issue-meta", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 04162efd9b7..e84402cfa59 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -116,6 +116,7 @@ IssuePaginatedViewSet, IssueDetailEndpoint, IssueBulkUpdateDateEndpoint, + IssueMetaEndpoint, ) from .issue.activity import IssueActivityEndpoint diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index c37a3cad12f..e769cedce24 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -1096,3 +1096,19 @@ 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, + ) From 659656db4c78421323ec2357aa96d8d16df2122e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 4 Feb 2025 22:14:41 +0530 Subject: [PATCH 02/20] chore: add detail endpoint --- apiserver/plane/app/urls/issue.py | 6 + apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/issue/base.py | 159 ++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 86bbebb2860..6c5e450331f 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -27,6 +27,7 @@ IssueVersionEndpoint, IssueDescriptionVersionEndpoint, IssueMetaEndpoint, + IssueDetailIdentifierEndpoint, ) urlpatterns = [ @@ -284,4 +285,9 @@ IssueMetaEndpoint.as_view(), name="issue-meta", ), + path( + "workspaces//work-items/-/", + IssueDetailIdentifierEndpoint.as_view(), + name="issue-detail-identifier", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index e84402cfa59..684179d9045 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -117,6 +117,7 @@ IssueDetailEndpoint, IssueBulkUpdateDateEndpoint, IssueMetaEndpoint, + IssueDetailIdentifierEndpoint, ) from .issue.activity import IssueActivityEndpoint diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index e769cedce24..dd469155083 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -1112,3 +1112,162 @@ def get(self, request, slug, project_id, issue_id): }, 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) From 629decd8206c3adce0c4172c3e5d0cca785378c2 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 00:08:38 +0530 Subject: [PATCH 03/20] chore: getIssueMetaFromURL and retrieveWithIdentifier endpoint added --- web/core/services/issue/issue.service.ts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts index 8fa711ff431..203b319c7d3 100644 --- a/web/core/services/issue/issue.service.ts +++ b/web/core/services/issue/issue.service.ts @@ -439,4 +439,44 @@ export class IssueService extends APIService { throw error?.response?.data; }); } + + async getIssueMetaFromURL( + workspaceSlug: string, + projectId: string, + issueId: string + ): Promise<{ + project_identifier: string; + sequence_id: string; + }> { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/meta/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async retrieveWithIdentifier( + workspaceSlug: string, + project_identifier: string, + issue_sequence: string, + queries?: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/work-items/${project_identifier}-${issue_sequence}/`, { + params: queries, + }) + .then((response) => { + // skip issue update when the service type is epic + if (response.data && this.serviceType === EIssueServiceType.ISSUES) { + updateIssue({ ...response.data, is_local_update: 1 }); + } + // add is_epic flag when the service type is epic + if (response.data && this.serviceType === EIssueServiceType.EPICS) { + response.data.is_epic = true; + } + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } } From a97ca4b194815d3df1b3fe4e1d4b08670f0a5430 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 00:12:15 +0530 Subject: [PATCH 04/20] chore: issue store updated --- .../store/issue/issue-details/issue.store.ts | 68 +++++++++++++++++++ .../store/issue/issue-details/root.store.ts | 2 + web/core/store/issue/issue.store.ts | 29 ++++++++ 3 files changed, 99 insertions(+) diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts index 37541cc1204..fb6ad36cc6b 100644 --- a/web/core/store/issue/issue-details/issue.store.ts +++ b/web/core/store/issue/issue-details/issue.store.ts @@ -32,6 +32,7 @@ export interface IIssueStoreActions { removeModuleIds: string[] ) => Promise; removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise; + fetchIssueWithIdentifier: (workspaceSlug: string, project_identifier: string, sequence_id: string) => Promise; } export interface IIssueStore extends IIssueStoreActions { @@ -39,6 +40,7 @@ export interface IIssueStore extends IIssueStoreActions { getIsLocalDBIssueDescription: (issueId: string | undefined) => boolean; // helper methods getIssueById: (issueId: string) => TIssue | undefined; + getIssueIdByIdentifier: (issueIdentifier: string) => string | undefined; } export class IssueStore implements IIssueStore { @@ -86,6 +88,11 @@ export class IssueStore implements IIssueStore { return this.rootIssueDetailStore.rootIssueStore.issues.getIssueById(issueId) ?? undefined; }); + getIssueIdByIdentifier = computedFn((issueIdentifier: string) => { + if (!issueIdentifier) return undefined; + return this.rootIssueDetailStore.rootIssueStore.issues.getIssueIdByIdentifier(issueIdentifier) ?? undefined; + }); + // actions fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueStatus = "DEFAULT") => { const query = { @@ -285,4 +292,65 @@ export class IssueStore implements IIssueStore { await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); return currentModule; }; + + fetchIssueWithIdentifier = async (workspaceSlug: string, project_identifier: string, sequence_id: string) => { + const query = { + expand: "issue_reactions,issue_attachments,issue_link,parent", + }; + const issue = await this.issueService.retrieveWithIdentifier(workspaceSlug, project_identifier, sequence_id, query); + const issueIdentifier = `${project_identifier}-${sequence_id}`; + const issueId = issue?.id; + const projectId = issue?.project_id; + + if (!issue || !projectId || !issueId) throw new Error("Issue not found"); + + const issuePayload = this.addIssueToStore(issue); + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]); + + // handle parent issue if exists + if (issue?.parent && issue?.parent?.id && issue?.parent?.project_id) { + this.issueService.retrieve(workspaceSlug, issue.parent.project_id, issue.parent.id).then((res) => { + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([res]); + }); + } + + // add identifiers to map + this.rootIssueDetailStore.rootIssueStore.issues.addIssueIdentifier(issueIdentifier, issueId); + + // add related data + if (issue.issue_reactions) this.rootIssueDetailStore.addReactions(issue.id, issue.issue_reactions); + if (issue.issue_link) this.rootIssueDetailStore.addLinks(issue.id, issue.issue_link); + if (issue.issue_attachments) this.rootIssueDetailStore.addAttachments(issue.id, issue.issue_attachments); + this.rootIssueDetailStore.addSubscription(issue.id, issue.is_subscribed); + + // fetch related data + // issue reactions + if (issue.issue_reactions) this.rootIssueDetailStore.addReactions(issueId, issue.issue_reactions); + + // fetch issue links + if (issue.issue_link) this.rootIssueDetailStore.addLinks(issueId, issue.issue_link); + + // fetch issue attachments + if (issue.issue_attachments) this.rootIssueDetailStore.addAttachments(issueId, issue.issue_attachments); + + this.rootIssueDetailStore.addSubscription(issueId, issue.is_subscribed); + + // fetch issue activity + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + + // fetch issue comments + this.rootIssueDetailStore.comment.fetchComments(workspaceSlug, projectId, issueId); + + // fetch sub issues + this.rootIssueDetailStore.subIssues.fetchSubIssues(workspaceSlug, projectId, issueId); + + // fetch issue relations + this.rootIssueDetailStore.relation.fetchRelations(workspaceSlug, projectId, issueId); + + // fetching states + // TODO: check if this function is required + this.rootIssueDetailStore.rootIssueStore.rootStore.state.fetchProjectStates(workspaceSlug, projectId); + + return issue; + }; } diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts index c72ef1a77d7..63faf0571a3 100644 --- a/web/core/store/issue/issue-details/root.store.ts +++ b/web/core/store/issue/issue-details/root.store.ts @@ -259,6 +259,8 @@ export class IssueDetail implements IIssueDetail { issueId: string, issueStatus: "DEFAULT" | "DRAFT" = "DEFAULT" ) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueStatus); + fetchIssueWithIdentifier = async (workspaceSlug: string, projectIdentifier: string, sequenceId: string) => + this.issue.fetchIssueWithIdentifier(workspaceSlug, projectIdentifier, sequenceId); updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => this.issue.updateIssue(workspaceSlug, projectId, issueId, data); removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => diff --git a/web/core/store/issue/issue.store.ts b/web/core/store/issue/issue.store.ts index 5f40f2443ee..6efd1dccdc1 100644 --- a/web/core/store/issue/issue.store.ts +++ b/web/core/store/issue/issue.store.ts @@ -15,19 +15,23 @@ import { IssueService } from "@/services/issue"; export type IIssueStore = { // observables issuesMap: Record; // Record defines issue_id as key and TIssue as value + issuesIdentifierMap: Record; // Record defines issue_identifier as key and issue_id as value // actions getIssues(workspaceSlug: string, projectId: string, issueIds: string[]): Promise; addIssue(issues: TIssue[]): void; + addIssueIdentifier(issueIdentifier: string, issueId: string): void; updateIssue(issueId: string, issue: Partial): void; removeIssue(issueId: string): void; // helper methods getIssueById(issueId: string): undefined | TIssue; + getIssueIdByIdentifier(issueIdentifier: string): undefined | string; getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): TIssue[]; // Record defines issue_id as key and TIssue as value }; export class IssueStore implements IIssueStore { // observables issuesMap: { [issue_id: string]: TIssue } = {}; + issuesIdentifierMap: { [issue_identifier: string]: string } = {}; // service issueService; @@ -35,8 +39,10 @@ export class IssueStore implements IIssueStore { makeObservable(this, { // observable issuesMap: observable, + issuesIdentifierMap: observable, // actions addIssue: action, + addIssueIdentifier: action, updateIssue: action, removeIssue: action, }); @@ -59,6 +65,19 @@ export class IssueStore implements IIssueStore { }); }; + /** + * @description This method will add issue_identifier to the issuesIdentifierMap + * @param issueIdentifier + * @param issueId + * @returns {void} + */ + addIssueIdentifier = (issueIdentifier: string, issueId: string) => { + if (!issueIdentifier || !issueId) return; + runInAction(() => { + set(this.issuesIdentifierMap, issueIdentifier, issueId); + }); + }; + getIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => { const issues = await this.issueService.retrieveIssues(workspaceSlug, projectId, issueIds); @@ -116,6 +135,16 @@ export class IssueStore implements IIssueStore { return this.issuesMap[issueId]; }); + /** + * @description This method will return the issue_id from the issuesIdentifierMap + * @param {string} issueIdentifier + * @returns {string | undefined} + */ + getIssueIdByIdentifier = computedFn((issueIdentifier: string) => { + if (!issueIdentifier || !this.issuesIdentifierMap[issueIdentifier]) return undefined; + return this.issuesIdentifierMap[issueIdentifier]; + }); + /** * @description This method will return the issues from the issuesMap * @param {string[]} issueIds From 0a9bf13bc053284c4e2180150c7fcca75cb972e3 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 00:14:17 +0530 Subject: [PATCH 05/20] chore: move issue detail to new route and add redirection for old route --- .../(detail) => browse/[workItem]}/header.tsx | 25 ++-- .../(detail) => browse/[workItem]}/layout.tsx | 0 .../(projects)/browse/[workItem]/page.tsx | 113 ++++++++++++++++++ .../issues/(detail)/[issueId]/page.tsx | 106 ++++------------ 4 files changed, 150 insertions(+), 94 deletions(-) rename web/app/[workspaceSlug]/(projects)/{projects/(detail)/[projectId]/issues/(detail) => browse/[workItem]}/header.tsx (69%) rename web/app/[workspaceSlug]/(projects)/{projects/(detail)/[projectId]/issues/(detail) => browse/[workItem]}/layout.tsx (100%) create mode 100644 web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx similarity index 69% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/header.tsx rename to web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx index 699c9865fff..8d1c61f2297 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx @@ -16,14 +16,17 @@ import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; export const ProjectIssueDetailsHeader = observer(() => { // router const router = useAppRouter(); - const { workspaceSlug, projectId, issueId } = useParams(); + const { workspaceSlug, workItem } = useParams(); // store hooks - const { currentProjectDetails, loader } = useProject(); + const { getProjectById, loader } = useProject(); const { - issue: { getIssueById }, + 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 (
@@ -48,9 +51,7 @@ export const ProjectIssueDetailsHeader = observer(() => { link={ } @@ -59,11 +60,13 @@ export const ProjectIssueDetailsHeader = observer(() => { - + {projectId && issueId && ( + + )}
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/layout.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/layout.tsx rename to web/app/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx new file mode 100644 index 00000000000..75f3660658c --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx @@ -0,0 +1,113 @@ +"use client"; + +import React, { useEffect } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { useTheme } from "next-themes"; +import useSWR from "swr"; +// ui +import { Loader } from "@plane/ui"; +// components +import { EmptyState } from "@/components/common"; +import { PageHead } from "@/components/core"; +import { IssueDetailRoot } from "@/components/issues"; +// hooks +import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; +// assets +import { useAppRouter } from "@/hooks/use-app-router"; +import emptyIssueDark from "@/public/empty-state/search/issues-dark.webp"; +import emptyIssueLight from "@/public/empty-state/search/issues-light.webp"; + +const IssueDetailsPage = observer(() => { + // router + const router = useAppRouter(); + const { workspaceSlug, workItem } = useParams(); + // hooks + const { resolvedTheme } = useTheme(); + // store hooks + const { + fetchIssueWithIdentifier, + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + const { toggleIssueDetailSidebar, issueDetailSidebarCollapsed } = useAppTheme(); + + const projectIdentifier = workItem?.toString().split("-")[0]; + const sequence_id = workItem?.toString().split("-")[1]; + + // fetching issue details + const { data, isLoading, error } = useSWR( + workspaceSlug && projectIdentifier && sequence_id + ? `ISSUE_DETAIL_${workspaceSlug}_${projectIdentifier}_${sequence_id}` + : null, + workspaceSlug && projectIdentifier && sequence_id + ? () => fetchIssueWithIdentifier(workspaceSlug.toString(), projectIdentifier, sequence_id) + : null + ); + console.log("data", data); + const issueId = data?.id; + const projectId = data?.project_id; + // derived values + const issue = getIssueById(issueId?.toString() || "") || undefined; + const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined; + const issueLoader = !issue || isLoading; + const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; + + useEffect(() => { + const handleToggleIssueDetailSidebar = () => { + if (window && window.innerWidth < 768) { + toggleIssueDetailSidebar(true); + } + if (window && issueDetailSidebarCollapsed && window.innerWidth >= 768) { + toggleIssueDetailSidebar(false); + } + }; + window.addEventListener("resize", handleToggleIssueDetailSidebar); + handleToggleIssueDetailSidebar(); + return () => window.removeEventListener("resize", handleToggleIssueDetailSidebar); + }, [issueDetailSidebarCollapsed, toggleIssueDetailSidebar]); + + return ( + <> + + {error ? ( + router.push(`/${workspaceSlug}/projects/${projectId}/issues`), + }} + /> + ) : issueLoader ? ( + +
+ + + + +
+
+ + + + +
+
+ ) : ( + workspaceSlug && + projectId && + issueId && ( + + ) + )} + + ); +}); + +export default IssueDetailsPage; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx index 5b68ae688ed..0ad3aca950d 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx @@ -1,103 +1,43 @@ "use client"; -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { useTheme } from "next-themes"; -import useSWR from "swr"; -// ui -import { Loader } from "@plane/ui"; // components -import { EmptyState } from "@/components/common"; -import { PageHead } from "@/components/core"; -import { IssueDetailRoot } from "@/components/issues"; +import { LogoSpinner } from "@/components/common"; // hooks -import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; -// assets import { useAppRouter } from "@/hooks/use-app-router"; -import emptyIssueDark from "@/public/empty-state/search/issues-dark.webp"; -import emptyIssueLight from "@/public/empty-state/search/issues-light.webp"; +// services +import { IssueService } from "@/services/issue/issue.service"; + +const issueService = new IssueService(); const IssueDetailsPage = observer(() => { - // router const router = useAppRouter(); const { workspaceSlug, projectId, issueId } = useParams(); - // hooks - const { resolvedTheme } = useTheme(); - // store hooks - const { - fetchIssue, - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - const { toggleIssueDetailSidebar, issueDetailSidebarCollapsed } = useAppTheme(); - // fetching issue details - const { isLoading, error } = useSWR( - workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, - workspaceSlug && projectId && issueId - ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString()) - : null - ); - // derived values - const issue = getIssueById(issueId?.toString() || "") || undefined; - const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined; - const issueLoader = !issue || isLoading; - const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; useEffect(() => { - const handleToggleIssueDetailSidebar = () => { - if (window && window.innerWidth < 768) { - toggleIssueDetailSidebar(true); - } - if (window && issueDetailSidebarCollapsed && window.innerWidth >= 768) { - toggleIssueDetailSidebar(false); + const redirectToBrowseUrl = async () => { + if (!workspaceSlug || !projectId || !issueId) return; + try { + const meta = await issueService.getIssueMetaFromURL( + workspaceSlug.toString(), + projectId.toString(), + issueId.toString() + ); + router.push(`/${workspaceSlug}/browse/${meta.project_identifier}-${meta.sequence_id}`); + } catch (error) { + console.error(error); } }; - window.addEventListener("resize", handleToggleIssueDetailSidebar); - handleToggleIssueDetailSidebar(); - return () => window.removeEventListener("resize", handleToggleIssueDetailSidebar); - }, [issueDetailSidebarCollapsed, toggleIssueDetailSidebar]); + + redirectToBrowseUrl(); + }, [workspaceSlug, projectId, issueId, router]); return ( - <> - - {error ? ( - router.push(`/${workspaceSlug}/projects/${projectId}/issues`), - }} - /> - ) : issueLoader ? ( - -
- - - - -
-
- - - - -
-
- ) : ( - workspaceSlug && - projectId && - issueId && ( - - ) - )} - +
+ +
); }); From e18962be8ecd587b3c75d37dab1f917d8ac40c73 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 00:15:15 +0530 Subject: [PATCH 06/20] fix: issue details permission --- web/core/components/issues/issue-detail/root.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/core/components/issues/issue-detail/root.tsx b/web/core/components/issues/issue-detail/root.tsx index 136edab8e20..02ced1eecb6 100644 --- a/web/core/components/issues/issue-detail/root.tsx +++ b/web/core/components/issues/issue-detail/root.tsx @@ -332,7 +332,12 @@ export const IssueDetailRoot: FC = observer((props) => { // issue details const issue = getIssueById(issueId); // checking if issue is editable, based on user role - const isEditable = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT); + const isEditable = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug, + projectId + ); return ( <> From 4f5326355cd18042f7a0c6beec21baa4af44b180 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 00:29:05 +0530 Subject: [PATCH 07/20] fix: work item detail header --- .../(projects)/browse/[workItem]/header.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx index 8d1c61f2297..9fbed762221 100644 --- a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx @@ -2,16 +2,15 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +import { Briefcase } from "lucide-react"; // ui -import { Breadcrumbs, LayersIcon, Header } from "@plane/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"; -// plane web -import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; export const ProjectIssueDetailsHeader = observer(() => { // router @@ -33,7 +32,27 @@ export const ProjectIssueDetailsHeader = observer(() => {
- + + + + ) + ) : ( + + + + ) + } + /> + } + /> Date: Wed, 5 Feb 2025 15:37:18 +0530 Subject: [PATCH 08/20] chore: generateWorkItemLink helper function added --- web/helpers/issue.helper.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index dacfe874431..ef3441e45c8 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -325,3 +325,27 @@ export const getIssuesShouldFallbackToServer = (queries: any) => { return false; }; + +export const generateWorkItemLink = ({ + workspaceSlug, + projectId, + issueId, + projectIdentifier, + sequenceId, + isArchived = false, + isEpic = false, +}: { + workspaceSlug: string | undefined | null; + projectId: string | undefined | null; + issueId: string | undefined | null; + projectIdentifier: string | undefined | null; + sequenceId: string | number | undefined | null; + isArchived?: boolean; + isEpic?: boolean; +}): string => { + const archiveIssueLink = `/${workspaceSlug}/projects/${projectId}/archives/issues/${issueId}`; + const epicLink = `/${workspaceSlug}/projects/${projectId}/epics/${issueId}`; + const workItemLink = `/${workspaceSlug}/browse/${projectIdentifier}-${sequenceId}/`; + + return isArchived ? archiveIssueLink : isEpic ? epicLink : workItemLink; +}; From d68c199857887e6424629eb3feb8af742e3d8446 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 15:38:02 +0530 Subject: [PATCH 09/20] chore: copyTextToClipboard helper function updated --- web/helpers/string.helper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 85cc872bf88..0f8e9082dc0 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -72,10 +72,10 @@ export const copyTextToClipboard = async (text: string) => { * const text = copyUrlToClipboard("path"); * copied URL: origin_url/path */ -export const copyUrlToClipboard = async (path: string) => { +export const copyUrlToClipboard = async (path: string, addSlash: boolean = true) => { const originUrl = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - await copyTextToClipboard(`${originUrl}/${path}`); + await copyTextToClipboard(`${originUrl}${addSlash ? "/" : ""}${path}`); }; export const generateRandomColor = (string: string): string => { From 39392718361afc8e0f0833a28492c96c6bdf3f12 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 15:40:02 +0530 Subject: [PATCH 10/20] chore: workItemLink updated --- web/core/components/core/activity.tsx | 13 ++++++-- .../modals/existing-issues-list-modal.tsx | 9 +++++- .../widgets/issue-panels/issue-list-item.tsx | 31 +++++++++++++++++-- .../inbox/content/inbox-issue-header.tsx | 22 +++++++------ .../content/inbox-issue-mobile-header.tsx | 20 +++++++++--- .../inbox/content/issue-properties.tsx | 13 ++++++-- .../create-issue-toast-action-items.tsx | 18 ++++++++--- .../issue-detail-widgets/relations/helper.tsx | 2 +- .../issue-detail-quick-actions.tsx | 16 ++++++++-- .../issue-layouts/calendar/issue-block.tsx | 19 ++++++++++-- .../issues/issue-layouts/gantt/blocks.tsx | 16 ++++++++-- .../issues/issue-layouts/kanban/block.tsx | 19 +++++++++--- .../issues/issue-layouts/list/block.tsx | 12 ++++++- .../properties/all-properties.tsx | 24 +++++++------- .../quick-action-dropdowns/all-issue.tsx | 17 +++++++--- .../quick-action-dropdowns/cycle-issue.tsx | 17 +++++++--- .../quick-action-dropdowns/module-issue.tsx | 17 +++++++--- .../quick-action-dropdowns/project-issue.tsx | 18 ++++++++--- .../issue-layouts/spreadsheet/issue-row.tsx | 13 +++++++- .../issues/parent-issues-list-modal.tsx | 9 +++++- .../issues/peek-overview/header.tsx | 18 ++++++++--- .../issues/relations/issue-list-item.tsx | 18 ++++++++--- .../issues/sub-issues/issue-list-item.tsx | 11 ++++++- .../use-issue-peek-overview-redirection.tsx | 19 +++++++++--- web/helpers/string.helper.ts | 1 + 25 files changed, 308 insertions(+), 84 deletions(-) diff --git a/web/core/components/core/activity.tsx b/web/core/components/core/activity.tsx index 9a06b336b4f..0f8acdd3c25 100644 --- a/web/core/components/core/activity.tsx +++ b/web/core/components/core/activity.tsx @@ -24,6 +24,7 @@ import { IIssueActivity } from "@plane/types"; import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon, Intake } from "@plane/ui"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { capitalizeFirstLetter } from "@/helpers/string.helper"; import { useLabel } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -34,6 +35,14 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const { workspaceSlug } = useParams(); const { isMobile } = usePlatformOS(); + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString() ?? activity.workspace_detail?.slug, + projectId: activity?.project, + issueId: activity?.issue, + projectIdentifier: activity?.project_detail?.identifier, + sequenceId: activity?.issue_detail?.sequence_id, + }); + return ( { {activity?.issue_detail ? ( = (props) => { {issue.name}
= observ const projectDetails = getProjectById(issue.project_id); const targetDate = getDate(issue.target_date); + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issue?.sequence_id, + }); + return ( onClick(issue)} className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" > @@ -253,9 +262,17 @@ export const CreatedOverdueIssueListItem: React.FC = observe const dueBy: number = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issue?.sequence_id, + }); + return ( onClick(issue)} className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" > @@ -311,9 +328,17 @@ export const CreatedCompletedIssueListItem: React.FC = obser const projectDetails = getProjectById(issue.project_id); + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issue?.sequence_id, + }); + return ( onClick(issue)} className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" > diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index cb0dbcfc5f1..5be319ee650 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -30,6 +30,7 @@ import { CreateUpdateIssueModal, NameDescriptionUpdateStatus } from "@/component // helpers import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; import { EInboxIssueStatus } from "@/helpers/inbox.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks import { useUser, useProjectInbox, useProject, useUserPermissions } from "@/hooks/store"; @@ -102,7 +103,6 @@ export const InboxIssueActionsHeader: FC = observer((p const currentInboxIssueId = inboxIssue?.issue?.id; - const issueLink = `${workspaceSlug}/projects/${issue?.project_id}/issues/${currentInboxIssueId}`; const intakeIssueLink = `${workspaceSlug}/projects/${issue?.project_id}/inbox/?currentTab=${currentTab}&inboxIssueId=${currentInboxIssueId}`; const redirectIssue = (): string | undefined => { @@ -227,6 +227,14 @@ export const InboxIssueActionsHeader: FC = observer((p if (!inboxIssue) return null; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId: issue?.project_id, + issueId: currentInboxIssueId, + projectIdentifier: currentProjectDetails?.identifier, + sequenceId: issue?.sequence_id, + }); + return ( <> <> @@ -354,17 +362,11 @@ export const InboxIssueActionsHeader: FC = observer((p variant="neutral-primary" prependIcon={} size="sm" - onClick={() => handleCopyIssueLink(issueLink)} + onClick={() => handleCopyIssueLink(workItemLink)} > Copy issue link - - router.push(`/${workspaceSlug}/projects/${issue?.project_id}/issues/${currentInboxIssueId}`) - } - target="_self" - > + router.push(workItemLink)} target="_self"> @@ -434,7 +436,7 @@ export const InboxIssueActionsHeader: FC = observer((p handleCopyIssueLink(issueLink)} + handleCopyIssueLink={() => handleCopyIssueLink(workItemLink)} setAcceptIssueModal={setAcceptIssueModal} setDeclineIssueModal={setDeclineIssueModal} handleIssueSnoozeAction={handleIssueSnoozeAction} diff --git a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx index 987a663a5ea..0fce42d8b24 100644 --- a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx @@ -23,7 +23,9 @@ import { NameDescriptionUpdateStatus } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks +import { useProject } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // store types import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; @@ -77,6 +79,8 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = handleActionWithPermission, } = props; const router = useAppRouter(); + const { getProjectIdentifierById } = useProject(); + const issue = inboxIssue?.issue; const currentInboxIssueId = issue?.id; // days left for snooze @@ -84,6 +88,16 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = if (!issue || !inboxIssue) return null; + const projectIdentifier = getProjectIdentifierById(issue?.project_id); + + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId: issue?.project_id, + issueId: currentInboxIssueId, + projectIdentifier, + sequenceId: issue?.sequence_id, + }); + return (
{isNotificationEmbed && ( @@ -132,11 +146,7 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = )} {isAcceptedOrDeclined && ( - - router.push(`/${workspaceSlug}/projects/${issue?.project_id}/issues/${currentInboxIssueId}`) - } - > + router.push(workItemLink)}>
Open issue diff --git a/web/core/components/inbox/content/issue-properties.tsx b/web/core/components/inbox/content/issue-properties.tsx index db7ad8e4189..e189df9fa5c 100644 --- a/web/core/components/inbox/content/issue-properties.tsx +++ b/web/core/components/inbox/content/issue-properties.tsx @@ -10,6 +10,7 @@ import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "@ import { IssueLabel, TIssueOperations } from "@/components/issues"; // helper import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useProject } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; @@ -34,6 +35,14 @@ export const InboxIssueContentProperties: React.FC = observer((props) => minDate?.setDate(minDate.getDate()); if (!issue || !issue?.id) return <>; + const duplicateWorkItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId, + issueId: duplicateIssueDetails?.id, + projectIdentifier: currentProjectDetails?.identifier, + sequenceId: duplicateIssueDetails?.sequence_id, + }); + return (
@@ -169,9 +178,9 @@ export const InboxIssueContentProperties: React.FC = observer((props) =>
{ - router.push(`/${workspaceSlug}/projects/${projectId}/issues/${duplicateIssueDetails?.id}`); + router.push(duplicateWorkItemLink); }} target="_self" > diff --git a/web/core/components/issues/create-issue-toast-action-items.tsx b/web/core/components/issues/create-issue-toast-action-items.tsx index aff00f00031..64433d43c54 100644 --- a/web/core/components/issues/create-issue-toast-action-items.tsx +++ b/web/core/components/issues/create-issue-toast-action-items.tsx @@ -2,9 +2,10 @@ import React, { FC, useState } from "react"; import { observer } from "mobx-react"; // helpers +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useIssueDetail } from "@/hooks/store"; +import { useIssueDetail, useProject } from "@/hooks/store"; type TCreateIssueToastActionItems = { workspaceSlug: string; @@ -21,17 +22,26 @@ export const CreateIssueToastActionItems: FC = obs const { issue: { getIssueById }, } = useIssueDetail(); + const { getProjectIdentifierById } = useProject(); // derived values const issue = getIssueById(issueId); + const projectIdentifier = getProjectIdentifierById(issue?.project_id); if (!issue) return null; - const issueLink = `${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}/${issueId}`; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId, + projectIdentifier, + sequenceId: issue?.sequence_id, + isEpic, + }); const copyToClipboard = async (e: React.MouseEvent) => { try { - await copyUrlToClipboard(issueLink); + await copyUrlToClipboard(workItemLink, false); setCopied(true); setTimeout(() => setCopied(false), 3000); } catch (error) { @@ -44,7 +54,7 @@ export const CreateIssueToastActionItems: FC = obs return (
({ copyText: (text: string) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${text}`).then(() => { + copyTextToClipboard(`${originURL}${text}`).then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Link Copied!", diff --git a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx index 31858bff15f..9d7f904225e 100644 --- a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx +++ b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx @@ -13,12 +13,14 @@ import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker"; import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { useEventTracker, useIssueDetail, useIssues, + useProject, useProjectState, useUser, useUserPermissions, @@ -49,6 +51,7 @@ export const IssueDetailQuickActions: FC = observer((props) => { const { allowPermissions } = useUserPermissions(); const { isMobile } = usePlatformOS(); const { getStateById } = useProjectState(); + const { getProjectIdentifierById } = useProject(); const { issue: { getIssueById }, removeIssue, @@ -68,11 +71,20 @@ export const IssueDetailQuickActions: FC = observer((props) => { if (!issue) return <>; const stateDetails = getStateById(issue.state_id); + const projectIdentifier = getProjectIdentifierById(projectId); + + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug, + projectId, + issueId, + projectIdentifier, + sequenceId: issue?.sequence_id, + }); // handlers const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => { + copyTextToClipboard(`${originURL}${workItemLink}`).then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Link Copied!", @@ -141,7 +153,7 @@ export const IssueDetailQuickActions: FC = observer((props) => { title: "Restore success", message: "Your issue can be found in project issues.", }); - router.push(`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`); + router.push(workItemLink); }) .catch(() => { setToast({ diff --git a/web/core/components/issues/issue-layouts/calendar/issue-block.tsx b/web/core/components/issues/issue-layouts/calendar/issue-block.tsx index 8a03ef4c840..127ebfb475e 100644 --- a/web/core/components/issues/issue-layouts/calendar/issue-block.tsx +++ b/web/core/components/issues/issue-layouts/calendar/issue-block.tsx @@ -13,8 +13,9 @@ import { TIssue } from "@plane/types"; import { Tooltip, ControlLink } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks -import { useIssueDetail, useIssues, useProjectState } from "@/hooks/store"; +import { useIssueDetail, useIssues, useProject, useProjectState } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -40,15 +41,17 @@ export const CalendarIssueBlock = observer( const blockRef = useRef(null); const menuActionRef = useRef(null); // hooks - const { workspaceSlug, projectId } = useParams(); + const { workspaceSlug } = useParams(); const { getProjectStates } = useProjectState(); const { getIsIssuePeeked } = useIssueDetail(); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic); const { isMobile } = usePlatformOS(); const storeType = useIssueStoreType() as CalendarStoreType; const { issuesFilter } = useIssues(storeType); + const { getProjectIdentifierById } = useProject(); const stateColor = getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; + const projectIdentifier = getProjectIdentifierById(issue?.project_id); // handlers const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug.toString(), issue, isMobile); @@ -72,10 +75,20 @@ export const CalendarIssueBlock = observer( const placement = isMenuActionRefAboveScreenBottom ? "bottom-end" : "top-end"; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier, + sequenceId: issue?.sequence_id, + isEpic, + isArchived: !!issue?.archived_at, + }); + return ( handleIssuePeekOverview(issue)} className="block w-full text-sm text-custom-text-100 rounded border-b md:border-[1px] border-custom-border-200 hover:border-custom-border-400" disabled={!!issue?.tempId || isMobile} diff --git a/web/core/components/issues/issue-layouts/gantt/blocks.tsx b/web/core/components/issues/issue-layouts/gantt/blocks.tsx index bd25dc916ed..f74e42232b1 100644 --- a/web/core/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/core/components/issues/issue-layouts/gantt/blocks.tsx @@ -8,8 +8,9 @@ import { Tooltip, ControlLink } from "@plane/ui"; import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks -import { useIssueDetail, useIssues, useProjectState } from "@/hooks/store"; +import { useIssueDetail, useIssues, useProject, useProjectState } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -90,12 +91,14 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => { const { isMobile } = usePlatformOS(); const storeType = useIssueStoreType() as GanttStoreType; const { issuesFilter } = useIssues(storeType); + const { getProjectIdentifierById } = useProject(); // handlers const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic); // derived values const issueDetails = getIssueById(issueId); + const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id); const handleIssuePeekOverview = (e: any) => { e.stopPropagation(true); @@ -103,10 +106,19 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => { handleRedirection(workspaceSlug, issueDetails, isMobile); }; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issueDetails?.project_id, + issueId, + projectIdentifier, + sequenceId: issueDetails?.sequence_id, + isEpic, + }); + return ( = observer((props) => { const { workspaceSlug: routerWorkspaceSlug } = useParams(); const workspaceSlug = routerWorkspaceSlug?.toString(); // hooks + const { getProjectIdentifierById } = useProject(); const { getIsIssuePeeked } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic); const { isMobile } = usePlatformOS(); @@ -147,6 +149,17 @@ export const KanbanIssueBlock: React.FC = observer((props) => { const canEditIssueProperties = canEditProperties(issue?.project_id ?? undefined); const isDragAllowed = canDragIssuesInCurrentGrouping && !issue?.tempId && canEditIssueProperties; + const projectIdentifier = getProjectIdentifierById(issue?.project_id); + + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId, + projectIdentifier, + sequenceId: issue?.sequence_id, + isEpic, + isArchived: !!issue?.archived_at, + }); useOutsideClickDetector(cardRef, () => { cardRef?.current?.classList?.remove(HIGHLIGHT_CLASS); @@ -215,9 +228,7 @@ export const KanbanIssueBlock: React.FC = observer((props) => { > { //TODO: add better logic. This is to have a min width for ID/Key based on the length of project identifier const keyMinWidth = displayProperties?.key ? (projectIdentifier?.length ?? 0) * 7 : 0; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId, + projectIdentifier, + sequenceId: issue?.sequence_id, + isEpic, + isArchived: !!issue?.archived_at, + }); return ( handleIssuePeekOverview(issue)} className="w-full cursor-pointer" disabled={!!issue?.tempId || issue?.is_draft} diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index c23d3052299..2889eff2fab 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -25,7 +25,7 @@ import { ISSUE_UPDATED } from "@/constants/event-tracker"; // helpers import { cn } from "@/helpers/common.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; +import { generateWorkItemLink, shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; // hooks import { useEventTracker, useLabel, useIssues, useProjectState, useProject, useProjectEstimates } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; @@ -243,17 +243,17 @@ export const IssueProperties: React.FC = observer((props) => { }); }; - const redirectToIssueDetail = () => { - router.push( - `/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${issue.id}#sub-issues` - ); - // router.push({ - // pathname: `/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${ - // issue.id - // }`, - // hash: "sub-issues", - // }); - }; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issue?.sequence_id, + isArchived: !!issue?.archived_at, + isEpic, + }); + + const redirectToIssueDetail = () => router.push(`${workItemLink}#sub-issues`); if (!displayProperties || !issue.project_id) return null; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index f7bce8e7219..01830528b49 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -16,9 +16,10 @@ import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/c import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useProjectState } from "@/hooks/store"; +import { useEventTracker, useProject, useProjectState } from "@/hooks/store"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -44,18 +45,26 @@ export const AllIssueQuickActions: React.FC = observer((props // store hooks const { setTrackElement } = useEventTracker(); const { getStateById } = useProjectState(); + const { getProjectIdentifierById } = useProject(); // derived values const stateDetails = getStateById(issue.state_id); const isEditingAllowed = !readOnly; + const projectIdentifier = getProjectIdentifierById(issue?.project_id); // auth const isArchivingAllowed = handleArchive && isEditingAllowed; const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier, + sequenceId: issue?.sequence_id, + }); - const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); + const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); const handleCopyIssueLink = () => - copyUrlToClipboard(issueLink).then(() => + copyUrlToClipboard(workItemLink, false).then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: "Link copied", diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index dff81f55f2e..46d1a8c8b3e 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -16,9 +16,10 @@ import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/c import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useIssues, useProjectState, useUserPermissions } from "@/hooks/store"; +import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -48,8 +49,10 @@ export const CycleIssueQuickActions: React.FC = observer((pro const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const { allowPermissions } = useUserPermissions(); const { getStateById } = useProjectState(); + const { getProjectIdentifierById } = useProject(); // derived values const stateDetails = getStateById(issue.state_id); + const projectIdentifier = getProjectIdentifierById(issue?.project_id); // auth const isEditingAllowed = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; @@ -59,12 +62,18 @@ export const CycleIssueQuickActions: React.FC = observer((pro const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier, + sequenceId: issue?.sequence_id, + }); - const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); + const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); const handleCopyIssueLink = () => - copyUrlToClipboard(issueLink).then(() => + copyUrlToClipboard(workItemLink, false).then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: "Link copied", diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index c6c8e691eca..86b2d7f8dfa 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -16,9 +16,10 @@ import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/c import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useIssues, useEventTracker, useProjectState, useUserPermissions } from "@/hooks/store"; +import { useIssues, useEventTracker, useProjectState, useUserPermissions, useProject } from "@/hooks/store"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -48,8 +49,10 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const { issuesFilter } = useIssues(EIssuesStoreType.MODULE); const { allowPermissions } = useUserPermissions(); const { getStateById } = useProjectState(); + const { getProjectIdentifierById } = useProject(); // derived values const stateDetails = getStateById(issue.state_id); + const projectIdentifier = getProjectIdentifierById(issue?.project_id); // auth const isEditingAllowed = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; @@ -59,12 +62,18 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier, + sequenceId: issue?.sequence_id, + }); - const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); + const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); const handleCopyIssueLink = () => - copyUrlToClipboard(issueLink).then(() => + copyUrlToClipboard(workItemLink, false).then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: "Link copied", diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index f9f5c88d7a5..b17629c465e 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -16,9 +16,10 @@ import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/c import { ARCHIVABLE_STATE_GROUPS } from "@/constants/state"; // helpers import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useIssues, useProjectState, useUserPermissions } from "@/hooks/store"; +import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -48,9 +49,11 @@ export const ProjectIssueQuickActions: React.FC = observer((p const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const { getStateById } = useProjectState(); + const { getProjectIdentifierById } = useProject(); // derived values const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const stateDetails = getStateById(issue.state_id); + const projectIdentifier = getProjectIdentifierById(issue?.project_id); // auth const isEditingAllowed = allowPermissions( @@ -63,16 +66,23 @@ export const ProjectIssueQuickActions: React.FC = observer((p const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier, + sequenceId: issue?.sequence_id, + }); + const handleCopyIssueLink = () => - copyUrlToClipboard(issueLink).then(() => + copyUrlToClipboard(workItemLink, false).then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: "Link copied", message: "Issue link copied to clipboard", }) ); - const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); + const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); const isDraftIssue = pathname?.includes("draft-issues") || false; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 0ce6465512b..2d7f4426e12 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -18,6 +18,7 @@ import RenderIfVisible from "@/components/core/render-if-visible-HOC"; import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet"; // helper import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useIssues, useProject } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; @@ -233,6 +234,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { const disableUserActions = !canEditProperties(issueDetail.project_id ?? undefined); const subIssuesCount = issueDetail?.sub_issues_count ?? 0; const isIssueSelected = selectionHelpers.getIsEntitySelected(issueDetail.id); + const projectIdentifier = getProjectIdentifierById(issueDetail.project_id); const canSelectIssues = !disableUserActions && !selectionHelpers.isSelectionDisabled; @@ -241,6 +243,15 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { ? (getProjectIdentifierById(issueDetail.project_id)?.length ?? 0 + 5) * 7 : 0; + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId: issueDetail?.project_id, + issueId, + projectIdentifier, + sequenceId: issueDetail?.sequence_id, + isEpic, + }); + return ( <> { className="relative md:sticky left-0 z-10 group/list-block bg-custom-background-100" > handleIssuePeekOverview(issueDetail)} className={cn( "group clickable cursor-pointer h-11 w-[28rem] flex items-center text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200 bg-transparent group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10", diff --git a/web/core/components/issues/parent-issues-list-modal.tsx b/web/core/components/issues/parent-issues-list-modal.tsx index 5d2d46caf68..1ee57378336 100644 --- a/web/core/components/issues/parent-issues-list-modal.tsx +++ b/web/core/components/issues/parent-issues-list-modal.tsx @@ -13,6 +13,7 @@ import { Loader } from "@plane/ui"; // components import { IssueSearchModalEmptyState } from "@/components/core"; // helpers +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import useDebounce from "@/hooks/use-debounce"; @@ -192,7 +193,13 @@ export const ParentIssuesListModal: React.FC = ({ {issue.name}
= observer((pr } = useIssueDetail(); const { getStateById } = useProjectState(); const { isMobile } = usePlatformOS(); + const { getProjectIdentifierById } = useProject(); // derived values const issueDetails = getIssueById(issueId); const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined; const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); + const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id); - const issueLink = `${workspaceSlug}/projects/${projectId}/${isArchived ? "archives/" : ""}issues/${issueId}`; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issueDetails?.project_id, + issueId, + projectIdentifier, + sequenceId: issueDetails?.sequence_id, + isArchived, + }); const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); - copyUrlToClipboard(issueLink).then(() => { + copyUrlToClipboard(workItemLink, false).then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Link Copied!", @@ -123,7 +133,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr - removeRoutePeekId()}> + removeRoutePeekId()}> diff --git a/web/core/components/issues/relations/issue-list-item.tsx b/web/core/components/issues/relations/issue-list-item.tsx index bb669762ae6..76ac69169e4 100644 --- a/web/core/components/issues/relations/issue-list-item.tsx +++ b/web/core/components/issues/relations/issue-list-item.tsx @@ -9,6 +9,8 @@ import { TIssue, TIssueServiceType } from "@plane/types"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; // components import { RelationIssueProperty } from "@/components/issues/relations"; +// helpers +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useProject, useProjectState } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; @@ -60,13 +62,21 @@ export const RelationIssueListItem: FC = observer((props) => { (issue?.project_id && getProjectStates(issue?.project_id)?.find((state) => issue?.state_id == state.id)) || undefined; if (!issue || !projectId) return <>; - const issueLink = `/${workspaceSlug}/projects/${projectId}/${issue.is_epic ? "epics" : "issues"}/${issue.id}`; + + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier: projectDetail?.identifier, + sequenceId: issue?.sequence_id, + isEpic: issue?.is_epic, + }); // handlers const handleIssuePeekOverview = (issue: TIssue) => { if (issue.is_epic) { // open epics in new tab - window.open(issueLink, "_blank"); + window.open(workItemLink, "_blank"); return; } handleRedirection(workspaceSlug, issue, isMobile); @@ -89,7 +99,7 @@ export const RelationIssueListItem: FC = observer((props) => { const handleCopyIssueLink = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); - issueOperations.copyText(issueLink); + issueOperations.copyText(workItemLink); }; const handleRemoveRelation = (e: React.MouseEvent) => { @@ -102,7 +112,7 @@ export const RelationIssueListItem: FC = observer((props) => {
handleIssuePeekOverview(issue)} className="w-full cursor-pointer" > diff --git a/web/core/components/issues/sub-issues/issue-list-item.tsx b/web/core/components/issues/sub-issues/issue-list-item.tsx index 28f6f47f31d..f43c56fe262 100644 --- a/web/core/components/issues/sub-issues/issue-list-item.tsx +++ b/web/core/components/issues/sub-issues/issue-list-item.tsx @@ -9,6 +9,7 @@ import { TIssue, TIssueServiceType } from "@plane/types"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useProject, useProjectState } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; @@ -84,11 +85,19 @@ export const IssueListItem: React.FC = observer((props) => { // check if current issue is the root issue const isCurrentIssueRoot = issueId === rootIssueId; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier: projectDetail?.identifier, + sequenceId: issue?.sequence_id, + }); + return (
handleIssuePeekOverview(issue)} className="w-full cursor-pointer" > diff --git a/web/core/hooks/use-issue-peek-overview-redirection.tsx b/web/core/hooks/use-issue-peek-overview-redirection.tsx index f1ad94c5963..beb27b70b39 100644 --- a/web/core/hooks/use-issue-peek-overview-redirection.tsx +++ b/web/core/hooks/use-issue-peek-overview-redirection.tsx @@ -3,8 +3,10 @@ import { useRouter } from "next/navigation"; import { EIssueServiceType } from "@plane/constants"; // types import { TIssue } from "@plane/types"; +// helpers +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks -import { useIssueDetail } from "./store"; +import { useIssueDetail, useProject } from "./store"; const useIssuePeekOverviewRedirection = (isEpic: boolean = false) => { // router @@ -13,6 +15,7 @@ const useIssuePeekOverviewRedirection = (isEpic: boolean = false) => { const { getIsIssuePeeked, setPeekIssue } = useIssueDetail( isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES ); + const { getProjectIdentifierById } = useProject(); const handleRedirection = ( workspaceSlug: string | undefined, @@ -22,12 +25,20 @@ const useIssuePeekOverviewRedirection = (isEpic: boolean = false) => { ) => { if (!issue) return; const { project_id, id, archived_at, tempId } = issue; + const projectIdentifier = getProjectIdentifierById(issue?.project_id); + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: project_id, + issueId: id, + projectIdentifier, + sequenceId: issue?.sequence_id, + isEpic, + isArchived: !!archived_at, + }); if (workspaceSlug && project_id && id && !getIsIssuePeeked(id) && !tempId) { - const issuePath = `/${workspaceSlug}/projects/${project_id}/${archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${id}`; - if (isMobile) { - router.push(issuePath); + router.push(workItemLink); } else { setPeekIssue({ workspaceSlug, projectId: project_id, issueId: id, nestingLevel, isArchived: !!archived_at }); } diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 0f8e9082dc0..3f31d73ac1a 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -68,6 +68,7 @@ export const copyTextToClipboard = async (text: string) => { /** * @description: This function copies the url to clipboard after prepending the origin URL to it * @param {string} path + * @param {boolean} addSlash * @example: * const text = copyUrlToClipboard("path"); * copied URL: origin_url/path From 39fbd3d5644a1db600bf10de428052c252194782 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 16:03:38 +0530 Subject: [PATCH 11/20] chore: workItemLink updated --- web/core/components/issues/sub-issues/issue-list-item.tsx | 2 +- web/core/components/issues/sub-issues/root.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/sub-issues/issue-list-item.tsx b/web/core/components/issues/sub-issues/issue-list-item.tsx index f43c56fe262..913f82f0e99 100644 --- a/web/core/components/issues/sub-issues/issue-list-item.tsx +++ b/web/core/components/issues/sub-issues/issue-list-item.tsx @@ -202,7 +202,7 @@ export const IssueListItem: React.FC = observer((props) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - subIssueOperations.copyText(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`); + subIssueOperations.copyText(workItemLink); }} >
diff --git a/web/core/components/issues/sub-issues/root.tsx b/web/core/components/issues/sub-issues/root.tsx index 84db36c4c09..bcb50525d16 100644 --- a/web/core/components/issues/sub-issues/root.tsx +++ b/web/core/components/issues/sub-issues/root.tsx @@ -135,7 +135,7 @@ export const SubIssuesRoot: FC = observer((props) => { () => ({ copyText: (text: string) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${text}`).then(() => { + copyTextToClipboard(`${originURL}${text}`).then(() => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Link Copied!", From 1a523ef17b31c0d875394f1b05425b3a523f33b2 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 5 Feb 2025 16:47:23 +0530 Subject: [PATCH 12/20] chore: workItemLink updated --- packages/types/src/issues/activity/base.d.ts | 2 +- .../components/command-palette/helpers.tsx | 10 ++++++- .../widgets/issue-panels/issue-list-item.tsx | 30 +++++++++++++++++-- .../activity/actions/helpers/issue-link.tsx | 15 ++++++---- .../issue-detail/parent/sibling-item.tsx | 17 +++++++---- .../issues/issue-detail/relation-select.tsx | 9 +++++- 6 files changed, 67 insertions(+), 16 deletions(-) diff --git a/packages/types/src/issues/activity/base.d.ts b/packages/types/src/issues/activity/base.d.ts index 63f365d893c..7b5653aede9 100644 --- a/packages/types/src/issues/activity/base.d.ts +++ b/packages/types/src/issues/activity/base.d.ts @@ -26,7 +26,7 @@ export type TIssueActivityProjectDetail = { export type TIssueActivityIssueDetail = { id: string; - sequence_id: boolean; + sequence_id: number; sort_order: boolean; name: string; description_html: string; diff --git a/web/core/components/command-palette/helpers.tsx b/web/core/components/command-palette/helpers.tsx index 99c8c310e43..eb34c512a26 100644 --- a/web/core/components/command-palette/helpers.tsx +++ b/web/core/components/command-palette/helpers.tsx @@ -11,6 +11,8 @@ import { } from "@plane/types"; // ui import { ContrastIcon, DiceIcon } from "@plane/ui"; +// helpers +import { generateWorkItemLink } from "@/helpers/issue.helper"; // plane web components import { IssueIdentifier } from "@/plane-web/components/issues"; @@ -48,7 +50,13 @@ export const commandGroups: {
), path: (issue: IWorkspaceIssueSearchResult) => - `/${issue?.workspace__slug}/projects/${issue?.project_id}/issues/${issue?.id}`, + generateWorkItemLink({ + workspaceSlug: issue?.workspace__slug, + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier: issue.project__identifier, + sequenceId: issue?.sequence_id, + }), title: "Issues", }, issue_view: { diff --git a/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx index c4098aa09cd..75471c6fd60 100644 --- a/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx +++ b/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx @@ -42,9 +42,17 @@ export const AssignedUpcomingIssueListItem: React.FC = obser const targetDate = getDate(issueDetails.target_date); + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issueDetails?.project_id, + issueId: issueDetails?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issueDetails?.sequence_id, + }); + return ( onClick(issueDetails)} className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" > @@ -102,9 +110,17 @@ export const AssignedOverdueIssueListItem: React.FC = observ const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issueDetails?.project_id, + issueId: issueDetails?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issueDetails?.sequence_id, + }); + return ( onClick(issueDetails)} className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" > @@ -155,9 +171,17 @@ export const AssignedCompletedIssueListItem: React.FC = obse const projectDetails = getProjectById(issueDetails.project_id); + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issueDetails?.project_id, + issueId: issueDetails?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issueDetails?.sequence_id, + }); + return ( onClick(issueDetails)} className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" > diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx index 91d50b0daba..28097270d4a 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx @@ -3,6 +3,7 @@ import { FC } from "react"; // hooks import { Tooltip } from "@plane/ui"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; import { useIssueDetail } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // ui @@ -21,6 +22,14 @@ export const IssueLink: FC = (props) => { const activity = getActivityById(activityId); if (!activity) return <>; + + const workItemLink = generateWorkItemLink({ + workspaceSlug: activity.workspace_detail?.slug, + projectId: activity.project, + issueId: activity.issue, + projectIdentifier: activity.project_detail.identifier, + sequenceId: activity.issue_detail.sequence_id, + }); return ( = (props) => { >
= observer((pro issue: { getIssueById }, } = useIssueDetail(); + // derived values const issueDetail = (issueId && getIssueById(issueId)) || undefined; if (!issueDetail) return <>; const projectDetails = (issueDetail.project_id && getProjectById(issueDetail.project_id)) || undefined; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: issueDetail?.project_id, + issueId: issueDetail?.id, + projectIdentifier: projectDetails?.identifier, + sequenceId: issueDetail?.sequence_id, + }); + return ( <> - + {issueDetail.project_id && projectDetails?.identifier && ( = observer((pro > Date: Wed, 5 Feb 2025 16:57:35 +0530 Subject: [PATCH 13/20] fix: issues navigation tab active status --- .../components/workspace/sidebar/project-navigation.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/core/components/workspace/sidebar/project-navigation.tsx b/web/core/components/workspace/sidebar/project-navigation.tsx index 9e7cde6a264..f1029435f31 100644 --- a/web/core/components/workspace/sidebar/project-navigation.tsx +++ b/web/core/components/workspace/sidebar/project-navigation.tsx @@ -154,7 +154,11 @@ export const ProjectNavigation: FC = observer((props) => {
Date: Thu, 6 Feb 2025 13:04:26 +0530 Subject: [PATCH 14/20] fix: invalid workitem error state --- .../[workspaceSlug]/(projects)/browse/[workItem]/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx index 75f3660658c..58343150a5a 100644 --- a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx @@ -37,14 +37,11 @@ const IssueDetailsPage = observer(() => { // fetching issue details const { data, isLoading, error } = useSWR( - workspaceSlug && projectIdentifier && sequence_id - ? `ISSUE_DETAIL_${workspaceSlug}_${projectIdentifier}_${sequence_id}` - : null, - workspaceSlug && projectIdentifier && sequence_id + workspaceSlug && workItem ? `ISSUE_DETAIL_${workspaceSlug}_${projectIdentifier}_${sequence_id}` : null, + workspaceSlug && workItem ? () => fetchIssueWithIdentifier(workspaceSlug.toString(), projectIdentifier, sequence_id) : null ); - console.log("data", data); const issueId = data?.id; const projectId = data?.project_id; // derived values From 974ad13fbeb04d0012b5ed5656764af4662c28bc Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 6 Feb 2025 14:02:13 +0530 Subject: [PATCH 15/20] chore: peek view parent issue redirection improvement --- .../issues/issue-detail/parent/root.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/web/core/components/issues/issue-detail/parent/root.tsx b/web/core/components/issues/issue-detail/parent/root.tsx index c96ecd69ac0..5d333d9e098 100644 --- a/web/core/components/issues/issue-detail/parent/root.tsx +++ b/web/core/components/issues/issue-detail/parent/root.tsx @@ -2,13 +2,16 @@ import { FC } from "react"; import { observer } from "mobx-react"; +import { useRouter } from "next/navigation"; import { MinusCircle } from "lucide-react"; import { TIssue } from "@plane/types"; // component // ui import { ControlLink, CustomMenu } from "@plane/ui"; +// helpers +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks -import { useIssues, useProjectState } from "@/hooks/store"; +import { useIssues, useProject, useProjectState } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components @@ -27,13 +30,19 @@ export type TIssueParentDetail = { export const IssueParentDetail: FC = observer((props) => { const { workspaceSlug, projectId, issueId, issue, issueOperations } = props; + // router + const router = useRouter(); // hooks const { issueMap } = useIssues(); const { getProjectStates } = useProjectState(); const { handleRedirection } = useIssuePeekOverviewRedirection(); const { isMobile } = usePlatformOS(); + const { getProjectIdentifierById } = useProject(); + // derived values const parentIssue = issueMap?.[issue.parent_id || ""] || undefined; + const isParentEpic = parentIssue?.is_epic; + const projectIdentifier = getProjectIdentifierById(parentIssue?.project_id); const issueParentState = getProjectStates(parentIssue?.project_id)?.find( (state) => state?.id === parentIssue?.state_id @@ -42,13 +51,24 @@ export const IssueParentDetail: FC = observer((props) => { if (!parentIssue) return <>; + const workItemLink = generateWorkItemLink({ + workspaceSlug, + projectId: parentIssue?.project_id, + issueId: parentIssue.id, + projectIdentifier, + sequenceId: parentIssue.sequence_id, + isEpic: isParentEpic, + }); + + const handleParentIssueClick = () => { + if (isParentEpic) router.push(workItemLink); + else handleRedirection(workspaceSlug, parentIssue, isMobile); + }; + return ( <>
- handleRedirection(workspaceSlug, parentIssue, isMobile)} - > +
From 7d67c0f2aa68a73a944562faec552aa5dc3990fb Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 6 Feb 2025 22:04:00 +0530 Subject: [PATCH 16/20] fix: issue detail endpoint to not return epics and intake issue --- apiserver/plane/app/views/issue/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index dd469155083..fbd4d7c504f 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -1102,7 +1102,7 @@ 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( + issue = Issue.issue_objects.only("sequence_id", "project__identifier").get( id=issue_id, project_id=project_id, workspace__slug=slug ) return Response( @@ -1138,7 +1138,7 @@ def get(self, request, slug, project_identifier, issue_identifier): # Fetch the issue issue = ( - Issue.objects.filter(project_id=project.id) + 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") From b69256bb502ffec8c4c324f482ca9f2e86066197 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 11 Feb 2025 15:36:36 +0530 Subject: [PATCH 17/20] fix: workitem empty state redirection and header --- web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx index 58343150a5a..10a7bbc009e 100644 --- a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx @@ -74,7 +74,7 @@ const IssueDetailsPage = observer(() => { description="The issue you are looking for does not exist, has been archived, or has been deleted." primaryButton={{ text: "View other issues", - onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`), + onClick: () => router.push(`/${workspaceSlug}/workspace-views/all-issues/`), }} /> ) : issueLoader ? ( From 0d538dcff380d55a180525f90cf89280470310e4 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 11 Feb 2025 15:38:17 +0530 Subject: [PATCH 18/20] fix: workitem empty state redirection and header --- .../[workspaceSlug]/(projects)/browse/[workItem]/header.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx index 9fbed762221..bf19d8f0de2 100644 --- a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx @@ -27,6 +27,8 @@ export const ProjectIssueDetailsHeader = observer(() => { const projectId = issueDetails ? issueDetails?.project_id : undefined; const projectDetails = projectId ? getProjectById(projectId?.toString()) : undefined; + if (!workspaceSlug || !projectId || !issueId) return null; + return (
@@ -45,7 +47,7 @@ export const ProjectIssueDetailsHeader = observer(() => { ) ) : ( - + ) From 332c4c6b7c8fa5b1bca59f19bfa50e2d4fa367e6 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 12 Feb 2025 13:24:19 +0530 Subject: [PATCH 19/20] chore: code refactor --- web/core/components/home/widgets/recents/issue.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/web/core/components/home/widgets/recents/issue.tsx b/web/core/components/home/widgets/recents/issue.tsx index 0c3f9ac6a1b..63675da80f1 100644 --- a/web/core/components/home/widgets/recents/issue.tsx +++ b/web/core/components/home/widgets/recents/issue.tsx @@ -7,8 +7,9 @@ import { ListItem } from "@/components/core/list"; import { MemberDropdown } from "@/components/dropdowns"; // helpers import { calculateTimeAgo } from "@/helpers/date-time.helper"; +import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks -import { useIssueDetail, useProjectState } from "@/hooks/store"; +import { useIssueDetail, useProject, useProjectState } from "@/hooks/store"; // plane web components import { IssueIdentifier } from "@/plane-web/components/issues"; @@ -22,13 +23,22 @@ export const RecentIssue = (props: BlockProps) => { // hooks const { getStateById } = useProjectState(); const { setPeekIssue } = useIssueDetail(); + const { getProjectIdentifierById } = useProject(); // derived values const issueDetails: TIssueEntityData = activity.entity_data as TIssueEntityData; + const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id); if (!issueDetails) return <>; const state = getStateById(issueDetails?.state); - const workItemLink = `/${workspaceSlug}/projects/${issueDetails?.project_id}/issues/${issueDetails.id}`; + + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId: issueDetails?.project_id, + issueId: issueDetails?.id, + projectIdentifier, + sequenceId: issueDetails?.sequence_id, + }); return ( Date: Fri, 14 Feb 2025 20:00:03 +0530 Subject: [PATCH 20/20] chore: project auth wrapper improvement --- .../(projects)/browse/[workItem]/page.tsx | 23 +++++++++++-------- .../(projects)/projects/(detail)/layout.tsx | 13 ++++++++--- web/ce/layouts/project-wrapper.tsx | 10 ++++++-- .../layouts/auth-layout/project-wrapper.tsx | 7 +++--- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx index 10a7bbc009e..e315f354970 100644 --- a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx @@ -5,7 +5,8 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; import useSWR from "swr"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Loader } from "@plane/ui"; // components import { EmptyState } from "@/components/common"; @@ -15,6 +16,7 @@ import { IssueDetailRoot } from "@/components/issues"; import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; // assets import { useAppRouter } from "@/hooks/use-app-router"; +import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper"; import emptyIssueDark from "@/public/empty-state/search/issues-dark.webp"; import emptyIssueLight from "@/public/empty-state/search/issues-light.webp"; @@ -25,6 +27,7 @@ const IssueDetailsPage = observer(() => { // hooks const { resolvedTheme } = useTheme(); // store hooks + const { t } = useTranslation(); const { fetchIssueWithIdentifier, issue: { getIssueById }, @@ -70,10 +73,10 @@ const IssueDetailsPage = observer(() => { {error ? ( router.push(`/${workspaceSlug}/workspace-views/all-issues/`), }} /> @@ -96,11 +99,13 @@ const IssueDetailsPage = observer(() => { workspaceSlug && projectId && issueId && ( - + + + ) )} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx index cdd4f708077..8e1d194356a 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx @@ -1,11 +1,18 @@ "use client"; import { ReactNode } from "react"; +import { useParams } from "next/navigation"; // plane web layouts import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper"; -const ProjectDetailLayout = ({ children }: { children: ReactNode }) => ( - {children} -); +const ProjectDetailLayout = ({ children }: { children: ReactNode }) => { + // router + const { workspaceSlug, projectId } = useParams(); + return ( + + {children} + + ); +}; export default ProjectDetailLayout; diff --git a/web/ce/layouts/project-wrapper.tsx b/web/ce/layouts/project-wrapper.tsx index a9223210994..585ed567bf3 100644 --- a/web/ce/layouts/project-wrapper.tsx +++ b/web/ce/layouts/project-wrapper.tsx @@ -4,12 +4,18 @@ import { observer } from "mobx-react"; import { ProjectAuthWrapper as CoreProjectAuthWrapper } from "@/layouts/auth-layout"; export type IProjectAuthWrapper = { + workspaceSlug: string; + projectId: string; children: React.ReactNode; }; export const ProjectAuthWrapper: FC = observer((props) => { // props - const { children } = props; + const { workspaceSlug, projectId, children } = props; - return {children}; + return ( + + {children} + + ); }); diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index 7ab6b6f605e..b2653fbb656 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -2,7 +2,6 @@ import { FC, ReactNode, useEffect } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; import useSWR from "swr"; // plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; @@ -33,14 +32,14 @@ import { persistence } from "@/local-db/storage.sqlite"; // plane web constants interface IProjectAuthWrapper { + workspaceSlug: string; + projectId: string; children: ReactNode; isLoading?: boolean; } export const ProjectAuthWrapper: FC = observer((props) => { - const { children, isLoading: isParentLoading = false } = props; - // router - const { workspaceSlug, projectId } = useParams(); + const { workspaceSlug, projectId, children, isLoading: isParentLoading = false } = props; // plane hooks const { t } = useTranslation(); // store hooks