From 5903e47b34c494f03d9bbba9d2323bdba04c1a2b Mon Sep 17 00:00:00 2001 From: Andriy Sheredko Date: Thu, 8 Jan 2026 22:54:28 +0100 Subject: [PATCH 1/2] fix(addons): Fix GitLab pagination Load More button not showing --- .../storage-item-selector.component.spec.ts | 142 ++++++++++++++++++ .../storage-item-selector.component.ts | 4 +- 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts index 2e3797ea6..dd307274f 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts @@ -2,9 +2,11 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { DialogService } from 'primeng/dynamicdialog'; +import { signal, WritableSignal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { OperationNames } from '@shared/enums/operation-names.enum'; +import { OperationInvocation } from '@shared/models/addons/operation-invocation.model'; import { AddonsSelectors } from '@shared/stores/addons'; import { GoogleFilePickerComponent } from '../../google-file-picker/google-file-picker.component'; @@ -115,4 +117,144 @@ describe('StorageItemSelectorComponent', () => { expect(breadcrumbs[0].id).toBe(itemId); expect(breadcrumbs[0].label).toBe(itemName); }); + + describe('showLoadMoreButton', () => { + let mockOperationInvocation: WritableSignal; + + beforeEach(async () => { + mockDialogService = DialogServiceMockBuilder.create().withOpenMock().build(); + mockOperationInvocation = signal(null); + + await TestBed.resetTestingModule() + .configureTestingModule({ + imports: [ + StorageItemSelectorComponent, + OSFTestingModule, + ...MockComponents(GoogleFilePickerComponent, SelectComponent), + ], + providers: [ + provideMockStore({ + signals: [ + { + selector: AddonsSelectors.getSelectedStorageItem, + value: null, + }, + { + selector: AddonsSelectors.getOperationInvocationSubmitting, + value: false, + }, + { + selector: AddonsSelectors.getCreatedOrUpdatedConfiguredAddonSubmitting, + value: false, + }, + { + selector: AddonsSelectors.getOperationInvocation, + value: mockOperationInvocation, + }, + ], + }), + MockProvider(DialogService, mockDialogService), + ], + }) + .compileComponents(); + + fixture = TestBed.createComponent(StorageItemSelectorComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('isGoogleFilePicker', false); + fixture.componentRef.setInput('accountName', 'test-account'); + fixture.componentRef.setInput('accountId', 'test-id'); + fixture.componentRef.setInput('operationInvocationResult', []); + }); + + it('should return false when operationInvocation is null', () => { + mockOperationInvocation.set(null); + fixture.detectChanges(); + + expect(component.showLoadMoreButton()).toBe(false); + }); + + it('should return false when nextSampleCursor is not present', () => { + mockOperationInvocation.set({ + id: 'test-id', + type: 'operation-invocation', + invocationStatus: 'success', + operationName: 'list_root_items', + operationKwargs: {}, + operationResult: [], + itemCount: 10, + thisSampleCursor: 'cursor-1', + }); + fixture.detectChanges(); + + expect(component.showLoadMoreButton()).toBe(false); + }); + + it('should return true when nextSampleCursor differs from thisSampleCursor', () => { + mockOperationInvocation.set({ + id: 'test-id', + type: 'operation-invocation', + invocationStatus: 'success', + operationName: 'list_root_items', + operationKwargs: {}, + operationResult: [], + itemCount: 20, + thisSampleCursor: 'cursor-1', + nextSampleCursor: 'cursor-2', + }); + fixture.detectChanges(); + + expect(component.showLoadMoreButton()).toBe(true); + }); + + it('should return true for opaque/base64 cursors like GitLab uses', () => { + // GitLab uses base64-encoded cursors where lexicographic comparison doesn't work + mockOperationInvocation.set({ + id: 'test-id', + type: 'operation-invocation', + invocationStatus: 'success', + operationName: 'list_root_items', + operationKwargs: {}, + operationResult: [], + itemCount: 20, + thisSampleCursor: 'eyJpZCI6MTIzfQ==', + nextSampleCursor: 'eyJpZCI6MTQ1fQ==', + }); + fixture.detectChanges(); + + expect(component.showLoadMoreButton()).toBe(true); + }); + + it('should return false when nextSampleCursor equals thisSampleCursor', () => { + mockOperationInvocation.set({ + id: 'test-id', + type: 'operation-invocation', + invocationStatus: 'success', + operationName: 'list_root_items', + operationKwargs: {}, + operationResult: [], + itemCount: 10, + thisSampleCursor: 'cursor-1', + nextSampleCursor: 'cursor-1', + }); + fixture.detectChanges(); + + expect(component.showLoadMoreButton()).toBe(false); + }); + + it('should return true when nextSampleCursor exists but thisSampleCursor is undefined', () => { + mockOperationInvocation.set({ + id: 'test-id', + type: 'operation-invocation', + invocationStatus: 'success', + operationName: 'list_root_items', + operationKwargs: {}, + operationResult: [], + itemCount: 20, + nextSampleCursor: 'cursor-2', + }); + fixture.detectChanges(); + + expect(component.showLoadMoreButton()).toBe(true); + }); + }); }); diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts index 597e887e2..27b66d12d 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts @@ -193,10 +193,10 @@ export class StorageItemSelectorComponent implements OnInit { readonly showLoadMoreButton = computed(() => { const invocation = this.operationInvocation(); - if (!invocation?.nextSampleCursor || !invocation?.thisSampleCursor) { + if (!invocation?.nextSampleCursor) { return false; } - return invocation.nextSampleCursor > invocation.thisSampleCursor; + return invocation.nextSampleCursor !== invocation.thisSampleCursor; }); handleCreateOperationInvocation( From bc63dc888c788b6d267361190c5ad4768f2a7370 Mon Sep 17 00:00:00 2001 From: Andriy Sheredko Date: Fri, 23 Jan 2026 16:44:38 +0200 Subject: [PATCH 2/2] fix(addons): Fix GitLab pagination Load More button not showing --- .../storage-item-selector.component.spec.ts | 57 ++++--------------- 1 file changed, 10 insertions(+), 47 deletions(-) diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts index dd307274f..f3961bd7d 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts @@ -22,9 +22,11 @@ describe('StorageItemSelectorComponent', () => { let component: StorageItemSelectorComponent; let fixture: ComponentFixture; let mockDialogService: ReturnType; + let mockOperationInvocation: WritableSignal; beforeEach(async () => { mockDialogService = DialogServiceMockBuilder.create().withOpenMock().build(); + mockOperationInvocation = signal(null); await TestBed.configureTestingModule({ imports: [ @@ -47,6 +49,10 @@ describe('StorageItemSelectorComponent', () => { selector: AddonsSelectors.getCreatedOrUpdatedConfiguredAddonSubmitting, value: false, }, + { + selector: AddonsSelectors.getOperationInvocation, + value: mockOperationInvocation, + }, ], }), MockProvider(DialogService, mockDialogService), @@ -55,6 +61,10 @@ describe('StorageItemSelectorComponent', () => { fixture = TestBed.createComponent(StorageItemSelectorComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('isGoogleFilePicker', false); + fixture.componentRef.setInput('accountName', 'test-account'); + fixture.componentRef.setInput('accountId', 'test-id'); + fixture.componentRef.setInput('operationInvocationResult', []); }); it('should create', () => { @@ -119,53 +129,6 @@ describe('StorageItemSelectorComponent', () => { }); describe('showLoadMoreButton', () => { - let mockOperationInvocation: WritableSignal; - - beforeEach(async () => { - mockDialogService = DialogServiceMockBuilder.create().withOpenMock().build(); - mockOperationInvocation = signal(null); - - await TestBed.resetTestingModule() - .configureTestingModule({ - imports: [ - StorageItemSelectorComponent, - OSFTestingModule, - ...MockComponents(GoogleFilePickerComponent, SelectComponent), - ], - providers: [ - provideMockStore({ - signals: [ - { - selector: AddonsSelectors.getSelectedStorageItem, - value: null, - }, - { - selector: AddonsSelectors.getOperationInvocationSubmitting, - value: false, - }, - { - selector: AddonsSelectors.getCreatedOrUpdatedConfiguredAddonSubmitting, - value: false, - }, - { - selector: AddonsSelectors.getOperationInvocation, - value: mockOperationInvocation, - }, - ], - }), - MockProvider(DialogService, mockDialogService), - ], - }) - .compileComponents(); - - fixture = TestBed.createComponent(StorageItemSelectorComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('isGoogleFilePicker', false); - fixture.componentRef.setInput('accountName', 'test-account'); - fixture.componentRef.setInput('accountId', 'test-id'); - fixture.componentRef.setInput('operationInvocationResult', []); - }); - it('should return false when operationInvocation is null', () => { mockOperationInvocation.set(null); fixture.detectChanges();