From 97f553ad4618d01667bb53666cfe8f87b65ee087 Mon Sep 17 00:00:00 2001 From: Ben Stokes Date: Wed, 12 Aug 2026 16:44:34 -0700 Subject: [PATCH] feat: open in new tab for paginated dashboard lists --- .../connected-accounts-list.component.html | 2 +- .../connected-accounts-list.component.ts | 5 ---- .../customer-detail.component.html | 6 ++--- .../customer-detail.component.ts | 12 --------- .../customer-list.component.html | 2 +- .../customer-list/customer-list.component.ts | 7 ----- .../invoice-list/invoice-list.component.html | 2 +- .../invoice-list/invoice-list.component.ts | 6 ----- .../payment-intent-list.component.html | 2 +- .../payment-intent-list.component.ts | 7 +---- .../payment-link-list.component.html | 2 +- .../payment-link-list.component.ts | 4 --- .../product-catalogue.component.html | 2 +- .../product-catalogue.component.ts | 7 ----- .../product-detail.component.html | 2 +- .../product-detail.component.ts | 4 --- .../subscription-detail.component.html | 2 +- .../subscription-detail.component.ts | 7 +---- .../subscription-list.component.html | 2 +- .../subscription-list.component.ts | 6 ----- .../paginated-list.component.html | 24 ++++++++++++++--- .../paginated-list.component.scss | 25 ++++++++++++++++- .../paginated-list.component.ts | 27 ++++++++++++++----- 23 files changed, 78 insertions(+), 87 deletions(-) diff --git a/apps/web/src/app/features/account/connected-accounts/views/connected-accounts-list/connected-accounts-list.component.html b/apps/web/src/app/features/account/connected-accounts/views/connected-accounts-list/connected-accounts-list.component.html index d5a5025..22e7c67 100644 --- a/apps/web/src/app/features/account/connected-accounts/views/connected-accounts-list/connected-accounts-list.component.html +++ b/apps/web/src/app/features/account/connected-accounts/views/connected-accounts-list/connected-accounts-list.component.html @@ -34,7 +34,7 @@

Connected Accounts

[columns]="connectedAccountColumns" [limit]="20" [queryParams]="accountsQueryParams()" - (rowClick)="OnConnectedAccountClick($event)" + rowLinkPrefix="/account/connected-accounts" > Subscriptions [expand]="subscriptionExpand()" [paginationEnabled]="false" [showResultCount]="true" - (rowClick)="OnSubscriptionClick($event)" + rowLinkPrefix="/account/subscriptions" >
@@ -61,7 +61,7 @@

Payments

[limit]="5" [queryParams]="paymentQueryParams()" [paginationEnabled]="false" - (rowClick)="OnPaymentClick($event)" + rowLinkPrefix="/account/payments" >
@@ -91,7 +91,7 @@

Invoices

