дизайн меню
This commit is contained in:
@@ -14,12 +14,14 @@ interface CategoryFieldsSelectorProps {
|
||||
selectedValues: AttributeValue[]
|
||||
onValuesChange: (values: AttributeValue[]) => void
|
||||
disabled?: boolean
|
||||
compact?: boolean // Новый проп для компактного отображения
|
||||
}
|
||||
|
||||
export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
|
||||
selectedValues,
|
||||
onValuesChange,
|
||||
disabled = false,
|
||||
compact = false, // По умолчанию обычный режим
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
@@ -56,7 +58,10 @@ export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
|
||||
|
||||
return (
|
||||
<div key={attribute.id} className="space-y-2">
|
||||
<Label htmlFor={attribute.id} className="text-sm font-medium">
|
||||
<Label
|
||||
htmlFor={attribute.id}
|
||||
className={`text-sm font-medium ${compact ? 'text-xs' : ''}`}
|
||||
>
|
||||
{attribute.name}
|
||||
{attribute.is_required && (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
@@ -68,20 +73,48 @@ export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
|
||||
id={attribute.id}
|
||||
value={value}
|
||||
onChange={e => updateAttributeValue(attribute.id, e.target.value)}
|
||||
placeholder={`Введите ${attribute.name.toLowerCase()}`}
|
||||
placeholder={
|
||||
compact ? attribute.name : `Введите ${attribute.name.toLowerCase()}`
|
||||
}
|
||||
className={compact ? 'h-8 text-sm' : ''}
|
||||
{...fieldProps}
|
||||
/>
|
||||
|
||||
{attribute.is_required && !value && (
|
||||
<p className="flex items-center gap-1 text-xs text-red-500">
|
||||
<p
|
||||
className={`flex items-center gap-1 text-red-500 ${compact ? 'text-xs' : 'text-xs'}`}
|
||||
>
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
Это поле обязательно для заполнения
|
||||
{compact ? 'Обязательно' : 'Это поле обязательно для заполнения'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderAttributeSection = (
|
||||
attributesList: Attribute[],
|
||||
title: string,
|
||||
titleColor: string = ''
|
||||
) => {
|
||||
if (attributesList.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className={compact ? 'space-y-3' : 'space-y-4'}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
className={`text-sm font-semibold ${titleColor} ${compact ? 'text-xs' : ''}`}
|
||||
>
|
||||
{title}
|
||||
</Label>
|
||||
</div>
|
||||
<div className={compact ? 'grid grid-cols-2 gap-3' : 'space-y-4'}>
|
||||
{attributesList.map(renderAttribute)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -93,34 +126,16 @@ export const CategoryFieldsSelector: React.FC<CategoryFieldsSelectorProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className={compact ? 'space-y-4' : 'space-y-6'}>
|
||||
{/* Обязательные атрибуты */}
|
||||
{requiredAttributes.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-semibold text-red-600">
|
||||
Обязательные атрибуты
|
||||
</Label>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{requiredAttributes.map(renderAttribute)}
|
||||
</div>
|
||||
</div>
|
||||
{renderAttributeSection(
|
||||
requiredAttributes,
|
||||
'Обязательные атрибуты',
|
||||
'text-red-600'
|
||||
)}
|
||||
|
||||
{/* Необязательные атрибуты */}
|
||||
{optionalAttributes.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
Дополнительные атрибуты
|
||||
</Label>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{optionalAttributes.map(renderAttribute)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{renderAttributeSection(optionalAttributes, 'Дополнительные атрибуты')}
|
||||
|
||||
{attributes.length === 0 && (
|
||||
<div className="py-4 text-center">
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
TemplateFilters,
|
||||
} from '@/component/TemplateManager/TemplateFilterSidebar'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -32,7 +32,18 @@ import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { Template, TemplateAttributeDetail } from '@/type/template'
|
||||
import TemplateCard from '@/widget/template/ui/TemplateCard'
|
||||
import clsx from 'clsx'
|
||||
import { CheckSquare, Plus, Square, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
CheckSquare,
|
||||
Copy,
|
||||
Edit3,
|
||||
FileText,
|
||||
Plus,
|
||||
Settings2,
|
||||
Square,
|
||||
Tag,
|
||||
Trash2,
|
||||
Type,
|
||||
} from 'lucide-react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
interface AttributeValue {
|
||||
@@ -65,6 +76,172 @@ const fromCategoryAttributeValues = (
|
||||
}))
|
||||
}
|
||||
|
||||
// Компактный селектор атрибутов
|
||||
interface CompactCategoryFieldsSelectorProps {
|
||||
selectedValues: CategoryAttributeValue[]
|
||||
onValuesChange: (values: CategoryAttributeValue[]) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const CompactCategoryFieldsSelector: React.FC<
|
||||
CompactCategoryFieldsSelectorProps
|
||||
> = ({ selectedValues, onValuesChange, isLoading = false }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="py-4 text-center text-sm text-muted-foreground">
|
||||
Загрузка атрибутов...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<CategoryFieldsSelector
|
||||
selectedValues={selectedValues}
|
||||
onValuesChange={onValuesChange}
|
||||
compact={true} // Передаем флаг для компактного отображения
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Общий компонент формы шаблона
|
||||
interface TemplateFormProps {
|
||||
title: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
iconColor: string
|
||||
name: string
|
||||
onNameChange: (value: string) => void
|
||||
templateDescription: string
|
||||
onDescriptionChange: (value: string) => void
|
||||
attributeValues: AttributeValue[]
|
||||
onAttributeValuesChange: (values: AttributeValue[]) => void
|
||||
attributesLoading: boolean
|
||||
onSubmit: () => void
|
||||
onCancel: () => void
|
||||
submitText: string
|
||||
submitIcon: React.ReactNode
|
||||
isSubmitDisabled: boolean
|
||||
nameLabel?: string
|
||||
descriptionLabel?: string
|
||||
}
|
||||
|
||||
const TemplateForm: React.FC<TemplateFormProps> = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconColor,
|
||||
name,
|
||||
onNameChange,
|
||||
templateDescription,
|
||||
onDescriptionChange,
|
||||
attributeValues,
|
||||
onAttributeValuesChange,
|
||||
attributesLoading,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitText,
|
||||
submitIcon,
|
||||
isSubmitDisabled,
|
||||
nameLabel = 'Название шаблона',
|
||||
descriptionLabel = 'Описание',
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<DialogHeader className="pb-4">
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
{icon}
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="max-h-[50vh] space-y-4 overflow-y-auto pr-2">
|
||||
{/* Основные настройки */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className={`h-4 w-4 ${iconColor}`} />
|
||||
<CardTitle className="text-sm">Основная информация</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||||
<Type className="h-3 w-3" />
|
||||
{nameLabel}
|
||||
</Label>
|
||||
<Input
|
||||
placeholder={`Введите ${nameLabel.toLowerCase()}`}
|
||||
value={name}
|
||||
onChange={e => onNameChange(e.target.value)}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||||
<FileText className="h-3 w-3" />
|
||||
{descriptionLabel}
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder={`${descriptionLabel}...`}
|
||||
value={templateDescription}
|
||||
onChange={e => onDescriptionChange(e.target.value)}
|
||||
className="min-h-[80px] resize-none"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Атрибуты */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-4 w-4 text-purple-600" />
|
||||
<CardTitle className="text-sm">Атрибуты шаблона</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CompactCategoryFieldsSelector
|
||||
selectedValues={toCategoryAttributeValues(attributeValues)}
|
||||
onValuesChange={values =>
|
||||
onAttributeValuesChange(fromCategoryAttributeValues(values))
|
||||
}
|
||||
isLoading={attributesLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{!name.trim() && (
|
||||
<span className="text-amber-600">
|
||||
Заполните {nameLabel.toLowerCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitDisabled}
|
||||
className="min-w-[130px]"
|
||||
>
|
||||
{submitIcon}
|
||||
{submitText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const TemplatesOverviewPage = () => {
|
||||
const {
|
||||
templates,
|
||||
@@ -360,6 +537,7 @@ export const TemplatesOverviewPage = () => {
|
||||
Выбрать
|
||||
</Button>
|
||||
|
||||
{/* Диалог создания шаблона */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
@@ -367,67 +545,25 @@ export const TemplatesOverviewPage = () => {
|
||||
Создать
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Создать новый шаблон</DialogTitle>
|
||||
<DialogDescription>
|
||||
Заполните основную информацию о шаблоне
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="template-name" className="text-right">
|
||||
Название
|
||||
</Label>
|
||||
<Input
|
||||
id="template-name"
|
||||
className="col-span-3"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
placeholder="Введите название шаблона"
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
|
||||
<TemplateForm
|
||||
title="Создать новый шаблон"
|
||||
description="Заполните основную информацию о шаблоне протокола"
|
||||
icon={<Plus className="h-5 w-5 text-blue-600" />}
|
||||
iconColor="text-blue-600"
|
||||
name={newName}
|
||||
onNameChange={setNewName}
|
||||
templateDescription={newDescription}
|
||||
onDescriptionChange={setNewDescription}
|
||||
attributeValues={newAttributeValues}
|
||||
onAttributeValuesChange={setNewAttributeValues}
|
||||
attributesLoading={attributesLoading}
|
||||
onSubmit={handleCreate}
|
||||
onCancel={() => setIsDialogOpen(false)}
|
||||
submitText="Создать"
|
||||
submitIcon={<Plus className="mr-2 h-4 w-4" />}
|
||||
isSubmitDisabled={!newName.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="template-description"
|
||||
className="text-right"
|
||||
>
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="template-description"
|
||||
className="col-span-3"
|
||||
value={newDescription}
|
||||
onChange={e => setNewDescription(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label className="self-start pt-2 text-right">
|
||||
Атрибуты
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
{!attributesLoading && (
|
||||
<CategoryFieldsSelector
|
||||
selectedValues={toCategoryAttributeValues(
|
||||
newAttributeValues
|
||||
)}
|
||||
onValuesChange={values =>
|
||||
setNewAttributeValues(
|
||||
fromCategoryAttributeValues(values)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={handleCreate}>Создать</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -436,72 +572,27 @@ export const TemplatesOverviewPage = () => {
|
||||
open={isCopyDialogOpen}
|
||||
onOpenChange={setIsCopyDialogOpen}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Копировать шаблон</DialogTitle>
|
||||
<DialogDescription>
|
||||
Укажите название и описание для копии шаблона
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="copy-name" className="text-right">
|
||||
Название
|
||||
</Label>
|
||||
<Input
|
||||
id="copy-name"
|
||||
className="col-span-3"
|
||||
value={copyName}
|
||||
onChange={e => setCopyName(e.target.value)}
|
||||
placeholder="Введите название"
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
|
||||
<TemplateForm
|
||||
title="Копировать шаблон"
|
||||
description="Укажите название и описание для копии шаблона"
|
||||
icon={<Copy className="h-5 w-5 text-green-600" />}
|
||||
iconColor="text-green-600"
|
||||
name={copyName}
|
||||
onNameChange={setCopyName}
|
||||
templateDescription={copyDescription}
|
||||
onDescriptionChange={setCopyDescription}
|
||||
attributeValues={copyAttributeValues}
|
||||
onAttributeValuesChange={setCopyAttributeValues}
|
||||
attributesLoading={attributesLoading}
|
||||
onSubmit={handleCopyConfirm}
|
||||
onCancel={() => setIsCopyDialogOpen(false)}
|
||||
submitText="Копировать"
|
||||
submitIcon={<Copy className="mr-2 h-4 w-4" />}
|
||||
isSubmitDisabled={!copyName.trim()}
|
||||
nameLabel="Название копии"
|
||||
descriptionLabel="Описание копии"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="copy-description"
|
||||
className="text-right"
|
||||
>
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="copy-description"
|
||||
className="col-span-3"
|
||||
value={copyDescription}
|
||||
onChange={e => setCopyDescription(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label className="self-start pt-2 text-right">
|
||||
Атрибуты
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
{!attributesLoading && (
|
||||
<CategoryFieldsSelector
|
||||
selectedValues={toCategoryAttributeValues(
|
||||
copyAttributeValues
|
||||
)}
|
||||
onValuesChange={values =>
|
||||
setCopyAttributeValues(
|
||||
fromCategoryAttributeValues(values)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={handleCopyConfirm}
|
||||
disabled={!copyName.trim()}
|
||||
>
|
||||
Копировать
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -510,72 +601,25 @@ export const TemplatesOverviewPage = () => {
|
||||
open={isEditDialogOpen}
|
||||
onOpenChange={setIsEditDialogOpen}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать шаблон</DialogTitle>
|
||||
<DialogDescription>
|
||||
Измените название и описание шаблона
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit-name" className="text-right">
|
||||
Название
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
className="col-span-3"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
placeholder="Введите название"
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-hidden">
|
||||
<TemplateForm
|
||||
title="Редактировать шаблон"
|
||||
description="Измените основную информацию о шаблоне"
|
||||
icon={<Edit3 className="h-5 w-5 text-orange-600" />}
|
||||
iconColor="text-orange-600"
|
||||
name={editName}
|
||||
onNameChange={setEditName}
|
||||
templateDescription={editDescription}
|
||||
onDescriptionChange={setEditDescription}
|
||||
attributeValues={editAttributeValues}
|
||||
onAttributeValuesChange={setEditAttributeValues}
|
||||
attributesLoading={attributesLoading}
|
||||
onSubmit={handleEditConfirm}
|
||||
onCancel={() => setIsEditDialogOpen(false)}
|
||||
submitText="Сохранить"
|
||||
submitIcon={<Edit3 className="mr-2 h-4 w-4" />}
|
||||
isSubmitDisabled={!editName.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="edit-description"
|
||||
className="text-right"
|
||||
>
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
className="col-span-3"
|
||||
value={editDescription}
|
||||
onChange={e => setEditDescription(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label className="self-start pt-2 text-right">
|
||||
Атрибуты
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
{!attributesLoading && (
|
||||
<CategoryFieldsSelector
|
||||
selectedValues={toCategoryAttributeValues(
|
||||
editAttributeValues
|
||||
)}
|
||||
onValuesChange={values =>
|
||||
setEditAttributeValues(
|
||||
fromCategoryAttributeValues(values)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={handleEditConfirm}
|
||||
disabled={!editName.trim()}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user