So we can use pagination

This commit is contained in:
shamoon committed 2026-09-05 21:05:07 -07:00
1 parent 26b288f61a
commit ce1dffbbd5
3 files changed
+74 -34

No files matched your search

@@ -149,6 +149,20 @@
</tbody> </tbody>
</table> </table>
</div> </div>
@if (total() > pageSize) {
<div class="d-flex justify-content-end mt-3">
<ngb-pagination
[pageSize]="pageSize"
[collectionSize]="total()"
[page]="page()"
[maxSize]="5"
(pageChange)="setPage($event)"
size="sm"
aria-label="Share link bundles pagination"
i18n-aria-label
></ngb-pagination>
</div>
}
} }
} }
</div> </div>
@@ -13,7 +13,7 @@ import { environment } from 'src/environments/environment'
import { ShareLinkBundleListComponent } from './share-link-bundle-list.component' import { ShareLinkBundleListComponent } from './share-link-bundle-list.component'
class MockShareLinkBundleService { class MockShareLinkBundleService {
listAllBundles = jest.fn() list = jest.fn()
delete = jest.fn() delete = jest.fn()
rebuildBundle = jest.fn() rebuildBundle = jest.fn()
} }
@@ -36,7 +36,7 @@ describe('ShareLinkBundleListComponent', () => {
toastService = new MockToastService() toastService = new MockToastService()
originalApiBaseUrl = environment.apiBaseUrl originalApiBaseUrl = environment.apiBaseUrl
service.listAllBundles.mockReturnValue(of([])) service.list.mockReturnValue(of({ count: 0, results: [] }))
service.delete.mockReturnValue(of(true)) service.delete.mockReturnValue(of(true))
service.rebuildBundle.mockReturnValue(of(sampleBundle())) service.rebuildBundle.mockReturnValue(of(sampleBundle()))
@@ -80,28 +80,28 @@ describe('ShareLinkBundleListComponent', () => {
it('loads bundles on init and polls periodically', () => { it('loads bundles on init and polls periodically', () => {
jest.useFakeTimers() jest.useFakeTimers()
const bundles = [sampleBundle({ status: ShareLinkBundleStatus.Ready })] const bundles = [sampleBundle({ status: ShareLinkBundleStatus.Ready })]
service.listAllBundles.mockReset() service.list.mockReset()
service.listAllBundles service.list
.mockReturnValueOnce(of(bundles)) .mockReturnValueOnce(of({ count: bundles.length, results: bundles }))
.mockReturnValue(of(bundles)) .mockReturnValue(of({ count: bundles.length, results: bundles }))
fixture.detectChanges() fixture.detectChanges()
expect(service.listAllBundles).toHaveBeenCalledTimes(1) expect(service.list).toHaveBeenCalledWith(1, 25, 'created', true)
expect(component.bundles()).toEqual(bundles) expect(component.bundles()).toEqual(bundles)
expect(component.loading()).toBe(false) expect(component.loading()).toBe(false)
expect(component.error()).toBeNull() expect(component.error()).toBeNull()
jest.advanceTimersByTime(5000) jest.advanceTimersByTime(5000)
expect(service.listAllBundles).toHaveBeenCalledTimes(2) expect(service.list).toHaveBeenCalledTimes(2)
}) })
it('handles errors when loading bundles', () => { it('handles errors when loading bundles', () => {
jest.useFakeTimers() jest.useFakeTimers()
service.listAllBundles.mockReset() service.list.mockReset()
service.listAllBundles service.list
.mockReturnValueOnce(throwError(() => new Error('load fail'))) .mockReturnValueOnce(throwError(() => new Error('load fail')))
.mockReturnValue(of([])) .mockReturnValue(of({ count: 0, results: [] }))
fixture.detectChanges() fixture.detectChanges()
@@ -110,7 +110,15 @@ describe('ShareLinkBundleListComponent', () => {
expect(component.loading()).toBe(false) expect(component.loading()).toBe(false)
jest.advanceTimersByTime(5000) jest.advanceTimersByTime(5000)
expect(service.listAllBundles).toHaveBeenCalledTimes(2) expect(service.list).toHaveBeenCalledTimes(2)
})
it('loads another page', () => {
fixture.detectChanges()
component.setPage(2)
expect(service.list).toHaveBeenLastCalledWith(2, 25, 'created', true)
}) })
it('copies bundle links when ready', () => { it('copies bundle links when ready', () => {
@@ -150,7 +158,7 @@ describe('ShareLinkBundleListComponent', () => {
}) })
it('deletes bundles and refreshes list', () => { it('deletes bundles and refreshes list', () => {
service.listAllBundles.mockReturnValue(of([])) service.list.mockReturnValue(of({ count: 0, results: [] }))
service.delete.mockReturnValue(of(true)) service.delete.mockReturnValue(of(true))
fixture.detectChanges() fixture.detectChanges()
@@ -161,12 +169,12 @@ describe('ShareLinkBundleListComponent', () => {
expect(toastService.showInfo).toHaveBeenCalledWith( expect(toastService.showInfo).toHaveBeenCalledWith(
expect.stringContaining('deleted.') expect.stringContaining('deleted.')
) )
expect(service.listAllBundles).toHaveBeenCalledTimes(2) expect(service.list).toHaveBeenCalledTimes(2)
expect(component.loading()).toBe(false) expect(component.loading()).toBe(false)
}) })
it('handles delete errors gracefully', () => { it('handles delete errors gracefully', () => {
service.listAllBundles.mockReturnValue(of([])) service.list.mockReturnValue(of({ count: 0, results: [] }))
service.delete.mockReturnValue(throwError(() => new Error('delete fail'))) service.delete.mockReturnValue(throwError(() => new Error('delete fail')))
fixture.detectChanges() fixture.detectChanges()
@@ -178,7 +186,7 @@ describe('ShareLinkBundleListComponent', () => {
}) })
it('retries bundle build and replaces existing entry', () => { it('retries bundle build and replaces existing entry', () => {
service.listAllBundles.mockReturnValue(of([])) service.list.mockReturnValue(of({ count: 0, results: [] }))
const updated = sampleBundle({ status: ShareLinkBundleStatus.Ready }) const updated = sampleBundle({ status: ShareLinkBundleStatus.Ready })
service.rebuildBundle.mockReturnValue(of(updated)) service.rebuildBundle.mockReturnValue(of(updated))
@@ -193,7 +201,7 @@ describe('ShareLinkBundleListComponent', () => {
}) })
it('adds new bundle when retry returns unknown entry', () => { it('adds new bundle when retry returns unknown entry', () => {
service.listAllBundles.mockReturnValue(of([])) service.list.mockReturnValue(of({ count: 0, results: [] }))
service.rebuildBundle.mockReturnValue( service.rebuildBundle.mockReturnValue(
of(sampleBundle({ id: 99, slug: 'new-slug' })) of(sampleBundle({ id: 99, slug: 'new-slug' }))
) )
@@ -207,7 +215,7 @@ describe('ShareLinkBundleListComponent', () => {
}) })
it('handles retry errors', () => { it('handles retry errors', () => {
service.listAllBundles.mockReturnValue(of([])) service.list.mockReturnValue(of({ count: 0, results: [] }))
service.rebuildBundle.mockReturnValue(throwError(() => new Error('fail'))) service.rebuildBundle.mockReturnValue(throwError(() => new Error('fail')))
fixture.detectChanges() fixture.detectChanges()
@@ -218,7 +226,7 @@ describe('ShareLinkBundleListComponent', () => {
}) })
it('maps status and file version helpers', () => { it('maps status and file version helpers', () => {
service.listAllBundles.mockReturnValue(of([])) service.list.mockReturnValue(of({ count: 0, results: [] }))
fixture.detectChanges() fixture.detectChanges()
expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain( expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain(
@@ -1,7 +1,10 @@
import { Clipboard } from '@angular/cdk/clipboard' import { Clipboard } from '@angular/cdk/clipboard'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core' import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import {
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs' import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
import { FileVersion } from 'src/app/data/share-link' import { FileVersion } from 'src/app/data/share-link'
@@ -25,6 +28,7 @@ import { LoadingComponentWithPermissions } from 'src/app/components/loading-comp
imports: [ imports: [
ConfirmButtonComponent, ConfirmButtonComponent,
CommonModule, CommonModule,
NgbPaginationModule,
NgbPopoverModule, NgbPopoverModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
FileSizePipe, FileSizePipe,
@@ -41,6 +45,9 @@ export class ShareLinkBundleListComponent
readonly bundles = signal<ShareLinkBundleSummary[]>([]) readonly bundles = signal<ShareLinkBundleSummary[]>([])
readonly error = signal<string | null>(null) readonly error = signal<string | null>(null)
readonly copiedSlug = signal<string | null>(null) readonly copiedSlug = signal<string | null>(null)
readonly total = signal(0)
readonly page = signal(1)
readonly pageSize = 25
readonly statuses = ShareLinkBundleStatus readonly statuses = ShareLinkBundleStatus
readonly fileVersions = FileVersion readonly fileVersions = FileVersion
@@ -55,25 +62,28 @@ export class ShareLinkBundleListComponent
this.loading.set(true) this.loading.set(true)
} }
this.error.set(null) this.error.set(null)
return this.shareLinkBundleService.listAllBundles().pipe( return this.shareLinkBundleService
catchError((error) => { .list(this.page(), this.pageSize, 'created', true)
if (!silent) { .pipe(
this.loading.set(false) catchError((error) => {
} if (!silent) {
this.error.set($localize`Failed to load share link bundles.`) this.loading.set(false)
this.toastService.showError( }
$localize`Error retrieving share link bundles.`, this.error.set($localize`Failed to load share link bundles.`)
error this.toastService.showError(
) $localize`Error retrieving share link bundles.`,
return of(null) error
}) )
) return of(null)
})
)
}), }),
takeUntil(this.unsubscribeNotifier) takeUntil(this.unsubscribeNotifier)
) )
.subscribe((results) => { .subscribe((results) => {
if (results) { if (results) {
this.bundles.set(results) this.bundles.set(results.results)
this.total.set(results.count)
this.copiedSlug.set(null) this.copiedSlug.set(null)
} }
this.loading.set(false) this.loading.set(false)
@@ -96,6 +106,11 @@ export class ShareLinkBundleListComponent
}` }`
} }
setPage(page: number): void {
this.page.set(page)
this.triggerRefresh(false)
}
copy(bundle: ShareLinkBundleSummary): void { copy(bundle: ShareLinkBundleSummary): void {
if (bundle.status !== ShareLinkBundleStatus.Ready) { if (bundle.status !== ShareLinkBundleStatus.Ready) {
return return
@@ -114,6 +129,9 @@ export class ShareLinkBundleListComponent
this.loading.set(true) this.loading.set(true)
this.shareLinkBundleService.delete(bundle).subscribe({ this.shareLinkBundleService.delete(bundle).subscribe({
next: () => { next: () => {
if (this.bundles().length === 1 && this.page() > 1) {
this.page.update((page) => page - 1)
}
this.toastService.showInfo($localize`Share link bundle deleted.`) this.toastService.showInfo($localize`Share link bundle deleted.`)
this.triggerRefresh(false) this.triggerRefresh(false)
}, },