Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions apiserver/plane/app/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters

from collections import defaultdict

class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self):
Expand Down Expand Up @@ -784,22 +784,13 @@ def get(self, request, slug, project_id, issue_id):
queryset=IssueReaction.objects.select_related("actor"),
)
)
.annotate(state_group=F("state__group"))
)

state_distribution = (
State.objects.filter(
workspace__slug=slug, state_issue__parent_id=issue_id
)
.annotate(state_group=F("group"))
.values("state_group")
.annotate(state_count=Count("state_group"))
.order_by("state_group")
)

result = {
item["state_group"]: item["state_count"]
for item in state_distribution
}
# create's a dict with state group name with their respective issue id's
result = defaultdict(list)
for sub_issue in sub_issues:
result[sub_issue.state_group].append(str(sub_issue.id))

serializer = IssueSerializer(
sub_issues,
Expand Down Expand Up @@ -831,7 +822,7 @@ def post(self, request, slug, project_id, issue_id):

_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)

updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group"))

# Track the issue
_ = [
Expand All @@ -846,11 +837,24 @@ def post(self, request, slug, project_id, issue_id):
)
for sub_issue_id in sub_issue_ids
]

# create's a dict with state group name with their respective issue id's
result = defaultdict(list)
for sub_issue in updated_sub_issues:
result[sub_issue.state_group].append(str(sub_issue.id))

serializer = IssueSerializer(
updated_sub_issues,
many=True,
)
return Response(
IssueSerializer(updated_sub_issues, many=True).data,
{
"sub_issues": serializer.data,
"state_distribution": result,
},
status=status.HTTP_200_OK,
)



class IssueLinkViewSet(BaseViewSet):
Expand Down
10 changes: 5 additions & 5 deletions packages/types/src/issues/issue_sub_issues.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { TIssue } from "./issue";

export type TSubIssuesStateDistribution = {
backlog: number;
unstarted: number;
started: number;
completed: number;
cancelled: number;
backlog: string[];
unstarted: string[];
started: string[];
completed: string[];
cancelled: string[];
};

export type TIssueSubIssues = {
Expand Down
2 changes: 1 addition & 1 deletion web/components/core/sidebar/progress-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) =>
));

