From e0e8fbd1e4dbcf686a5ea784256d1d9c760238c5 Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Wed, 29 Jul 2026 20:26:51 +0330
Subject: [PATCH] feat: implement BackButton component for consistent
navigation
- Added BackButton component to standardize back navigation across pages.
- Integrated BackButton into various pages, replacing custom back buttons for consistency.
- Updated PageHeader to accept backTo prop for displaying BackButton when navigating from subpages.
- Created useGoBack hook to handle navigation logic, determining whether to go back in history or redirect to a fallback page.
- Added tests for BackButton and its integration with PageHeader to ensure expected behavior.
---
CLAUDE.md | 15 +++-
assets/admin/components/PatientCaseBanner.tsx | 9 +-
.../admin/components/ui/BackButton.test.tsx | 90 +++++++++++++++++++
assets/admin/components/ui/BackButton.tsx | 23 +++++
assets/admin/components/ui/PageHeader.tsx | 13 ++-
assets/admin/hooks/useGoBack.ts | 24 +++++
assets/admin/pages/AppointmentCreatePage.tsx | 5 +-
assets/admin/pages/AppointmentDetailPage.tsx | 8 +-
assets/admin/pages/AppointmentEditPage.tsx | 5 +-
assets/admin/pages/BlogFormPage.tsx | 1 +
assets/admin/pages/ClaimPatientDetailPage.tsx | 1 +
.../pages/ClinicAppointmentSettingsPage.tsx | 4 +
assets/admin/pages/ClinicDetailPage.tsx | 4 +-
assets/admin/pages/ClinicDoctorsPage.tsx | 4 +
assets/admin/pages/ClinicFormPage.tsx | 5 +-
assets/admin/pages/DoctorDetailPage.tsx | 3 +
assets/admin/pages/DoctorFormPage.tsx | 5 ++
assets/admin/pages/EditSessionPage.tsx | 16 +---
assets/admin/pages/MyPaymentDetailPage.tsx | 1 +
assets/admin/pages/NewSessionPage.tsx | 21 +----
assets/admin/pages/PatientRecordFormPage.tsx | 3 +-
assets/admin/pages/PaymentDetailPage.tsx | 1 +
assets/admin/pages/PaymentSuccessPage.tsx | 5 ++
.../pages/RepresentationBlogFormPage.tsx | 1 +
.../admin/pages/RepresentationDetailPage.tsx | 3 +
.../admin/pages/ReserveAppointmentsPage.tsx | 4 +
assets/admin/pages/SecretaryDetailPage.tsx | 2 +-
assets/admin/pages/ServiceDetailPage.tsx | 1 +
assets/admin/pages/SessionPaymentPage.tsx | 21 +----
assets/admin/pages/SettlementDetailPage.tsx | 5 +-
assets/admin/pages/UserDetailPage.tsx | 3 +
31 files changed, 224 insertions(+), 82 deletions(-)
create mode 100644 assets/admin/components/ui/BackButton.test.tsx
create mode 100644 assets/admin/components/ui/BackButton.tsx
create mode 100644 assets/admin/hooks/useGoBack.ts
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 && (