[queryParams]="invoiceQueryParams()" [expand]="invoiceExpand()" [paginationEnabled]="false" - (rowClick)="OnInvoiceClick($event)" + rowLinkPrefix="/account/invoices" >
diff --git a/apps/web/src/app/features/account/customers/views/customer-detail/customer-detail.component.ts b/apps/web/src/app/features/account/customers/views/customer-detail/customer-detail.component.ts index 4a1d7bd..0d9d238 100644 --- a/apps/web/src/app/features/account/customers/views/customer-detail/customer-detail.component.ts +++ b/apps/web/src/app/features/account/customers/views/customer-detail/customer-detail.component.ts @@ -277,18 +277,6 @@ export class CustomerDetailComponent implements OnInit, OnDestroy { this.invoiceQueryParams.set({ customer: customerId }); } - OnSubscriptionClick(subscription: SubscriptionType): void { - void this.router.navigate(['/account/subscriptions', subscription.id]); - } - - OnPaymentClick(paymentIntent: PaymentIntent): void { - void this.router.navigate(['/account/payments', paymentIntent.id]); - } - - OnInvoiceClick(invoice: Invoice): void { - void this.router.navigate(['/account/invoices', invoice.id]); - } - OnEdit(): void { const p = this.customer(); if (p) this.actions.OpenEdit(p); diff --git a/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.html b/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.html index a778693..1bb8a04 100644 --- a/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.html +++ b/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.html @@ -15,7 +15,7 @@

Customers

[columns]="customerColumns" [limit]="10" [queryParams]="customersQueryParams()" - (rowClick)="OnCustomerListClick($event)" + rowLinkPrefix="/account/customers" > diff --git a/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.ts b/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.ts index 18e2912..5e5c40b 100644 --- a/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.ts +++ b/apps/web/src/app/features/account/customers/views/customer-list/customer-list.component.ts @@ -12,7 +12,6 @@ import { PaginatedListComponent, PaginatedListColumn, } from '../../../../../shared'; -import { Router, ActivatedRoute } from '@angular/router'; import type { Customer } from '@zoneless/shared-types'; import { Subscription } from 'rxjs'; @@ -28,8 +27,6 @@ import { MetaService } from '../../../../../core'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CustomerListComponent implements OnInit, OnDestroy { - readonly router = inject(Router); - readonly route = inject(ActivatedRoute); private sub?: Subscription; private readonly metaService = inject(MetaService); readonly actions = inject(CustomerActionsService); @@ -101,8 +98,4 @@ export class CustomerListComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.sub?.unsubscribe(); } - - OnCustomerListClick(customer: Customer): void { - this.router.navigate(['/account/customers', customer.id]); - } } diff --git a/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.html b/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.html index 1c14fa9..ab2ebcc 100644 --- a/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.html +++ b/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.html @@ -65,5 +65,5 @@

Invoices

