Files
urbanLifeline/dify/web/app/components/base/with-input-validation/index.stories.tsx
2025-12-01 17:21:38 +08:00

492 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
},
}