Files
urbanLifeline/dify/web/app/components/base/with-input-validation/index.stories.tsx

492 lines
14 KiB
TypeScript
Raw Normal View History

2025-12-01 17:21:38 +08:00
import type { Meta, StoryObj } from '@storybook/nextjs'
import { z } from 'zod'
import withValidation from '.'
// Sample components to wrap with validation
type UserCardProps = {
name: string
email: string
age: number
role?: string
}
const UserCard = ({ name, email, age, role }: UserCardProps) => {
return (
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="mb-2 text-lg font-semibold">{name}</h3>
<div className="space-y-1 text-sm text-gray-600">
<div>Email: {email}</div>
<div>Age: {age}</div>
{role && <div>Role: {role}</div>}
</div>
</div>
)
}
type ProductCardProps = {
name: string
price: number
category: string
inStock: boolean
}
const ProductCard = ({ name, price, category, inStock }: ProductCardProps) => {
return (
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="mb-2 text-lg font-semibold">{name}</h3>
<div className="space-y-1 text-sm">
<div className="text-xl font-bold text-green-600">${price}</div>
<div className="text-gray-600">Category: {category}</div>
<div className={inStock ? 'text-green-600' : 'text-red-600'}>
{inStock ? '✓ In Stock' : '✗ Out of Stock'}
</div>
</div>
</div>
)
}
// Create validated versions
const userSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
age: z.number().min(0).max(150),
})
const productSchema = z.object({
name: z.string().min(1, 'Product name required'),
price: z.number().positive('Price must be positive'),
category: z.string().min(1, 'Category required'),
inStock: z.boolean(),
})
const ValidatedUserCard = withValidation(UserCard, userSchema)
const ValidatedProductCard = withValidation(ProductCard, productSchema)
const meta = {
title: 'Base/Data Entry/WithInputValidation',
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Higher-order component (HOC) for wrapping components with Zod schema validation. Validates props before rendering and returns null if validation fails, logging errors to console.',
},
},
},
tags: ['autodocs'],
} satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
// Valid data example
export const ValidData: Story = {
render: () => (
<div style={{ width: '400px' }}>
<h3 className="mb-4 text-lg font-semibold">Valid Props (Renders Successfully)</h3>
<ValidatedUserCard
name="John Doe"
email="john@example.com"
age={30}
role="Developer"
/>
</div>
),
}
// Invalid email
export const InvalidEmail: Story = {
render: () => (
<div style={{ width: '400px' }}>
<h3 className="mb-4 text-lg font-semibold">Invalid Email (Returns null)</h3>
<p className="mb-4 text-sm text-gray-600">
Check console for validation error. Component won't render.
</p>
<ValidatedUserCard
name="John Doe"
email="invalid-email"
age={30}
role="Developer"
/>
<div className="mt-4 rounded-lg bg-red-50 p-3 text-sm text-red-800">
Validation failed: Invalid email format
</div>
</div>
),
}
// Invalid age
export const InvalidAge: Story = {
render: () => (
<div style={{ width: '400px' }}>
<h3 className="mb-4 text-lg font-semibold">Invalid Age (Returns null)</h3>
<p className="mb-4 text-sm text-gray-600">
Age must be between 0 and 150. Check console.
</p>
<ValidatedUserCard
name="John Doe"
email="john@example.com"
age={200}
role="Developer"
/>
<div className="mt-4 rounded-lg bg-red-50 p-3 text-sm text-red-800">
Validation failed: Age must be 150
</div>
</div>
),
}
// Product validation - valid
export const ValidProduct: Story = {
render: () => (
<div style={{ width: '400px' }}>
<h3 className="mb-4 text-lg font-semibold">Valid Product</h3>
<ValidatedProductCard
name="Laptop Pro"
price={1299}
category="Electronics"
inStock={true}
/>
</div>
),
}
// Product validation - invalid price
export const InvalidPrice: Story = {
render: () => (
<div style={{ width: '400px' }}>
<h3 className="mb-4 text-lg font-semibold">Invalid Price (Returns null)</h3>
<p className="mb-4 text-sm text-gray-600">
Price must be positive. Check console.
</p>
<ValidatedProductCard
name="Laptop Pro"
price={-100}
category="Electronics"
inStock={true}
/>
<div className="mt-4 rounded-lg bg-red-50 p-3 text-sm text-red-800">
Validation failed: Price must be positive
</div>
</div>
),
}
// Comparison: validated vs unvalidated
export const ValidationComparison: Story = {
render: () => (
<div style={{ width: '700px' }} className="space-y-6">
<div>
<h3 className="mb-4 text-lg font-semibold">Without Validation</h3>
<div className="space-y-3">
<UserCard
name="John Doe"
email="invalid-email"
age={200}
role="Developer"
/>
<div className="text-xs text-gray-500">
Renders with invalid data (no validation)
</div>
</div>
</div>
<div className="border-t border-gray-200 pt-6">
<h3 className="mb-4 text-lg font-semibold">With Validation (HOC)</h3>
<div className="space-y-3">
<ValidatedUserCard
name="John Doe"
email="invalid-email"
age={200}
role="Developer"
/>
<div className="text-xs text-gray-500">
Returns null when validation fails (check console)
</div>
</div>
</div>
</div>
),
}
// Real-world example - Form submission
export const FormSubmission: Story = {
render: () => {
const handleSubmit = (data: UserCardProps) => {
console.log('Submitting:', data)
}
const validData: UserCardProps = {
name: 'Jane Smith',
email: 'jane@example.com',
age: 28,
role: 'Designer',
}
const invalidData: UserCardProps = {
name: '',
email: 'not-an-email',
age: -5,
role: 'Designer',
}
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Form Submission with Validation</h3>
<div className="space-y-6">
<div>
<h4 className="mb-2 text-sm font-medium text-gray-700">Valid Data</h4>
<ValidatedUserCard {...validData} />
<button
className="mt-3 w-full rounded-lg bg-green-600 px-4 py-2 text-white hover:bg-green-700"
onClick={() => handleSubmit(validData)}
>
Submit Valid Data
</button>
</div>
<div className="border-t border-gray-200 pt-6">
<h4 className="mb-2 text-sm font-medium text-gray-700">Invalid Data</h4>
<ValidatedUserCard {...invalidData} />
<button
className="mt-3 w-full rounded-lg bg-red-600 px-4 py-2 text-white hover:bg-red-700"
onClick={() => handleSubmit(invalidData)}
>
Try to Submit Invalid Data
</button>
<div className="mt-2 text-xs text-red-600">
Component returns null, preventing invalid data rendering
</div>
</div>
</div>
</div>
)
},
}
// Real-world example - API response validation
export const APIResponseValidation: Story = {
render: () => {
const mockAPIResponses = [
{
name: 'Laptop',
price: 999,
category: 'Electronics',
inStock: true,
},
{
name: 'Invalid Product',
price: -50, // Invalid: negative price
category: 'Electronics',
inStock: true,
},
{
name: '', // Invalid: empty name
price: 100,
category: 'Electronics',
inStock: false,
},
]
return (
<div style={{ width: '700px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">API Response Validation</h3>
<p className="mb-4 text-sm text-gray-600">
Only valid products render. Invalid ones return null (check console).
</p>
<div className="grid grid-cols-2 gap-4">
{mockAPIResponses.map((product, index) => (
<div key={index}>
<ValidatedProductCard {...product} />
{!product.name || product.price <= 0 ? (
<div className="mt-2 text-xs text-red-600">
Validation failed for product {index + 1}
</div>
) : null}
</div>
))}
</div>
</div>
)
},
}
// Real-world example - Configuration validation
export const ConfigurationValidation: Story = {
render: () => {
type ConfigPanelProps = {
apiUrl: string
timeout: number
retries: number
debug: boolean
}
const ConfigPanel = ({ apiUrl, timeout, retries, debug }: ConfigPanelProps) => (
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="mb-3 text-base font-semibold">Configuration</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">API URL:</span>
<span className="font-mono">{apiUrl}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Timeout:</span>
<span>{timeout}ms</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Retries:</span>
<span>{retries}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Debug Mode:</span>
<span>{debug ? '✓ Enabled' : '✗ Disabled'}</span>
</div>
</div>
</div>
)
const configSchema = z.object({
apiUrl: z.string().url('Must be valid URL'),
timeout: z.number().min(0).max(30000),
retries: z.number().min(0).max(5),
debug: z.boolean(),
})
const ValidatedConfigPanel = withValidation(ConfigPanel, configSchema)
const validConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
debug: true,
}
const invalidConfig = {
apiUrl: 'not-a-url',
timeout: 50000, // Too high
retries: 10, // Too many
debug: true,
}
return (
<div style={{ width: '600px' }} className="space-y-6">
<div>
<h4 className="mb-2 text-sm font-medium text-gray-700">Valid Configuration</h4>
<ValidatedConfigPanel {...validConfig} />
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-gray-700">Invalid Configuration</h4>
<ValidatedConfigPanel {...invalidConfig} />
<div className="mt-2 text-xs text-red-600">
Validation errors: Invalid URL, timeout too high, too many retries
</div>
</div>
</div>
)
},
}
// Usage documentation
export const UsageDocumentation: Story = {
render: () => (
<div style={{ width: '700px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-xl font-bold">withValidation HOC</h3>
<div className="space-y-6">
<div>
<h4 className="mb-2 text-sm font-semibold text-gray-900">Purpose</h4>
<p className="text-sm text-gray-600">
Wraps React components with Zod schema validation for their props.
Returns null and logs errors if validation fails.
</p>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold text-gray-900">Usage Example</h4>
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-4 text-xs text-gray-100">
{`import { z } from 'zod'
import withValidation from './withValidation'
// Define your component
const UserCard = ({ name, email, age }) => (
<div>{name} - {email} - {age}</div>
)
// Define validation schema
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(0).max(150),
})
// Wrap with validation
const ValidatedUserCard = withValidation(UserCard, schema)
// Use validated component
<ValidatedUserCard
name="John"
email="john@example.com"
age={30}
/>`}
</pre>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold text-gray-900">Key Features</h4>
<ul className="list-inside list-disc space-y-1 text-sm text-gray-600">
<li>Type-safe validation using Zod schemas</li>
<li>Returns null on validation failure</li>
<li>Logs validation errors to console</li>
<li>Only validates props defined in schema</li>
<li>Preserves all original props</li>
</ul>
</div>
<div>
<h4 className="mb-2 text-sm font-semibold text-gray-900">Use Cases</h4>
<ul className="list-inside list-disc space-y-1 text-sm text-gray-600">
<li>API response validation before rendering</li>
<li>Form data validation</li>
<li>Configuration panel validation</li>
<li>Preventing invalid data from reaching components</li>
</ul>
</div>
</div>
</div>
),
}
// Interactive playground
export const Playground: Story = {
render: () => {
return (
<div style={{ width: '600px' }} className="space-y-6">
<div>
<h4 className="mb-2 text-sm font-medium text-gray-700">Try Valid Data</h4>
<ValidatedUserCard
name="Alice Johnson"
email="alice@example.com"
age={25}
role="Engineer"
/>
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-gray-700">Try Invalid Data</h4>
<ValidatedUserCard
name="Bob"
email="invalid-email"
age={-10}
role="Manager"
/>
<p className="mt-2 text-xs text-gray-500">
Open browser console to see validation errors
</p>
</div>
</div>
)
},
}