[WEB-4951] [WEB-4884] feat: work item filters revamp (#7810)

This commit is contained in:
Prateek Shourya 2025-09-19 18:27:36 +05:30 committed by GitHub
parent e6a7ca4c72
commit 9aef5d4aa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
160 changed files with 5879 additions and 4881 deletions

View file

@ -10,7 +10,8 @@ import { Tab } from "@headlessui/react";
import { useTranslation } from "@plane/i18n";
import { PriorityIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { EIssuesStoreType, ICycle, IIssueFilterOptions } from "@plane/types";
import { TWorkItemFilterCondition } from "@plane/shared-state";
import { EIssuesStoreType, ICycle } from "@plane/types";
// ui
import { Loader, Avatar } from "@plane/ui";
import { cn, renderFormattedDate, renderFormattedDateWithoutYear, getFileURL } from "@plane/utils";
@ -35,7 +36,7 @@ export type ActiveCycleStatsProps = {
projectId: string;
cycle: ICycle | null;
cycleId?: string | null;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
handleFiltersUpdate: (conditions: TWorkItemFilterCondition[]) => void;
cycleIssueDetails: ActiveCycleIssueDetails;
};
@ -185,7 +186,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
issueId: issue.id,
isArchived: !!issue.archived_at,
});
handleFiltersUpdate("priority", ["urgent", "high"], true);
handleFiltersUpdate([
{ property: "priority", operator: "in", value: ["urgent", "high"] },
]);
}
}}
>
@ -275,7 +278,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
total={assignee.total_issues}
onClick={() => {
if (assignee.assignee_id) {
handleFiltersUpdate("assignees", [assignee.assignee_id], true);
handleFiltersUpdate([
{ property: "assignee_id", operator: "in", value: [assignee.assignee_id] },
]);
}
}}
/>
@ -332,11 +337,15 @@ 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);
}
}}
onClick={
label.label_id
? () => {
if (label.label_id) {
handleFiltersUpdate([{ property: "label_id", operator: "in", value: [label.label_id] }]);
}
}
: undefined
}
/>
))
) : (

View file

@ -2,30 +2,28 @@
import { FC } from "react";
import { observer } from "mobx-react";
// plane package imports
// plane imports
import { PROGRESS_STATE_GROUPS_DETAILS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ICycle, IIssueFilterOptions } from "@plane/types";
import { TWorkItemFilterCondition } from "@plane/shared-state";
import { ICycle } from "@plane/types";
import { LinearProgressIndicator, Loader } from "@plane/ui";
// components
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
// hooks
import { useProjectState } from "@/hooks/store/use-project-state";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export type ActiveCycleProgressProps = {
cycle: ICycle | null;
workspaceSlug: string;
projectId: string;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
handleFiltersUpdate: (conditions: TWorkItemFilterCondition[]) => void;
};
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props) => {
const { handleFiltersUpdate, cycle } = props;
// plane hooks
const { t } = useTranslation();
// store hooks
const { groupedProjectStates } = useProjectState();
// derived values
const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({
id: index,
@ -68,10 +66,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
<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);
}
handleFiltersUpdate([{ property: "state_group", operator: "in", value: [group] }]);
}}
>
<div className="flex items-center gap-1.5">

View file

