کشوی موجودیت با نشانی (EntityDrawer)

پنل کناری یک ردیف که موجودیت بازش در نشانی صفحه است — پیوند مستقیم، بازگشت مرورگر، J و K برای ردیف بعد و قبل، Esc و برگشت فوکوس

معرفی

در یک کنسول عملیات، کاربر ردیف‌ها را یکی‌یکی از خود فهرست بررسی می‌کند: حساب‌های یک ناوگان، کارهای یک صف، درخواست‌های یک خرید. هر ردیف جزئیات دارد ولی صفحهٔ جدا لازم ندارد — کاربر می‌خواهد کنار فهرست بماند و با صفحه‌کلید ردیف بعد را ببیند. useEntityDrawer و EntityDrawer الگوی تأییدشدهٔ همین کارند:

  • موجودیت باز در نشانی است (?account=a-12): پیوند مستقیم آن را باز می‌کند، «بازگشت» مرورگر می‌بندد؛
  • J ردیف بعد و K ردیف قبل (کلیدهای فیزیکی، با هر زبان صفحه‌کلید)، با «3 از 25» و دکمه‌های قبلی/بعدی؛
  • Esc می‌بندد و فوکوس به ردیف موجودیتی برمی‌گردد که آخر دیده شد؛
  • بدنه حالت خودش را دارد (state)، و پاورقی اقدام‌های خودش را — کشو ناحیهٔ مستقلی است با یک اقدام اصلی خودش (یا DecisionActions برای تأیید و رد).

کشو، صفحه یا دیالوگ؟

نیازپاسخ
موجودیت بخش‌ها و زبانه‌هایی با نشانی خودشان دارد، یا در منو جایی داردDetailPage
موجودیت از فهرستش بررسی می‌شود، کاربر ردیف‌ها را پشت هم می‌بیند، پیوند مستقیم لازم استEntityDrawer + useEntityDrawer
کار کوتاه دو سه فیلدی یا تأییدDialog

استفاده

'use client'
import * as React from 'react'
import { Button, DataTable, useAsync } from '@partodata/ui'
import { EntityDrawer, ListPage, pageState, useEntityDrawer } from '@partodata/ui/templates'

type Account = { id: string; name: string; status: string }

async function getAccounts(): Promise<Account[]> {
  const response = await fetch('/api/accounts')
  if (!response.ok) throw new Error(`accounts: ${response.status}`)
  return response.json()
}
async function getAccount(id: string): Promise<Account> {
  const response = await fetch(`/api/accounts/${id}`)
  if (!response.ok) throw new Error(`account: ${response.status}`)
  return response.json()
}

export function AccountsScreen() {
  const list = useAsync<Account[]>()
  const loadList = React.useCallback(() => list.run(() => getAccounts()), [list.run])
  React.useEffect(() => {
    loadList()
  }, [loadList])
  const rows = list.data ?? []
  const drawer = useEntityDrawer({ param: 'account', ids: rows.map((row) => row.id) })
  const account = useAsync<Account>()
  const runAccount = account.run
  const loadAccount = React.useCallback(() => {
    if (drawer.openId) runAccount(() => getAccount(drawer.openId!))
  }, [runAccount, drawer.openId])
  React.useEffect(() => {
    loadAccount()
  }, [loadAccount])
  const columns = [
    {
      id: 'name',
      header: 'حساب',
      cell: (row: Account) => (
        <Button variant="link" {...drawer.triggerProps(row.id)}>
          {row.name}
        </Button>
      ),
    },
    { id: 'status', header: 'وضعیت', cell: (row: Account) => row.status },
  ]
  return (
    <ListPage
      title="حساب‌ها"
      state={pageState({
        data: list.data,
        isLoading: list.isLoading,
        error: list.error,
        onRetry: loadList,
        emptyCopy: { title: 'هنوز حسابی افزوده نشده است' },
      })}
    >
      <DataTable columns={columns} data={rows} />
      <EntityDrawer
        drawer={drawer}
        title={rows.find((row) => row.id === drawer.openId)?.name ?? 'حساب'}
        state={pageState({
          data: account.data,
          isLoading: account.isLoading,
          error: account.error,
          onRetry: loadAccount,
        })}
      >
        <p>{account.data?.status}</p>
      </EntityDrawer>
    </ListPage>
  )
}
  • param یک واژهٔ کوچک است که موجودیت را نام می‌برد؛ صفحه هرگز خودش آن را نمی‌خواند یا نمی‌نویسد (نه useSearchParams، نه router.push('?…')).
  • ids شناسهٔ ردیف‌های روی صفحه است، به همان ترتیب: J و K میان همین‌ها جابه‌جا می‌شوند.
  • باز کردن یک ورودی تاریخچه می‌سازد (بازگشت مرورگر می‌بندد)؛ J و K همان ورودی را عوض می‌کنند (بازگشت باز هم می‌بندد، نه ردیف‌به‌ردیف عقب می‌رود)؛ پیوند مستقیمی که باز رسیده با بستن فقط پارامتر را برمی‌دارد.
  • عنوان کشو نام موجودیت است؛ تا بارگذاری شود، نامش از ردیف (یا نوعش: «حساب»).

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

sizeعرض
defaultنیمی از صفحه از lg
wideدو سوم صفحه، برای موجودیتی که بدنه‌اش یک جدول است

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

بکنید

  • دکمهٔ باز کردن ردیف را Button variant="link" با {...drawer.triggerProps(row.id)} کنید؛ همان‌جا فوکوس برمی‌گردد.
  • اقدام اصلی کشو را در primaryAction خودش بدهید؛ تأیید و رد یک DecisionActions است.

نکنید

  • موجودیت باز را خودتان در نشانی ننویسید یا از آن نخوانید.
  • کشو را به صفحه تبدیل نکنید فقط برای این‌که نشانی داشته باشد: نشانی را همین الگو می‌دهد.
  • کلیدهای میان‌بر J و K را خودتان روی صفحه نبندید.

Props

UseEntityDrawerOptions

Prop

Type

EntityDrawer

Prop

Type

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

  • کشو یک دیالوگ با نام موجودیت است؛ فوکوس در آن می‌ماند و Esc می‌بندد.
  • دکمه‌های قبلی و بعدی نام دارند و میان‌برشان را با aria-keyshortcuts (K و J) می‌گویند؛ در انتهای فهرست غیرفعال‌اند ولی فوکوس را نگه می‌دارند (aria-disabled). J و K در فیلدی که تایپ می‌شود حرف‌اند، نه میان‌بر.
  • دکمهٔ باز کردن ردیف aria-haspopup="dialog" و aria-expanded دارد؛ با بستن، فوکوس به ردیفی برمی‌گردد که آخر دیده شد (پس از J و K، ردیف تازه).

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

  • Sheet — پنل کناری‌ای که کشو روی آن ساخته شده است.
  • DecisionActions — تأیید و رد در پاورقی کشو.
  • ListPage — فهرستی که کشو از آن باز می‌شود.