746 lines
28 KiB
TypeScript
746 lines
28 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Trash2 } from "lucide-react";
|
||
import { TemplateConfig } from "@/components/admin/builder/templates";
|
||
import { ScreenVariantsConfig } from "../forms/ScreenVariantsConfig";
|
||
import {
|
||
useBuilderDispatch,
|
||
useBuilderSelectedScreen,
|
||
useBuilderState,
|
||
} from "@/lib/admin/builder/context";
|
||
import type { BuilderScreen } from "@/lib/admin/builder/types";
|
||
import type {
|
||
NavigationRuleDefinition,
|
||
ScreenDefinition,
|
||
ScreenVariantDefinition,
|
||
} from "@/lib/funnel/types";
|
||
import { cn } from "@/lib/utils";
|
||
import { validateBuilderState } from "@/lib/admin/builder/validation";
|
||
import { Section } from "./Section";
|
||
import { ValidationSummary } from "./ValidationSummary";
|
||
import { isListScreen, type ValidationIssues } from "./types";
|
||
|
||
export function BuilderSidebar() {
|
||
const state = useBuilderState();
|
||
const dispatch = useBuilderDispatch();
|
||
const selectedScreen = useBuilderSelectedScreen();
|
||
|
||
const [activeTab, setActiveTab] = useState<"funnel" | "screen">(
|
||
selectedScreen ? "screen" : "funnel"
|
||
);
|
||
const selectedScreenId = selectedScreen?.id ?? null;
|
||
|
||
useEffect(() => {
|
||
setActiveTab((previous) => {
|
||
if (selectedScreenId) {
|
||
return "screen";
|
||
}
|
||
return previous === "screen" ? "funnel" : previous;
|
||
});
|
||
}, [selectedScreenId]);
|
||
|
||
// ✅ Оптимизированная validation - только критичные поля
|
||
const screenIds = useMemo(
|
||
() => state.screens.map((s) => s.id).join(","),
|
||
[state.screens]
|
||
);
|
||
const validation = useMemo(
|
||
() => validateBuilderState(state),
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Оптимизация: пересчитываем только при изменении критичных полей
|
||
[state.meta.id, state.meta.firstScreenId, screenIds, state.screens.length]
|
||
);
|
||
const screenValidationIssues = useMemo(() => {
|
||
if (!selectedScreenId) {
|
||
return [] as ValidationIssues;
|
||
}
|
||
|
||
return validation.issues.filter(
|
||
(issue) => issue.screenId === selectedScreenId
|
||
);
|
||
}, [selectedScreenId, validation]);
|
||
|
||
const screenOptions = useMemo(
|
||
() =>
|
||
state.screens.map((screen) => ({
|
||
id: screen.id,
|
||
title: screen.title?.text,
|
||
})),
|
||
[state.screens]
|
||
);
|
||
|
||
// ✅ Handlers для text inputs
|
||
const handleMetaChange = useCallback(
|
||
(field: keyof typeof state.meta, value: string) => {
|
||
dispatch({ type: "set-meta", payload: { [field]: value } });
|
||
},
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps -- dispatch стабилен из context
|
||
[dispatch]
|
||
);
|
||
|
||
const handleFirstScreenChange = (value: string) => {
|
||
dispatch({ type: "set-meta", payload: { firstScreenId: value } });
|
||
};
|
||
|
||
const handleDefaultTextsChange = useCallback(
|
||
(field: keyof NonNullable<typeof state.defaultTexts>, value: string) => {
|
||
dispatch({ type: "set-default-texts", payload: { [field]: value } });
|
||
},
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps -- dispatch стабилен из context
|
||
[dispatch]
|
||
);
|
||
|
||
const handleScreenIdChange = (currentId: string, newId: string) => {
|
||
if (newId === currentId) {
|
||
return;
|
||
}
|
||
|
||
// Разрешаем пустые ID для полного переименования
|
||
if (newId.trim() === "") {
|
||
// Просто обновляем на пустое значение, пользователь сможет ввести новое
|
||
dispatch({
|
||
type: "update-screen",
|
||
payload: {
|
||
screenId: currentId,
|
||
screen: { id: newId },
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Обновляем ID экрана
|
||
dispatch({
|
||
type: "update-screen",
|
||
payload: {
|
||
screenId: currentId,
|
||
screen: { id: newId },
|
||
},
|
||
});
|
||
|
||
// Если это был первый экран в мета данных, обновляем и там
|
||
if (state.meta.firstScreenId === currentId) {
|
||
dispatch({ type: "set-meta", payload: { firstScreenId: newId } });
|
||
}
|
||
};
|
||
|
||
const getScreenById = (screenId: string): BuilderScreen | undefined =>
|
||
state.screens.find((item) => item.id === screenId);
|
||
|
||
const updateNavigation = (
|
||
screen: BuilderScreen,
|
||
navigationUpdates: Partial<BuilderScreen["navigation"]> = {}
|
||
) => {
|
||
dispatch({
|
||
type: "update-navigation",
|
||
payload: {
|
||
screenId: screen.id,
|
||
navigation: {
|
||
defaultNextScreenId:
|
||
navigationUpdates.defaultNextScreenId ??
|
||
screen.navigation?.defaultNextScreenId,
|
||
rules: navigationUpdates.rules ?? screen.navigation?.rules ?? [],
|
||
isEndScreen:
|
||
navigationUpdates.isEndScreen ?? screen.navigation?.isEndScreen,
|
||
},
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleDefaultNextChange = (
|
||
screenId: string,
|
||
nextScreenId: string | ""
|
||
) => {
|
||
const screen = getScreenById(screenId);
|
||
if (!screen) {
|
||
return;
|
||
}
|
||
|
||
updateNavigation(screen, {
|
||
defaultNextScreenId: nextScreenId || undefined,
|
||
});
|
||
};
|
||
|
||
const updateRules = (screenId: string, rules: NavigationRuleDefinition[]) => {
|
||
const screen = getScreenById(screenId);
|
||
if (!screen) {
|
||
return;
|
||
}
|
||
|
||
updateNavigation(screen, { rules });
|
||
};
|
||
|
||
const handleRuleOperatorChange = (
|
||
screenId: string,
|
||
index: number,
|
||
operator: NavigationRuleDefinition["conditions"][0]["operator"]
|
||
) => {
|
||
const screen = getScreenById(screenId);
|
||
if (!screen) {
|
||
return;
|
||
}
|
||
|
||
const rules = screen.navigation?.rules ?? [];
|
||
const nextRules = rules.map((rule, ruleIndex) =>
|
||
ruleIndex === index
|
||
? {
|
||
...rule,
|
||
conditions: rule.conditions.map((condition, conditionIndex) =>
|
||
conditionIndex === 0
|
||
? {
|
||
...condition,
|
||
operator,
|
||
}
|
||
: condition
|
||
),
|
||
}
|
||
: rule
|
||
);
|
||
|
||
updateRules(screenId, nextRules);
|
||
};
|
||
|
||
const handleRuleOptionToggle = (
|
||
screenId: string,
|
||
ruleIndex: number,
|
||
optionId: string
|
||
) => {
|
||
const screen = getScreenById(screenId);
|
||
if (!screen) {
|
||
return;
|
||
}
|
||
|
||
const rules = screen.navigation?.rules ?? [];
|
||
const nextRules = rules.map((rule, currentIndex) => {
|
||
if (currentIndex !== ruleIndex) {
|
||
return rule;
|
||
}
|
||
|
||
const [condition] = rule.conditions;
|
||
const optionIds = new Set(condition.optionIds ?? []);
|
||
if (optionIds.has(optionId)) {
|
||
optionIds.delete(optionId);
|
||
} else {
|
||
optionIds.add(optionId);
|
||
}
|
||
|
||
return {
|
||
...rule,
|
||
conditions: [
|
||
{
|
||
...condition,
|
||
optionIds: Array.from(optionIds),
|
||
},
|
||
],
|
||
};
|
||
});
|
||
|
||
updateRules(screenId, nextRules);
|
||
};
|
||
|
||
const handleRuleNextScreenChange = (
|
||
screenId: string,
|
||
ruleIndex: number,
|
||
nextScreenId: string
|
||
) => {
|
||
const screen = getScreenById(screenId);
|
||
if (!screen) {
|
||
return;
|
||
}
|
||
|
||
const rules = screen.navigation?.rules ?? [];
|
||
const nextRules = rules.map((rule, currentIndex) =>
|
||
currentIndex === ruleIndex ? { ...rule, nextScreenId } : rule
|
||
);
|
||
|
||
updateRules(screenId, nextRules);
|
||
};
|
||
|
||
const handleAddRule = (screen: BuilderScreen) => {
|
||
if (!isListScreen(screen)) {
|
||
return;
|
||
}
|
||
|
||
const defaultCondition: NavigationRuleDefinition["conditions"][number] = {
|
||
screenId: screen.id,
|
||
operator: "includesAny",
|
||
optionIds: screen.list.options.slice(0, 1).map((option) => option.id),
|
||
};
|
||
|
||
const nextRules = [
|
||
...(screen.navigation?.rules ?? []),
|
||
{
|
||
nextScreenId: state.screens[0]?.id ?? screen.id,
|
||
conditions: [defaultCondition],
|
||
},
|
||
];
|
||
updateNavigation(screen, { rules: nextRules });
|
||
};
|
||
|
||
const handleRemoveRule = (screenId: string, ruleIndex: number) => {
|
||
const screen = getScreenById(screenId);
|
||
if (!screen) {
|
||
return;
|
||
}
|
||
|
||
const rules = screen.navigation?.rules ?? [];
|
||
const nextRules = rules.filter((_, index) => index !== ruleIndex);
|
||
updateNavigation(screen, { rules: nextRules });
|
||
};
|
||
|
||
const handleDeleteScreen = (screenId: string) => {
|
||
if (state.screens.length <= 1) {
|
||
return;
|
||
}
|
||
dispatch({ type: "remove-screen", payload: { screenId } });
|
||
};
|
||
|
||
const handleTemplateUpdate = (
|
||
screenId: string,
|
||
updates: Partial<ScreenDefinition>
|
||
) => {
|
||
dispatch({
|
||
type: "update-screen",
|
||
payload: {
|
||
screenId,
|
||
screen: updates as Partial<BuilderScreen>,
|
||
},
|
||
});
|
||
};
|
||
|
||
const handleVariantsChange = (
|
||
screenId: string,
|
||
variants: ScreenVariantDefinition<ScreenDefinition>[]
|
||
) => {
|
||
dispatch({
|
||
type: "update-screen",
|
||
payload: {
|
||
screenId,
|
||
screen: {
|
||
variants: variants.length > 0 ? variants : undefined,
|
||
} as Partial<BuilderScreen>,
|
||
},
|
||
});
|
||
};
|
||
|
||
const selectedScreenIsListType = selectedScreen
|
||
? isListScreen(selectedScreen)
|
||
: false;
|
||
|
||
return (
|
||
<div className="flex h-full flex-col">
|
||
<div className="sticky top-0 z-20 border-b border-border/60 bg-background/95 px-4 py-3">
|
||
<div className="flex flex-col gap-1">
|
||
<h1 className="text-base font-semibold">Настройки</h1>
|
||
</div>
|
||
<div className="mt-3 flex rounded-lg bg-muted/40 p-1 text-sm font-medium">
|
||
<button
|
||
type="button"
|
||
className={cn(
|
||
"flex-1 rounded-md px-3 py-1.5 transition",
|
||
activeTab === "funnel"
|
||
? "bg-background text-foreground shadow"
|
||
: "text-muted-foreground hover:text-foreground"
|
||
)}
|
||
onClick={() => setActiveTab("funnel")}
|
||
>
|
||
Воронка
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={cn(
|
||
"flex-1 rounded-md px-3 py-1.5 transition",
|
||
activeTab === "screen"
|
||
? "bg-background text-foreground shadow"
|
||
: "text-muted-foreground hover:text-foreground",
|
||
!selectedScreen && "cursor-not-allowed opacity-60"
|
||
)}
|
||
onClick={() => selectedScreen && setActiveTab("screen")}
|
||
>
|
||
Экран
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||
{activeTab === "funnel" ? (
|
||
<div className="flex flex-col gap-4">
|
||
{/* Валидация всегда вверху, без заголовка */}
|
||
<ValidationSummary issues={validation.issues} />
|
||
|
||
<Section title="Настройки воронки" description="Общие параметры">
|
||
<TextInput
|
||
label="ID воронки"
|
||
value={state.meta.id}
|
||
onChange={(event) => handleMetaChange("id", event.target.value)}
|
||
/>
|
||
<TextInput
|
||
label="Название"
|
||
value={state.meta.title ?? ""}
|
||
onChange={(event) =>
|
||
handleMetaChange("title", event.target.value)
|
||
}
|
||
/>
|
||
<TextInput
|
||
label="Описание"
|
||
value={state.meta.description ?? ""}
|
||
onChange={(event) =>
|
||
handleMetaChange("description", event.target.value)
|
||
}
|
||
/>
|
||
<label className="flex flex-col gap-2">
|
||
<span className="text-sm font-medium text-muted-foreground">
|
||
Первый экран
|
||
</span>
|
||
<select
|
||
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||
value={state.meta.firstScreenId ?? state.screens[0]?.id ?? ""}
|
||
onChange={(event) =>
|
||
handleFirstScreenChange(event.target.value)
|
||
}
|
||
>
|
||
{screenOptions.map((screen) => (
|
||
<option key={screen.id} value={screen.id}>
|
||
{screen.title}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
</Section>
|
||
|
||
<Section
|
||
title="Дефолтные тексты"
|
||
description="Текст кнопок и баннеров"
|
||
>
|
||
<TextInput
|
||
label="Текст кнопки Next/Continue"
|
||
placeholder="Next"
|
||
value={state.defaultTexts?.nextButton ?? ""}
|
||
onChange={(event) =>
|
||
handleDefaultTextsChange("nextButton", event.target.value)
|
||
}
|
||
/>
|
||
<TextInput
|
||
label="Баннер приватности"
|
||
placeholder="Мы не передаем личную информацию..."
|
||
value={state.defaultTexts?.privacyBanner ?? ""}
|
||
onChange={(event) =>
|
||
handleDefaultTextsChange("privacyBanner", event.target.value)
|
||
}
|
||
/>
|
||
</Section>
|
||
|
||
<Section title="Экраны">
|
||
<div className="flex flex-col gap-3 rounded-lg border border-border/60 bg-muted/20 p-3 text-sm text-muted-foreground">
|
||
<div className="flex items-center justify-between">
|
||
<span>Всего экранов</span>
|
||
<span className="font-semibold text-foreground">
|
||
{state.screens.length}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col gap-1 text-xs">
|
||
{state.screens.map((screen, index) => (
|
||
<span
|
||
key={screen.id}
|
||
className="flex items-center justify-between"
|
||
>
|
||
<span className="truncate">
|
||
{index + 1}. {screen.title?.text}
|
||
</span>
|
||
<span className="uppercase text-muted-foreground/80">
|
||
{screen.template}
|
||
</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
</div>
|
||
) : selectedScreen ? (
|
||
<div className="flex flex-col gap-4">
|
||
{/* Валидация всегда вверху, без заголовка */}
|
||
<ValidationSummary issues={screenValidationIssues} />
|
||
|
||
<div className="rounded-lg border border-primary/20 bg-primary/5 p-3">
|
||
<div className="flex items-center justify-between text-sm">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-muted-foreground">
|
||
#{selectedScreen.id}
|
||
</span>
|
||
<span className="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary uppercase">
|
||
{selectedScreen.template}
|
||
</span>
|
||
</div>
|
||
<span className="text-xs text-muted-foreground">
|
||
{state.screens.findIndex(
|
||
(screen) => screen.id === selectedScreen.id
|
||
) + 1}
|
||
/{state.screens.length}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Section title="Общие данные">
|
||
<TextInput
|
||
label="ID экрана"
|
||
value={selectedScreen.id}
|
||
onChange={(event) =>
|
||
handleScreenIdChange(selectedScreen.id, event.target.value)
|
||
}
|
||
/>
|
||
</Section>
|
||
|
||
<Section title="Контент и оформление">
|
||
<TemplateConfig
|
||
screen={selectedScreen}
|
||
onUpdate={(updates) =>
|
||
handleTemplateUpdate(selectedScreen.id, updates)
|
||
}
|
||
/>
|
||
</Section>
|
||
|
||
<Section title="Вариативность">
|
||
<ScreenVariantsConfig
|
||
screen={selectedScreen}
|
||
allScreens={state.screens}
|
||
onChange={(variants) =>
|
||
handleVariantsChange(selectedScreen.id, variants)
|
||
}
|
||
/>
|
||
</Section>
|
||
|
||
<Section title="Навигация">
|
||
{/* 🎯 ЧЕКБОКС ДЛЯ ФИНАЛЬНОГО ЭКРАНА */}
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedScreen.navigation?.isEndScreen ?? false}
|
||
onChange={(e) => {
|
||
updateNavigation(selectedScreen, {
|
||
isEndScreen: e.target.checked,
|
||
});
|
||
}}
|
||
className="rounded border-border"
|
||
/>
|
||
<div className="flex flex-col">
|
||
<span className="text-sm font-medium text-foreground">
|
||
Финальный экран
|
||
</span>
|
||
<span className="text-xs text-muted-foreground">
|
||
Этот экран завершает воронку (переход не требуется)
|
||
</span>
|
||
</div>
|
||
</label>
|
||
|
||
{/* ОБЫЧНАЯ НАВИГАЦИЯ - показываем только если НЕ финальный экран */}
|
||
{!selectedScreen.navigation?.isEndScreen && (
|
||
<label className="flex flex-col gap-2">
|
||
<span className="text-sm font-medium text-muted-foreground">
|
||
Экран по умолчанию
|
||
</span>
|
||
<select
|
||
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||
value={selectedScreen.navigation?.defaultNextScreenId ?? ""}
|
||
onChange={(event) =>
|
||
handleDefaultNextChange(
|
||
selectedScreen.id,
|
||
event.target.value
|
||
)
|
||
}
|
||
>
|
||
<option value="">—</option>
|
||
{screenOptions
|
||
.filter((screen) => screen.id !== selectedScreen.id)
|
||
.map((screen) => (
|
||
<option key={screen.id} value={screen.id}>
|
||
{screen.title}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
)}
|
||
</Section>
|
||
|
||
{selectedScreenIsListType &&
|
||
!selectedScreen.navigation?.isEndScreen && (
|
||
<Section
|
||
title="Правила переходов"
|
||
description="Условная навигация"
|
||
>
|
||
<div className="flex flex-col gap-3">
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-xs text-muted-foreground">
|
||
Направляйте пользователей на разные экраны в зависимости
|
||
от выбора.
|
||
</p>
|
||
<Button
|
||
className="h-8 w-8 p-0 flex items-center justify-center"
|
||
onClick={() => handleAddRule(selectedScreen)}
|
||
>
|
||
<span className="text-lg leading-none">+</span>
|
||
</Button>
|
||
</div>
|
||
|
||
{(selectedScreen.navigation?.rules ?? []).length === 0 && (
|
||
<div className="rounded-lg border border-border/60 bg-muted/30 p-3 text-center text-xs text-muted-foreground">
|
||
Правил пока нет
|
||
</div>
|
||
)}
|
||
|
||
{(selectedScreen.navigation?.rules ?? []).map(
|
||
(rule, ruleIndex) => (
|
||
<div
|
||
key={ruleIndex}
|
||
className="flex flex-col gap-3 rounded-xl border border-border/80 bg-background/60 p-3"
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-xs font-semibold uppercase text-muted-foreground">
|
||
Правило {ruleIndex + 1}
|
||
</span>
|
||
<Button
|
||
variant="ghost"
|
||
className="h-8 px-2 text-destructive hover:bg-destructive/10"
|
||
onClick={() =>
|
||
handleRemoveRule(selectedScreen.id, ruleIndex)
|
||
}
|
||
>
|
||
<Trash2 className="h-3 w-3 mr-1" />
|
||
<span className="text-xs">Удалить</span>
|
||
</Button>
|
||
</div>
|
||
<label className="flex flex-col gap-2">
|
||
<span className="text-sm font-medium text-muted-foreground">
|
||
Оператор
|
||
</span>
|
||
<select
|
||
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||
value={
|
||
rule.conditions[0]?.operator ?? "includesAny"
|
||
}
|
||
onChange={(event) =>
|
||
handleRuleOperatorChange(
|
||
selectedScreen.id,
|
||
ruleIndex,
|
||
event.target
|
||
.value as NavigationRuleDefinition["conditions"][0]["operator"]
|
||
)
|
||
}
|
||
>
|
||
<option value="includesAny">contains any</option>
|
||
<option value="includesAll">contains all</option>
|
||
<option value="includesExactly">
|
||
exact match
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
{selectedScreen.template === "list" ? (
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-sm font-medium text-muted-foreground">
|
||
Варианты ответа
|
||
</span>
|
||
<div className="flex flex-col gap-2 rounded-lg border border-border/60 p-3">
|
||
{selectedScreen.list.options.map((option) => {
|
||
const condition = rule.conditions[0];
|
||
const isChecked =
|
||
condition.optionIds?.includes(option.id) ??
|
||
false;
|
||
return (
|
||
<label
|
||
key={option.id}
|
||
className="flex items-center gap-2 text-sm"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={isChecked}
|
||
onChange={() =>
|
||
handleRuleOptionToggle(
|
||
selectedScreen.id,
|
||
ruleIndex,
|
||
option.id
|
||
)
|
||
}
|
||
/>
|
||
<span>
|
||
{option.label}
|
||
<span className="text-muted-foreground">
|
||
{" "}
|
||
({option.id})
|
||
</span>
|
||
</span>
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col gap-2 rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||
Навигационные правила с вариантами ответа доступны
|
||
только для экранов со списком.
|
||
</div>
|
||
)}
|
||
|
||
<label className="flex flex-col gap-2">
|
||
<span className="text-sm font-medium text-muted-foreground">
|
||
Следующий экран
|
||
</span>
|
||
<select
|
||
className="rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||
value={rule.nextScreenId}
|
||
onChange={(event) =>
|
||
handleRuleNextScreenChange(
|
||
selectedScreen.id,
|
||
ruleIndex,
|
||
event.target.value
|
||
)
|
||
}
|
||
>
|
||
{screenOptions
|
||
.filter(
|
||
(screen) => screen.id !== selectedScreen.id
|
||
)
|
||
.map((screen) => (
|
||
<option key={screen.id} value={screen.id}>
|
||
{screen.title}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
)
|
||
)}
|
||
</div>
|
||
</Section>
|
||
)}
|
||
|
||
<Section title="Управление">
|
||
<div className="rounded-lg border border-destructive/20 bg-destructive/5 p-4">
|
||
<p className="mb-3 text-sm text-muted-foreground">
|
||
Удаление экрана нельзя отменить. Все связи с этим экраном
|
||
будут потеряны.
|
||
</p>
|
||
<Button
|
||
variant="destructive"
|
||
className="h-9 text-sm"
|
||
disabled={state.screens.length <= 1}
|
||
onClick={() => handleDeleteScreen(selectedScreen.id)}
|
||
>
|
||
<Trash2 className="h-4 w-4 mr-2" />
|
||
{state.screens.length <= 1
|
||
? "Нельзя удалить последний экран"
|
||
: "Удалить экран"}
|
||
</Button>
|
||
</div>
|
||
</Section>
|
||
</div>
|
||
) : (
|
||
<div className="flex h-full items-center justify-center rounded-xl border border-dashed border-border/60 bg-muted/20 p-6 text-center text-sm text-muted-foreground">
|
||
Выберите экран в списке слева, чтобы настроить его параметры.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|