اقدام بستهشده (GatedAction)
اقدامی که کاربر اکنون نمیتواند انجامش دهد — همانجا در جایگاهش، غیرفعال، با دلیلش؛ یک پاسخ بهجای پنهان کردن یا دکمهٔ غیرفعال بیدلیل
معرفی
GatedAction پاسخ سیستم طراحی به عدم دسترسی در سطح اقدام است: اقدامی که کاربر اکنون نمیتواند انجامش دهد —
اجازهاش را ندارد، سهمیهاش تمام شده، یا وضعیت خود صفحه اجازه نمیدهد (ارسال بولتنی که هنوز بخشی ندارد) — پنهان
نمیشود. در جایگاه خودش میماند، غیرفعال، و دلیلش را میگوید:
- با
allowedدرست، همانButtonبی هیچ تغییری رندر میشود؛ - با
allowedنادرست، همانButtonباaria-disabled(هنوز با صفحهکلید در دسترس، تا کاربر صفحهکلید و صفحهخوان هم به آن برسد و دلیل را بهعنوان توضیح دکمه بشنود)، کمرنگ، بیکلیک و بیپیوند؛ وreasonدر یک راهنمای ابزار با hover، فوکوس و لمس (صفحهٔ لمسی hover ندارد).
این همان الگویی است که گزارش ممیزی در قرارداد صفحه آورده (دکمهٔ غیرفعال با راهنمای علت) و در هر جایگاه اقدام قالبهای
صفحه کار میکند: primaryAction، secondaryActions، actions یک بخش، و bulkActions انتخاب یک ListPage.
چه زمانی استفاده کنیم:
- اقدامی که به دسترسی کاربر بسته است («ساخت هشدار» فقط برای مدیران فضای کار).
- اقدامی که شرطی بیرون از این صفحه دارد و کاربر باید بداند چرا کار نمیکند (سهمیهٔ خروجی ماه تمام شده است).
- اقدامی که وضعیت خود صفحه آن را بسته است و کاربر میتواند عوضش کند (ارسال بولتنی که هنوز بخشی ندارد): همین
GatedAction، وreasonمیگوید چه چیزی آن را ممکن میکند («ارسال بولتن وقتی ممکن است که دستکم یک بخش داشته باشد»)؛ نهdisabledبیدلیل، نه پنهان کردن. - نه برای خروجی جدولی که ردیفی ندارد: روی صفحهٔ قالب،
DataTableExportButtonباdataخالی را خود قالب با همینGatedActionو دلیل خودش میبندد (بخش «خروجی جدول» پایینتر)؛ آن را همیشه بی هیچ شرطی بنویسید.
چه زمانی استفاده نکنیم:
- کاربر اجازهٔ دیدن کل صفحه را ندارد:
UtilityPageنوع403، داخل قاب. - اقدام در حال اجراست:
isLoadingخودButton، نهdisabled. - دکمهٔ ارسال فرمی که هنوز تغییری ندارد:
SettingsSectionآن را خودش غیرفعال میکند.
استفاده
GatedAction از @partodata/ui/templates میآید، کنار قالبها. فرزندش یک Button است که همانجا نوشته شده،
دقیقاً همانطور که جایگاهش میخواهد (اقدام اصلی بی variant، اقدام ثانوی variant="default")، یا خروجی جدول،
DataTableExportButton. هیچ چیز دیگری فرزندش نمیشود (نه ConfirmDialog، نه کامپوننتی از خود برنامه که Button را
در بر دارد): GatedAction فقط همان دکمه را میتواند غیرفعال کند و هر چیز دیگری را، وقتی اجازه نیست، بیاثر و بیدلیل
رندر میکند (در حالت توسعه هشدار میدهد و قاعدهٔ ESLint هم نشانش میدهد):
'use client'
import { Button } from '@partodata/ui'
import { Icons } from '@partodata/ui/icons'
import { GatedAction, ListPage, pageState } from '@partodata/ui/templates'
type Alert = { id: string; name: string }
export function AlertsScreen({ alerts, canCreate }: { alerts: Alert[]; canCreate: boolean }) {
return (
<ListPage
title="هشدارها"
primaryAction={
<GatedAction allowed={canCreate} reason="ساخت هشدار فقط برای مدیران فضای کار ممکن است">
<Button onClick={() => {}} iconStart={<Icons.plus />}>
ساخت هشدار
</Button>
</GatedAction>
}
state={pageState({
data: alerts,
isLoading: false,
error: null,
onRetry: () => {},
emptyCopy: { title: 'هنوز هشداری ساخته نشده است' },
})}
>
<ul>
{alerts.map((alert) => (
<li key={alert.id}>{alert.name}</li>
))}
</ul>
</ListPage>
)
}reason یک جملهٔ رسمی است که میگوید چرا و چه کسی میتواند («… فقط برای مدیران فضای کار ممکن است»)، نه
«دسترسی ندارید». allowed خود شرط است (canCreate، permissions.alerts.create، can('create', 'Alert')).
حالتها و انواع
پیوند
اقدامی که به صفحهٔ دیگری میرود (<Button asChild><Link href="…">…</Link></Button>) وقتی اجازه نیست، دکمهای
معمولی و غیرفعال با همان برچسب میشود: پیوند غیرفعال هنوز با کلیک وسط یا منوی راستکلیک باز میشد.
در فرم
GatedAction دور دکمهٔ type="submit" فرم را هرگز ارسال نمیکند.
اقدامی که با پنجرهٔ تأیید انجام میشود
اقدامی مثل حذف که ConfirmDialog تأییدش میکند: GatedAction ماشهٔ پنجره است (trigger)، نه پنجره درون
GatedAction. وقتی اجازه نیست، پنجره هرگز باز نمیشود و کلیک دلیل را نشان میدهد؛ وقتی اجازه هست، دکمه پنجره را باز
میکند و پس از بسته شدن آن فوکوس به همان دکمه برمیگردد. اقدام گروهی ListPage هم همینطور نوشته میشود:
'use client'
import { Button, ConfirmDialog } from '@partodata/ui'
import { GatedAction } from '@partodata/ui/templates'
export function DeleteAlertAction({ canDelete, onDelete }: { canDelete: boolean; onDelete: () => Promise<void> }) {
return (
<ConfirmDialog
trigger={
<GatedAction allowed={canDelete} reason="حذف هشدار فقط برای مدیران فضای کار ممکن است">
<Button variant="default">حذف</Button>
</GatedAction>
}
title="این هشدار حذف شود؟"
description="هشدار و تاریخچهٔ آن برای همیشه حذف میشوند."
variant="destructive"
confirmLabel="حذف"
onConfirm={onDelete}
/>
)
}خروجی جدول
خروجی جدول (DataTableExportButton) با data خالی (هنگام بارگذاری، «نتیجهای یافت نشد» یا خطا) روی صفحهٔ قالب
خودکار بسته میشود: قالب آن را در همین GatedAction با دلیل خودش میگذارد («خروجی وقتی ممکن است که فهرست دستکم
یک ردیف داشته باشد») و منویش باز نمیشود؛ وقتی ردیفها میرسند همان دکمه فعال میشود و فوکوس رویش میماند. پس در
secondaryActions همیشه بی هیچ شرطی نوشته میشود — نه GatedAction روی تعداد ردیفها، نه disabled، نه
rows.length > 0 && … (قاعدهٔ parto/page-primary-action هر سه را نشان میدهد).
GatedAction دور خروجی فقط برای اجازه (یا سهمیه) است: وقتی اجازه نیست، همان دکمه غیرفعال با دلیل همان
GatedAction است و منویش با کلیک، Enter یا پیکان پایین باز نمیشود (خروجی CSV در مرورگر ساخته میشود و سرور جلویش را
نمیگیرد):
'use client'
import { DataTableExportButton, type DataTableColumn } from '@partodata/ui'
import { GatedAction } from '@partodata/ui/templates'
type Mention = { id: string; author: string }
export function MentionsExport({
rows,
columns,
canExport,
}: {
rows: Mention[]
columns: DataTableColumn<Mention>[]
canExport: boolean
}) {
return (
<GatedAction allowed={canExport} reason="خروجی گرفتن فقط برای اعضای فضای کار ممکن است">
<DataTableExportButton columns={columns} data={rows} filename="mentions.csv" label="خروجی CSV" />
</GatedAction>
)
}اقدامهای منوی هر ردیف
اقدامی در منوی عملیات یک ردیف (DropdownMenuItem) که کاربر اجازهاش را ندارد از منو کنار گذاشته میشود
({canDelete && <DropdownMenuItem …>حذف</DropdownMenuItem>})، نه غیرفعال و نه در GatedAction. دلیلش این است که
گزینهٔ غیرفعال منو با کلیدهای پیکان رد میشود و hover و لمس هم نمیگیرد، پس هیچوقت نمیتواند بگوید چرا؛ و
GatedAction فقط یک Button را میتواند غیرفعال کند. منوی ردیف فهرست کارهایی است که کاربر روی همان ردیف میتواند
انجام دهد. اقدام صفحه فرق دارد: دکمهای است که همیشه دیده میشود، پس در جایگاهش غیرفعال با دلیلش میماند. اگر کاربر هیچیک
از اقدامهای ردیف را نمیتواند انجام دهد، ستون عملیات هم نیست. قاعدهٔ parto/page-primary-action
DropdownMenuItem disabled={!canDelete} و GatedAction دور گزینهٔ منو را نشان میدهد.
راهنمای استفاده
بکنید
- هر اقدامی را که کاربر اکنون نمیتواند انجام دهد در جایگاه خودش با
GatedActionبگذارید، اقدام اصلی یا ثانوی یا گروهی. - در
reasonبگویید چه کسی میتواند این کار را بکند، یا وقتی وضعیت صفحه تصمیم میگیرد، چه چیزی آن را ممکن میکند. - اقدامی را که با
ConfirmDialogتأیید میشود باGatedActionدرtriggerپنجره بنویسید.
نکنید
- اقدام را پنهان نکنید: نه
canCreate && <Button/>، نهcanCreate ? <Button/> : null(قاعدهٔ ESLintparto/page-primary-actionهر دو را نشان میدهد). Button disabledبی دلیل در جایگاه اقدام صفحه نگذارید، وTooltipخودتان را دورش نسازید؛GatedActionهر دو را دارد (همان قاعده این را هم نشان میدهد).- برای صفحهای که کاربر اجازهٔ دیدنش را ندارد اقدامها را یکییکی نبندید:
UtilityPage kind="403". ConfirmDialogیا کامپوننت خودتان را درونGatedActionنگذارید: فرزندش فقط یکButton(یاDataTableExportButton) است (قاعدهٔparto/page-primary-actionاین را هم نشان میدهد).- خروجی جدول را برای فهرست بیردیف نبندید و پنهان نکنید (
GatedAction allowed={rows.length > 0}،disabled،rows.length > 0 && …): قالب صفحه این کار را با دلیلش میکند. - گزینهٔ منوی ردیف را با
disabled={!canDelete}یاGatedActionنبندید: آن را از منو کنار بگذارید.
Props
GatedAction
دسترسیپذیری
- اقدام غیرفعال
aria-disabled="true"است، نهdisabled: در ترتیب Tab میماند و صفحهخوان آن را «غیرفعال» با دلیلش (aria-describedby) میخواند. - دلیل با فوکوس، hover، کلیک یا لمس در راهنمای ابزار باز میشود و Escape آن را میبندد.
- Enter، Space یا کلیک روی اقدام غیرفعال هیچ کاری جز نشان دادن دلیل نمیکند؛ خروجی جدول منویش را باز نمیکند.
- وقتی
allowedدر حین کار با صفحه عوض میشود (بار شدن فهرست، رسیدن نخستین نتیجه)، همان دکمه میماند و فوکوس کاربری که با Tab به آن رسیده از آن نمیرود (بهجز اقدام پیوندی، که وقتی اجازه نیست دکمه است نه پیوند).
کامپوننتهای مرتبط
- انتخاب قالب صفحه — جایگاههای اقدام هر قالب.
UtilityPage— نوع403برای صفحهای که کاربر اجازهٔ دیدنش را ندارد.Tooltip— راهنمای ابزاری کهGatedActionبرای دلیل به کار میبرد.ConfirmDialog— پنجرهٔ تأییدی کهGatedActionماشهاش میشود.ListPage— اقدامهای گروهی انتخاب ردیفها.