UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: русификация и редизайн аналитики внутреннего контура
This commit is contained in:
parent
cf6fca20aa
commit
52bd017d82
|
|
@ -22,7 +22,9 @@ function AnalyticsWrapper(props: Props) {
|
|||
<div className={cn("flex flex-col gap-4 pb-5", className)}>
|
||||
<div className="nodedc-external-panel flex items-center justify-between gap-3 px-5 py-4 md:px-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-11 font-medium tracking-[0.24em] text-secondary uppercase">Analytics</span>
|
||||
<span className="text-11 font-medium tracking-[0.24em] text-secondary uppercase">
|
||||
{t("workspace_analytics.label")}
|
||||
</span>
|
||||
<h1 className="text-20 font-semibold text-primary md:text-[1.45rem]">{t(i18nTitle)}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -65,18 +65,18 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="nodedc-analytics-table space-y-4">
|
||||
<div className="nodedc-analytics-table-toolbar flex w-full items-center justify-between">
|
||||
<div className="relative flex max-w-[300px] items-center gap-4">
|
||||
{table.getHeaderGroups()?.[0]?.headers?.[0]?.id && (
|
||||
<div className="flex items-center gap-2 text-13 whitespace-nowrap text-placeholder">
|
||||
<div className="nodedc-analytics-table-count flex items-center gap-2 text-13 whitespace-nowrap text-placeholder">
|
||||
{searchPlaceholder}
|
||||
</div>
|
||||
)}
|
||||
{!isSearchOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="-mr-5 grid place-items-center rounded-sm p-2 text-placeholder hover:bg-layer-1"
|
||||
className="nodedc-analytics-search-button -mr-5 grid place-items-center rounded-sm p-2 text-placeholder hover:bg-layer-1"
|
||||
onClick={() => {
|
||||
setIsSearchOpen(true);
|
||||
inputRef.current?.focus();
|
||||
|
|
@ -87,7 +87,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
|||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"mr-auto flex w-0 items-center justify-start gap-1 overflow-hidden rounded-md border border-transparent bg-surface-1 text-placeholder opacity-0 transition-[width] ease-linear",
|
||||
"nodedc-analytics-search-field mr-auto flex w-0 items-center justify-start gap-1 overflow-hidden rounded-md border border-transparent bg-surface-1 text-placeholder opacity-0 transition-[width] ease-linear",
|
||||
{
|
||||
"w-64 border-subtle px-2.5 py-1.5 opacity-100": isSearchOpen,
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
|||
<input
|
||||
ref={inputRef}
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-13 text-primary placeholder:text-placeholder focus:outline-none"
|
||||
placeholder="Search"
|
||||
placeholder="Поиск"
|
||||
value={table.getColumn(table.getHeaderGroups()?.[0]?.headers?.[0]?.id)?.getFilterValue() as string}
|
||||
onChange={(e) => {
|
||||
const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id;
|
||||
|
|
@ -129,7 +129,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
|||
{actions && <div>{actions(table)}</div>}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md">
|
||||
<div className="nodedc-analytics-table-surface rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export function InsightTable<T extends Exclude<TAnalyticsTabsBase, "overview">>(
|
|||
actions={(table: Table<AnalyticsTableDataMap[T]>) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="nodedc-analytics-export-button"
|
||||
prependIcon={<Download className="h-3.5 w-3.5" />}
|
||||
onClick={() => onExport?.(table.getFilteredRowModel().rows)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/types";
|
||||
|
||||
const X_AXIS_LABELS: Partial<Record<ChartXAxisProperty, string>> = {
|
||||
[ChartXAxisProperty.STATES]: "Статус",
|
||||
[ChartXAxisProperty.STATE_GROUPS]: "Группа статуса",
|
||||
[ChartXAxisProperty.PRIORITY]: "Приоритет",
|
||||
[ChartXAxisProperty.LABELS]: "Метка",
|
||||
[ChartXAxisProperty.ASSIGNEES]: "Исполнитель",
|
||||
[ChartXAxisProperty.ESTIMATE_POINTS]: "Оценка",
|
||||
[ChartXAxisProperty.CYCLES]: "Цикл",
|
||||
[ChartXAxisProperty.MODULES]: "Модуль",
|
||||
[ChartXAxisProperty.COMPLETED_AT]: "Дата завершения",
|
||||
[ChartXAxisProperty.TARGET_DATE]: "Срок",
|
||||
[ChartXAxisProperty.START_DATE]: "Дата начала",
|
||||
[ChartXAxisProperty.CREATED_AT]: "Дата создания",
|
||||
[ChartXAxisProperty.CREATED_BY]: "Автор",
|
||||
[ChartXAxisProperty.WORK_ITEM_TYPES]: "Тип задачи",
|
||||
[ChartXAxisProperty.PROJECTS]: "Проект",
|
||||
[ChartXAxisProperty.EPICS]: "Эпик",
|
||||
};
|
||||
|
||||
const Y_AXIS_LABELS: Partial<Record<ChartYAxisMetric, string>> = {
|
||||
[ChartYAxisMetric.WORK_ITEM_COUNT]: "Количество задач",
|
||||
[ChartYAxisMetric.ESTIMATE_POINT_COUNT]: "Оценка",
|
||||
[ChartYAxisMetric.PENDING_WORK_ITEM_COUNT]: "Ожидающие задачи",
|
||||
[ChartYAxisMetric.COMPLETED_WORK_ITEM_COUNT]: "Завершённые задачи",
|
||||
[ChartYAxisMetric.IN_PROGRESS_WORK_ITEM_COUNT]: "Задачи в процессе",
|
||||
[ChartYAxisMetric.WORK_ITEM_DUE_THIS_WEEK_COUNT]: "Срок на этой неделе",
|
||||
[ChartYAxisMetric.WORK_ITEM_DUE_TODAY_COUNT]: "Срок сегодня",
|
||||
[ChartYAxisMetric.BLOCKED_WORK_ITEM_COUNT]: "Заблокированные задачи",
|
||||
[ChartYAxisMetric.EPIC_WORK_ITEM_COUNT]: "Количество эпиков",
|
||||
};
|
||||
|
||||
const VALUE_LABELS: Record<string, string> = {
|
||||
none: "Нет",
|
||||
null: "Нет",
|
||||
undefined: "Нет",
|
||||
unassigned: "Без исполнителя",
|
||||
urgent: "Срочно",
|
||||
high: "Высокий",
|
||||
medium: "Средний",
|
||||
low: "Низкий",
|
||||
backlog: "Бэклог",
|
||||
unstarted: "Туду",
|
||||
started: "В процессе",
|
||||
completed: "Готово",
|
||||
cancelled: "Отменено",
|
||||
canceled: "Отменено",
|
||||
"no priority": "Без приоритета",
|
||||
"no value": "Нет",
|
||||
"state name": "Статус",
|
||||
"state group": "Группа статуса",
|
||||
priority: "Приоритет",
|
||||
label: "Метка",
|
||||
assignee: "Исполнитель",
|
||||
"estimate point": "Оценка",
|
||||
cycle: "Цикл",
|
||||
module: "Модуль",
|
||||
"completed date": "Дата завершения",
|
||||
"due date": "Срок",
|
||||
"start date": "Дата начала",
|
||||
"created date": "Дата создания",
|
||||
count: "Количество",
|
||||
};
|
||||
|
||||
const DURATION_LABELS: Record<string, string> = {
|
||||
yesterday: "Вчера",
|
||||
last_7_days: "Последние 7 дней",
|
||||
last_30_days: "Последние 30 дней",
|
||||
last_3_months: "Последние 3 месяца",
|
||||
};
|
||||
|
||||
export const getAnalyticsXAxisLabel = (value: ChartXAxisProperty | string, fallback?: string) =>
|
||||
X_AXIS_LABELS[value as ChartXAxisProperty] ?? fallback ?? String(value);
|
||||
|
||||
export const getAnalyticsYAxisLabel = (value: ChartYAxisMetric | string, fallback?: string) =>
|
||||
Y_AXIS_LABELS[value as ChartYAxisMetric] ?? fallback ?? String(value);
|
||||
|
||||
export const getAnalyticsDurationLabel = (value: string, fallback?: string) =>
|
||||
DURATION_LABELS[value] ?? fallback ?? value;
|
||||
|
||||
export const getAnalyticsValueLabel = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") return "Нет";
|
||||
|
||||
const stringValue = String(value);
|
||||
return VALUE_LABELS[stringValue.trim().toLowerCase()] ?? stringValue;
|
||||
};
|
||||
|
|
@ -77,7 +77,7 @@ const ProjectInsights = observer(function ProjectInsights() {
|
|||
radars={[
|
||||
{
|
||||
key: "count",
|
||||
name: "Count",
|
||||
name: "Количество",
|
||||
fill: "var(--text-color-accent-primary)",
|
||||
stroke: "var(--text-color-accent-primary)",
|
||||
fillOpacity: 0.6,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type { IAnalyticsParams } from "@plane/types";
|
|||
import { ChartYAxisMetric } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// plane web components
|
||||
import { getAnalyticsXAxisLabel, getAnalyticsYAxisLabel } from "../labels";
|
||||
import { SelectXAxis } from "./select-x-axis";
|
||||
import { SelectYAxis } from "./select-y-axis";
|
||||
|
||||
|
|
@ -31,17 +32,33 @@ type Props = {
|
|||
export const AnalyticsSelectParams = observer(function AnalyticsSelectParams(props: Props) {
|
||||
const { control, params, classNames, isEpic } = props;
|
||||
const xAxisOptions = useMemo(
|
||||
() => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.group_by),
|
||||
() =>
|
||||
ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.group_by).map((option) => ({
|
||||
...option,
|
||||
label: getAnalyticsXAxisLabel(option.value, option.label),
|
||||
})),
|
||||
[params.group_by]
|
||||
);
|
||||
const groupByOptions = useMemo(
|
||||
() => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis),
|
||||
() =>
|
||||
ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis).map((option) => ({
|
||||
...option,
|
||||
label: getAnalyticsXAxisLabel(option.value, option.label),
|
||||
})),
|
||||
[params.x_axis]
|
||||
);
|
||||
const yAxisOptions = useMemo(
|
||||
() =>
|
||||
ANALYTICS_Y_AXIS_VALUES.map((option) => ({
|
||||
...option,
|
||||
label: getAnalyticsYAxisLabel(option.value, option.label),
|
||||
})),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("flex w-full justify-between", classNames)}>
|
||||
<div className={`flex items-center gap-2`}>
|
||||
<div className="nodedc-analytics-filter-row flex items-center gap-2">
|
||||
<Controller
|
||||
name="y_axis"
|
||||
control={control}
|
||||
|
|
@ -51,7 +68,7 @@ export const AnalyticsSelectParams = observer(function AnalyticsSelectParams(pro
|
|||
onChange={(val: ChartYAxisMetric | null) => {
|
||||
onChange(val);
|
||||
}}
|
||||
options={ANALYTICS_Y_AXIS_VALUES}
|
||||
options={yAxisOptions}
|
||||
hiddenOptions={[
|
||||
ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
||||
isEpic ? ChartYAxisMetric.WORK_ITEM_COUNT : ChartYAxisMetric.EPIC_WORK_ITEM_COUNT,
|
||||
|
|
@ -72,7 +89,7 @@ export const AnalyticsSelectParams = observer(function AnalyticsSelectParams(pro
|
|||
<div className="flex items-center gap-2">
|
||||
<CalendarLayoutIcon className="h-3 w-3" />
|
||||
<span className={cn("text-secondary", value && "text-primary")}>
|
||||
{xAxisOptions.find((v) => v.value === value)?.label || "Add Property"}
|
||||
{xAxisOptions.find((v) => v.value === value)?.label || "Добавить поле"}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -93,12 +110,12 @@ export const AnalyticsSelectParams = observer(function AnalyticsSelectParams(pro
|
|||
<div className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="h-3 w-3" />
|
||||
<span className={cn("text-secondary", value && "text-primary")}>
|
||||
{groupByOptions.find((v) => v.value === value)?.label || "Add Property"}
|
||||
{groupByOptions.find((v) => v.value === value)?.label || "Группировка"}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
options={groupByOptions}
|
||||
placeholder="Group By"
|
||||
placeholder="Группировка"
|
||||
allowNoValue
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { useTranslation } from "@plane/i18n";
|
|||
// types
|
||||
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
||||
import type { TDropdownProps } from "@/components/dropdowns/types";
|
||||
import { getAnalyticsDurationLabel } from "../labels";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
value: string | null;
|
||||
|
|
@ -27,12 +28,12 @@ type Props = TDropdownProps & {
|
|||
tabIndex?: number;
|
||||
};
|
||||
|
||||
function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) {
|
||||
function DurationDropdown({ placeholder = "Период", onChange, value }: Props) {
|
||||
useTranslation();
|
||||
|
||||
const options = ANALYTICS_DURATION_FILTER_OPTIONS.map((option) => ({
|
||||
key: option.value,
|
||||
title: option.name,
|
||||
title: getAnalyticsDurationLabel(option.value, option.name),
|
||||
isChecked: value === option.value,
|
||||
onClick: () => onChange(option.value),
|
||||
}));
|
||||
|
|
@ -42,10 +43,16 @@ function DurationDropdown({ placeholder = "Duration", onChange, value }: Props)
|
|||
menuButton={
|
||||
<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}
|
||||
{value
|
||||
? getAnalyticsDurationLabel(
|
||||
value,
|
||||
ANALYTICS_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name
|
||||
)
|
||||
: placeholder}
|
||||
</div>
|
||||
}
|
||||
menuButtonWrapperClassName="flex items-center rounded-full border-0 outline-none"
|
||||
menuButtonWrapperClassName="nodedc-analytics-select-trigger"
|
||||
dropdownClassName="nodedc-analytics-dropdown"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,13 +58,13 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
|
|||
>
|
||||
<ProjectIcon className="h-4 w-4" />
|
||||
{value && value.length > 3
|
||||
? `3+ projects`
|
||||
? `3+ проекта`
|
||||
: value && value.length > 0
|
||||
? projectIds
|
||||
?.filter((p) => value.includes(p))
|
||||
.map((p) => getProjectById(p)?.name)
|
||||
.join(", ")
|
||||
: "All projects"}
|
||||
: "Все проекты"}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,18 @@ type Props = {
|
|||
};
|
||||
|
||||
export function SelectXAxis(props: Props) {
|
||||
const { value, onChange, options, hiddenOptions, allowNoValue, label } = props;
|
||||
const { value, onChange, options, placeholder = "Выбрать", hiddenOptions, allowNoValue, label } = props;
|
||||
return (
|
||||
<SelectionDropdown
|
||||
menuButton={label ?? "Select"}
|
||||
menuButton={label ?? placeholder}
|
||||
menuButtonWrapperClassName="nodedc-analytics-select-trigger"
|
||||
dropdownClassName="nodedc-analytics-dropdown"
|
||||
options={[
|
||||
...(allowNoValue
|
||||
? [
|
||||
{
|
||||
key: "__none__",
|
||||
title: "No value",
|
||||
title: "Без группировки",
|
||||
isChecked: value == null,
|
||||
onClick: () => onChange(null),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,10 +45,12 @@ export const SelectYAxis = observer(function SelectYAxis({ value, onChange, hidd
|
|||
|
||||
return (
|
||||
<SelectionDropdown
|
||||
menuButtonWrapperClassName="nodedc-analytics-select-trigger"
|
||||
dropdownClassName="nodedc-analytics-dropdown"
|
||||
menuButton={
|
||||
<div className="flex items-center gap-2">
|
||||
<ProjectIcon className="h-3 w-3" />
|
||||
<span>{options.find((v) => v.value === value)?.label ?? "Add Metric"}</span>
|
||||
<span>{options.find((v) => v.value === value)?.label ?? "Выбрать метрику"}</span>
|
||||
</div>
|
||||
}
|
||||
options={options
|
||||
|
|
|
|||
|
|
@ -65,24 +65,24 @@ const CreatedVsResolved = observer(function CreatedVsResolved() {
|
|||
() => [
|
||||
{
|
||||
key: "completed_issues",
|
||||
label: "Resolved",
|
||||
fill: "#19803833",
|
||||
label: "Решено",
|
||||
fill: "rgba(195, 255, 102, 0.18)",
|
||||
fillOpacity: 1,
|
||||
stackId: "bar-one",
|
||||
showDot: false,
|
||||
smoothCurves: true,
|
||||
strokeColor: "#198038",
|
||||
strokeColor: "#C3FF66",
|
||||
strokeOpacity: 1,
|
||||
},
|
||||
{
|
||||
key: "created_issues",
|
||||
label: "Created",
|
||||
fill: "#1192E833",
|
||||
label: "Создано",
|
||||
fill: "rgba(245, 247, 251, 0.14)",
|
||||
fillOpacity: 1,
|
||||
stackId: "bar-one",
|
||||
showDot: false,
|
||||
smoothCurves: true,
|
||||
strokeColor: "#1192E8",
|
||||
strokeColor: "#F5F7FB",
|
||||
strokeOpacity: 1,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const WorkItemsModalHeader = observer(function WorkItemsModalHeader(props
|
|||
return (
|
||||
<div className="flex items-center justify-between gap-4 bg-surface-1 px-5 py-4 text-13">
|
||||
<h3 className="break-words">
|
||||
Analytics for {title} {cycle && `in ${cycle.name}`} {module && `in ${module.name}`}
|
||||
Аналитика: {title} {cycle && `в цикле ${cycle.name}`} {module && `в модуле ${module.name}`}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -8,27 +8,27 @@ import { useMemo } from "react";
|
|||
import type { ColumnDef, Row, RowData, Table } from "@tanstack/react-table";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// plane package imports
|
||||
import { Download } from "lucide-react";
|
||||
import type { ChartXAxisDateGrouping } from "@plane/constants";
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, CHART_COLOR_PALETTES, EChartModels } from "@plane/constants";
|
||||
import { EChartModels } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { BarChart } from "@plane/propel/charts/bar-chart";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import type { TBarItem, TChart, TChartDatum, ChartXAxisProperty, ChartYAxisMetric } from "@plane/types";
|
||||
// plane web components
|
||||
import { generateExtendedColors, parseChartData } from "@/components/chart/utils";
|
||||
import { parseChartData } from "@/components/chart/utils";
|
||||
// hooks
|
||||
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { AnalyticsService } from "@/services/analytics.service";
|
||||
import { exportCSV } from "../export";
|
||||
import { DataTable } from "../insight-table/data-table";
|
||||
import { getAnalyticsValueLabel, getAnalyticsXAxisLabel, getAnalyticsYAxisLabel } from "../labels";
|
||||
import { ChartLoader } from "../loaders";
|
||||
import { generateBarColor } from "./utils";
|
||||
import { generateBarColor, NODEDC_ANALYTICS_COLORS } from "./utils";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
|
@ -55,7 +55,6 @@ const PriorityChart = observer(function PriorityChart(props: Props) {
|
|||
// store hooks
|
||||
const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics();
|
||||
const { workspaceStates } = useProjectState();
|
||||
const { resolvedTheme } = useTheme();
|
||||
// router
|
||||
const params = useParams();
|
||||
const workspaceSlug = params.workspaceSlug.toString();
|
||||
|
|
@ -83,70 +82,71 @@ const PriorityChart = observer(function PriorityChart(props: Props) {
|
|||
priorityChartData && parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping),
|
||||
[priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping]
|
||||
);
|
||||
const localizedData = useMemo(() => {
|
||||
if (!parsedData) return undefined;
|
||||
|
||||
const schema = Object.fromEntries(
|
||||
Object.entries(parsedData.schema).map(([key, value]) => [key, getAnalyticsValueLabel(value)])
|
||||
);
|
||||
const data = parsedData.data.map((datum) => ({
|
||||
...datum,
|
||||
name: getAnalyticsValueLabel(datum.name),
|
||||
}));
|
||||
|
||||
return { ...parsedData, schema, data };
|
||||
}, [parsedData]);
|
||||
const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC;
|
||||
|
||||
const bars: TBarItem<string>[] = useMemo(() => {
|
||||
if (!parsedData) return [];
|
||||
let parsedBars: TBarItem<string>[];
|
||||
const schemaKeys = Object.keys(parsedData.schema);
|
||||
const baseColors = CHART_COLOR_PALETTES[0]?.[resolvedTheme === "dark" ? "dark" : "light"];
|
||||
const extendedColors = generateExtendedColors(baseColors ?? [], schemaKeys.length);
|
||||
const baseColors = NODEDC_ANALYTICS_COLORS;
|
||||
if (chart_model === EChartModels.BASIC) {
|
||||
parsedBars = [
|
||||
{
|
||||
key: "count",
|
||||
label: "Count",
|
||||
label: "Количество",
|
||||
stackId: "bar-one",
|
||||
fill: (payload) => generateBarColor(payload.key, { x_axis, y_axis, group_by }, baseColors, workspaceStates),
|
||||
textClassName: "",
|
||||
showPercentage: false,
|
||||
borderRadius: 11,
|
||||
showTopBorderRadius: () => true,
|
||||
showBottomBorderRadius: () => true,
|
||||
},
|
||||
];
|
||||
} else if (chart_model === EChartModels.STACKED && parsedData.schema) {
|
||||
const parsedExtremes: {
|
||||
[key: string]: {
|
||||
top: string | null;
|
||||
bottom: string | null;
|
||||
};
|
||||
} = {};
|
||||
parsedData.data.forEach((datum) => {
|
||||
let top = null;
|
||||
let bottom = null;
|
||||
for (let i = 0; i < schemaKeys.length; i++) {
|
||||
const key = schemaKeys[i];
|
||||
if (datum[key] === 0) continue;
|
||||
if (!bottom) bottom = key;
|
||||
top = key;
|
||||
}
|
||||
parsedExtremes[datum.key] = { top, bottom };
|
||||
});
|
||||
|
||||
parsedBars = schemaKeys.map((key, index) => ({
|
||||
parsedBars = schemaKeys.map((key) => ({
|
||||
key: key,
|
||||
label: parsedData.schema[key],
|
||||
label: localizedData?.schema[key] ?? getAnalyticsValueLabel(parsedData.schema[key]),
|
||||
stackId: "bar-one",
|
||||
fill: extendedColors[index],
|
||||
fill: generateBarColor(
|
||||
key,
|
||||
{ x_axis: group_by ?? x_axis, y_axis, group_by: undefined },
|
||||
baseColors,
|
||||
workspaceStates
|
||||
),
|
||||
textClassName: "",
|
||||
showPercentage: false,
|
||||
showTopBorderRadius: (value, payload: TChartDatum) => parsedExtremes[payload.key].top === value,
|
||||
showBottomBorderRadius: (value, payload: TChartDatum) => parsedExtremes[payload.key].bottom === value,
|
||||
borderRadius: 10,
|
||||
showTopBorderRadius: () => true,
|
||||
showBottomBorderRadius: () => true,
|
||||
}));
|
||||
} else {
|
||||
parsedBars = [];
|
||||
}
|
||||
return parsedBars;
|
||||
}, [chart_model, group_by, parsedData, resolvedTheme, workspaceStates, x_axis, y_axis]);
|
||||
}, [chart_model, group_by, localizedData?.schema, parsedData, workspaceStates, x_axis, y_axis]);
|
||||
|
||||
const yAxisLabel = useMemo(
|
||||
() => ANALYTICS_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis,
|
||||
[props.y_axis]
|
||||
);
|
||||
const xAxisLabel = useMemo(
|
||||
() => ANALYTICS_X_AXIS_VALUES.find((item) => item.value === props.x_axis)?.label ?? props.x_axis,
|
||||
[props.x_axis]
|
||||
);
|
||||
const yAxisLabel = useMemo(() => getAnalyticsYAxisLabel(props.y_axis), [props.y_axis]);
|
||||
const xAxisLabel = useMemo(() => getAnalyticsXAxisLabel(props.x_axis), [props.x_axis]);
|
||||
const chartWidth = useMemo(() => {
|
||||
const itemCount = localizedData?.data.length ?? 0;
|
||||
const widthPerItem = chart_model === EChartModels.STACKED ? 88 : 100;
|
||||
|
||||
return Math.max(560, itemCount * widthPerItem);
|
||||
}, [chart_model, localizedData?.data.length]);
|
||||
|
||||
const defaultColumns: ColumnDef<TChartDatum>[] = useMemo(
|
||||
() => [
|
||||
|
|
@ -163,13 +163,13 @@ const PriorityChart = observer(function PriorityChart(props: Props) {
|
|||
},
|
||||
{
|
||||
accessorKey: "count",
|
||||
header: () => <div className="text-right">Count</div>,
|
||||
header: () => <div className="text-right">Количество</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original.count}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key: "Count",
|
||||
key: "Количество",
|
||||
value: (row) => row.original.count,
|
||||
label: "Count",
|
||||
label: "Количество",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -179,55 +179,64 @@ const PriorityChart = observer(function PriorityChart(props: Props) {
|
|||
|
||||
const columns: ColumnDef<TChartDatum>[] = useMemo(
|
||||
() =>
|
||||
parsedData
|
||||
? Object.keys(parsedData?.schema ?? {}).map((key) => ({
|
||||
localizedData
|
||||
? Object.keys(localizedData?.schema ?? {}).map((key) => ({
|
||||
accessorKey: key,
|
||||
header: () => <div className="text-right">{parsedData.schema[key]}</div>,
|
||||
header: () => <div className="text-right">{localizedData.schema[key]}</div>,
|
||||
cell: ({ row }) => <div className="text-right">{row.original[key]}</div>,
|
||||
meta: {
|
||||
export: {
|
||||
key,
|
||||
value: (row) => row.original[key],
|
||||
label: parsedData.schema[key],
|
||||
label: localizedData.schema[key],
|
||||
},
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
[parsedData]
|
||||
[localizedData]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12">
|
||||
<div className="nodedc-analytics-chart-stack flex flex-col gap-8">
|
||||
{priorityChartLoading ? (
|
||||
<ChartLoader />
|
||||
) : parsedData?.data && parsedData.data.length > 0 ? (
|
||||
) : localizedData?.data && localizedData.data.length > 0 ? (
|
||||
<>
|
||||
<div className="nodedc-analytics-chart-viewport">
|
||||
<div className="nodedc-analytics-chart-inner h-[370px]" style={{ width: `${chartWidth}px` }}>
|
||||
<BarChart
|
||||
className="h-[370px] w-full"
|
||||
data={parsedData.data}
|
||||
className="nodedc-analytics-bar-chart h-full w-full"
|
||||
data={localizedData.data}
|
||||
bars={bars}
|
||||
barSize={chart_model === EChartModels.STACKED ? 72 : 86}
|
||||
margin={{
|
||||
bottom: 30,
|
||||
top: 12,
|
||||
right: 16,
|
||||
bottom: 34,
|
||||
left: 8,
|
||||
}}
|
||||
xAxis={{
|
||||
key: "name",
|
||||
label: xAxisLabel.replace("_", " "),
|
||||
label: xAxisLabel,
|
||||
dy: 30,
|
||||
}}
|
||||
yAxis={{
|
||||
key: "count",
|
||||
label: t("common.no_of", { entity: yAxisLabel.replace("_", " ") }),
|
||||
label: yAxisLabel,
|
||||
offset: -60,
|
||||
dx: -26,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
data={parsedData.data}
|
||||
data={localizedData.data}
|
||||
columns={[...defaultColumns, ...columns]}
|
||||
searchPlaceholder={`${parsedData.data.length} ${xAxisLabel}`}
|
||||
searchPlaceholder={`${localizedData.data.length} ${xAxisLabel}`}
|
||||
actions={(table: Table<TChartDatum>) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="nodedc-analytics-export-button"
|
||||
prependIcon={<Download className="h-3.5 w-3.5" />}
|
||||
onClick={() => exportCSV(table.getRowModel().rows, [...defaultColumns, ...columns], workspaceSlug)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
// plane package imports
|
||||
import type { ChartYAxisMetric, IState } from "@plane/types";
|
||||
import type { ChartYAxisMetric, IState, TStateGroups } from "@plane/types";
|
||||
import { ChartXAxisProperty } from "@plane/types";
|
||||
|
||||
interface ParamsProps {
|
||||
|
|
@ -14,26 +14,56 @@ interface ParamsProps {
|
|||
group_by?: ChartXAxisProperty;
|
||||
}
|
||||
|
||||
export const NODEDC_ANALYTICS_COLORS = [
|
||||
"#C3FF66",
|
||||
"#F5F7FB",
|
||||
"#7C7F85",
|
||||
"#050505",
|
||||
"#A6ADBA",
|
||||
"#DDE3EA",
|
||||
"#2A2B2E",
|
||||
"#9BFF38",
|
||||
];
|
||||
|
||||
const STATE_GROUP_COLORS: Record<TStateGroups, string> = {
|
||||
backlog: "#050505",
|
||||
unstarted: "#7C7F85",
|
||||
started: "#FFFFFF",
|
||||
completed: "#C3FF66",
|
||||
cancelled: "#050505",
|
||||
};
|
||||
|
||||
const PRIORITY_COLORS: Record<string, string> = {
|
||||
urgent: "#C3FF66",
|
||||
high: "#F5F7FB",
|
||||
medium: "#7C7F85",
|
||||
low: "#2A2B2E",
|
||||
none: "#050505",
|
||||
};
|
||||
|
||||
const getFallbackColor = (value: string, baseColors: string[]) => {
|
||||
const fallbackColors = baseColors.length > 0 ? baseColors : NODEDC_ANALYTICS_COLORS;
|
||||
const index = Math.abs(value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)) % fallbackColors.length;
|
||||
|
||||
return fallbackColors[index];
|
||||
};
|
||||
|
||||
export const generateBarColor = (
|
||||
value: string | null | undefined,
|
||||
params: ParamsProps,
|
||||
baseColors: string[],
|
||||
workspaceStates?: IState[]
|
||||
): string => {
|
||||
if (!value) return baseColors[0];
|
||||
let color = baseColors[0];
|
||||
if (!value) return baseColors[0] ?? NODEDC_ANALYTICS_COLORS[0];
|
||||
let color = getFallbackColor(value, baseColors);
|
||||
// Priority
|
||||
if (params.x_axis === ChartXAxisProperty.PRIORITY) {
|
||||
color =
|
||||
value === "urgent"
|
||||
? "#ef4444"
|
||||
: value === "high"
|
||||
? "#f97316"
|
||||
: value === "medium"
|
||||
? "#eab308"
|
||||
: value === "low"
|
||||
? "#22c55e"
|
||||
: "#ced4da";
|
||||
color = PRIORITY_COLORS[value] ?? color;
|
||||
}
|
||||
|
||||
// State group
|
||||
if (params.x_axis === ChartXAxisProperty.STATE_GROUPS) {
|
||||
color = STATE_GROUP_COLORS[value as TStateGroups] ?? color;
|
||||
}
|
||||
|
||||
// State
|
||||
|
|
@ -41,10 +71,9 @@ export const generateBarColor = (
|
|||
if (workspaceStates && workspaceStates.length > 0) {
|
||||
const state = workspaceStates.find((s) => s.id === value);
|
||||
if (state) {
|
||||
color = state.color;
|
||||
color = STATE_GROUP_COLORS[state.group] ?? color;
|
||||
} else {
|
||||
const index = Math.abs(value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)) % baseColors.length;
|
||||
color = baseColors[index];
|
||||
color = getFallbackColor(value, baseColors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ const WorkItemsInsightTable = observer(function WorkItemsInsightTable() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="break-words text-secondary">{row.original.display_name ?? t(`Unassigned`)}</span>
|
||||
<span className="break-words text-secondary">{row.original.display_name ?? "Без исполнителя"}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2573,6 +2573,201 @@
|
|||
outline: none !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-filter-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nodedc-analytics-select-trigger {
|
||||
display: inline-flex !important;
|
||||
min-height: 2.35rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
border-radius: 999px !important;
|
||||
background: rgba(255, 255, 255, 0.06) !important;
|
||||
padding: 0 0.95rem !important;
|
||||
color: var(--text-color-secondary) !important;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 650;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||
transition:
|
||||
background 160ms ease,
|
||||
color 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.nodedc-analytics-select-trigger:hover,
|
||||
.nodedc-analytics-select-trigger:focus-visible {
|
||||
background: rgba(255, 255, 255, 0.095) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--nodedc-card-active-rgb), 0.2) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-select-trigger .text-primary,
|
||||
.nodedc-analytics-select-trigger svg {
|
||||
color: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-dropdown {
|
||||
border-radius: 1.25rem !important;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.014) 100%), rgba(10, 10, 12, 0.96) !important;
|
||||
box-shadow:
|
||||
0 24px 58px rgba(0, 0, 0, 0.34),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.025) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-chart-stack {
|
||||
gap: 1.35rem !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-chart-viewport {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border-radius: 1.35rem;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
padding: 0.45rem 0.35rem 0.15rem;
|
||||
scrollbar-color: rgba(var(--nodedc-card-active-rgb), 0.7) rgba(255, 255, 255, 0.045);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.nodedc-analytics-chart-viewport::-webkit-scrollbar {
|
||||
height: 0.55rem;
|
||||
}
|
||||
|
||||
.nodedc-analytics-chart-viewport::-webkit-scrollbar-track {
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
}
|
||||
|
||||
.nodedc-analytics-chart-viewport::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--nodedc-card-active-rgb), 0.7);
|
||||
}
|
||||
|
||||
.nodedc-analytics-chart-inner {
|
||||
min-width: 35rem;
|
||||
}
|
||||
|
||||
.nodedc-analytics-bar-chart .recharts-cartesian-grid line {
|
||||
stroke: rgba(255, 255, 255, 0.055) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-bar-chart .recharts-cartesian-axis-tick-value,
|
||||
.nodedc-analytics-bar-chart .recharts-label {
|
||||
fill: rgba(255, 255, 255, 0.56) !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-bar-chart .recharts-tooltip-cursor {
|
||||
fill: rgba(255, 255, 255, 0.055) !important;
|
||||
rx: 18px;
|
||||
ry: 18px;
|
||||
}
|
||||
|
||||
.nodedc-analytics-bar-chart path {
|
||||
stroke: none !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-toolbar {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-count {
|
||||
color: rgba(255, 255, 255, 0.54) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-search-button {
|
||||
margin-right: 0 !important;
|
||||
border-radius: 999px !important;
|
||||
background: rgba(255, 255, 255, 0.055) !important;
|
||||
color: var(--text-color-secondary) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-search-button:hover {
|
||||
background: rgba(255, 255, 255, 0.095) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-search-field {
|
||||
border: 0 !important;
|
||||
border-radius: 999px !important;
|
||||
background: rgba(255, 255, 255, 0.065) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface {
|
||||
overflow-x: auto;
|
||||
border-radius: 1.25rem !important;
|
||||
background: rgba(255, 255, 255, 0.026) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.014) !important;
|
||||
scrollbar-color: rgba(var(--nodedc-card-active-rgb), 0.62) rgba(255, 255, 255, 0.035);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface::-webkit-scrollbar {
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface::-webkit-scrollbar-track {
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--nodedc-card-active-rgb), 0.62);
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface table {
|
||||
min-width: max-content;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface th,
|
||||
.nodedc-analytics-table-surface td {
|
||||
border-color: rgba(255, 255, 255, 0.055) !important;
|
||||
padding: 0.75rem 0.9rem !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface th {
|
||||
color: rgba(255, 255, 255, 0.62) !important;
|
||||
font-size: 0.73rem !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface td {
|
||||
color: rgba(255, 255, 255, 0.84) !important;
|
||||
font-size: 0.78rem !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-table-surface tbody tr:hover td {
|
||||
background: rgba(255, 255, 255, 0.04) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-export-button {
|
||||
min-height: 2.35rem;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
border-radius: 999px !important;
|
||||
background: rgba(255, 255, 255, 0.06) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
padding-inline: 0.95rem !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-export-button:hover {
|
||||
background: rgba(255, 255, 255, 0.095) !important;
|
||||
}
|
||||
|
||||
.nodedc-analytics-export-button,
|
||||
.nodedc-analytics-export-button * {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.nodedc-attachment-upload {
|
||||
min-height: 4.5rem;
|
||||
border: 0 !important;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ interface TBarProps extends TShapeProps {
|
|||
showPercentage?: boolean;
|
||||
showTopBorderRadius?: boolean;
|
||||
showBottomBorderRadius?: boolean;
|
||||
borderRadius?: number;
|
||||
dotted?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +96,7 @@ const CustomBar = React.memo(function CustomBar(props: TBarProps) {
|
|||
showPercentage,
|
||||
showTopBorderRadius,
|
||||
showBottomBorderRadius,
|
||||
borderRadius,
|
||||
} = props;
|
||||
|
||||
if (!height) return null;
|
||||
|
|
@ -109,8 +111,8 @@ const CustomBar = React.memo(function CustomBar(props: TBarProps) {
|
|||
currentBarPercentage !== undefined &&
|
||||
!Number.isNaN(currentBarPercentage);
|
||||
|
||||
const topBorderRadius = showTopBorderRadius ? BAR_TOP_BORDER_RADIUS : 0;
|
||||
const bottomBorderRadius = showBottomBorderRadius ? BAR_BOTTOM_BORDER_RADIUS : 0;
|
||||
const topBorderRadius = showTopBorderRadius ? (borderRadius ?? BAR_TOP_BORDER_RADIUS) : 0;
|
||||
const bottomBorderRadius = showBottomBorderRadius ? (borderRadius ?? BAR_BOTTOM_BORDER_RADIUS) : 0;
|
||||
|
||||
return (
|
||||
<g>
|
||||
|
|
@ -177,6 +179,7 @@ const createShapeVariant =
|
|||
showPercentage={bar.showPercentage}
|
||||
showTopBorderRadius={!!showTopBorderRadius}
|
||||
showBottomBorderRadius={!!showBottomBorderRadius}
|
||||
borderRadius={bar.borderRadius}
|
||||
{...factoryProps}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export type TBarItem<T extends string> = {
|
|||
stackId: string;
|
||||
showTopBorderRadius?: (barKey: string, payload: any) => boolean;
|
||||
showBottomBorderRadius?: (barKey: string, payload: any) => boolean;
|
||||
borderRadius?: number;
|
||||
shapeVariant?: TBarChartShapeVariant;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -180,13 +180,10 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
|
|||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active }) =>
|
||||
cn(
|
||||
"nodedc-dropdown-option",
|
||||
{
|
||||
cn("nodedc-dropdown-option", {
|
||||
"bg-white/6": active,
|
||||
"cursor-not-allowed text-placeholder opacity-60": option.disabled,
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
if (!multiple) closeDropdown();
|
||||
|
|
@ -216,7 +213,7 @@ export function CustomSearchSelect(props: ICustomSearchSelectProps) {
|
|||
<p className="px-1.5 py-1 text-placeholder italic">{noResultsMessage}</p>
|
||||
)
|
||||
) : (
|
||||
<p className="px-1.5 py-1 text-placeholder italic">Loading...</p>
|
||||
<p className="px-1.5 py-1 text-placeholder italic">Загрузка...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
|
|
|
|||
Loading…
Reference in New Issue