Files
leocrm/frontend/src/components/mail/TemplatePicker.tsx
T
Agent Zero e9b8936091 fix: prevent [object Object] rendering in 25 frontend components
Replace direct {error} JSX rendering with typeof check + .message fallback.
When error is an object (not a string), React showed [object Object].
Now renders error.message or fallback string.
2026-08-21 11:36:55 +02:00

101 lines
3.3 KiB
TypeScript

/**
* Template picker dropdown for compose modal.
* Lists available templates and inserts selected template body into editor.
*/
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { fetchTemplates, substituteTemplate, type MailTemplate } from '@/api/mail';
import { EmptyState } from '@/components/ui/EmptyState';
import { Loader2 } from 'lucide-react';
export interface TemplatePickerProps {
onSelect: (body: string, subject: string) => void;
}
export function TemplatePicker({ onSelect }: TemplatePickerProps) {
const { t } = useTranslation();
const [templates, setTemplates] = useState<MailTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchTemplates()
.then((data) => {
if (!cancelled) {
setTemplates(data);
setError(null);
}
})
.catch((err) => {
if (!cancelled) {
setError(err instanceof Error ? err.message : String(err));
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, []);
const handleSelect = (template: MailTemplate) => {
substituteTemplate({
template_id: template.id,
variables: {},
})
.then((result) => {
onSelect(result.body, result.subject || template.subject);
})
.catch(() => {
onSelect(template.body, template.subject);
});
};
if (loading) {
return (
<div className="flex items-center justify-center py-4" data-testid="template-picker-loading">
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div>
);
}
if (error) {
return (
<div className="p-4 bg-danger-50 border border-danger-200 rounded-md" role="alert" data-testid="template-picker-error">
<p className="text-sm text-danger-700">{typeof error === "string" ? error : (error as any)?.message || "Ein Fehler ist aufgetreten"}</p>
</div>
);
}
if (templates.length === 0) {
return (
<EmptyState title={t('mail.noTemplates')} description={t('mail.noTemplatesDesc')} data-testid="template-picker-empty" />
);
}
return (
<div className="border border-secondary-200 rounded-md p-2 bg-secondary-50" data-testid="template-picker">
<p className="text-sm font-medium text-secondary-700 mb-2">{t('mail.selectTemplate')}</p>
<ul className="space-y-1" role="list">
{templates.map((template) => (
<li key={template.id}>
<button
onClick={() => handleSelect(template)}
className="w-full text-left px-3 py-2 rounded-md hover:bg-white text-sm min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
data-testid={`template-option-${template.id}`}
>
<span className="font-medium text-secondary-800">{template.name}</span>
{template.variables.length > 0 && (
<span className="ml-2 text-xs text-secondary-400">({template.variables.join(', ')})</span>
)}
</button>
</li>
))}
</ul>
</div>
);
}