const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, totalIssues }) => {
const chartData = Object.keys(distribution).map((key) => ({
const chartData = Object.keys(distribution ?? []).map((key) => ({
currentDate: renderFormattedDateWithoutYear(key),
pending: distribution[key],
}));
Expand Down
2 changes: 1 addition & 1 deletion web/components/core/sidebar/sidebar-progress-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
)}
</Tab.Panel>
<Tab.Panel as="div" className="flex h-44 w-full flex-col gap-1.5 overflow-y-auto pt-3.5">
{distribution.labels.length > 0 ? (
{distribution?.labels.length > 0 ? (
distribution.labels.map((label, index) => (
<SingleProgressStats
key={label.label_id ?? `no-label-${index}`}
Expand Down
14 changes: 10 additions & 4 deletions web/components/issues/description-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,16 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
async (formData: Partial<TIssue>) => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;

await issueOperations.update(workspaceSlug, projectId, issueId, {
name: formData.name ?? "",
description_html: formData.description_html ?? "<p></p>",
});
await issueOperations.update(
workspaceSlug,
projectId,
issueId,
{
name: formData.name ?? "",
description_html: formData.description_html ?? "<p></p>",
},
false
);
},
[workspaceSlug, projectId, issueId, issueOperations]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const LabelListItem: FC<TLabelListItem> = (props) => {
const label = getLabelById(labelId);

const handleLabel = async () => {
if (issue) {
if (issue && !disabled) {
const currentLabels = issue.label_ids.filter((_labelId) => _labelId !== labelId);
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels });
}
Expand Down
2 changes: 1 addition & 1 deletion web/components/issues/issue-detail/links/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
linkOperations={handleLinkOperations}
/>

<div className={`py-1 text-xs ${disabled ? "opacity-60" : ""}`}>
<div className="py-1 text-xs">
<div className="flex items-center justify-between gap-2">
<h4>Links</h4>
{!disabled && (
Expand Down
5 changes: 2 additions & 3 deletions web/components/issues/issue-detail/main-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,9 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
<SubIssuesRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
parentIssueId={issueId}
currentUser={currentUser}
is_archived={is_archived}
is_editable={is_editable}
disabled={!is_editable}
/>
)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions web/components/issues/issue-detail/parent/siblings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => {
} = useIssueDetail();

const { isLoading } = useSWR(
peekIssue && parentIssue
peekIssue && parentIssue && parentIssue.project_id
? `ISSUE_PARENT_CHILD_ISSUES_${peekIssue?.workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
: null,
peekIssue && parentIssue
peekIssue && parentIssue && parentIssue.project_id
? () => fetchSubIssues(peekIssue?.workspaceSlug, parentIssue.project_id, parentIssue.id)
: null
);
Expand Down
47 changes: 36 additions & 11 deletions web/components/issues/issue-detail/root.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { FC, useMemo } from "react";
import { useRouter } from "next/router";
// components
import { IssuePeekOverview } from "components/issues";
import { IssueMainContent } from "./main-content";
import { IssueDetailsSidebar } from "./sidebar";
// ui
import { EmptyState } from "components/common";
// images
import emptyIssue from "public/empty-state/issue.svg";
// hooks
import { useIssueDetail, useUser } from "hooks/store";
import { useIssueDetail, useIssues, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// types
import { TIssue } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";

export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
update: (
workspaceSlug: string,
projectId: string,
issueId: string,
data: Partial<TIssue>,
showToast?: boolean
) => Promise<void>;
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
Expand Down Expand Up @@ -47,6 +55,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
addIssueToModule,
removeIssueFromModule,
} = useIssueDetail();
const {
issues: { removeIssue: removeArchivedIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const { setToastAlert } = useToast();
const {
membership: { currentProjectRole },
Expand All @@ -61,14 +72,22 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
console.error("Error fetching the parent issue");
}
},
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
update: async (
workspaceSlug: string,
projectId: string,
issueId: string,
data: Partial<TIssue>,
showToast: boolean = true
) => {
try {
await updateIssue(workspaceSlug, projectId, issueId, data);
setToastAlert({
title: "Issue updated successfully",
type: "success",
message: "Issue updated successfully",
});
if (showToast) {
setToastAlert({
title: "Issue updated successfully",
type: "success",
message: "Issue updated successfully",
});
}
} catch (error) {
setToastAlert({
title: "Issue update failed",
Expand All @@ -79,7 +98,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
},
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await removeIssue(workspaceSlug, projectId, issueId);
if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId);
else await removeIssue(workspaceSlug, projectId, issueId);
setToastAlert({
title: "Issue deleted successfully",
type: "success",
Expand Down Expand Up @@ -159,9 +179,11 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
},
}),
[
is_archived,
fetchIssue,
updateIssue,
removeIssue,
removeArchivedIssue,
addIssueToCycle,
removeIssueFromCycle,
addIssueToModule,
Expand All @@ -170,9 +192,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
]
);

// Issue details
// issue details
const issue = getIssueById(issueId);
// Check if issue is editable, based on user role
// checking if issue is editable, based on user role
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;

return (
Expand Down Expand Up @@ -211,6 +233,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
</div>
</div>
)}

{/* peek overview */}
<IssuePeekOverview />
</>
);
};
8 changes: 4 additions & 4 deletions web/components/issues/issue-detail/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,25 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
/>
)}

{/* {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
<button
type="button"
className="rounded-md border border-custom-border-200 p-2 shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary"
onClick={handleCopyText}
>
<LinkIcon className="h-3.5 w-3.5" />
</button>
)} */}
)}

{/* {isAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
{is_editable && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
<button
type="button"
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
onClick={() => setDeleteIssueModal(true)}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
)} */}
)}
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ export const ModuleListLayout: React.FC = observer(() => {
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;

await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue);
await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString());
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;

await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id);
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString());
},
[EIssueActions.REMOVE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;
Expand Down
31 changes: 18 additions & 13 deletions web/components/issues/issue-layouts/roots/cycle-layout-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CycleKanBanLayout,
CycleListLayout,
CycleSpreadsheetLayout,
IssuePeekOverview,
} from "components/issues";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui
Expand Down Expand Up @@ -73,19 +74,23 @@ export const CycleLayoutRoot: React.FC = observer(() => {
cycleId={cycleId.toString()}
/>
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
<>
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
)}
Expand Down
Loading