mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: name the template, advanced search and admin server fields
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
import { Plus, Trash2, ChevronDown, ChevronRight } from '@/components/icons';
|
||||
import type { JmapServerEntry } from '@/lib/admin/jmap-servers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -101,6 +101,9 @@ const fieldClass = cn(inputClass, 'w-full');
|
||||
|
||||
export function JmapServersSection({ value, source, onChange, onRevert }: Props) {
|
||||
const [drafts, setDrafts] = useState<RowDraft[]>(() => value.map(entryToDraft));
|
||||
// Ids that tie each row's labels to its fields.
|
||||
const idBase = useId();
|
||||
const labelFor = (row: number) => (key: string) => `${idBase}-${row}-${key}`;
|
||||
const lastEmittedRef = useRef(value);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -166,8 +169,9 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
<div key={i} className="rounded-md border border-border bg-muted/20 p-3 space-y-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-12 gap-2 items-start">
|
||||
<div className="sm:col-span-3">
|
||||
<label className={fieldLabelClass}>ID</label>
|
||||
<label htmlFor={labelFor(i)("id")} className={fieldLabelClass}>ID</label>
|
||||
<input
|
||||
id={labelFor(i)("id")}
|
||||
type="text"
|
||||
value={d.id}
|
||||
onChange={(e) => update(i, { id: e.target.value })}
|
||||
@@ -177,8 +181,9 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
{isDuplicate && <p className="text-[10px] text-destructive mt-0.5">Duplicate id</p>}
|
||||
</div>
|
||||
<div className="sm:col-span-4">
|
||||
<label className={fieldLabelClass}>Label</label>
|
||||
<label htmlFor={labelFor(i)("label")} className={fieldLabelClass}>Label</label>
|
||||
<input
|
||||
id={labelFor(i)("label")}
|
||||
type="text"
|
||||
value={d.label}
|
||||
onChange={(e) => update(i, { label: e.target.value })}
|
||||
@@ -187,9 +192,10 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-5">
|
||||
<label className={fieldLabelClass}>JMAP URL</label>
|
||||
<label htmlFor={labelFor(i)("url")} className={fieldLabelClass}>JMAP URL</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id={labelFor(i)("url")}
|
||||
type="url"
|
||||
value={d.url}
|
||||
onChange={(e) => update(i, { url: e.target.value })}
|
||||
@@ -209,10 +215,11 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={fieldLabelClass}>
|
||||
<label htmlFor={labelFor(i)("domains")} className={fieldLabelClass}>
|
||||
Email domains (comma-separated, used for auto-pick)
|
||||
</label>
|
||||
<input
|
||||
id={labelFor(i)("domains")}
|
||||
type="text"
|
||||
value={d.domains}
|
||||
onChange={(e) => update(i, { domains: e.target.value })}
|
||||
@@ -231,8 +238,9 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
{d.oauthExpanded && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 ps-4 border-s border-border">
|
||||
<div>
|
||||
<label className={fieldLabelClass}>OAuth Client ID</label>
|
||||
<label htmlFor={labelFor(i)("oauth-client-id")} className={fieldLabelClass}>OAuth Client ID</label>
|
||||
<input
|
||||
id={labelFor(i)("oauth-client-id")}
|
||||
type="text"
|
||||
value={d.oauthClientId}
|
||||
onChange={(e) => update(i, { oauthClientId: e.target.value })}
|
||||
@@ -240,8 +248,9 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={fieldLabelClass}>OAuth Issuer URL</label>
|
||||
<label htmlFor={labelFor(i)("oauth-issuer")} className={fieldLabelClass}>OAuth Issuer URL</label>
|
||||
<input
|
||||
id={labelFor(i)("oauth-issuer")}
|
||||
type="url"
|
||||
value={d.oauthIssuerUrl}
|
||||
onChange={(e) => update(i, { oauthIssuerUrl: e.target.value })}
|
||||
@@ -250,8 +259,9 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={fieldLabelClass}>OAuth Client Secret</label>
|
||||
<label htmlFor={labelFor(i)("oauth-secret")} className={fieldLabelClass}>OAuth Client Secret</label>
|
||||
<input
|
||||
id={labelFor(i)("oauth-secret")}
|
||||
type="password"
|
||||
value={d.oauthClientSecret}
|
||||
onChange={(e) => update(i, { oauthClientSecret: e.target.value })}
|
||||
@@ -260,8 +270,9 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-3">
|
||||
<label className={fieldLabelClass}>Sign-in button label</label>
|
||||
<label htmlFor={labelFor(i)("oauth-button")} className={fieldLabelClass}>Sign-in button label</label>
|
||||
<input
|
||||
id={labelFor(i)("oauth-button")}
|
||||
type="text"
|
||||
value={d.oauthButtonLabel}
|
||||
onChange={(e) => update(i, { oauthButtonLabel: e.target.value })}
|
||||
@@ -274,8 +285,9 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
|
||||
</p>
|
||||
</div>
|
||||
<div className="sm:col-span-3">
|
||||
<label className={fieldLabelClass}>Provider logout URL</label>
|
||||
<label htmlFor={labelFor(i)("logout-url")} className={fieldLabelClass}>Provider logout URL</label>
|
||||
<input
|
||||
id={labelFor(i)("logout-url")}
|
||||
type="url"
|
||||
value={d.oauthEndSessionUrl}
|
||||
onChange={(e) => update(i, { oauthEndSessionUrl: e.target.value })}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import { unnamedFields } from '@/lib/__tests__/helpers/form-labels';
|
||||
import { ContactForm } from '@/components/contacts/contact-form';
|
||||
import type { ContactCard } from '@/lib/jmap/types';
|
||||
import { TemplateForm } from '@/components/templates/template-form';
|
||||
import { MvSearchForm } from '@/components/search/mv-search-form';
|
||||
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
||||
import { JmapServersSection } from '@/app/(main)/admin/_pages/_jmap-servers-section';
|
||||
|
||||
// Every field of the big forms has a name a screen reader reads out (WCAG
|
||||
// 1.3.1, 3.3.2, 4.1.2): a <label> tied to it or an aria-label, never just
|
||||
@@ -36,4 +40,27 @@ describe('form fields have labels', () => {
|
||||
expect(document.querySelectorAll('input, select, textarea').length).toBeGreaterThan(20);
|
||||
expect(unnamedFields()).toEqual([]);
|
||||
});
|
||||
|
||||
it('template form', () => {
|
||||
render(<TemplateForm onSave={vi.fn()} onCancel={vi.fn()} />);
|
||||
expect(unnamedFields()).toEqual([]);
|
||||
});
|
||||
|
||||
it('advanced search form', () => {
|
||||
render(<MvSearchForm filters={DEFAULT_SEARCH_FILTERS} onChange={vi.fn()} onSearch={vi.fn()} onClear={vi.fn()} />);
|
||||
expect(document.querySelectorAll('input').length).toBeGreaterThan(5);
|
||||
expect(unnamedFields()).toEqual([]);
|
||||
});
|
||||
|
||||
it('admin server list, with two servers', () => {
|
||||
render(
|
||||
<JmapServersSection
|
||||
value={[{ id: 'a', label: 'A', url: 'https://a.example' }, { id: 'b', label: 'B', url: 'https://b.example' }] as never}
|
||||
onChange={vi.fn()}
|
||||
onRevert={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(document.querySelectorAll('input').length).toBeGreaterThanOrEqual(8);
|
||||
expect(unnamedFields()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1222,7 +1222,8 @@ export function EventModal({
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label htmlFor={labelFor("title")} className="text-sm font-medium mb-1 block">{t("form.title")}</label>
|
||||
<Input id={labelFor("title")}
|
||||
<Input
|
||||
id={labelFor("title")}
|
||||
ref={titleRef}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
@@ -1233,7 +1234,8 @@ export function EventModal({
|
||||
|
||||
<div>
|
||||
<label htmlFor={labelFor("description")} className="text-sm font-medium mb-1 block">{t("form.description")}</label>
|
||||
<textarea id={labelFor("description")}
|
||||
<textarea
|
||||
id={labelFor("description")}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t("form.description")}
|
||||
@@ -1245,7 +1247,8 @@ export function EventModal({
|
||||
|
||||
<div>
|
||||
<label htmlFor={labelFor("location")} className="text-sm font-medium mb-1 block">{t("form.location")}</label>
|
||||
<Input id={labelFor("location")}
|
||||
<Input
|
||||
id={labelFor("location")}
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
placeholder={t("form.location")}
|
||||
@@ -1344,7 +1347,8 @@ export function EventModal({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label htmlFor={labelFor("start_date")} className="text-sm font-medium mb-1 block">{t("form.start_date")}</label>
|
||||
<input id={labelFor("start_date")}
|
||||
<input
|
||||
id={labelFor("start_date")}
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => handleStartDateChange(e.target.value)}
|
||||
@@ -1354,7 +1358,8 @@ export function EventModal({
|
||||
{!allDay && (
|
||||
<div>
|
||||
<label htmlFor={labelFor("start_time")} className="text-sm font-medium mb-1 block">{t("form.start_time")}</label>
|
||||
<input id={labelFor("start_time")}
|
||||
<input
|
||||
id={labelFor("start_time")}
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={(e) => handleStartTimeChange(e.target.value)}
|
||||
@@ -1364,7 +1369,8 @@ export function EventModal({
|
||||
)}
|
||||
<div>
|
||||
<label htmlFor={labelFor("end_date")} className="text-sm font-medium mb-1 block">{t("form.end_date")}</label>
|
||||
<input id={labelFor("end_date")}
|
||||
<input
|
||||
id={labelFor("end_date")}
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
@@ -1374,7 +1380,8 @@ export function EventModal({
|
||||
{!allDay && (
|
||||
<div>
|
||||
<label htmlFor={labelFor("end_time")} className="text-sm font-medium mb-1 block">{t("form.end_time")}</label>
|
||||
<input id={labelFor("end_time")}
|
||||
<input
|
||||
id={labelFor("end_time")}
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
@@ -1407,7 +1414,8 @@ export function EventModal({
|
||||
{selectableCalendars.length > 1 && (
|
||||
<div>
|
||||
<label htmlFor={labelFor("calendar_select")} className="text-sm font-medium mb-1 block">{t("form.calendar_select")}</label>
|
||||
<select id={labelFor("calendar_select")}
|
||||
<select
|
||||
id={labelFor("calendar_select")}
|
||||
value={calendarId}
|
||||
onChange={(e) => setCalendarId(e.target.value)}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
@@ -1424,7 +1432,8 @@ export function EventModal({
|
||||
<div>
|
||||
<label htmlFor={labelFor("title-1")} className="text-sm font-medium mb-1 block">{t("recurrence.title")}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<select id={labelFor("title-1")}
|
||||
<select
|
||||
id={labelFor("title-1")}
|
||||
value={recurrence}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as RecurrenceOption;
|
||||
|
||||
@@ -65,6 +65,7 @@ export function MvSearchForm({ filters, onChange, onSearch, onClear, folderSelec
|
||||
inputMode="numeric"
|
||||
className={FIELD}
|
||||
placeholder={t("size_min")}
|
||||
aria-label={t("size_min")}
|
||||
value={filters.minSizeKb}
|
||||
onChange={(e) => onChange({ minSizeKb: e.target.value })}
|
||||
/>
|
||||
@@ -74,6 +75,7 @@ export function MvSearchForm({ filters, onChange, onSearch, onClear, folderSelec
|
||||
inputMode="numeric"
|
||||
className={FIELD}
|
||||
placeholder={t("size_max")}
|
||||
aria-label={t("size_max")}
|
||||
value={filters.maxSizeKb}
|
||||
onChange={(e) => onChange({ maxSizeKb: e.target.value })}
|
||||
/>
|
||||
|
||||
@@ -87,7 +87,8 @@ function PasswordChangeSection() {
|
||||
<div>
|
||||
<label htmlFor={labelFor("current")} className="text-xs text-muted-foreground mb-1 block">{t('password.current')}</label>
|
||||
<div className="relative">
|
||||
<Input id={labelFor("current")}
|
||||
<Input
|
||||
id={labelFor("current")}
|
||||
type={showCurrent ? 'text' : 'password'}
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
@@ -108,7 +109,8 @@ function PasswordChangeSection() {
|
||||
{otpEnabled && (
|
||||
<div>
|
||||
<label htmlFor={labelFor("verification_code")} className="text-xs text-muted-foreground mb-1 block">{t('totp.verification_code')}</label>
|
||||
<Input id={labelFor("verification_code")}
|
||||
<Input
|
||||
id={labelFor("verification_code")}
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value)}
|
||||
required
|
||||
@@ -121,7 +123,8 @@ function PasswordChangeSection() {
|
||||
<div>
|
||||
<label htmlFor={labelFor("new")} className="text-xs text-muted-foreground mb-1 block">{t('password.new')}</label>
|
||||
<div className="relative">
|
||||
<Input id={labelFor("new")}
|
||||
<Input
|
||||
id={labelFor("new")}
|
||||
type={showNew ? 'text' : 'password'}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
@@ -142,7 +145,8 @@ function PasswordChangeSection() {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={labelFor("confirm")} className="text-xs text-muted-foreground mb-1 block">{t('password.confirm')}</label>
|
||||
<Input id={labelFor("confirm")}
|
||||
<Input
|
||||
id={labelFor("confirm")}
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
@@ -567,7 +571,8 @@ function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRem
|
||||
<form onSubmit={handleAdd} className="p-3 bg-muted rounded-md space-y-2">
|
||||
<div>
|
||||
<label htmlFor={labelFor("field6")} className="text-xs text-muted-foreground mb-1 block">{tk('name_label')}</label>
|
||||
<Input id={labelFor("field6")}
|
||||
<Input
|
||||
id={labelFor("field6")}
|
||||
value={newDescription}
|
||||
onChange={(e) => setNewDescription(e.target.value)}
|
||||
placeholder={tk('name_placeholder')}
|
||||
@@ -580,7 +585,8 @@ function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRem
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={labelFor("allowed_ips_label")} className="text-xs text-muted-foreground mb-1 block">{t('app_passwords.allowed_ips_label')}</label>
|
||||
<textarea id={labelFor("allowed_ips_label")}
|
||||
<textarea
|
||||
id={labelFor("allowed_ips_label")}
|
||||
value={allowedIpsRaw}
|
||||
onChange={(e) => setAllowedIpsRaw(e.target.value)}
|
||||
placeholder={t('app_passwords.allowed_ips_placeholder')}
|
||||
@@ -779,7 +785,8 @@ function PublicKeysSection() {
|
||||
<form onSubmit={handleAdd} className="p-3 bg-muted rounded-md space-y-2">
|
||||
<div>
|
||||
<label htmlFor={labelFor("field9")} className="text-xs text-muted-foreground mb-1 block">{tk('name_label')}</label>
|
||||
<Input id={labelFor("field9")}
|
||||
<Input
|
||||
id={labelFor("field9")}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={tk('name_placeholder')}
|
||||
@@ -788,7 +795,8 @@ function PublicKeysSection() {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={labelFor("field10")} className="text-xs text-muted-foreground mb-1 block">{tk('key_label')}</label>
|
||||
<textarea id={labelFor("field10")}
|
||||
<textarea
|
||||
id={labelFor("field10")}
|
||||
value={publicKey}
|
||||
onChange={(e) => setPublicKey(e.target.value)}
|
||||
placeholder={tk('key_placeholder')}
|
||||
@@ -815,7 +823,8 @@ function PublicKeysSection() {
|
||||
|
||||
<div>
|
||||
<label htmlFor={labelFor("algorithm_label")} className="text-xs text-muted-foreground mb-1 block">{t('encryption.algorithm_label')}</label>
|
||||
<select id={labelFor("algorithm_label")}
|
||||
<select
|
||||
id={labelFor("algorithm_label")}
|
||||
value={selectedAlgorithm}
|
||||
onChange={(e) => setSelectedAlgorithm(e.target.value as 'Aes128' | 'Aes256')}
|
||||
className="w-full text-xs px-3 py-1.5 rounded-md border border-border bg-background focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { useState, useMemo, useEffect, useId, useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -43,6 +43,9 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
const t = useTranslations('templates');
|
||||
const tSettings = useTranslations('settings.templates');
|
||||
const tComposer = useTranslations('email_composer');
|
||||
// Ids that tie each visible label to its field.
|
||||
const labelIdBase = useId();
|
||||
const labelFor = (key: string) => `${labelIdBase}-${key}`;
|
||||
|
||||
const { identities } = useAuthStore();
|
||||
const browserList = useTemplateStore((s) => s.templates);
|
||||
@@ -139,24 +142,28 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{tSettings('name')}</label>
|
||||
<label htmlFor={labelFor("name")} className="text-sm font-medium text-foreground">{tSettings('name')}</label>
|
||||
<Input
|
||||
id={labelFor("name")}
|
||||
value={name}
|
||||
onChange={(e) => { setName(e.target.value); setNameError(null); }}
|
||||
placeholder={tSettings('name_placeholder')}
|
||||
className={cn('mt-1', nameError && 'border-destructive')}
|
||||
aria-invalid={nameError ? true : undefined}
|
||||
aria-describedby={nameError ? labelFor("name-error") : undefined}
|
||||
autoFocus
|
||||
/>
|
||||
{nameError && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||
<p id={labelFor("name-error")} className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||
{tSettings(`validation.${nameError}`)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{tSettings('category')}</label>
|
||||
<label htmlFor={labelFor("category")} className="text-sm font-medium text-foreground">{tSettings('category')}</label>
|
||||
<Input
|
||||
id={labelFor("category")}
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
placeholder={tSettings('category_placeholder')}
|
||||
@@ -174,10 +181,11 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">{tSettings('subject')}</label>
|
||||
<label htmlFor={labelFor("subject")} className="text-sm font-medium text-foreground">{tSettings('subject')}</label>
|
||||
{placeholderButton('subject')}
|
||||
</div>
|
||||
<Input
|
||||
id={labelFor("subject")}
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder={tSettings('subject_placeholder')}
|
||||
@@ -187,10 +195,11 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">{tSettings('body')}</label>
|
||||
<label htmlFor={labelFor("body")} className="text-sm font-medium text-foreground">{tSettings('body')}</label>
|
||||
{placeholderButton('body')}
|
||||
</div>
|
||||
<textarea
|
||||
id={labelFor("body")}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
placeholder={tSettings('body_placeholder')}
|
||||
@@ -209,8 +218,9 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{tComposer('to')}</label>
|
||||
<label htmlFor={labelFor("to")} className="text-sm font-medium text-foreground">{tComposer('to')}</label>
|
||||
<Input
|
||||
id={labelFor("to")}
|
||||
value={toRecipients}
|
||||
onChange={(e) => setToRecipients(e.target.value)}
|
||||
placeholder={tSettings('recipients_placeholder')}
|
||||
@@ -218,8 +228,9 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{tComposer('cc')}</label>
|
||||
<label htmlFor={labelFor("cc")} className="text-sm font-medium text-foreground">{tComposer('cc')}</label>
|
||||
<Input
|
||||
id={labelFor("cc")}
|
||||
value={ccRecipients}
|
||||
onChange={(e) => setCcRecipients(e.target.value)}
|
||||
placeholder={tSettings('recipients_placeholder')}
|
||||
@@ -227,8 +238,9 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{tComposer('bcc')}</label>
|
||||
<label htmlFor={labelFor("bcc")} className="text-sm font-medium text-foreground">{tComposer('bcc')}</label>
|
||||
<Input
|
||||
id={labelFor("bcc")}
|
||||
value={bccRecipients}
|
||||
onChange={(e) => setBccRecipients(e.target.value)}
|
||||
placeholder={tSettings('recipients_placeholder')}
|
||||
@@ -239,8 +251,9 @@ export function TemplateForm({ template, initialData, onSave, onCancel, shared =
|
||||
|
||||
{!shared && identities.length > 1 && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{tSettings('identity')}</label>
|
||||
<label htmlFor={labelFor("identity")} className="text-sm font-medium text-foreground">{tSettings('identity')}</label>
|
||||
<select
|
||||
id={labelFor("identity")}
|
||||
value={identityId}
|
||||
onChange={(e) => setIdentityId(e.target.value)}
|
||||
className="mt-1 w-full px-3 py-2 text-sm rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
|
||||
Reference in New Issue
Block a user