fix(cashflow): read period from server props instead of window (#302)
## Summary - Fixes [PHP-LARAVEL-9](https://whisper-money.sentry.io/issues/PHP-LARAVEL-9): `ReferenceError: window is not defined` during Inertia SSR of `CashflowPage`. - `CashflowController` now reads + validates the `period` query param (`YYYY-MM`) and passes it as an Inertia prop. - Client `useState` initializer reads from `usePage().props.period` instead of `window.location.search`, making SSR safe and avoiding hydration mismatch. - `useEffect` URL sync now compares against the prop, not `window.location.search`. ## Why (c) not a `typeof window` guard Server-provided params avoid hydration mismatch and keep URL as single source of truth. ## Tests New `tests/Feature/CashflowPageTest.php`: - guest redirect - no query param → `period` prop `null` - valid `?period=2025-03` → prop `'2025-03'` - invalid value → `null` - malformed format (`2025-3`) → `null` All 5 pass (50 assertions). ## Scope Cashflow only. Other pages with similar `window.location.search` in `useState` initializers (`onboarding/index.tsx`, `welcome.tsx`, `auth/login.tsx`) left for follow-up. Fixes PHP-LARAVEL-9
This commit is contained in:
parent
cfa54a2d9d
commit
22952c4e75
|
|
@ -34,10 +34,16 @@ class CashflowController extends Controller
|
|||
->orderBy('name')
|
||||
->get(['id', 'name', 'logo']);
|
||||
|
||||
$period = $request->query('period');
|
||||
$validPeriod = is_string($period) && preg_match('/^\d{4}-\d{2}$/', $period) === 1
|
||||
? $period
|
||||
: null;
|
||||
|
||||
return Inertia::render('cashflow/index', [
|
||||
'categories' => $categories,
|
||||
'accounts' => $accounts,
|
||||
'banks' => $banks,
|
||||
'period' => $validPeriod,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,18 +22,17 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||
];
|
||||
|
||||
export default function CashflowPage() {
|
||||
const { auth } = usePage<{ auth: { user: { currency_code: string } } }>()
|
||||
.props;
|
||||
const { auth, period: initialPeriod } = usePage<{
|
||||
auth: { user: { currency_code: string } };
|
||||
period: string | null;
|
||||
}>().props;
|
||||
|
||||
// Initialize currentDate from URL query param or default to current month
|
||||
// Initialize currentDate from server-provided period prop or default to current month
|
||||
const [currentDate, setCurrentDate] = useState<Date>(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const periodParam = urlParams.get('period');
|
||||
|
||||
if (periodParam) {
|
||||
if (initialPeriod) {
|
||||
try {
|
||||
// Parse YYYY-MM format
|
||||
const parsedDate = parse(periodParam, 'yyyy-MM', new Date());
|
||||
const parsedDate = parse(initialPeriod, 'yyyy-MM', new Date());
|
||||
// Validate it's a valid date
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
return parsedDate;
|
||||
|
|
@ -63,19 +62,16 @@ export default function CashflowPage() {
|
|||
// Update URL when currentDate changes
|
||||
useEffect(() => {
|
||||
const periodParam = format(currentDate, 'yyyy-MM');
|
||||
const currentPeriodParam = new URLSearchParams(
|
||||
window.location.search,
|
||||
).get('period');
|
||||
|
||||
// Only update if the period has changed
|
||||
if (currentPeriodParam !== periodParam) {
|
||||
if (initialPeriod !== periodParam) {
|
||||
router.visit(cashflow({ query: { period: periodParam } }).url, {
|
||||
preserveScroll: true,
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
}, [currentDate]);
|
||||
}, [currentDate, initialPeriod]);
|
||||
|
||||
return (
|
||||
<AppSidebarLayout breadcrumbs={breadcrumbs}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
|
||||
test('guests are redirected to login', function () {
|
||||
$this->get(route('cashflow'))->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('period prop is null when no query param given', function () {
|
||||
$this->actingAs(User::factory()->onboarded()->create());
|
||||
|
||||
$this->get(route('cashflow'))
|
||||
->assertOk()
|
||||
->assertInertia(
|
||||
fn (AssertableInertia $page) => $page
|
||||
->component('cashflow/index')
|
||||
->where('period', null)
|
||||
);
|
||||
});
|
||||
|
||||
test('valid period query param is passed to page props', function () {
|
||||
$this->actingAs(User::factory()->onboarded()->create());
|
||||
|
||||
$this->get(route('cashflow', ['period' => '2025-03']))
|
||||
->assertOk()
|
||||
->assertInertia(
|
||||
fn (AssertableInertia $page) => $page
|
||||
->component('cashflow/index')
|
||||
->where('period', '2025-03')
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid period query param is sanitized to null', function () {
|
||||
$this->actingAs(User::factory()->onboarded()->create());
|
||||
|
||||
$this->get(route('cashflow', ['period' => 'not-a-date']))
|
||||
->assertOk()
|
||||
->assertInertia(
|
||||
fn (AssertableInertia $page) => $page
|
||||
->component('cashflow/index')
|
||||
->where('period', null)
|
||||
);
|
||||
});
|
||||
|
||||
test('malformed period format is rejected', function () {
|
||||
$this->actingAs(User::factory()->onboarded()->create());
|
||||
|
||||
$this->get(route('cashflow', ['period' => '2025-3']))
|
||||
->assertOk()
|
||||
->assertInertia(
|
||||
fn (AssertableInertia $page) => $page
|
||||
->component('cashflow/index')
|
||||
->where('period', null)
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue