Skip to content

Commit

Permalink
[WEB-1999] dev: interactive active cycle stats (makeplane#5280)
Browse files Browse the repository at this point in the history
* chore: list layout item improvement

* dev: active cycle interactive stats implementation

* dev: in cycle list interactive date picker added
  • Loading branch information
anmolsinghbhatia authored Aug 1, 2024
1 parent daaa04c commit ee76cb1
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 50 deletions.
20 changes: 10 additions & 10 deletions web/core/components/core/list/list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,21 @@ export const ListItem: FC<IListItemProps> = (props) => {
)}
>
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<ControlLink
href={itemLink}
target="_self"
className="flex items-center gap-4 truncate"
onClick={handleControlLinkClick}
disabled={disableLink}
>
<ControlLink
className="relative flex w-full items-center gap-3 overflow-hidden"
href={itemLink}
target="_self"
onClick={handleControlLinkClick}
disabled={disableLink}
>
<div className="flex items-center gap-4 truncate">
{prependTitleElement && <span className="flex items-center flex-shrink-0">{prependTitleElement}</span>}
<Tooltip tooltipContent={title} position="top" isMobile={isMobile}>
<span className="truncate text-sm">{title}</span>
</Tooltip>
</ControlLink>
</div>
{appendTitleElement && <span className="flex items-center flex-shrink-0">{appendTitleElement}</span>}
</div>
</ControlLink>
{quickActionElement && quickActionElement}
</div>
{actionableItems && (
Expand Down
28 changes: 22 additions & 6 deletions web/core/components/cycles/active-cycle/cycle-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

import { FC, Fragment, useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import useSWR from "swr";
import { CalendarCheck } from "lucide-react";
// headless ui
import { Tab } from "@headlessui/react";
// types
import { ICycle } from "@plane/types";
import { ICycle, IIssueFilterOptions } from "@plane/types";
// ui
import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui";
// components
Expand All @@ -32,10 +31,11 @@ export type ActiveCycleStatsProps = {
projectId: string;
cycle: ICycle | null;
cycleId?: string | null;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
};

export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
const { workspaceSlug, projectId, cycle, cycleId } = props;
const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate } = props;

const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");

Expand All @@ -59,6 +59,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
} = useIssues(EIssuesStoreType.CYCLE);
const {
issue: { getIssueById },
setPeekIssue,
} = useIssueDetail();

const { currentProjectDetails } = useProject();
Expand Down Expand Up @@ -171,10 +172,15 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
if (!issue) return null;

return (
<Link
<div
key={issue.id}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
className="group flex cursor-pointer items-center justify-between gap-2 rounded-md hover:bg-custom-background-90 p-1"
onClick={() => {
if (issue.id) {
setPeekIssue({ workspaceSlug, projectId, issueId: issue.id });
handleFiltersUpdate("priority", ["urgent", "high"], true);
}
}}
>
<div className="flex items-center gap-1.5 flex-grow w-full min-w-24 truncate">
<PriorityIcon priority={issue.priority} withContainer size={12} />
Expand Down Expand Up @@ -215,7 +221,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
</Tooltip>
)}
</div>
</Link>
</div>
);
})}
{(cycleIssueDetails.nextPageResults === undefined || cycleIssueDetails.nextPageResults) && (
Expand Down Expand Up @@ -262,6 +268,11 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
}
completed={assignee.completed_issues}
total={assignee.total_issues}
onClick={() => {
if (assignee.assignee_id) {
handleFiltersUpdate("assignees", [assignee.assignee_id], true);
}
}}
/>
);
else
Expand Down Expand Up @@ -317,6 +328,11 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
}
completed={label.completed_issues}
total={label.total_issues}
onClick={() => {
if (label.label_id) {
handleFiltersUpdate("labels", [label.label_id], true);
}
}}
/>
))
) : (
Expand Down
34 changes: 21 additions & 13 deletions web/core/components/cycles/active-cycle/progress.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
"use client";

import { FC } from "react";
import Link from "next/link";
import { observer } from "mobx-react";
// types
import { ICycle } from "@plane/types";
import { ICycle, IIssueFilterOptions } from "@plane/types";
// ui
import { LinearProgressIndicator, Loader } from "@plane/ui";
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { PROGRESS_STATE_GROUPS_DETAILS } from "@/constants/common";
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useProjectState } from "@/hooks/store";

