From 6d5ff221555c8856c2e768121d85f28c1ab2b485 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Thu, 9 Oct 2025 11:16:08 +0300 Subject: [PATCH] feat(ang-858): addons pagination --- .../configure-addon.component.html | 3 ++ .../configure-addon.component.ts | 18 ++++++++++ .../connect-configured-addon.component.html | 3 ++ .../connect-configured-addon.component.ts | 17 +++++++++ .../storage-item-selector.component.html | 10 ++++++ .../storage-item-selector.component.ts | 23 ++++++++++++ src/app/shared/mappers/addon.mapper.ts | 10 ++++++ .../addon-operations-json-api.models.ts | 3 ++ .../models/addons/addon-utils.models.ts | 1 + .../addons/operation-invocation.model.ts | 3 ++ .../addon-operation-invocation.service.ts | 35 +++++++++++++------ src/app/shared/stores/addons/addons.state.ts | 13 ++++++- src/assets/i18n/en.json | 3 +- 13 files changed, 129 insertions(+), 13 deletions(-) diff --git a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html index 012a4cb9a..ea7895a9b 100644 --- a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html +++ b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html @@ -76,6 +76,9 @@

[currentAddonType]="addonTypeString()" [supportedResourceTypes]="supportedResourceTypes()" (operationInvoke)="handleCreateOperationInvocation($event.operationName, $event.itemId)" + (operationInvokeWithCursor)=" + handleCreateOperationInvocationWithCursor($event.operationName, $event.itemId, $event.pageCursor) + " [(selectedStorageItemId)]="selectedStorageItemId" [(selectedStorageItemUrl)]="selectedStorageItemUrl" [(selectedResourceType)]="selectedResourceType" diff --git a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts index 325515090..17bfd6e70 100644 --- a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts +++ b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts @@ -161,6 +161,24 @@ export class ConfigureAddonComponent implements OnInit { this.actions.createAddonOperationInvocation(payload); } + handleCreateOperationInvocationWithCursor( + operationName: OperationNames, + folderId: string, + pageCursor?: string + ): void { + const addon = this.addon(); + if (!addon) return; + + const payload = this.operationInvocationService.createOperationInvocationPayload( + addon, + operationName, + folderId, + pageCursor + ); + + this.actions.createAddonOperationInvocation(payload); + } + ngOnInit(): void { this.handleCreateOperationInvocation(OperationNames.GET_ITEM_INFO, this.selectedStorageItemId()); } diff --git a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html index 3ad785625..b3a940436 100644 --- a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html +++ b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html @@ -135,6 +135,9 @@

