- emit API/network failures through a shared toast event bridge - remove per-page inline error notifications to avoid duplicate error UI - dedupe repeated toast messages to keep feedback readable
144 lines
4.4 KiB
TypeScript
144 lines
4.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { KeyRound, X, Save } from 'lucide-react';
|
|
import { credentials } from '../../api';
|
|
import type { Credential } from '../../api';
|
|
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
|
|
import styles from '../Page.module.css';
|
|
|
|
export function EditCredentialPage() {
|
|
const { t } = useTranslation();
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
|
|
const [credential, setCredential] = useState<Credential | null>(null);
|
|
const [name, setName] = useState('');
|
|
const [data, setData] = useState('');
|
|
const [nameError, setNameError] = useState('');
|
|
const [dataError, setDataError] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
credentials
|
|
.get(id)
|
|
.then((c) => {
|
|
setCredential(c);
|
|
setName(c.name);
|
|
setData(c.data ?? '');
|
|
})
|
|
.catch((err: Error) => setError(err.message))
|
|
.finally(() => setLoading(false));
|
|
}, [id]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
let valid = true;
|
|
if (!name.trim()) {
|
|
setNameError(t('credentials.form_name_required'));
|
|
valid = false;
|
|
}
|
|
if (data.trim()) {
|
|
try {
|
|
JSON.parse(data.trim());
|
|
} catch {
|
|
setDataError(t('credentials.form_data_invalid_json'));
|
|
valid = false;
|
|
}
|
|
}
|
|
if (!valid) return;
|
|
setSaving(true);
|
|
setError(null);
|
|
try {
|
|
await credentials.update(id!, {
|
|
name: name.trim(),
|
|
data: data.trim() || null,
|
|
});
|
|
toast.success('Credential updated');
|
|
navigate(`/credentials/${id}`);
|
|
} catch (err) {
|
|
const message = (err as Error).message;
|
|
setError(message);
|
|
toast.error(message);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className={styles.pageToolbar}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: t('credentials.title'), icon: <KeyRound size={14} />, onClick: () => navigate('/credentials') },
|
|
{
|
|
label: credential?.name ?? `#${id}`,
|
|
onClick: () => navigate(`/credentials/${id}`),
|
|
},
|
|
{ label: t('credentials.edit_title') },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
{loading && <p className={styles.muted}>{t('credentials.loading')}</p>}
|
|
|
|
{!loading && credential && (
|
|
<form onSubmit={handleSubmit} noValidate>
|
|
<Card className={styles.formCardFull}>
|
|
<div className={styles.formFields}>
|
|
<Input
|
|
label={t('credentials.form_name')}
|
|
placeholder={t('credentials.form_name_placeholder')}
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
setNameError('');
|
|
}}
|
|
error={nameError || undefined}
|
|
required
|
|
autoFocus
|
|
/>
|
|
<div className={styles.formField}>
|
|
<label className={styles.fieldLabel}>
|
|
{t('credentials.form_data')}
|
|
<span className={styles.requiredMark}> *</span>
|
|
</label>
|
|
<CodeEditor
|
|
language="json"
|
|
value={data}
|
|
onChange={(v) => {
|
|
setData(v);
|
|
setDataError('');
|
|
}}
|
|
rows={6}
|
|
error={!!dataError}
|
|
/>
|
|
{dataError && <span className={styles.fieldError}>{dataError}</span>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.formActions}>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() => navigate(`/credentials/${id}`)}
|
|
>
|
|
<X size={14} />
|
|
{t('credentials.action_cancel')}
|
|
</Button>
|
|
<Button type="submit" loading={saving}>
|
|
<Save size={14} />
|
|
{t('credentials.action_update')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</form>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|