Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WEB-682] feat: cycles list filtering and searching #3910

Merged
merged 11 commits into from
Mar 11, 2024
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import type { TIssue, IIssueFilterOptions } from "@plane/types";

export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";

export type TCycleGroups = "current" | "upcoming" | "completed" | "draft";

export type TCycleLayout = "list" | "board" | "gantt";

export interface ICycle {
backlog_issues: number;
cancelled_issues: number;
Expand Down
27 changes: 27 additions & 0 deletions packages/types/src/cycle/cycle_filters.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type TCycleTabOptions = "active" | "all";

export type TCycleLayoutOptions = "list" | "board" | "gantt";

export type TCycleOrderByOptions =
| "name"
| "-name"
| "end_date"
| "-end_date"
| "sort_order";

export type TCycleDisplayFilters = {
active_tab?: TCycleTabOptions;
layout?: TCycleLayoutOptions;
order_by?: TCycleOrderByOptions;
};

export type TCycleFilters = {
end_date?: string[] | null;
start_date?: string[] | null;
status?: string[] | null;
};

export type TCycleStoredFilters = {
display_filters?: TCycleDisplayFilters;
filters?: TCycleFilters;
};
2 changes: 2 additions & 0 deletions packages/types/src/cycle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./cycle_filters";
export * from "./cycle";
2 changes: 1 addition & 1 deletion packages/types/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from "./users";
export * from "./workspace";
export * from "./cycles";
export * from "./cycle";
export * from "./dashboard";
export * from "./projects";
export * from "./state";
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/icons/cycle/circle-dot-full-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ISvgIcons } from "../type";

export const CircleDotFullIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 16 16" className={`${className} stroke-1`} fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<circle cx="8.33333" cy="8.33333" r="5.33333" stroke="currentColor" stroke-linecap="round" />
<circle cx="8.33333" cy="8.33333" r="5.33333" stroke="currentColor" strokeLinecap="round" />
<circle cx="8.33333" cy="8.33333" r="4.33333" fill="currentColor" />
</svg>
);
4 changes: 4 additions & 0 deletions web/components/cycles/active-cycle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./root";
export * from "./stats";
export * from "./upcoming-cycles-list-item";
export * from "./upcoming-cycles-list";
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,61 @@ import {
Avatar,
CycleGroupIcon,
setPromiseToast,
getButtonStyling,
} from "@plane/ui";
// components
import ProgressChart from "components/core/sidebar/progress-chart";
import { ActiveCycleProgressStats } from "components/cycles";
import { ActiveCycleProgressStats, UpcomingCyclesList } from "components/cycles";
import { StateDropdown } from "components/dropdowns";
import { EmptyState } from "components/empty-state";
// icons
import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
// helpers
import { renderFormattedDate, findHowManyDaysLeft, renderFormattedDateWithoutYear } from "helpers/date-time.helper";
import { truncateText } from "helpers/string.helper";
import { cn } from "helpers/common.helper";
// types
import { ICycle, TCycleGroups } from "@plane/types";
// constants
import { EIssuesStoreType } from "constants/issue";
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
import { EmptyStateType } from "constants/empty-state";
import useCycleFilters from "hooks/use-cycle-filters";

interface IActiveCycleDetails {
workspaceSlug: string;
projectId: string;
}

export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props) => {
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
// props
const { workspaceSlug, projectId } = props;
// store hooks
const {
issues: { fetchActiveCycleIssues },
} = useIssues(EIssuesStoreType.CYCLE);
const {
fetchActiveCycle,
currentProjectActiveCycleId,
currentProjectUpcomingCycleIds,
fetchActiveCycle,
getActiveCycleById,
addCycleToFavorites,
removeCycleFromFavorites,
} = useCycle();
const { currentProjectDetails } = useProject();
const { getUserDetails } = useMember();

// cycle filters hook
const { handleUpdateDisplayFilters } = useCycleFilters(projectId);
// derived values
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by_id) : undefined;
// fetch active cycle details
const { isLoading } = useSWR(
workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null,
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
);

const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
const cycleOwnerDetails = activeCycle ? getUserDetails(activeCycle.owned_by_id) : undefined;

// fetch active cycle issues
const { data: activeCycleIssues } = useSWR(
workspaceSlug && projectId && currentProjectActiveCycleId
? CYCLE_ISSUES_WITH_PARAMS(currentProjectActiveCycleId, { priority: "urgent,high" })
Expand All @@ -73,18 +80,52 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
? () => fetchActiveCycleIssues(workspaceSlug, projectId, currentProjectActiveCycleId)
: null
);

// show loader if active cycle is loading
if (!activeCycle && isLoading)
return (
<Loader>
<Loader.Item height="250px" />
</Loader>
);

