- add CodeBlock for read-only syntax-highlighted display (hljs, js/json) - add CodeEditor wrapping Monaco editor with theme sync, focus state, and error state - replace code textareas in snippet, credential, and step pages with CodeEditor - replace code <pre> blocks in snippet and credential detail pages with CodeBlock - make form cards full-width on pages with code editors - add resize:vertical support to CodeEditor wrapper with automaticLayout
107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { credentials } from '../../api';
|
|
import { Breadcrumbs, Button, Card, Input, CodeEditor } from '../../ui';
|
|
import styles from '../Page.module.css';
|
|
|
|
export function CreateCredentialPage() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
|
|
const [name, setName] = useState('');
|
|
const [data, setData] = useState('');
|
|
const [nameError, setNameError] = useState('');
|
|
const [dataError, setDataError] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
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 {
|
|
const credential = await credentials.create(name.trim(), data.trim() || undefined);
|
|
navigate(`/credentials/${credential.id}`);
|
|
} catch (err) {
|
|
setError((err as Error).message);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div className={styles.pageToolbar}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: t('credentials.title'), onClick: () => navigate('/credentials') },
|
|
{ label: t('credentials.create_title') },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
{error && <p className={styles.error}>{error}</p>}
|
|
|
|
<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')}>
|
|
{t('credentials.action_cancel')}
|
|
</Button>
|
|
<Button type="submit" loading={saving}>
|
|
{t('credentials.action_save')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|