Files
bakery-ia/frontend/src/components/domain/unified-wizard/shared/JsonEditor.tsx
2025-11-16 22:13:52 +01:00

159 lines
4.3 KiB
TypeScript

/**
* JsonEditor - Better UX for JSONB fields
*
* Provides syntax highlighting, validation, and error messages
* instead of raw textarea for JSON editing.
*
* Used by: QualityTemplateWizard, CustomerOrderWizard
*/
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, CheckCircle } from 'lucide-react';
interface JsonEditorProps {
value: any;
onChange: (value: any) => void;
label?: string;
placeholder?: string;
required?: boolean;
rows?: number;
}
export const JsonEditor: React.FC<JsonEditorProps> = ({
value,
onChange,
label,
placeholder,
required = false,
rows = 6,
}) => {
const { t } = useTranslation('wizards');
const [jsonString, setJsonString] = useState('');
const [error, setError] = useState<string | null>(null);
const [isValid, setIsValid] = useState(true);
// Initialize from value
useEffect(() => {
try {
if (value === null || value === undefined || value === '') {
setJsonString('');
} else if (typeof value === 'string') {
// Try to parse to validate
JSON.parse(value);
setJsonString(value);
} else {
setJsonString(JSON.stringify(value, null, 2));
}
setIsValid(true);
setError(null);
} catch (e) {
setJsonString(typeof value === 'string' ? value : '');
setIsValid(false);
}
}, []);
const handleChange = (newValue: string) => {
setJsonString(newValue);
// Validate JSON
if (newValue.trim() === '') {
setError(null);
setIsValid(true);
onChange(null);
return;
}
try {
const parsed = JSON.parse(newValue);
setError(null);
setIsValid(true);
onChange(parsed);
} catch (e) {
setError(e instanceof Error ? e.message : 'Invalid JSON');
setIsValid(false);
// Don't update parent with invalid JSON
}
};
const formatJson = () => {
try {
const parsed = JSON.parse(jsonString);
const formatted = JSON.stringify(parsed, null, 2);
setJsonString(formatted);
setError(null);
setIsValid(true);
onChange(parsed);
} catch (e) {
setError(e instanceof Error ? e.message : 'Invalid JSON');
setIsValid(false);
}
};
return (
<div>
{label && (
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{label}
{required && ' *'}
</label>
)}
<div className="relative">
<textarea
value={jsonString}
onChange={(e) => handleChange(e.target.value)}
placeholder={placeholder || '{\n "key": "value"\n}'}
rows={rows}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 font-mono text-sm ${
isValid
? 'border-[var(--border-secondary)] focus:ring-[var(--color-primary)]'
: 'border-red-500 focus:ring-red-500'
} bg-[var(--bg-primary)] text-[var(--text-primary)]`}
/>
{/* Validation indicator */}
<div className="absolute top-2 right-2">
{jsonString && isValid && (
<CheckCircle className="w-5 h-5 text-green-500" />
)}
{jsonString && !isValid && (
<AlertCircle className="w-5 h-5 text-red-500" />
)}
</div>
</div>
{/* Error message */}
{error && (
<div className="mt-2 flex items-start gap-2 text-sm text-red-600">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>{error}</span>
</div>
)}
{/* Format button */}
{jsonString && (
<div className="mt-2">
<button
type="button"
onClick={formatJson}
className="text-sm px-3 py-1 rounded transition-colors"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)',
border: '1px solid var(--border-secondary)',
}}
>
{t('common.actions.formatJson', 'Format JSON')}
</button>
</div>
)}
{/* Help text */}
<p className="mt-2 text-xs" style={{ color: 'var(--text-tertiary)' }}>
{t('common.hints.jsonEditor', 'Enter valid JSON. Use the format button to auto-format.')}
</p>
</div>
);
};