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",