feat(03-02): create FieldRenderer and AzureAuthToggle

- FieldRenderer: registry-driven input renderer (text, password, select, hidden provider)
- AzureAuthToggle: segmented SAS/Key toggle, defaults to SAS, both fields always registered via CSS show/hide
- Both fields preserved on toggle (no conditional rendering that would lose react-hook-form state)
This commit is contained in:
2026-03-27 09:29:51 +01:00
parent cec45e8f17
commit 1c9c337d3f
2 changed files with 152 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
import type { UseFormRegister, FieldError } from 'react-hook-form';
import type { FieldDef } from '../../schemas/registry';
import { PasswordField } from './PasswordField';
interface FieldRendererProps {
field: FieldDef;
register: UseFormRegister<any>;
error?: FieldError;
}
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
// provider field: single-option select — hide from UI, auto-register with default value
if (field.key === 'provider' && field.options?.length === 1) {
return (
<input
type="hidden"
value={field.options[0].value}
{...register(field.key)}
/>
);
}
if (field.inputType === 'password') {
return (
<PasswordField
id={field.key}
label={field.label}
error={error}
registration={register(field.key)}
placeholder={field.placeholder}
helpText={field.helpText}
/>
);
}
if (field.inputType === 'select' && field.options) {
return (
<div className="flex flex-col gap-1">
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
{field.label}
{field.required && <span className="ml-1 text-red-500">*</span>}
</label>
<select
id={field.key}
className={[
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
].join(' ')}
{...register(field.key)}
>
{field.options.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
{error && <p className="text-xs text-red-600">{error.message}</p>}
</div>
);
}
// text (default)
return (
<div className="flex flex-col gap-1">
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
{field.label}
{field.required && <span className="ml-1 text-red-500">*</span>}
</label>
<input
id={field.key}
type="text"
placeholder={field.placeholder}
className={[
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
].join(' ')}
{...register(field.key)}
/>
{field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
{error && <p className="text-xs text-red-600">{error.message}</p>}
</div>
);
}
+70
View File
@@ -0,0 +1,70 @@
import { useState } from 'react';
import type { UseFormRegister, FieldError } from 'react-hook-form';
import { PasswordField } from '../ui/PasswordField';
type AuthMethod = 'sas' | 'key';
interface AzureAuthToggleProps {
register: UseFormRegister<any>;
errors: {
key?: FieldError;
sas_url?: FieldError;
};
}
export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
const [authMethod, setAuthMethod] = useState<AuthMethod>('sas');
return (
<div className="flex flex-col gap-3">
{/* Segmented control */}
<div className="flex rounded-md border border-gray-300 overflow-hidden">
<button
type="button"
onClick={() => setAuthMethod('sas')}
className={[
'flex-1 py-1.5 text-sm font-medium transition-colors',
authMethod === 'sas'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50',
].join(' ')}
>
SAS URL
</button>
<button
type="button"
onClick={() => setAuthMethod('key')}
className={[
'flex-1 py-1.5 text-sm font-medium transition-colors',
authMethod === 'key'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50',
].join(' ')}
>
Access Key
</button>
</div>
{/* Both fields always registered — only active one visible via CSS */}
<div className={authMethod === 'sas' ? 'block' : 'hidden'}>
<PasswordField
id="sas_url"
label="SAS URL"
error={errors.sas_url}
registration={register('sas_url')}
placeholder="https://mystorageaccount.blob.core.windows.net/?sv=..."
helpText="Full SAS URL including account and container"
/>
</div>
<div className={authMethod === 'key' ? 'block' : 'hidden'}>
<PasswordField
id="key"
label="Access Key"
error={errors.key}
registration={register('key')}
helpText="Base64-encoded storage account key"
/>
</div>
</div>
);
}