قالب جریان چندمرحله‌ای (WizardPage)

جریانی که در چند مرحله یک چیز می‌سازد — نشانگر مراحل، «قبلی» و «بعدی» با اعتبارسنجی هر مرحله، مرحلهٔ بازبینی، و مرحله در نشانی

معرفی

WizardPage قالب صفحه‌ای است که یک چیز را در چند مرحله می‌سازد: تحلیل تازه (منبع ← برچسب‌ها ← بازبینی)، بولتن تازه، افزودن منبع. هر تصمیم آن در خود قالب است:

  • عرض باریک (768)، مثل هر فرم؛ سرِ صفحه با عنوان (تنها h1) و پیوند بازگشت؛
  • نشانگر مراحل (مرحلهٔ جاری، مراحل گذشته با تیک)؛
  • فیلدهای مرحلهٔ جاری در یک کارت، با عنوان مرحله («مرحلهٔ 2 از 3 · برچسب‌ها»)؛
  • پاورقی کارت: راه برگشت در ابتدای خط («انصراف» در مرحلهٔ اول، «قبلی» بعد از آن) و راه پیش در انتهای خط («بعدی»، اقدام اصلی؛ در مرحلهٔ آخر دکمهٔ ثبت)؛
  • «بعدی» (و Enter در یک فیلد) پیش از رفتن مرحله را بررسی می‌کند (onValidateStep)؛
  • با رفتن به مرحلهٔ دیگر، فوکوس روی عنوان مرحلهٔ تازه می‌رود؛
  • مرحلهٔ آخر، در جریانی با بیش از دو مرحله، بازبینی است: WizardReview همهٔ پاسخ‌ها را به تفکیک مرحله، هر گروه با «ویرایش»، نشان می‌دهد؛
  • با stepInUrl هر مرحله یک ورودی تاریخچه است (?step=) و «بازگشت» مرورگر یک مرحله عقب می‌رود.

چه زمانی استفاده کنیم: ساختن چیزی که فیلدهایش به ترتیب به هم وابسته‌اند یا یک‌جا زیادند (منبع داده، سپس برچسب‌ها بر اساس همان منبع).

چه زمانی استفاده نکنیم:

  • فرمی با یک ثبت که در یک نگاه جا می‌شود (حتی با گروه‌ها): FormPage.
  • کاری کوتاه در دو سه فیلد: یک Dialog.
  • مراحل یک کار در حال اجرا (پیشرفت پردازش): دادهٔ زنده.

استفاده

همهٔ مراحل یک فرم react-hook-form‌اند (useForm روی همهٔ فیلدها)؛ مرحلهٔ جاری وضعیت جزء است و بررسی هر مرحله form.trigger همان فیلدهای مرحله:

'use client'
import * as React from 'react'
import { useForm } from 'react-hook-form'
import { Input, toast } from '@partodata/ui'
import { Form, FormField } from '@partodata/ui/form'
import { FormRow, WizardPage, WizardReview } from '@partodata/ui/templates'

type NewAnalysis = { name: string; link: string; labels: string }

const STEPS = [
  { id: 'source', label: 'منبع' },
  { id: 'labels', label: 'برچسب‌ها' },
  { id: 'review', label: 'بازبینی' },
]
const FIELDS: Record<string, Array<keyof NewAnalysis>> = { source: ['name', 'link'], labels: ['labels'], review: [] }

async function createAnalysis(values: NewAnalysis): Promise<void> {
  const response = await fetch('/api/analyses', { method: 'POST', body: JSON.stringify(values) })
  if (!response.ok) throw new Error(`analyses: ${response.status}`)
}