{{ 'settings.addons.connectAddon.configure' | translate }} {{ a [(selectedStorageItemUrl)]="selectedStorageItemUrl" [(selectedResourceType)]="selectedResourceType" (operationInvoke)="handleCreateOperationInvocation($event.operationName, $event.itemId)" + (operationInvokeWithCursor)=" + handleCreateOperationInvocationWithCursor($event.operationName, $event.itemId, $event.pageCursor) + " (save)="handleCreateConfiguredAddon()" (cancelSelection)="handleNavigateToAccountSelection()" /> diff --git a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts index 22ecd4278..2c11cabb6 100644 --- a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts +++ b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts @@ -323,6 +323,23 @@ export class ConnectConfiguredAddonComponent { this.actions.createAddonOperationInvocation(payload); } + handleCreateOperationInvocationWithCursor(operationName: OperationNames, itemId: string, pageCursor?: string): void { + const selectedAccount = this.currentAuthorizedAddonAccounts().find( + (account) => account.id === this.chosenAccountId() + ); + + if (!selectedAccount) return; + + const payload = this.operationInvocationService.createInitialOperationInvocationPayload( + operationName, + selectedAccount, + itemId, + pageCursor + ); + + this.actions.createAddonOperationInvocation(payload); + } + handleNavigateToAccountSelection(): void { this.resetConfigurationForm(); this.stepper()?.value.set(ProjectAddonsStepperValue.CHOOSE_ACCOUNT); diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html index 67b0911a2..305f2d0ba 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html @@ -104,6 +104,16 @@

{{ 'settings.addons.configureAddon.selectFolder' | translate }}

{{ 'settings.addons.configureAddon.noFolders' | translate }}

} + @if (showLoadMoreButton() && !isOperationInvocationSubmitting()) { +
+ +
+ } } 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 4b58ec292..6d36a4dfb 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 @@ -75,6 +75,7 @@ export class StorageItemSelectorComponent implements OnInit { selectedStorageItemId = model('/'); selectedStorageItemUrl = model(''); operationInvoke = output(); + operationInvokeWithCursor = output(); save = output(); cancelSelection = output(); readonly OperationNames = OperationNames; @@ -109,6 +110,7 @@ export class StorageItemSelectorComponent implements OnInit { initiallySelectedStorageItem = select(AddonsSelectors.getSelectedStorageItem); isOperationInvocationSubmitting = select(AddonsSelectors.getOperationInvocationSubmitting); isSubmitting = select(AddonsSelectors.getCreatedOrUpdatedConfiguredAddonSubmitting); + operationInvocation = select(AddonsSelectors.getOperationInvocation); readonly homeBreadcrumb: MenuItem = { id: '/', label: this.translateService.instant('settings.addons.configureAddon.home'), @@ -180,6 +182,14 @@ export class StorageItemSelectorComponent implements OnInit { return this.hasInputChanged() || this.hasFolderChanged() || this.hasResourceTypeChanged(); }); + readonly showLoadMoreButton = computed(() => { + const invocation = this.operationInvocation(); + if (!invocation?.nextSampleCursor || !invocation?.thisSampleCursor) { + return false; + } + return invocation.nextSampleCursor > invocation.thisSampleCursor; + }); + handleCreateOperationInvocation( operationName: OperationNames, itemId: string, @@ -196,6 +206,19 @@ export class StorageItemSelectorComponent implements OnInit { this.trimBreadcrumbs(itemId); } + handleLoadMore(): void { + const invocation = this.operationInvocation(); + if (!invocation?.nextSampleCursor) { + return; + } + + this.operationInvokeWithCursor.emit({ + operationName: invocation.operationName as OperationNames, + itemId: invocation.operationKwargs.itemId || '/', + pageCursor: invocation.nextSampleCursor, + }); + } + handleSave(): void { this.selectedStorageItemId.set(this.selectedStorageItem()?.itemId || ''); this.selectedStorageItemUrl.set(this.selectedStorageItem()?.itemLink || ''); diff --git a/src/app/shared/mappers/addon.mapper.ts b/src/app/shared/mappers/addon.mapper.ts index c1b0412e4..81f5ac104 100644 --- a/src/app/shared/mappers/addon.mapper.ts +++ b/src/app/shared/mappers/addon.mapper.ts @@ -120,6 +120,15 @@ export class AddonMapper { mayContainRootCandidates: operationResult.may_contain_root_candidates ?? isLinkAddon, }, ]; + + const cursors = isOperationResult + ? { + ...(operationResult.this_sample_cursor && { thisSampleCursor: operationResult.this_sample_cursor }), + ...(operationResult.first_sample_cursor && { firstSampleCursor: operationResult.first_sample_cursor }), + ...(operationResult.next_sample_cursor && { nextSampleCursor: operationResult.next_sample_cursor }), + } + : {}; + return { type: response.type, id: response.id, @@ -131,6 +140,7 @@ export class AddonMapper { }, itemCount: isOperationResult ? operationResult.total_count : 0, operationResult: mappedOperationResult, + ...cursors, }; } } diff --git a/src/app/shared/models/addons/addon-operations-json-api.models.ts b/src/app/shared/models/addons/addon-operations-json-api.models.ts index 890cf73dd..7961ef7bb 100644 --- a/src/app/shared/models/addons/addon-operations-json-api.models.ts +++ b/src/app/shared/models/addons/addon-operations-json-api.models.ts @@ -10,6 +10,9 @@ export interface StorageItemResponseJsonApi { export interface OperationResultJsonApi { items: StorageItemResponseJsonApi[]; total_count: number; + this_sample_cursor?: string; + first_sample_cursor?: string; + next_sample_cursor?: string; } export interface OperationInvocationRequestJsonApi { diff --git a/src/app/shared/models/addons/addon-utils.models.ts b/src/app/shared/models/addons/addon-utils.models.ts index 1de52af7d..ac17caac5 100644 --- a/src/app/shared/models/addons/addon-utils.models.ts +++ b/src/app/shared/models/addons/addon-utils.models.ts @@ -42,4 +42,5 @@ export interface OAuthCallbacks { export interface OperationInvokeData { operationName: OperationNames; itemId: string; + pageCursor?: string; } diff --git a/src/app/shared/models/addons/operation-invocation.model.ts b/src/app/shared/models/addons/operation-invocation.model.ts index 964f3ea2e..c12943986 100644 --- a/src/app/shared/models/addons/operation-invocation.model.ts +++ b/src/app/shared/models/addons/operation-invocation.model.ts @@ -11,4 +11,7 @@ export interface OperationInvocation { }; operationResult: StorageItem[]; itemCount: number; + thisSampleCursor?: string; + firstSampleCursor?: string; + nextSampleCursor?: string; } diff --git a/src/app/shared/services/addons/addon-operation-invocation.service.ts b/src/app/shared/services/addons/addon-operation-invocation.service.ts index 15526dce9..e252fc038 100644 --- a/src/app/shared/services/addons/addon-operation-invocation.service.ts +++ b/src/app/shared/services/addons/addon-operation-invocation.service.ts @@ -27,10 +27,11 @@ export class AddonOperationInvocationService { createInitialOperationInvocationPayload( operationName: OperationNames, selectedAccount: AuthorizedAccountModel, - itemId?: string + itemId?: string, + pageCursor?: string ): OperationInvocationRequestJsonApi { const addonSpecificOperationName = this.getAddonSpecificOperationName(operationName, selectedAccount); - const operationKwargs = this.getOperationKwargs(addonSpecificOperationName, itemId); + const operationKwargs = this.getOperationKwargs(addonSpecificOperationName, itemId, pageCursor); return { data: { @@ -58,10 +59,11 @@ export class AddonOperationInvocationService { createOperationInvocationPayload( addon: ConfiguredAddonModel, operationName: OperationNames, - itemId: string + itemId: string, + pageCursor?: string ): OperationInvocationRequestJsonApi { const addonSpecificOperationName = this.getAddonSpecificOperationName(operationName, addon); - const operationKwargs = this.getOperationKwargs(addonSpecificOperationName, itemId); + const operationKwargs = this.getOperationKwargs(addonSpecificOperationName, itemId, pageCursor); return { data: { @@ -86,20 +88,31 @@ export class AddonOperationInvocationService { }; } - private getOperationKwargs(operationName: OperationNames, itemId?: string): Record { + private getOperationKwargs( + operationName: OperationNames, + itemId?: string, + pageCursor?: string + ): Record { const isRootOperation = operationName === OperationNames.LIST_ROOT_ITEMS || operationName === OperationNames.LIST_ROOT_COLLECTIONS; - if (!itemId || isRootOperation) { - return {}; + const baseKwargs: Record = {}; + + if (itemId && !isRootOperation) { + baseKwargs['item_id'] = itemId; } const isChildOperation = operationName === OperationNames.LIST_CHILD_ITEMS || operationName === OperationNames.LIST_COLLECTION_ITEMS; - return { - item_id: itemId, - ...(isChildOperation && { item_type: 'FOLDER' }), - }; + if (isChildOperation) { + baseKwargs['item_type'] = 'FOLDER'; + } + + if (pageCursor) { + baseKwargs['page_cursor'] = pageCursor; + } + + return baseKwargs; } } diff --git a/src/app/shared/stores/addons/addons.state.ts b/src/app/shared/stores/addons/addons.state.ts index 25d4b305f..c3c7fd098 100644 --- a/src/app/shared/stores/addons/addons.state.ts +++ b/src/app/shared/stores/addons/addons.state.ts @@ -559,9 +559,20 @@ export class AddonsState { return this.addonsService.createAddonOperationInvocation(action.payload).pipe( tap((response) => { + const isLoadMore = !!action.payload.data.attributes.operation_kwargs['page_cursor']; + const existingData = state.operationInvocation.data; + const shouldMerge = isLoadMore && existingData; + + const mergedResponse = shouldMerge + ? { + ...response, + operationResult: [...existingData.operationResult, ...response.operationResult], + } + : response; + ctx.patchState({ operationInvocation: { - data: response, + data: mergedResponse, isLoading: false, isSubmitting: false, error: null, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 313844ed2..e4d4434b4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -56,7 +56,8 @@ "selectAll": "Select All", "removeAll": "Remove All", "accept": "Accept", - "reject": "Reject" + "reject": "Reject", + "loadMore": "Load more" }, "accessibility": { "help": "Help",