elements constructor улучшен
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Save } from 'lucide-react'
|
||||
import { memo } from 'react'
|
||||
import { FormulaBarProps } from '../types'
|
||||
import { getColumnLabel } from '../utils'
|
||||
@@ -163,19 +164,7 @@ export const FormulaBar = memo(
|
||||
: 'Все изменения сохранены'
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
<Save className="h-3 w-3" />
|
||||
{lastSaveError
|
||||
? 'Повторить'
|
||||
: hasUnsavedChanges
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '../ui/dialog'
|
||||
import { Input } from '../ui/input'
|
||||
import {
|
||||
@@ -45,6 +44,7 @@ import {
|
||||
SelectValue,
|
||||
} from '../ui/select'
|
||||
import { Textarea } from '../ui/textarea'
|
||||
import { ElementFormulaBar } from './ElementFormulaBar'
|
||||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||
|
||||
interface ElementFormProps {
|
||||
@@ -705,6 +705,14 @@ interface ElementConstructorProps {
|
||||
) => void
|
||||
onElementDelete: (elementId: string) => void
|
||||
disabled?: boolean
|
||||
// Пропсы для сохранения
|
||||
hasUnsavedChanges?: boolean
|
||||
isSaving?: boolean
|
||||
lastSaveError?: string | null
|
||||
onManualSave?: () => Promise<void>
|
||||
onClearError?: () => void
|
||||
// Пропсы для настроек макета
|
||||
onLayoutSettingsChange?: (settings: FormLayoutSettings) => void
|
||||
}
|
||||
|
||||
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
@@ -713,6 +721,12 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
onElementUpdate,
|
||||
onElementDelete,
|
||||
disabled = false,
|
||||
hasUnsavedChanges = false,
|
||||
isSaving = false,
|
||||
lastSaveError = null,
|
||||
onManualSave,
|
||||
onClearError,
|
||||
onLayoutSettingsChange,
|
||||
}) => {
|
||||
const elements = template.elements
|
||||
const layoutSettings = template.layoutSettings || {
|
||||
@@ -877,94 +891,96 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 flex-col bg-gray-50">
|
||||
<div className="flex items-center justify-between border-b bg-white p-2">
|
||||
<div className="px-2">
|
||||
<h3 className="text-sm font-medium">Холст</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Перетаскивайте элементы для настройки их расположения
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" disabled={disabled}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить новый элемент</DialogTitle>
|
||||
<DialogDescription>
|
||||
Настройте параметры нового элемента формы.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
onClick={() => setIsAddDialogOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddElement}
|
||||
disabled={
|
||||
disabled ||
|
||||
!formData.label ||
|
||||
(formData.type !== 'standards' &&
|
||||
formData.type !== 'calibration-conditions' &&
|
||||
formData.type !== 'button-group' &&
|
||||
!formData.targetCells?.length)
|
||||
}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="flex h-full w-full flex-col bg-background">
|
||||
<ElementFormulaBar
|
||||
elementCount={elements.length}
|
||||
onAddElement={() => setIsAddDialogOpen(true)}
|
||||
onAutoArrange={handleAutoArrange}
|
||||
disabled={disabled}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
isSaving={isSaving}
|
||||
lastSaveError={lastSaveError}
|
||||
onManualSave={onManualSave}
|
||||
onClearError={onClearError}
|
||||
showGrid={layoutSettings.showGrid}
|
||||
onToggleGrid={
|
||||
onLayoutSettingsChange
|
||||
? () => {
|
||||
const newSettings = {
|
||||
...layoutSettings,
|
||||
showGrid: !layoutSettings.showGrid,
|
||||
}
|
||||
onLayoutSettingsChange(newSettings)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{editingElement && (
|
||||
<Dialog
|
||||
open={!!editingElement}
|
||||
onOpenChange={() => handleCancelEdit()}
|
||||
>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать элемент</DialogTitle>
|
||||
<DialogDescription>
|
||||
Измените параметры элемента формы.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button onClick={handleCancelEdit} variant="outline">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleUpdateElement} disabled={disabled}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<VisualLayoutEditor
|
||||
elements={elements}
|
||||
layoutSettings={layoutSettings}
|
||||
onElementUpdate={onElementUpdate}
|
||||
onElementDelete={handleDeleteAndSelect}
|
||||
onLayoutSettingsChange={() => {}} // Настройки макета только для чтения
|
||||
onElementEdit={handleEditElement}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<VisualLayoutEditor
|
||||
elements={elements}
|
||||
layoutSettings={layoutSettings}
|
||||
onElementUpdate={onElementUpdate}
|
||||
onElementDelete={handleDeleteAndSelect}
|
||||
onLayoutSettingsChange={onLayoutSettingsChange || (() => {})}
|
||||
onElementEdit={handleEditElement}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Диалог добавления элемента */}
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить новый элемент</DialogTitle>
|
||||
<DialogDescription>
|
||||
Настройте параметры нового элемента формы.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button onClick={() => setIsAddDialogOpen(false)} variant="outline">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddElement}
|
||||
disabled={
|
||||
disabled ||
|
||||
!formData.label ||
|
||||
(formData.type !== 'standards' &&
|
||||
formData.type !== 'calibration-conditions' &&
|
||||
formData.type !== 'button-group' &&
|
||||
!formData.targetCells?.length)
|
||||
}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Диалог редактирования элемента */}
|
||||
{editingElement && (
|
||||
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать элемент</DialogTitle>
|
||||
<DialogDescription>
|
||||
Измените параметры элемента формы.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button onClick={handleCancelEdit} variant="outline">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleUpdateElement} disabled={disabled}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
190
src/component/TemplateManager/ElementFormulaBar.tsx
Normal file
190
src/component/TemplateManager/ElementFormulaBar.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { Eye, EyeOff, Plus, Save, Wrench } from 'lucide-react'
|
||||
import { memo } from 'react'
|
||||
import { Button } from '../ui/button'
|
||||
|
||||
interface ElementFormulaBarProps {
|
||||
elementCount: number
|
||||
onAddElement: () => void
|
||||
onAutoArrange?: () => void
|
||||
disabled?: boolean
|
||||
// Пропсы для кнопки сохранения
|
||||
hasUnsavedChanges?: boolean
|
||||
isSaving?: boolean
|
||||
lastSaveError?: string | null
|
||||
onManualSave?: () => Promise<void>
|
||||
onClearError?: () => void
|
||||
// Пропсы для управления сеткой
|
||||
showGrid?: boolean
|
||||
onToggleGrid?: () => void
|
||||
}
|
||||
|
||||
export const ElementFormulaBar = memo(
|
||||
({
|
||||
elementCount,
|
||||
onAddElement,
|
||||
onAutoArrange,
|
||||
disabled = false,
|
||||
hasUnsavedChanges = false,
|
||||
isSaving = false,
|
||||
lastSaveError = null,
|
||||
onManualSave,
|
||||
onClearError,
|
||||
showGrid = true,
|
||||
onToggleGrid,
|
||||
}: ElementFormulaBarProps) => {
|
||||
return (
|
||||
<div className="formula-bar flex-shrink-0 border-b bg-background px-3 py-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex min-w-[100px] items-center justify-center rounded-md border bg-muted px-2 py-1">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{elementCount} элементов
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onAddElement}
|
||||
disabled={disabled}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Добавить
|
||||
</Button>
|
||||
{onAutoArrange && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onAutoArrange}
|
||||
disabled={disabled || elementCount === 0}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Wrench className="mr-1 h-3 w-3" />
|
||||
Упорядочить
|
||||
</Button>
|
||||
)}
|
||||
{onToggleGrid && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onToggleGrid}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
{showGrid ? (
|
||||
<EyeOff className="mr-1 h-3 w-3" />
|
||||
) : (
|
||||
<Eye className="mr-1 h-3 w-3" />
|
||||
)}
|
||||
{showGrid ? 'Скрыть сетку' : 'Показать'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Ошибка сохранения */}
|
||||
{lastSaveError && (
|
||||
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
|
||||
<svg
|
||||
className="h-3 w-3 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="text-xs text-destructive"
|
||||
title={lastSaveError}
|
||||
>
|
||||
Ошибка сохранения
|
||||
</span>
|
||||
{onClearError && (
|
||||
<button
|
||||
onClick={onClearError}
|
||||
className="ml-1 text-destructive/60 hover:text-destructive"
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Индикатор сохранения */}
|
||||
{isSaving && (
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<span className="ml-1 text-xs">Сохранение...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка сохранения */}
|
||||
{onManualSave && (
|
||||
<button
|
||||
onClick={onManualSave}
|
||||
disabled={isSaving}
|
||||
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
|
||||
hasUnsavedChanges
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: lastSaveError
|
||||
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
} disabled:opacity-50`}
|
||||
title={
|
||||
lastSaveError
|
||||
? 'Повторить сохранение'
|
||||
: hasUnsavedChanges
|
||||
? 'Есть несохраненные изменения'
|
||||
: 'Все изменения сохранены'
|
||||
}
|
||||
>
|
||||
<Save className="h-3 w-3" />
|
||||
{lastSaveError
|
||||
? 'Повторить'
|
||||
: hasUnsavedChanges
|
||||
? 'Сохранить'
|
||||
: 'Сохранено'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
128
src/component/TemplateManager/SaveButton.tsx
Normal file
128
src/component/TemplateManager/SaveButton.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
interface SaveButtonProps {
|
||||
hasUnsavedChanges: boolean
|
||||
isSaving: boolean
|
||||
lastSaveError: string | null
|
||||
onManualSave: () => Promise<void>
|
||||
onClearError: () => void
|
||||
}
|
||||
|
||||
export const SaveButton = memo(
|
||||
({
|
||||
hasUnsavedChanges,
|
||||
isSaving,
|
||||
lastSaveError,
|
||||
onManualSave,
|
||||
onClearError,
|
||||
}: SaveButtonProps) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Ошибка сохранения */}
|
||||
{lastSaveError && (
|
||||
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
|
||||
<svg
|
||||
className="h-3 w-3 text-destructive"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-xs text-destructive" title={lastSaveError}>
|
||||
Ошибка сохранения
|
||||
</span>
|
||||
<button
|
||||
onClick={onClearError}
|
||||
className="ml-1 text-destructive/60 hover:text-destructive"
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Индикатор сохранения */}
|
||||
{isSaving && (
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<svg
|
||||
className="h-3.5 w-3.5 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<span className="ml-1 text-xs">Сохранение...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка сохранения */}
|
||||
<button
|
||||
onClick={onManualSave}
|
||||
disabled={isSaving}
|
||||
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
|
||||
hasUnsavedChanges
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: lastSaveError
|
||||
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
} disabled:opacity-50`}
|
||||
title={
|
||||
lastSaveError
|
||||
? 'Повторить сохранение'
|
||||
: hasUnsavedChanges
|
||||
? 'Есть несохраненные изменения'
|
||||
: 'Все изменения сохранены'
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
{lastSaveError
|
||||
? 'Повторить'
|
||||
: hasUnsavedChanges
|
||||
? 'Сохранить'
|
||||
: 'Сохранено'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
Droplets,
|
||||
Edit,
|
||||
Edit3,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Gauge,
|
||||
Grid,
|
||||
Move,
|
||||
@@ -460,21 +458,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
||||
}, [layoutSettings, onLayoutSettingsChange])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gray-50">
|
||||
<div className="flex shrink-0 items-center justify-between border-b bg-white p-2">
|
||||
<span className="px-2 text-sm text-gray-600">
|
||||
{elements.length} элементов на холсте
|
||||
</span>
|
||||
<Button variant="outline" size="sm" onClick={toggleGrid}>
|
||||
{layoutSettings.showGrid ? (
|
||||
<EyeOff className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{layoutSettings.showGrid ? 'Скрыть сетку' : 'Показать сетку'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
<div
|
||||
className="relative flex-1 overflow-auto"
|
||||
onClick={handleCanvasClick}
|
||||
|
||||
Reference in New Issue
Block a user