feat(07-02): add tooltipText to registry fields + render ⓘ in FieldRenderer and PasswordField
- Populate tooltipText for azureblob.sas_url, azureblob.key, and onedrive.token in registry - Add tooltipText prop and showTooltip state to PasswordField with toggle button + panel - Add showTooltip state to FieldRenderer; pass tooltipText to PasswordField for password branch - Add ⓘ button + panel for text/select branches in FieldRenderer - Use sr-only span for accessible name to avoid getByLabelText collision
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
||||||
import type { FieldDef } from '../../schemas/registry';
|
import type { FieldDef } from '../../schemas/registry';
|
||||||
import { PasswordField } from './PasswordField';
|
import { PasswordField } from './PasswordField';
|
||||||
@@ -9,6 +10,8 @@ interface FieldRendererProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
// provider field: single-option select — hide from UI, auto-register with default value
|
// provider field: single-option select — hide from UI, auto-register with default value
|
||||||
if (field.key === 'provider' && field.options?.length === 1) {
|
if (field.key === 'provider' && field.options?.length === 1) {
|
||||||
return (
|
return (
|
||||||
@@ -29,6 +32,7 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
registration={register(field.key)}
|
registration={register(field.key)}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
helpText={field.helpText}
|
helpText={field.helpText}
|
||||||
|
tooltipText={field.tooltipText}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -36,10 +40,27 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
if (field.inputType === 'select' && field.options) {
|
if (field.inputType === 'select' && field.options) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
|
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
|
||||||
{field.label}
|
{field.label}
|
||||||
{field.required && <span className="ml-1 text-red-500">*</span>}
|
{field.required && <span className="ml-1 text-red-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
{field.tooltipText && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowTooltip(v => !v)}
|
||||||
|
aria-label={`More info about ${field.label}`}
|
||||||
|
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
|
||||||
|
>
|
||||||
|
ⓘ
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{field.tooltipText && showTooltip && (
|
||||||
|
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
|
||||||
|
{field.tooltipText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<select
|
<select
|
||||||
id={field.key}
|
id={field.key}
|
||||||
className={[
|
className={[
|
||||||
@@ -61,10 +82,27 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
|
|||||||
// text (default)
|
// text (default)
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
|
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
|
||||||
{field.label}
|
{field.label}
|
||||||
{field.required && <span className="ml-1 text-red-500">*</span>}
|
{field.required && <span className="ml-1 text-red-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
{field.tooltipText && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowTooltip(v => !v)}
|
||||||
|
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
|
||||||
|
>
|
||||||
|
<span className="sr-only">More info about {field.label}</span>
|
||||||
|
<span aria-hidden="true">ⓘ</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{field.tooltipText && showTooltip && (
|
||||||
|
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
|
||||||
|
{field.tooltipText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
id={field.key}
|
id={field.key}
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -8,15 +8,34 @@ interface PasswordFieldProps {
|
|||||||
registration: UseFormRegisterReturn;
|
registration: UseFormRegisterReturn;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
|
tooltipText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PasswordField({ id, label, error, registration, placeholder, helpText }: PasswordFieldProps) {
|
export function PasswordField({ id, label, error, registration, placeholder, helpText, tooltipText }: PasswordFieldProps) {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<label htmlFor={id} className="text-sm font-medium text-gray-700">
|
<label htmlFor={id} className="text-sm font-medium text-gray-700">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
{tooltipText && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowTooltip(v => !v)}
|
||||||
|
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
|
||||||
|
>
|
||||||
|
<span className="sr-only">More info about {label}</span>
|
||||||
|
<span aria-hidden="true">ⓘ</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{tooltipText && showTooltip && (
|
||||||
|
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
|
||||||
|
{tooltipText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
inputType: 'password',
|
inputType: 'password',
|
||||||
required: false,
|
required: false,
|
||||||
helpText: 'Base64-encoded storage account key. Provide either this or a SAS URL, not both.',
|
helpText: 'Base64-encoded storage account key. Provide either this or a SAS URL, not both.',
|
||||||
|
tooltipText: 'The full storage account key grants unrestricted read/write access to all containers in the account. Keep this secret. If you only need limited access, use a SAS URL instead.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sas_url',
|
key: 'sas_url',
|
||||||
@@ -49,6 +50,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
required: false,
|
required: false,
|
||||||
placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...',
|
placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...',
|
||||||
helpText: 'Full SAS URL including account and container. Provide either this or an access key, not both.',
|
helpText: 'Full SAS URL including account and container. Provide either this or an access key, not both.',
|
||||||
|
tooltipText: 'A SAS URL (Shared Access Signature) bundles the storage endpoint with a time-limited, scope-limited token. It grants access only to containers you specify and expires automatically. Use this if you want limited-access credentials. If you have the full account key, switch to Access Key.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -142,6 +144,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
required: true,
|
required: true,
|
||||||
placeholder: '{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}',
|
placeholder: '{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}',
|
||||||
helpText: 'Paste the JSON token from: rclone authorize "onedrive"',
|
helpText: 'Paste the JSON token from: rclone authorize "onedrive"',
|
||||||
|
tooltipText: 'This is the JSON token obtained by running `rclone authorize "onedrive"` on a machine with a browser. The command opens a browser window, you authenticate, and rclone prints a JSON token — paste that entire JSON blob here.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'drive_id',
|
key: 'drive_id',
|
||||||
|
|||||||
Reference in New Issue
Block a user