export function NewAnalysisScreen() {
  const form = useForm<NewAnalysis>({ defaultValues: { name: '', link: '', labels: '' } })
  const [step, setStep] = React.useState('source')
  const submit = form.handleSubmit(async (values) => {
    try {
      await createAnalysis(values)
      toast.success('تحلیل ساخته شد')
    } catch {
      form.setError('root', { message: 'ساخت تحلیل انجام نشد. لحظه‌ای بعد دوباره تلاش کنید.' })
    }
  })
  return (
    <Form {...form}>
      <WizardPage
        title="تحلیل تازه"
        back={{ href: '/analyses', label: 'تحلیل‌ها' }}
        steps={STEPS}
        step={step}
        onStepChange={setStep}
        onValidateStep={(id) => form.trigger(FIELDS[id], { shouldFocus: true })}
        onSubmit={submit}
        submitLabel="ساخت تحلیل"
        submitting={form.formState.isSubmitting}
        error={form.formState.errors.root?.message}
        onCancel={() => window.history.back()}
        stepInUrl
      >
        {step === 'source' && (
          <>
            <FormField
              control={form.control}
              name="name"
              rules={{ required: 'نام تحلیل را وارد کنید' }}
              render={({ field, fieldState }) => (
                <FormRow label="نام تحلیل" required error={fieldState.error?.message}>
                  <Input {...field} />
                </FormRow>
              )}
            />
            <FormField
              control={form.control}
              name="link"
              rules={{ required: 'پیوند پست را وارد کنید' }}
              render={({ field, fieldState }) => (
                <FormRow label="پیوند پست" required error={fieldState.error?.message}>
                  <Input kind="url" {...field} />
                </FormRow>
              )}
            />
          </>
        )}
        {step === 'labels' && (
          <FormField
            control={form.control}
            name="labels"
            render={({ field }) => (
              <FormRow label="برچسب‌ها" description="با ویرگول جدا کنید">
                <Input {...field} />
              </FormRow>
            )}
          />
        )}
        {step === 'review' && (
          <WizardReview
            items={[
              { step: 'source', label: 'نام تحلیل', value: form.getValues('name') },
              { step: 'source', label: 'پیوند پست', value: form.getValues('link') },
              { step: 'labels', label: 'برچسب‌ها', value: form.getValues('labels') || '—' },
            ]}
          />
        )}
      </WizardPage>
    </Form>
  )
}

حالت‌ها و انواع

مرحلهابتدای پاورقیانتهای پاورقی
اول«انصراف»«بعدی» (اقدام اصلی)
میانی«قبلی»«بعدی»
آخر«قبلی»submitLabel (دکمهٔ ثبت)
  • onValidateStep برای «بعدی» و Enter اجرا می‌شود؛ false در همان مرحله می‌ماند (پیام‌های فیلدها و فوکوس روی اولین فیلد نامعتبر با shouldFocus). «قبلی» و «ویرایش» بازبینی بی بررسی برمی‌گردند.
  • stepInUrl: مرحله در ?step= (مرحلهٔ اول بی پارامتر)؛ «بازگشت» و «جلو» مرورگر میان مراحلی که رسیده‌اید جابه‌جا می‌شوند؛ پیوندی به مرحله‌ای جلوتر، جریان را از مرحلهٔ اول باز می‌کند (پاسخ‌های مراحل قبل در نشانی نیستند).
  • 2 تا 6 مرحله؛ یک مرحله یعنی FormPage و بیش از شش، جریانی که باید تقسیم شود (در محیط توسعه هشدار).

راهنمای استفاده

بکنید

  • همهٔ مراحل را یک useForm کنید و فقط فیلدهای مرحلهٔ جاری را رندر کنید.
  • در جریان بیش از دو مرحله، مرحلهٔ آخر را بازبینی (WizardReview) کنید.
  • کنش فرعی جریان (مانند «ذخیرهٔ پیش‌نویس») را در secondaryActions بگذارید، نه در بدنهٔ مرحله.

نکنید

  • دکمه‌های «قبلی»، «بعدی» یا ثبت خودتان را نسازید؛ قالب آن‌ها را جای می‌دهد.
  • نشانگر مراحل (Stepper) یا کارت خودتان را اضافه نکنید.
  • مرحله را خودتان با useSearchParams در نشانی نگذارید: stepInUrl.

Props

WizardPage

Prop

Type

WizardStep

Prop

Type

WizardReview

Prop

Type

دسترسی‌پذیری

  • نشانگر مراحل فهرستی با نام «مراحل» است و هر مرحله وضعیتش را می‌گوید (گذشته، جاری، مانده).
  • عنوان هر مرحله h2 است و «مرحلهٔ n از m» را با خود دارد؛ با رفتن به مرحلهٔ دیگر فوکوس روی آن می‌رود، پس صفحه‌خوان مرحلهٔ تازه را اعلام می‌کند.
  • «ویرایش» هر گروه بازبینی نام همان مرحله را دارد («ویرایش منبع»).

کامپوننت‌های مرتبط

  • FormPage — فرمی با یک ثبت.
  • FormRow — فیلدهای هر مرحله.