diff --git a/CLAUDE.md b/CLAUDE.md index c3476d47..54dddf9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,13 +139,24 @@ Theming: **dark mode** overrides via `[data-theme="dark"]`; **compact density** Reuse these before building new ones: `DataTable` (sortable, search, skeleton loading, empty state, bulk) · `Modal` · `ConfirmDialog` · -`PageHeader` (title + breadcrumb + action) · `StatCard` · `StatusBadge` · `Pagination` · -`SearchableSelect` · `AppointmentStatusDropdown` · `PersianDateInput` / `PersianDatePicker` / +`PageHeader` (title + breadcrumb + action + `backTo`) · `BackButton` · `StatCard` · `StatusBadge` · +`Pagination` · `SearchableSelect` · `AppointmentStatusDropdown` · `PersianDateInput` / `PersianDatePicker` / `PersianCalendar` · `MobileInput` · `PriceInput` · `Portal` · `FeatureGate` · `Altcha` · `InviteDoctorModal` · `PwaInstallBanner` / `PwaLoginCard` / `NotificationMobileCard`. Feature composites (not generic) live one level up in `components/*.tsx`. +### دکمهٔ بازگشت — الزامی در صفحات زیرمجموعه + +هر صفحه‌ای که از دل صفحهٔ دیگری باز می‌شود (جزئیات، فرم ساخت/ویرایش، زیرصفحه‌های تنظیمات) +باید دکمهٔ «بازگشت» داشته باشد، با یک ظاهر و یک رفتار: + +- صفحاتی که `PageHeader` دارند: فقط `backTo="/admin/…"` بدهید. +- بقیه: `` بالای هدر صفحه. +- دکمهٔ دست‌ساز نسازید — ظاهر مرجع `cp-btn-secondary` با ارتفاع ۳۶ و آیکون `ChevronRightIcon` است + (همان دکمهٔ صفحهٔ سرویس‌ها) و رفتارش در `hooks/useGoBack.ts` متمرکز است: یک قدم عقب در تاریخچهٔ + پنل، و در ورود مستقیم/رفرش (`location.key === 'default'`) رفتن به `fallback`. + --- ## Conventions diff --git a/assets/admin/components/PatientCaseBanner.tsx b/assets/admin/components/PatientCaseBanner.tsx index e0b146e8..cc0f9e38 100644 --- a/assets/admin/components/PatientCaseBanner.tsx +++ b/assets/admin/components/PatientCaseBanner.tsx @@ -1,7 +1,7 @@ -import { Link } from 'react-router-dom'; import { formatDate } from '../lib/utils'; +import BackButton from './ui/BackButton'; import { - ArrowLeftPH, ArrowLeftD, FilesServicePhone, FilesServiceCalendar, + ArrowLeftD, FilesServicePhone, FilesServiceCalendar, FilesServiceNotification, FilesServiceMessage, } from './icons/FilesServiceIcons'; @@ -11,10 +11,7 @@ interface Tag { uuid: string; name: string; color: string } export function Breadcrumb({ name, backTo }: { name: string; backTo: string }) { return (
- - - بازگشت - + پرونده diff --git a/assets/admin/components/ui/BackButton.test.tsx b/assets/admin/components/ui/BackButton.test.tsx new file mode 100644 index 00000000..033859a9 --- /dev/null +++ b/assets/admin/components/ui/BackButton.test.tsx @@ -0,0 +1,90 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent, render } from '@testing-library/react'; +import { MemoryRouter, Route, Routes, Link } from 'react-router-dom'; +import BackButton from './BackButton'; +import PageHeader from './PageHeader'; + +/** + * رفتار «بازگشت» باید در همهٔ صفحات یکی باشد: یک قدم عقب در تاریخچهٔ پنل، و وقتی + * صفحه مستقیم باز شده (تاریخچه‌ای نیست) رفتن به صفحهٔ والد. + */ +function Detail({ fallback = '/admin/clinic-services' }: { fallback?: string }) { + return ( + <> + صفحهٔ جزئیات + + + ); +} + +function List() { + return ( + <> + صفحهٔ لیست + باز کردن جزئیات + + ); +} + +function renderAt(initialEntries: string[]) { + return render( + + + } /> + } /> + + , + ); +} + +beforeEach(() => vi.clearAllMocks()); + +describe('BackButton', () => { + it('ظاهر یکسان دارد: همان دکمهٔ بازگشتِ صفحهٔ سرویس‌ها', () => { + renderAt(['/admin/clinic-services/x1']); + + const btn = screen.getByRole('button', { name: /بازگشت/ }); + expect(btn).toHaveClass('cp-btn-secondary'); + expect(btn).toHaveAttribute('type', 'button'); + }); + + it('وقتی از صفحهٔ دیگری آمده‌ایم، یک قدم به همان صفحه برمی‌گردد', () => { + renderAt(['/admin/clinic-services']); + + fireEvent.click(screen.getByText('باز کردن جزئیات')); + expect(screen.getByText('صفحهٔ جزئیات')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /بازگشت/ })); + expect(screen.getByText('صفحهٔ لیست')).toBeInTheDocument(); + }); + + it('در ورود مستقیم (بدون تاریخچه) به صفحهٔ والد می‌رود', () => { + renderAt(['/admin/clinic-services/x1']); + + fireEvent.click(screen.getByRole('button', { name: /بازگشت/ })); + + expect(screen.getByText('صفحهٔ لیست')).toBeInTheDocument(); + }); +}); + +describe('PageHeader — backTo', () => { + it('با backTo دکمهٔ بازگشت را نشان می‌دهد', () => { + render( + + + , + ); + + expect(screen.getByRole('button', { name: /بازگشت/ })).toBeInTheDocument(); + }); + + it('بدون backTo دکمه‌ای رندر نمی‌شود (صفحات سطح‌اول)', () => { + render( + + + , + ); + + expect(screen.queryByRole('button', { name: /بازگشت/ })).not.toBeInTheDocument(); + }); +}); diff --git a/assets/admin/components/ui/BackButton.tsx b/assets/admin/components/ui/BackButton.tsx new file mode 100644 index 00000000..91a772ab --- /dev/null +++ b/assets/admin/components/ui/BackButton.tsx @@ -0,0 +1,23 @@ +import { ChevronRightIcon } from '@heroicons/react/24/outline'; +import { useGoBack } from '../../hooks/useGoBack'; + +interface Props { + /** مقصد وقتی تاریخچه‌ای برای برگشتن نیست (ورود مستقیم/رفرش) — معمولاً صفحهٔ لیستِ همان بخش. */ + fallback: string; + label?: string; +} + +/** + * دکمهٔ «بازگشت» — یک ظاهر و یک رفتار در همهٔ صفحاتی که از دل صفحهٔ دیگری باز می‌شوند. + * ظاهر مرجع، همان دکمهٔ بازگشتِ صفحهٔ سرویس‌هاست (`cp-btn-secondary` با ارتفاع ۳۶). + * در RTL، فلشِ «قبلی» راست‌سو است. + */ +export default function BackButton({ fallback, label = 'بازگشت' }: Props) { + const goBack = useGoBack(fallback); + + return ( + + ); +} diff --git a/assets/admin/components/ui/PageHeader.tsx b/assets/admin/components/ui/PageHeader.tsx index fee2539a..2d720e96 100644 --- a/assets/admin/components/ui/PageHeader.tsx +++ b/assets/admin/components/ui/PageHeader.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { ChevronLeftIcon } from '@heroicons/react/24/outline'; +import BackButton from './BackButton'; interface Crumb { label: string; @@ -12,12 +13,22 @@ interface Props { breadcrumbs?: Crumb[]; action?: React.ReactNode; description?: string; + /** + * صفحه از دل صفحهٔ دیگری باز می‌شود → دکمهٔ «بازگشت» بالای عنوان. + * مقدار، مقصدِ fallback است وقتی تاریخچه‌ای برای برگشتن نیست. + */ + backTo?: string; } -export default function PageHeader({ title, breadcrumbs, action, description }: Props) { +export default function PageHeader({ title, breadcrumbs, action, description, backTo }: Props) { return (
+ {backTo && ( +
+ +
+ )} {breadcrumbs && breadcrumbs.length > 0 && (