feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View 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>
);
});

View 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;

View 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
/>
);
});

View 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>
);
};

View 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>
);
});