if (!activeCycle) return <EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />;
if (!activeCycle) {
// show empty state if no active cycle is present
if (currentProjectUpcomingCycleIds?.length === 0)
return <EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />;
// show upcoming cycles list, if present
else
return (
<>
<div className="h-52 w-full grid place-items-center mb-6">
<div className="text-center">
<h5 className="text-xl font-medium mb-1">No active cycle</h5>
<p className="text-custom-text-400 text-base">
Create new cycles to find them here or check
<br />
{"'"}All{"'"} cycles tab to see all cycles or{" "}
<button
type="button"
className="text-custom-primary-100 font-medium"
onClick={() =>
handleUpdateDisplayFilters({
active_tab: "all",
})
}
>
click here
</button>
</p>
</div>
</div>
<UpcomingCyclesList />
</>
);
}

const endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(activeCycle.start_date ?? "");
const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;

const groupedIssues: any = {
backlog: activeCycle.backlog_issues,
Expand All @@ -94,8 +135,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cancelled: activeCycle.cancelled_issues,
};

const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups;

const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
Expand Down Expand Up @@ -148,8 +187,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
color: group.color,
}));

const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;

return (
<div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow">
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:grid-cols-3 lg:divide-x lg:divide-y-0">
Expand Down Expand Up @@ -203,27 +240,15 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props

<div className="flex items-center gap-4">
<div className="flex items-center gap-2.5 text-custom-text-200">
{cycleOwnerDetails?.avatar && cycleOwnerDetails?.avatar !== "" ? (
<img
src={cycleOwnerDetails?.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycleOwnerDetails?.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-background-100 capitalize">
{cycleOwnerDetails?.display_name.charAt(0)}
</span>
)}
<Avatar src={cycleOwnerDetails?.avatar} name={cycleOwnerDetails?.display_name} />
<span className="text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
</div>

{activeCycle.assignee_ids.length > 0 && (
<div className="flex items-center gap-1 text-custom-text-200">
<AvatarGroup>
{activeCycle.assignee_ids.map((assigne_id) => {
const member = getUserDetails(assigne_id);
{activeCycle.assignee_ids.map((assignee_id) => {
const member = getUserDetails(assignee_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
Expand All @@ -233,7 +258,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props

<div className="flex items-center gap-4 text-custom-text-200">
<div className="flex gap-2">
<LayersIcon className="h-4 w-4 flex-shrink-0" />
<LayersIcon className="h-3.5 w-3.5 flex-shrink-0" />
{activeCycle.total_issues} issues
</div>
<div className="flex items-center gap-2">
Expand All @@ -244,9 +269,9 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props

<Link
href={`/${workspaceSlug}/projects/${projectId}/cycles/${activeCycle.id}`}
className="w-min text-nowrap rounded-md bg-custom-primary px-4 py-2 text-center text-sm font-medium text-white hover:bg-custom-primary/90"
className={cn(getButtonStyling("primary", "lg"), "w-min whitespace-nowrap")}
>
View Cycle
View cycle
</Link>
</div>
</div>
Expand Down Expand Up @@ -287,11 +312,11 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div>
<div className="grid grid-cols-1 divide-y border-custom-border-200 lg:grid-cols-2 lg:divide-x lg:divide-y-0">
<div className="flex max-h-60 flex-col gap-3 overflow-hidden p-4">
<div className="text-custom-primary">High Priority Issues</div>
<div className="text-custom-primary">High priority issues</div>
<div className="flex h-full flex-col gap-2.5 overflow-y-scroll rounded-md">
{activeCycleIssues ? (
activeCycleIssues.length > 0 ? (
activeCycleIssues.map((issue: any) => (
activeCycleIssues.map((issue) => (
<Link
key={issue.id}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
Expand All @@ -314,9 +339,9 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div>
<div className="flex flex-shrink-0 items-center gap-1.5">
<StateDropdown
value={issue.state_id ?? undefined}
value={issue.state_id}
onChange={() => {}}
projectId={projectId?.toString() ?? ""}
projectId={projectId}
disabled
buttonVariant="background-with-text"
/>
Expand Down Expand Up @@ -359,10 +384,10 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</div>
<div className="flex items-center gap-1">
<span>
<LayersIcon className="h-5 w-5 flex-shrink-0 text-custom-text-200" />
<LayersIcon className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-200" />
</span>
<span>
Pending Issues -{" "}
Pending issues-{" "}
{activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)}
</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export const ActiveCycleProgressStats: React.FC<Props> = ({ cycle }) => {
</Tab.Panels>
) : (
<div className="mt-4 grid place-items-center text-center text-sm text-custom-text-200">
There are no high priority issues present in this cycle.
There are no issues present in this cycle.
</div>
)}
</Tab.Group>
Expand Down
Loading
Loading