feat: init
This commit is contained in:
102
apps/web/core/components/analytics/select/analytics-params.tsx
Normal file
102
apps/web/core/components/analytics/select/analytics-params.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { Control, UseFormSetValue } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { Calendar, SlidersHorizontal } from "lucide-react";
|
||||
// plane package imports
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "@plane/constants";
|
||||
import type { IAnalyticsParams } from "@plane/types";
|
||||
import { ChartYAxisMetric } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// plane web components
|
||||
import { SelectXAxis } from "./select-x-axis";
|
||||
import { SelectYAxis } from "./select-y-axis";
|
||||
|
||||
type Props = {
|
||||
control: Control<IAnalyticsParams, unknown>;
|
||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||
params: IAnalyticsParams;
|
||||
workspaceSlug: string;
|
||||
classNames?: string;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
export const AnalyticsSelectParams: React.FC<Props> = observer((props) => {
|
||||
const { control, params, classNames, isEpic } = props;
|
||||
const xAxisOptions = useMemo(
|
||||
() => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.group_by),
|
||||
[params.group_by]
|
||||
);
|
||||
const groupByOptions = useMemo(
|
||||
() => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis),
|
||||
[params.x_axis]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex w-full justify-between", classNames)}>
|
||||
<div className={`flex items-center gap-2`}>
|
||||
<Controller
|
||||
name="y_axis"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectYAxis
|
||||
value={value}
|
||||
onChange={(val: ChartYAxisMetric | null) => {
|
||||
onChange(val);
|
||||
}}
|
||||
options={ANALYTICS_Y_AXIS_VALUES}
|
||||
hiddenOptions={[
|
||||
ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
||||
isEpic ? ChartYAxisMetric.WORK_ITEM_COUNT : ChartYAxisMetric.EPIC_WORK_ITEM_COUNT,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="x_axis"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectXAxis
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span className={cn("text-custom-text-200", value && "text-custom-text-100")}>
|
||||
{xAxisOptions.find((v) => v.value === value)?.label || "Add Property"}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
options={xAxisOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="group_by"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectXAxis
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
}}
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="h-3 w-3" />
|
||||
<span className={cn("text-custom-text-200", value && "text-custom-text-100")}>
|
||||
{groupByOptions.find((v) => v.value === value)?.label || "Add Property"}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
options={groupByOptions}
|
||||
placeholder="Group By"
|
||||
allowNoValue
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
51
apps/web/core/components/analytics/select/duration.tsx
Normal file
51
apps/web/core/components/analytics/select/duration.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
// plane package imports
|
||||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import { Calendar } from "lucide-react";
|
||||
// plane package imports
|
||||
import { ANALYTICS_DURATION_FILTER_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CustomSearchSelect } from "@plane/ui";
|
||||
// types
|
||||
import type { TDropdownProps } from "@/components/dropdowns/types";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
value: string | null;
|
||||
onChange: (val: (typeof ANALYTICS_DURATION_FILTER_OPTIONS)[number]["value"]) => void;
|
||||
//optional
|
||||
button?: ReactNode;
|
||||
dropdownArrow?: boolean;
|
||||
dropdownArrowClassName?: string;
|
||||
onClose?: () => void;
|
||||
renderByDefault?: boolean;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) {
|
||||
useTranslation();
|
||||
|
||||
const options = ANALYTICS_DURATION_FILTER_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
query: option.name,
|
||||
content: (
|
||||
<div className="flex max-w-[300px] items-center gap-2">
|
||||
<span className="flex-grow truncate">{option.name}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
value={value ? [value] : []}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
label={
|
||||
<div className="flex items-center gap-2 p-1 ">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{value ? ANALYTICS_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DurationDropdown;
|
||||
62
apps/web/core/components/analytics/select/project.tsx
Normal file
62
apps/web/core/components/analytics/select/project.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { ProjectIcon } from "@plane/propel/icons";
|
||||
// plane package imports
|
||||
import { CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { Logo } from "@/components/common/logo";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
type Props = {
|
||||
value: string[] | undefined;
|
||||
onChange: (val: string[] | null) => void;
|
||||
projectIds: string[] | undefined;
|
||||
};
|
||||
|
||||
export const ProjectSelect: React.FC<Props> = observer((props) => {
|
||||
const { value, onChange, projectIds } = props;
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const options = projectIds?.map((projectId) => {
|
||||
const projectDetails = getProjectById(projectId);
|
||||
|
||||
return {
|
||||
value: projectDetails?.id,
|
||||
query: `${projectDetails?.name} ${projectDetails?.identifier}`,
|
||||
content: (
|
||||
<div className="flex max-w-[300px] items-center gap-2">
|
||||
{projectDetails?.logo_props ? (
|
||||
<Logo logo={projectDetails?.logo_props} size={16} />
|
||||
) : (
|
||||
<ProjectIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span className="flex-grow truncate">{projectDetails?.name}</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
value={value ?? []}
|
||||
onChange={(val: string[]) => onChange(val)}
|
||||
options={options}
|
||||
label={
|
||||
<div className="flex items-center gap-2 p-1 ">
|
||||
<ProjectIcon className="h-4 w-4" />
|
||||
{value && value.length > 3
|
||||
? `3+ projects`
|
||||
: value && value.length > 0
|
||||
? projectIds
|
||||
?.filter((p) => value.includes(p))
|
||||
.map((p) => getProjectById(p)?.name)
|
||||
.join(", ")
|
||||
: "All projects"}
|
||||
</div>
|
||||
}
|
||||
multiple
|
||||
/>
|
||||
);
|
||||
});
|
||||
31
apps/web/core/components/analytics/select/select-x-axis.tsx
Normal file
31
apps/web/core/components/analytics/select/select-x-axis.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
// plane package imports
|
||||
import type { ChartXAxisProperty } from "@plane/types";
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
value?: ChartXAxisProperty;
|
||||
onChange: (val: ChartXAxisProperty | null) => void;
|
||||
options: { value: ChartXAxisProperty; label: string }[];
|
||||
placeholder?: string;
|
||||
hiddenOptions?: ChartXAxisProperty[];
|
||||
allowNoValue?: boolean;
|
||||
label?: string | React.ReactNode;
|
||||
};
|
||||
|
||||
export const SelectXAxis: React.FC<Props> = (props) => {
|
||||
const { value, onChange, options, hiddenOptions, allowNoValue, label } = props;
|
||||
return (
|
||||
<CustomSelect value={value} label={label} onChange={onChange} maxHeight="lg">
|
||||
{allowNoValue && <CustomSelect.Option value={null}>No value</CustomSelect.Option>}
|
||||
{options.map((item) => {
|
||||
if (hiddenOptions?.includes(item.value)) return null;
|
||||
return (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
);
|
||||
};
|
||||
66
apps/web/core/components/analytics/select/select-y-axis.tsx
Normal file
66
apps/web/core/components/analytics/select/select-y-axis.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EEstimateSystem } from "@plane/constants";
|
||||
import { ProjectIcon } from "@plane/propel/icons";
|
||||
import type { ChartYAxisMetric } from "@plane/types";
|
||||
// plane package imports
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProjectEstimates } from "@/hooks/store/estimates";
|
||||
// plane web constants
|
||||
type Props = {
|
||||
value: ChartYAxisMetric;
|
||||
onChange: (val: ChartYAxisMetric | null) => void;
|
||||
hiddenOptions?: ChartYAxisMetric[];
|
||||
options: { value: ChartYAxisMetric; label: string }[];
|
||||
};
|
||||
|
||||
export const SelectYAxis: React.FC<Props> = observer(({ value, onChange, hiddenOptions, options }) => {
|
||||
// hooks
|
||||
const { projectId } = useParams();
|
||||
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
||||
|
||||
const isEstimateEnabled = (analyticsOption: string) => {
|
||||
if (analyticsOption === "estimate") {
|
||||
if (
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId.toString()) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
<ProjectIcon className="h-3 w-3" />
|
||||
<span>{options.find((v) => v.value === value)?.label ?? "Add Metric"}</span>
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
maxHeight="lg"
|
||||
>
|
||||
{options.map((item) => {
|
||||
if (hiddenOptions?.includes(item.value)) return null;
|
||||
return (
|
||||
isEstimateEnabled(item.value) && (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user