[limit]="10" [queryParams]="invoicesQueryParams()" [expand]="invoicesExpand()" - (rowClick)="OnInvoiceClick($event)" + rowLinkPrefix="/account/invoices" > diff --git a/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.ts b/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.ts index 1c66bf4..2b21cd7 100644 --- a/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.ts +++ b/apps/web/src/app/features/account/invoices/views/invoice-list/invoice-list.component.ts @@ -6,7 +6,6 @@ import { inject, OnInit, } from '@angular/core'; -import { Router } from '@angular/router'; import { PaginatedListComponent, PaginatedListColumn, @@ -31,7 +30,6 @@ type InvoicesStatusTab = 'all' | 'draft' | 'open' | 'paid'; }) export class InvoiceListComponent implements OnInit { private readonly metaService = inject(MetaService); - private readonly router = inject(Router); invoicesStatusTab: WritableSignal = signal('all'); @@ -131,10 +129,6 @@ export class InvoiceListComponent implements OnInit { this.invoicesQueryParams.set(params); } - OnInvoiceClick(invoice: Invoice): void { - void this.router.navigate(['/account/invoices', invoice.id]); - } - private CopyInvoiceId(invoice: Invoice): void { void navigator.clipboard.writeText(invoice.id); } diff --git a/apps/web/src/app/features/account/payment-intents/views/payment-intent-list/payment-intent-list.component.html b/apps/web/src/app/features/account/payment-intents/views/payment-intent-list/payment-intent-list.component.html index 95f3f1e..27f3566 100644 --- a/apps/web/src/app/features/account/payment-intents/views/payment-intent-list/payment-intent-list.component.html +++ b/apps/web/src/app/features/account/payment-intents/views/payment-intent-list/payment-intent-list.component.html @@ -112,7 +112,7 @@

Transactions

[limit]="20" [queryParams]="paymentIntentsQueryParams()" [expand]="paymentIntentsExpand()" - (rowClick)="OnPaymentIntentClick($event)" + rowLinkPrefix="/account/payments" > } @if (transactionsTab() === 'payouts') { = {}; if (this.customerFilter) { diff --git a/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.html b/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.html index 0086ef0..abf13e5 100644 --- a/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.html +++ b/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.html @@ -15,7 +15,7 @@

Payment Links

[columns]="paymentLinkColumns" [limit]="10" [queryParams]="paymentLinksQueryParams()" - (rowClick)="OnRowClick($event)" + rowLinkPrefix="/account/payment-links" > diff --git a/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.ts b/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.ts index d9a0f91..3120c32 100644 --- a/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.ts +++ b/apps/web/src/app/features/account/payment-links/views/payment-link-list/payment-link-list.component.ts @@ -105,10 +105,6 @@ export class PaymentLinkListComponent implements OnInit, OnDestroy { this.sub?.unsubscribe(); } - OnRowClick(paymentLink: PaymentLink): void { - void this.router.navigate(['/account/payment-links', paymentLink.id]); - } - private CopyPaymentLinkUrl(paymentLink: PaymentLink): void { void navigator.clipboard.writeText(paymentLink.url); } diff --git a/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.html b/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.html index f42ec19..c5ae616 100644 --- a/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.html +++ b/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.html @@ -65,7 +65,7 @@

Product Catalogue

[limit]="10" [queryParams]="productsQueryParams()" [expand]="productsExpand()" - (rowClick)="OnProductListClick($event)" + rowLinkPrefix="/account/products" > } diff --git a/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.ts b/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.ts index 1cfc73d..700c3fd 100644 --- a/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.ts +++ b/apps/web/src/app/features/account/products/views/product-catalogue/product-catalogue.component.ts @@ -12,7 +12,6 @@ import { PaginatedListComponent, PaginatedListColumn, } from '../../../../../shared'; -import { Router, ActivatedRoute } from '@angular/router'; import type { Product, Price } from '@zoneless/shared-types'; import { ProductService } from '../../../../../data'; @@ -31,8 +30,6 @@ import { FormatPriceDisplay } from '../../util/price-display'; }) export class ProductCatalogueComponent implements OnInit, OnDestroy { readonly productService = inject(ProductService); - readonly router = inject(Router); - readonly route = inject(ActivatedRoute); readonly actions = inject(ProductActionsService); private sub?: Subscription; private readonly metaService = inject(MetaService); @@ -126,10 +123,6 @@ export class ProductCatalogueComponent implements OnInit, OnDestroy { this.sub?.unsubscribe(); } - OnProductListClick(product: Product): void { - this.router.navigate(['/account/products', product.id]); - } - SetProductsTab(tab: 'all'): void { this.productsTab.set(tab); } diff --git a/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.html b/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.html index 34a6c02..4a6ebb5 100644 --- a/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.html +++ b/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.html @@ -83,7 +83,7 @@

Pricing

[limit]="10" [queryParams]="priceQueryParams()" [paginationEnabled]="false" - (rowClick)="OnPriceListClick($event)" + rowLinkPrefix="/account/prices" >
diff --git a/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.ts b/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.ts index d675889..a4d752e 100644 --- a/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.ts +++ b/apps/web/src/app/features/account/products/views/product-detail/product-detail.component.ts @@ -260,8 +260,4 @@ export class ProductDetailComponent implements OnInit, OnDestroy { const p = this.product(); if (p) this.priceActions.OpenCreate(p.id); } - - OnPriceListClick(price: Price): void { - this.router.navigate(['/account/prices', price.id]); - } } diff --git a/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.html b/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.html index 8ca29d7..86ca7a4 100644 --- a/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.html +++ b/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.html @@ -247,7 +247,7 @@

Invoices

