close the last three navigate-only menu items into links

This commit is contained in:
2026-08-07 14:07:38 +00:00
parent 15a960897f
commit f46a603892
4 changed files with 34 additions and 19 deletions
@@ -2,7 +2,7 @@ import type { Column } from './components';
import type { ConfirmState } from './ConfirmDialog';
import type { Address, Customer } from './shared';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { Link } from 'react-router';
import { Filter, MoreHorizontal, Plus, Trash2, Users, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
@@ -17,7 +17,7 @@ import {
import { EmptyState, ErrorState, Field, Pagination, SearchBox, SectionShell, SimpleTable } from './components';
import { ConfirmDialog } from './ConfirmDialog';
import { formatDate, formatMoney } from './format';
import { PAGE_SIZE } from './shared';
import { invoicesCustomerPath, PAGE_SIZE } from './shared';
import { useListFilters } from './useInvoicesSection';
import { useCurrency, useCustomerStats, useResourceList, useResourceMutations } from './useInvoiceShelfData';
@@ -30,7 +30,6 @@ type Props = { onEdit: (id: number | 'new') => void };
export const CustomersListView = ({ onEdit }: Props) => {
const { filters, patch, select, setPage, clearFilters } = useListFilters();
const currency = useCurrency();
const navigate = useNavigate();
const [showFilters, setShowFilters] = useState(false);
const [selection, setSelection] = useState<Set<number>>(new Set());
const [confirm, setConfirm] = useState<ConfirmState>(null);
@@ -120,11 +119,14 @@ export const CustomersListView = ({ onEdit }: Props) => {
<DropdownMenuItem onSelect={() => onEdit(row.id)}>Edit</DropdownMenuItem>
<DropdownMenuItem onSelect={() => select(row.id)}>View</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => navigate(`/invoices/invoices?customer=${row.id}`)}>
Their invoices
{/* Both go somewhere and change nothing, so they are anchors wearing a menu item — `asChild` hands
Radix's item semantics to the `<Link>` rather than nesting one inside the other. Middle-click
opens the filtered list in a tab, which is the point of the filter living in the URL. */}
<DropdownMenuItem asChild>
<Link to={invoicesCustomerPath('invoices', row.id)}>Their invoices</Link>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => navigate(`/invoices/payments?customer=${row.id}`)}>
Their payments
<DropdownMenuItem asChild>
<Link to={invoicesCustomerPath('payments', row.id)}>Their payments</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -3,6 +3,7 @@ import type { ConfirmState } from './ConfirmDialog';
import type { SendTarget } from './SendDocumentDialog';
import type { Invoice } from './shared';
import { useState } from 'react';
import { Link } from 'react-router';
import { Copy, FileText, Filter, MoreHorizontal, Plus, Send, Trash2, Wallet } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -50,9 +51,9 @@ import {
// `allow_edit` is honoured rather than ignored: once an invoice has payments against it, upstream locks
// editing, and letting a user open a form whose save will be rejected is worse than hiding the button.
type Props = { onEdit: (id: number | 'new') => void; onRecordPayment: (invoiceId: number) => void };
type Props = { onEdit: (id: number | 'new') => void; recordPaymentHref: (invoiceId: number) => string };
export const InvoicesListView = ({ onEdit, onRecordPayment }: Props) => {
export const InvoicesListView = ({ onEdit, recordPaymentHref }: Props) => {
const { filters, patch, select, setPage, clearFilters } = useListFilters();
const currency = useCurrency();
const { customers } = useLookups();
@@ -189,10 +190,15 @@ export const InvoicesListView = ({ onEdit, onRecordPayment }: Props) => {
Resend invoice
</DropdownMenuItem>
)}
{/* Opening the payment editor is an address (`?edit=new&pay=<id>`), not a side effect — the editor
prefills itself from those params — so it is an anchor, not a callback up to the parent and
back down through `navigate`. */}
{(row.status === 'SENT' || row.status === 'VIEWED') && (
<DropdownMenuItem onSelect={() => onRecordPayment(row.id)}>
<Wallet className="mr-2 h-3.5 w-3.5" />
Record payment
<DropdownMenuItem asChild>
<Link to={recordPaymentHref(row.id)}>
<Wallet className="mr-2 h-3.5 w-3.5" />
Record payment
</Link>
</DropdownMenuItem>
)}
{row.status === 'DRAFT' && (
@@ -12,7 +12,7 @@ import { PaymentsListView } from './PaymentsListView';
import { RecurringListView } from './RecurringListView';
import { ReportsView } from './ReportsView';
import { CustomerEditor, ExpenseEditor, ItemEditor, PaymentEditor } from './RecordEditors';
import { invoicesSectionPath } from './shared';
import { invoicesNewPaymentPath, invoicesSectionPath } from './shared';
import { useInvoiceShelfHealth } from './useInvoiceShelfData';
import { useInvoicesSection } from './useInvoicesSection';
@@ -100,12 +100,7 @@ export const InvoicesView = () => {
switch (section) {
case 'invoices':
return (
<InvoicesListView
onEdit={openEditor}
onRecordPayment={(invoiceId) => navigate(`/invoices/payments?edit=new&pay=${invoiceId}`)}
/>
);
return <InvoicesListView onEdit={openEditor} recordPaymentHref={invoicesNewPaymentPath} />;
case 'estimates':
return (
@@ -42,6 +42,18 @@ export const invoicesSectionPath = (id: InvoicesSectionId) => `/invoices/${id}`;
export const invoicesRecordPath = (section: InvoicesSectionId, id: number) =>
`${invoicesSectionPath(section)}?selected=${id}`;
/** A list section filtered to one customer — `useListFilters` reads `customer`. */
export const invoicesCustomerPath = (section: InvoicesSectionId, customerId: number) =>
`${invoicesSectionPath(section)}?customer=${customerId}`;
/**
* The payment editor, opened blank against an invoice — `/invoices/payments?edit=new&pay=12`.
* `InvoicesView` reads both params (`edit` → which editor, `pay` → which invoice it prefills from), so
* "record a payment for this invoice" is a whole address and not a callback.
*/
export const invoicesNewPaymentPath = (invoiceId: number) =>
`${invoicesSectionPath('payments')}?edit=new&pay=${invoiceId}`;
/** The sidecar resource slug behind each list section. `dashboard` and `reports` have none. */
export const SECTION_RESOURCE = {
invoices: 'invoices',