export type ActiveCycleProgressProps = {
workspaceSlug: string;
projectId: string;
cycle: ICycle | null;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
};

export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
const { workspaceSlug, projectId, cycle } = props;
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props) => {
const { cycle, handleFiltersUpdate } = props;
// store hooks
const { groupedProjectStates } = useProjectState();

const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({
id: index,
Expand All @@ -38,10 +41,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
: {};

return cycle ? (
<Link
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"
>
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-4">
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
Expand All @@ -62,7 +62,15 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
<>
{groupedIssues[group] > 0 && (
<div key={index}>
<div className="flex items-center justify-between gap-2 text-sm">
<div
className="flex items-center justify-between gap-2 text-sm cursor-pointer"
onClick={() => {
if (groupedProjectStates) {
const states = groupedProjectStates[group].map((state) => state.id);
handleFiltersUpdate("state", states, true);
}
}}
>
<div className="flex items-center gap-1.5">
<span
className="block h-3 w-3 rounded-full"
Expand Down Expand Up @@ -95,10 +103,10 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
</div>
)}
</Link>
</div>
) : (
<Loader className="flex flex-col min-h-[17rem] gap-5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
<Loader.Item width="100%" height="100%" />
</Loader>
);
};
});
44 changes: 41 additions & 3 deletions web/core/components/cycles/active-cycle/root.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"use client";

import { useCallback } from "react";
import isEqual from "lodash/isEqual";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
import useSWR from "swr";
// ui
import { Disclosure } from "@headlessui/react";
// types
import { IIssueFilterOptions } from "@plane/types";
// ui
import { Loader } from "@plane/ui";
// components
import {
Expand All @@ -16,8 +21,9 @@ import {
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
// hooks
import { useCycle } from "@/hooks/store";
import { useCycle, useIssues } from "@/hooks/store";

interface IActiveCycleDetails {
workspaceSlug: string;
Expand All @@ -27,7 +33,12 @@ interface IActiveCycleDetails {
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
// props
const { workspaceSlug, projectId } = props;
// router
const router = useRouter();
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
const { currentProjectActiveCycle, fetchActiveCycle, currentProjectActiveCycleId, getActiveCycleById } = useCycle();
// derived values
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
Expand All @@ -37,6 +48,32 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
);

const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => {
if (!workspaceSlug || !projectId || !currentProjectActiveCycleId) return;

const newFilters: IIssueFilterOptions = {};
Object.keys(issueFilters?.filters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = [];
});

let newValues: string[] = [];

if (isEqual(newValues, value)) newValues = [];
else newValues = value;

updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ ...newFilters, [key]: newValues },
currentProjectActiveCycleId.toString()
);
if (redirect) router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${currentProjectActiveCycleId}`);
},
[workspaceSlug, projectId, currentProjectActiveCycleId, issueFilters, updateFilters, router]
);

// show loader if active cycle is loading
if (!currentProjectActiveCycle && isLoading)
return (
Expand Down Expand Up @@ -69,7 +106,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
)}
<div className="bg-custom-background-100 pt-3 pb-6 px-6">
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
<ActiveCycleProgress cycle={activeCycle} handleFiltersUpdate={handleFiltersUpdate} />
<ActiveCycleProductivity
workspaceSlug={workspaceSlug}
projectId={projectId}
Expand All @@ -80,6 +117,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
projectId={projectId}
cycle={activeCycle}
cycleId={currentProjectActiveCycleId}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
</div>
Expand Down
Loading

0 comments on commit ee76cb1

Please sign in to comment.