[queryParams]="invoiceQueryParams()" [expand]="invoiceExpand()" [paginationEnabled]="false" - (rowClick)="OnInvoiceClick($event)" + rowLinkPrefix="/account/invoices" >
diff --git a/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.ts b/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.ts index b23b61c..eb1a5aa 100644 --- a/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.ts +++ b/apps/web/src/app/features/account/subscriptions/views/subscription-detail/subscription-detail.component.ts @@ -9,7 +9,7 @@ import { WritableSignal, } from '@angular/core'; import { DatePipe, DecimalPipe } from '@angular/common'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { ActivatedRoute, RouterLink } from '@angular/router'; import type { Invoice, Subscription } from '@zoneless/shared-types'; import { Subscription as RxSubscription } from 'rxjs'; import { SubscriptionService } from '../../../../../data'; @@ -76,7 +76,6 @@ import { }) export class SubscriptionDetailComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); - private readonly router = inject(Router); private readonly subscriptionService = inject(SubscriptionService); private readonly metaService = inject(MetaService); readonly actions = inject(SubscriptionActionsService); @@ -289,10 +288,6 @@ export class SubscriptionDetailComponent implements OnInit, OnDestroy { this.invoiceQueryParams.set({ subscription: subscriptionId }); } - OnInvoiceClick(invoice: Invoice): void { - this.router.navigate(['/account/invoices', invoice.id]); - } - OnEditMetadata(): void { const subscription = this.subscription(); if (subscription) this.actions.OpenEditMetadata(subscription); diff --git a/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.html b/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.html index 53ec861..94f1b7c 100644 --- a/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.html +++ b/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.html @@ -68,7 +68,7 @@

Subscriptions

[limit]="10" [queryParams]="subscriptionsQueryParams()" [expand]="subscriptionsExpand()" - (rowClick)="OnSubscriptionClick($event)" + rowLinkPrefix="/account/subscriptions" > diff --git a/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.ts b/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.ts index a307a05..43a8477 100644 --- a/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.ts +++ b/apps/web/src/app/features/account/subscriptions/views/subscription-list/subscription-list.component.ts @@ -8,7 +8,6 @@ import { OnDestroy, ViewChild, } from '@angular/core'; -import { Router } from '@angular/router'; import { PaginatedListComponent, PaginatedListColumn, @@ -37,7 +36,6 @@ type SubscriptionsStatusTab = 'active' | 'paused' | 'canceled' | 'all'; }) export class SubscriptionListComponent implements OnInit, OnDestroy { private readonly metaService = inject(MetaService); - private readonly router = inject(Router); private readonly actions = inject(SubscriptionActionsService); @ViewChild('subscriptionsList') subscriptionsList?: PaginatedListComponent; @@ -126,10 +124,6 @@ export class SubscriptionListComponent implements OnInit, OnDestroy { this.SyncSubscriptionsQueryParams(); } - OnSubscriptionClick(subscription: Subscription): void { - this.router.navigate(['/account/subscriptions', subscription.id]); - } - private SyncSubscriptionsQueryParams(): void { const params: Record = {}; switch (this.subscriptionsStatusTab()) { diff --git a/apps/web/src/app/shared/ui/paginated-list/paginated-list.component.html b/apps/web/src/app/shared/ui/paginated-list/paginated-list.component.html index b6f2dbc..534cb11 100644 --- a/apps/web/src/app/shared/ui/paginated-list/paginated-list.component.html +++ b/apps/web/src/app/shared/ui/paginated-list/paginated-list.component.html @@ -10,10 +10,26 @@ } @if (!loading() && items().length > 0) { @for (item of items(); track - item.id) { - - @for (column of columns; track column.field) { - + item.id) { @let rowLink = GetRowLink(item); + + @for (column of columns; track column.field; let isFirst = $first) { + + @if (rowLink && column.type !== 'actions') { + + }
/** Act on behalf of a connected account (Zoneless-Account header) */ @Input() zonelessAccount = ''; - /** Emits when a row is clicked */ + /** + * Router path prefix for each row (e.g. `/account/customers`). + * When set, rows become real links to `${rowLinkPrefix}/${item.id}` + * so the browser can open them in a new tab. + */ + @Input() rowLinkPrefix = ''; + + /** Emits when a row is clicked (used when the row is not a link) */ @Output() rowClick = new EventEmitter(); loading: WritableSignal = signal(false); @@ -122,7 +130,6 @@ export class PaginatedListComponent pageNumber: WritableSignal = signal(0); initialLoadComplete: WritableSignal = signal(false); totalCount: WritableSignal = signal(0); - openMenuItemId: WritableSignal = signal(null); // Store last item ID of each page for pagination // pageLastItems[N] = last item ID of page N @@ -262,11 +269,17 @@ export class PaginatedListComponent } } + GetRowLink(item: T): string[] | null { + if (!this.rowLinkPrefix) return null; + return [this.rowLinkPrefix, item.id]; + } + + IsRowInteractive(): boolean { + return Boolean(this.rowLinkPrefix) || this.rowClick.observed; + } + OnRowClick(item: T): void { - if (this.openMenuItemId() !== null) { - this.openMenuItemId.set(null); - return; - } + if (this.GetRowLink(item)) return; this.rowClick.emit(item); }