diff --git a/package.json b/package.json index c476f81cd5..047f312d18 100644 --- a/package.json +++ b/package.json @@ -41,5 +41,8 @@ "@types/react": "18.2.48" }, "packageManager": "yarn@1.22.22", - "name": "plane" + "name": "plane", + "dependencies": { + "recharts": "^2.12.7" + } } diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index a2b3814ed3..b3a3fef304 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -1,4 +1,4 @@ -import type {TIssue, IIssueFilterOptions} from "@plane/types"; +import type { TIssue, IIssueFilterOptions } from "@plane/types"; export type TCycleGroups = "current" | "upcoming" | "completed" | "draft"; @@ -107,7 +107,7 @@ export interface CycleIssueResponse { } export type SelectCycleType = - | (ICycle & {actionType: "edit" | "delete" | "create-issue"}) + | (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type CycleDateCheckData = { @@ -116,4 +116,5 @@ export type CycleDateCheckData = { cycle_id?: string; }; -export type TCyclePlotType = "burndown" | "points"; +export type TCycleEstimateType = "issues" | "points"; +export type TCyclePlotType = "burndown" | "burnup"; diff --git a/web/core/components/cycles/active-cycle/root.tsx b/web/core/components/cycles/active-cycle/root.tsx index 73b81e5181..55c06372d2 100644 --- a/web/core/components/cycles/active-cycle/root.tsx +++ b/web/core/components/cycles/active-cycle/root.tsx @@ -4,14 +4,6 @@ import { observer } from "mobx-react"; import { Disclosure } from "@headlessui/react"; // ui import { Row } from "@plane/ui"; -// components -import { - ActiveCycleProductivity, - ActiveCycleProgress, - ActiveCycleStats, - CycleListGroupHeader, - CyclesListItem, -} from "@/components/cycles"; import { EmptyState } from "@/components/empty-state"; // constants import { EmptyStateType } from "@/constants/empty-state"; @@ -23,6 +15,7 @@ import ActiveCycleChart from "./cycle-chart/chart"; import { useState } from "react"; import Summary from "./summary"; import Selection from "./selection"; +import useActiveCycle from "./use-active-cycle"; interface IActiveCycleDetails { workspaceSlug: string; @@ -31,14 +24,17 @@ interface IActiveCycleDetails { export const ActiveCycleRoot: React.FC = observer((props) => { const { workspaceSlug, projectId } = props; - const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle(); const { - handleFiltersUpdate, + plotType, + estimateType, + handlePlotChange, + handleEstimateChange, cycle: activeCycle, - cycleIssueDetails, - } = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId }); + } = useActiveCycle(workspaceSlug, projectId); const [areaToHighlight, setAreaToHighlight] = useState(""); - if (!activeCycle) return null; + + if (!activeCycle) return; + return ( <> @@ -48,14 +44,19 @@ export const ActiveCycleRoot: React.FC = observer((props) = - {!currentProjectActiveCycle ? ( + {!activeCycle ? ( ) : (
- +
- +
diff --git a/web/core/components/cycles/active-cycle/selection.tsx b/web/core/components/cycles/active-cycle/selection.tsx index f845a21588..1b120bd99d 100644 --- a/web/core/components/cycles/active-cycle/selection.tsx +++ b/web/core/components/cycles/active-cycle/selection.tsx @@ -1,11 +1,61 @@ -import { Row } from "@plane/ui"; +import { useCycle } from "@/hooks/store"; +import { ICycle, TCycleEstimateType, TCyclePlotType } from "@plane/types"; +import { CustomSelect, Row } from "@plane/ui"; +import { observer } from "mobx-react"; -const Selection = () => { +type options = { + value: string; + label: string; +}; +const cycleChartOptions: options[] = [ + { value: "burndown", label: "Burn-down" }, + { value: "burnup", label: "Burn-up" }, +]; +const cycleEstimateOptions: options[] = [ + { value: "issues", label: "issues" }, + { value: "points", label: "points" }, +]; + +export type TSelectionProps = { + plotType: TCyclePlotType; + estimateType: TCycleEstimateType; + handlePlotChange: (value: TCyclePlotType) => Promise; + handleEstimateChange: (value: TCycleEstimateType) => Promise; +}; +export type TDropdownProps = { + value: string; + onChange: (value: TCyclePlotType | TCycleEstimateType) => Promise; + options: any[]; +}; +const Dropdown = ({ value, onChange, options }: TDropdownProps) => { return ( - - Dropdowns to be inserted here - +
+ {options.find((v) => v.value === value)?.label ?? "None"}} + onChange={onChange} + maxHeight="lg" + buttonClassName="bg-custom-background-90 border-none rounded text-sm font-medium" + > + {options.map((item) => ( + + {item.label} + + ))} + +
); }; +const Selection = observer((props: TSelectionProps) => { + const { plotType, estimateType, handlePlotChange, handleEstimateChange } = props; + + return ( + + + for + + + ); +}); export default Selection; diff --git a/web/core/components/cycles/active-cycle/summary.tsx b/web/core/components/cycles/active-cycle/summary.tsx index f432d78f34..1192acbad0 100644 --- a/web/core/components/cycles/active-cycle/summary.tsx +++ b/web/core/components/cycles/active-cycle/summary.tsx @@ -3,7 +3,7 @@ import { Info, TrendingDown } from "lucide-react"; const Summary = (props) => { const { setAreaToHighlight } = props; return ( -
+
Summary of cycle issues
diff --git a/web/core/components/cycles/active-cycle/use-active-cycle.tsx b/web/core/components/cycles/active-cycle/use-active-cycle.tsx new file mode 100644 index 0000000000..12a2de2736 --- /dev/null +++ b/web/core/components/cycles/active-cycle/use-active-cycle.tsx @@ -0,0 +1,37 @@ +import { useCycle } from "@/hooks/store"; +import { ICycle, TCycleEstimateType, TCyclePlotType } from "@plane/types"; +import useCyclesDetails from "./use-cycles-details"; +import { useState } from "react"; + +const useActiveCycle = (workspaceSlug: string, projectId: string) => { + const { getPlotTypeByCycleId, getEstimateTypeByCycleId, setPlotType, setEstimateType, currentProjectActiveCycleId } = + useCycle(); + const { handleFiltersUpdate, cycle, cycleIssueDetails } = useCyclesDetails({ + workspaceSlug, + projectId, + cycleId: currentProjectActiveCycleId, + }); + + // derived values + const plotType: TCyclePlotType = (cycle && getPlotTypeByCycleId(cycle.id)) || "burndown"; + const estimateType: TCycleEstimateType = (cycle && getEstimateTypeByCycleId(cycle.id)) || "issues"; + + const handlePlotChange = async (value: TCyclePlotType) => { + if (!workspaceSlug || !projectId || !cycle || !cycle.id) return; + setPlotType(cycle.id, value); + }; + + const handleEstimateChange = async (value: TCycleEstimateType) => { + if (!workspaceSlug || !projectId || !cycle || !cycle.id) return; + setEstimateType(cycle.id, value); + }; + + return { + plotType, + estimateType, + handlePlotChange, + handleEstimateChange, + cycle, + }; +}; +export default useActiveCycle; diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index f1b8888537..6de63e029f 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -11,6 +11,7 @@ import { TProgressSnapshot, TCycleEstimateDistribution, TCycleDistribution, + TCycleEstimateType, } from "@plane/types"; // helpers import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper"; @@ -31,6 +32,7 @@ export interface ICycleStore { fetchedMap: Record; cycleMap: Record; plotType: Record; + estimatedType: Record; activeCycleIdMap: Record; // computed currentProjectCycleIds: string[] | null; @@ -51,10 +53,12 @@ export interface ICycleStore { getActiveCycleById: (cycleId: string) => ICycle | null; getProjectCycleIds: (projectId: string) => string[] | null; getPlotTypeByCycleId: (cycleId: string) => TCyclePlotType; + getEstimateTypeByCycleId: (cycleId: string) => TCycleEstimateType; // actions updateCycleDistribution: (distributionUpdates: DistributionUpdates, cycleId: string) => void; validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; setPlotType: (cycleId: string, plotType: TCyclePlotType) => void; + setEstimateType: (cycleId: string, estimateType: TCycleEstimateType) => void; // fetch fetchWorkspaceCycles: (workspaceSlug: string) => Promise; fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise; @@ -91,6 +95,7 @@ export class CycleStore implements ICycleStore { loader: boolean = false; cycleMap: Record = {}; plotType: Record = {}; + estimatedType: Record = {}; activeCycleIdMap: Record = {}; //loaders fetchedMap: Record = {}; @@ -108,6 +113,7 @@ export class CycleStore implements ICycleStore { loader: observable.ref, cycleMap: observable, plotType: observable, + estimatedType: observable, activeCycleIdMap: observable, fetchedMap: observable, // computed @@ -122,6 +128,7 @@ export class CycleStore implements ICycleStore { // actions setPlotType: action, + setEstimateType: action, fetchWorkspaceCycles: action, fetchAllCycles: action, fetchActiveCycle: action, @@ -377,9 +384,19 @@ export class CycleStore implements ICycleStore { getPlotTypeByCycleId = (cycleId: string) => { const { projectId } = this.rootStore.router; + return this.plotType[cycleId] || "burndown"; + }; + + /** + * @description gets the estimate type for the module store + * @param {TCycleEstimateType} estimateType + */ + getEstimateTypeByCycleId = (cycleId: string) => { + const { projectId } = this.rootStore.router; + return projectId && this.rootStore.projectEstimate.areEstimateEnabledByProjectId(projectId) - ? this.plotType[cycleId] || "burndown" - : "burndown"; + ? this.estimatedType[cycleId] || "points" + : "points"; }; /** @@ -390,6 +407,14 @@ export class CycleStore implements ICycleStore { set(this.plotType, [cycleId], plotType); }; + /** + * @description updates the estimate type for the module store + * @param {TCycleEstimateType} estimateType + */ + setEstimateType = (cycleId: string, estimateType: TCycleEstimateType) => { + set(this.estimatedType, [cycleId], estimateType); + }; + /** * @description fetch all cycles * @param workspaceSlug