e9b8936091
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.
101 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
} |