@ -1,12 +1,15 @@
import { useCallback } from "react";
import isEqual from "lodash/isEqual";
import { useRouter } from "next/navigation";
import useSWR from "swr";
import { EIssueFilterType } from "@plane/constants";
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
// plane imports
import { TWorkItemFilterCondition } from "@plane/shared-state";
import { EIssuesStoreType } from "@plane/types";
// constants
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues";
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
interface IActiveCycleDetails {
workspaceSlug: string;
@ -21,9 +24,10 @@ const useCyclesDetails = (props: IActiveCycleDetails) => {
const router = useRouter();
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
issuesFilter: { updateFilterExpression },
issues: { getActiveCycleById: getActiveCycleByIdFromIssue, fetchActiveCycleIssues },
} = useIssues(EIssuesStoreType.CYCLE);
const { updateFilterExpressionFromConditions } = useWorkItemFilters();
const { fetchActiveCycleProgress, getCycleById, fetchActiveCycleAnalytics } = useCycle();
// derived values
@ -62,29 +66,19 @@ const useCyclesDetails = (props: IActiveCycleDetails) => {
const cycleIssueDetails = cycle?.id ? getActiveCycleByIdFromIssue(cycle?.id) : { nextPageResults: false };
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => {
async (conditions: TWorkItemFilterCondition[]) => {
if (!workspaceSlug || !projectId || !cycleId) 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 },
cycleId.toString()
await updateFilterExpressionFromConditions(
EIssuesStoreType.CYCLE,
cycleId,
conditions,
updateFilterExpression.bind(updateFilterExpression, workspaceSlug, projectId, cycleId)
);
if (redirect) router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`);
router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters, router]
[workspaceSlug, projectId, cycleId, updateFilterExpressionFromConditions, updateFilterExpression, router]
);
return {
cycle,

View file

@ -1,21 +1,19 @@
"use client";
import { FC, useCallback, useMemo } from "react";
import { FC, useMemo } from "react";
import isEmpty from "lodash/isEmpty";
import isEqual from "lodash/isEqual";
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import { ChevronUp, ChevronDown } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// plane imports
import { EIssueFilterType } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EIssuesStoreType, ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types";
import { EIssuesStoreType, ICycle, TCyclePlotType, TProgressSnapshot } from "@plane/types";
import { getDate } from "@plane/utils";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues";
// plane web components
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
import { SidebarChartRoot } from "@/plane-web/components/cycles";
// local imports
import { CycleProgressStats } from "./progress-stats";
@ -60,23 +58,23 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
// router
const searchParams = useSearchParams();
const peekCycle = searchParams.get("peekCycle") || undefined;
const { getPlotTypeByCycleId, getEstimateTypeByCycleId, getCycleById } = useCycle();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
// plane hooks
const { t } = useTranslation();
// store hooks
const { getPlotTypeByCycleId, getEstimateTypeByCycleId, getCycleById } = useCycle();
const { getFilter, updateFilterValueFromSidebar } = useWorkItemFilters();
// derived values
const cycleFilter = getFilter(EIssuesStoreType.CYCLE, cycleId);
const selectedAssignees = cycleFilter?.findFirstConditionByPropertyAndOperator("assignee_id", "in");
const selectedLabels = cycleFilter?.findFirstConditionByPropertyAndOperator("label_id", "in");
const selectedStateGroups = cycleFilter?.findFirstConditionByPropertyAndOperator("state_group", "in");
const cycleDetails = validateCycleSnapshot(getCycleById(cycleId));
const plotType: TCyclePlotType = getPlotTypeByCycleId(cycleId);
const estimateType = getEstimateTypeByCycleId(cycleId);
const totalIssues = cycleDetails?.total_issues || 0;
const totalEstimatePoints = cycleDetails?.total_estimate_points || 0;
const chartDistributionData =
estimateType === "points" ? cycleDetails?.estimate_distribution : cycleDetails?.distribution || undefined;
const groupedIssues = useMemo(
() => ({
backlog:
@ -92,44 +90,12 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
}),
[estimateType, cycleDetails]
);
const cycleStartDate = getDate(cycleDetails?.start_date);
const cycleEndDate = getDate(cycleDetails?.end_date);
const isCycleStartDateValid = cycleStartDate && cycleStartDate <= new Date();
const isCycleEndDateValid = cycleStartDate && cycleEndDate && cycleEndDate >= cycleStartDate;
const isCycleDateValid = isCycleStartDateValid && isCycleEndDateValid;
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
let newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
if (key === "state") {
if (isEqual(newValues, value)) newValues = [];
else newValues = value;
} else {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
}
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ [key]: newValues },
cycleId
);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
);
if (!cycleDetails) return <></>;
return (
<div className="border-t border-custom-border-200 space-y-4 py-5">
@ -159,7 +125,6 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
</div>
</div>
)}
<Transition show={open}>
<Disclosure.Panel className="flex flex-col divide-y divide-custom-border-200">
{cycleStartDate && cycleEndDate ? (
@ -172,16 +137,24 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
<div className="w-full py-4">
<CycleProgressStats
cycleId={cycleId}
plotType={plotType}
distribution={chartDistributionData}
groupedIssues={groupedIssues}
totalIssuesCount={estimateType === "points" ? totalEstimatePoints || 0 : totalIssues || 0}
isEditable={Boolean(!peekCycle)}
size="xs"
roundedTab={false}
handleFiltersUpdate={updateFilterValueFromSidebar.bind(
updateFilterValueFromSidebar,
EIssuesStoreType.CYCLE,
cycleId
)}
isEditable={Boolean(!peekCycle) && cycleFilter !== undefined}
noBackground={false}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
plotType={plotType}
roundedTab={false}
selectedFilters={{
assignees: selectedAssignees,
labels: selectedLabels,
stateGroups: selectedStateGroups,
}}
size="xs"
totalIssuesCount={estimateType === "points" ? totalEstimatePoints || 0 : totalIssues || 0}
/>
</div>
)}

View file

@ -2,280 +2,67 @@
import { FC } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { StateGroupIcon } from "@plane/propel/icons";
import {
IIssueFilterOptions,
IIssueFilters,
TCycleDistribution,
TCycleEstimateDistribution,
TCyclePlotType,
TStateGroups,
} from "@plane/types";
import { Avatar } from "@plane/ui";
import { cn, getFileURL } from "@plane/utils";
import { TWorkItemFilterCondition } from "@plane/shared-state";
import { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils";
// components
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
import { AssigneeStatComponent, TAssigneeData } from "@/components/core/sidebar/progress-stats/assignee";
import { LabelStatComponent, TLabelData } from "@/components/core/sidebar/progress-stats/label";
import {
createFilterUpdateHandler,
PROGRESS_STATS,
TSelectedFilterProgressStats,
} from "@/components/core/sidebar/progress-stats/shared";
import { StateGroupStatComponent, TStateGroupData } from "@/components/core/sidebar/progress-stats/state_group";
// helpers
// hooks
import { useProjectState } from "@/hooks/store/use-project-state";
import useLocalStorage from "@/hooks/use-local-storage";
// public
import emptyLabel from "@/public/empty-state/empty_label.svg";
import emptyMembers from "@/public/empty-state/empty_members.svg";
// assignee types
type TAssigneeData = {
id: string | undefined;
title: string | undefined;
avatar_url: string | undefined;
completed: number;
total: number;
}[];
type TAssigneeStatComponent = {
distribution: TAssigneeData;
isEditable?: boolean;
filters?: IIssueFilters | undefined;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
};
// labelTypes
type TLabelData = {
id: string | undefined;
title: string | undefined;
color: string | undefined;
completed: number;
total: number;
}[];
type TLabelStatComponent = {
distribution: TLabelData;
isEditable?: boolean;
filters?: IIssueFilters | undefined;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
};
// stateTypes
type TStateData = {
state: string | undefined;
completed: number;
total: number;
}[];
type TStateStatComponent = {
distribution: TStateData;
totalIssuesCount: number;
isEditable?: boolean;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
};
export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) => {
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
const { t } = useTranslation();
return (
<div>
{distribution && distribution.length > 0 ? (
distribution.map((assignee, index) => {
if (assignee?.id)
return (
<SingleProgressStats
key={assignee?.id}
title={
<div className="flex items-center gap-2">
<Avatar name={assignee?.title ?? undefined} src={getFileURL(assignee?.avatar_url ?? "")} />
<span>{assignee?.title ?? ""}</span>
</div>
}
completed={assignee?.completed ?? 0}
total={assignee?.total ?? 0}
{...(isEditable && {
onClick: () => handleFiltersUpdate("assignees", assignee.id ?? ""),
selected: filters?.filters?.assignees?.includes(assignee.id ?? ""),
})}
/>
);
else
return (
<SingleProgressStats
key={`unassigned-${index}`}
title={
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
</div>
<span>{t("no_assignee")}</span>
</div>
}
completed={assignee?.completed ?? 0}
total={assignee?.total ?? 0}
/>
);
})
) : (
<div className="flex h-full flex-col items-center justify-center gap-2">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
<Image src={emptyMembers} className="h-12 w-12" alt="empty members" />
</div>
<h6 className="text-base text-custom-text-300">{t("no_assignee")}</h6>
</div>
)}
</div>
);
});
export const LabelStatComponent = observer((props: TLabelStatComponent) => {
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
const { t } = useTranslation();
return (
<div>
{distribution && distribution.length > 0 ? (
distribution.map((label, index) => {
if (label.id) {
return (
<SingleProgressStats
key={label.id}
title={
<div className="flex items-center gap-2 truncate">
<span
className="block h-3 w-3 rounded-full flex-shrink-0"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs text-ellipsis truncate">{label.title ?? t("no_labels_yet")}</span>
</div>
}
completed={label.completed}
total={label.total}
{...(isEditable && {
onClick: () => handleFiltersUpdate("labels", label.id ?? ""),
selected: filters?.filters?.labels?.includes(label.id ?? `no-label-${index}`),
})}
/>
);
} else {
return (
<SingleProgressStats
key={`no-label-${index}`}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs">{label.title ?? t("no_labels_yet")}</span>
</div>
}
completed={label.completed}
total={label.total}
/>
);
}
})
) : (
<div className="flex h-full flex-col items-center justify-center gap-2">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
<Image src={emptyLabel} className="h-12 w-12" alt="empty label" />
</div>
<h6 className="text-base text-custom-text-300">{t("no_labels_yet")}</h6>
</div>
)}
</div>
);
});
export const StateStatComponent = observer((props: TStateStatComponent) => {
const { distribution, isEditable, totalIssuesCount, handleFiltersUpdate } = props;
// hooks
const { groupedProjectStates } = useProjectState();
// derived values
const getStateGroupState = (stateGroup: string) => {
const stateGroupStates = groupedProjectStates?.[stateGroup];
const stateGroupStatesId = stateGroupStates?.map((state) => state.id);
return stateGroupStatesId;
};
return (
<div>
{distribution.map((group, index) => (
<SingleProgressStats
key={index}
title={
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup={group.state as TStateGroups} />
<span className="text-xs capitalize">{group.state}</span>
</div>
}
completed={group.completed}
total={totalIssuesCount}
{...(isEditable && {
onClick: () => group.state && handleFiltersUpdate("state", getStateGroupState(group.state) ?? []),
})}
/>
))}
</div>
);
});
const progressStats = [
{
key: "stat-states",
i18n_title: "common.states",
},
{
key: "stat-assignees",
i18n_title: "common.assignees",
},
{
key: "stat-labels",
i18n_title: "common.labels",
},
];
type TCycleProgressStats = {
cycleId: string;
plotType: TCyclePlotType;
distribution: TCycleDistribution | TCycleEstimateDistribution | undefined;
groupedIssues: Record<string, number>;
totalIssuesCount: number;
handleFiltersUpdate: (condition: TWorkItemFilterCondition) => void;
isEditable?: boolean;
filters?: IIssueFilters | undefined;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
size?: "xs" | "sm";
roundedTab?: boolean;
noBackground?: boolean;
plotType: TCyclePlotType;
roundedTab?: boolean;
selectedFilters: TSelectedFilterProgressStats;
size?: "xs" | "sm";
totalIssuesCount: number;
};
export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
const {
cycleId,
plotType,
distribution,
groupedIssues,
totalIssuesCount,
isEditable = false,
filters,
handleFiltersUpdate,
size = "sm",
roundedTab = false,
isEditable = false,
noBackground = false,
plotType,
roundedTab = false,
selectedFilters,
size = "sm",
totalIssuesCount,
} = props;
// hooks
// plane imports
const { t } = useTranslation();
// store imports
const { storedValue: currentTab, setValue: setCycleTab } = useLocalStorage(
`cycle-analytics-tab-${cycleId}`,
"stat-assignees"
);
const { t } = useTranslation();
// derived values
const currentTabIndex = (tab: string): number => progressStats.findIndex((stat) => stat.key === tab);
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
const currentDistribution = distribution as TCycleDistribution;
const currentEstimateDistribution = distribution as TCycleEstimateDistribution;
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
const selectedLabelIds = toFilterArray(selectedFilters?.labels?.value || []) as string[];
const selectedStateGroups = toFilterArray(selectedFilters?.stateGroups?.value || []) as string[];
const distributionAssigneeData: TAssigneeData =
plotType === "burndown"
@ -311,12 +98,24 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
total: label.total_estimates,
}));
const distributionStateData: TStateData = Object.keys(groupedIssues || {}).map((state) => ({
const distributionStateData: TStateGroupData = Object.keys(groupedIssues || {}).map((state) => ({
state: state,
completed: groupedIssues?.[state] || 0,
total: totalIssuesCount || 0,
}));
const handleAssigneeFiltersUpdate = createFilterUpdateHandler(
"assignee_id",
selectedAssigneeIds,
handleFiltersUpdate
);
const handleLabelFiltersUpdate = createFilterUpdateHandler("label_id", selectedLabelIds, handleFiltersUpdate);
const handleStateGroupFiltersUpdate = createFilterUpdateHandler(
"state_group",
selectedStateGroups,
handleFiltersUpdate
);
return (
<div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
@ -329,7 +128,7 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
size === "xs" ? `text-xs` : `text-sm`
)}
>
{progressStats.map((stat) => (
{PROGRESS_STATS.map((stat) => (
<Tab
className={cn(
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
@ -347,27 +146,28 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
</Tab.List>
<Tab.Panels className="py-3 text-custom-text-200">
<Tab.Panel key={"stat-states"}>
<StateStatComponent
<StateGroupStatComponent
distribution={distributionStateData}
totalIssuesCount={totalIssuesCount}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
isEditable={isEditable}
handleFiltersUpdate={handleFiltersUpdate}
selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount}
/>
</Tab.Panel>
<Tab.Panel key={"stat-assignees"}>
<AssigneeStatComponent
distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable}
filters={filters}
handleFiltersUpdate={handleFiltersUpdate}
selectedAssigneeIds={selectedAssigneeIds}
/>
</Tab.Panel>
<Tab.Panel key={"stat-labels"}>
<LabelStatComponent
distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable}
filters={filters}
handleFiltersUpdate={handleFiltersUpdate}
selectedLabelIds={selectedLabelIds}
/>
</Tab.Panel>
</Tab.Panels>