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 b3a940436..b775a6474 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
@@ -11,7 +11,7 @@
-
+
@@ -33,17 +33,29 @@
[routerLink]="[baseUrl() + '/addons']"
data-test-addon-cancel-button
>
-
+ @if (addonTypeString() === AddonType.REDIRECT) {
+
+ } @else {
+
+ }
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 26d157f23..6f6bcd880 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
@@ -146,6 +146,17 @@ export class ConnectConfiguredAddonComponent {
addonTypeString = computed(() => getAddonTypeString(this.addon()) as AddonType);
+ redirectUrl = computed(() => {
+ const addon = this.addon();
+ if (!addon || !addon.redirectUrl) {
+ return null;
+ }
+ const openURL = new URL(addon.redirectUrl);
+ openURL.searchParams.set('nodeIri', this.resourceUri());
+ openURL.searchParams.set('userIri', this.addonsUserReference()[0]?.attributes.user_uri);
+ return openURL.toString();
+ });
+
readonly baseUrl = computed(() => {
const currentUrl = this.router.url;
return currentUrl.split('/addons')[0];
@@ -255,6 +266,22 @@ export class ConnectConfiguredAddonComponent {
});
}
+ goToService() {
+ if (!this.redirectUrl()) return;
+
+ const newWindow = window.open(
+ this.redirectUrl()!.toString(),
+ '_blank',
+ 'popup,width=600,height=600,scrollbars=yes,resizable=yes'
+ );
+ if (newWindow) {
+ this.router.navigate([`${this.baseUrl()}/addons`]);
+ newWindow.focus();
+ } else {
+ this.toastService.showError('addons.redirect.popUpError');
+ }
+ }
+
private getDataForAccountCheck() {
const addonType = this.addonTypeString();
const referenceId = this.userReferenceId();
diff --git a/src/app/features/project/project-addons/project-addons.component.ts b/src/app/features/project/project-addons/project-addons.component.ts
index 5ad91d482..564163c60 100644
--- a/src/app/features/project/project-addons/project-addons.component.ts
+++ b/src/app/features/project/project-addons/project-addons.component.ts
@@ -47,6 +47,7 @@ import {
GetConfiguredLinkAddons,
GetConfiguredStorageAddons,
GetLinkAddons,
+ GetRedirectAddons,
GetStorageAddons,
} from '@shared/stores/addons';
import { CurrentResourceSelectors } from '@shared/stores/current-resource';
@@ -91,6 +92,7 @@ export class ProjectAddonsComponent implements OnInit {
storageAddons = select(AddonsSelectors.getStorageAddons);
citationAddons = select(AddonsSelectors.getCitationAddons);
linkAddons = select(AddonsSelectors.getLinkAddons);
+ redirectAddons = select(AddonsSelectors.getRedirectAddons);
configuredStorageAddons = select(AddonsSelectors.getConfiguredStorageAddons);
configuredCitationAddons = select(AddonsSelectors.getConfiguredCitationAddons);
configuredLinkAddons = select(AddonsSelectors.getConfiguredLinkAddons);
@@ -100,6 +102,8 @@ export class ProjectAddonsComponent implements OnInit {
isResourceReferenceLoading = select(AddonsSelectors.getAddonsResourceReferenceLoading);
isStorageAddonsLoading = select(AddonsSelectors.getStorageAddonsLoading);
isCitationAddonsLoading = select(AddonsSelectors.getCitationAddonsLoading);
+ isLinkAddonsLoading = select(AddonsSelectors.getLinkAddonsLoading);
+ isRedirectAddonsLoading = select(AddonsSelectors.getRedirectAddonsLoading);
isConfiguredStorageAddonsLoading = select(AddonsSelectors.getConfiguredStorageAddonsLoading);
isConfiguredCitationAddonsLoading = select(AddonsSelectors.getConfiguredCitationAddonsLoading);
isConfiguredLinkAddonsLoading = select(AddonsSelectors.getConfiguredLinkAddonsLoading);
@@ -108,6 +112,7 @@ export class ProjectAddonsComponent implements OnInit {
this.isStorageAddonsLoading() ||
this.isCitationAddonsLoading() ||
this.isLinkAddonsLoading() ||
+ this.isRedirectAddonsLoading() ||
this.isUserReferenceLoading() ||
this.isCurrentUserLoading()
);
@@ -135,8 +140,6 @@ export class ProjectAddonsComponent implements OnInit {
return categoryLoading || this.isResourceReferenceLoading() || this.isCurrentUserLoading();
});
- isLinkAddonsLoading = select(AddonsSelectors.getLinkAddonsLoading);
-
currentAddonsLoading = computed(() => {
switch (this.selectedCategory()) {
case AddonCategory.EXTERNAL_STORAGE_SERVICES:
@@ -145,6 +148,8 @@ export class ProjectAddonsComponent implements OnInit {
return this.isCitationAddonsLoading();
case AddonCategory.EXTERNAL_LINK_SERVICES:
return this.isLinkAddonsLoading();
+ case AddonCategory.EXTERNAL_REDIRECT_SERVICES:
+ return this.isRedirectAddonsLoading();
default:
return this.isStorageAddonsLoading();
}
@@ -158,6 +163,7 @@ export class ProjectAddonsComponent implements OnInit {
getStorageAddons: GetStorageAddons,
getCitationAddons: GetCitationAddons,
getLinkAddons: GetLinkAddons,
+ getRedirectAddons: GetRedirectAddons,
getConfiguredStorageAddons: GetConfiguredStorageAddons,
getConfiguredCitationAddons: GetConfiguredCitationAddons,
getConfiguredLinkAddons: GetConfiguredLinkAddons,
@@ -221,6 +227,8 @@ export class ProjectAddonsComponent implements OnInit {
return this.actions.getCitationAddons;
case AddonCategory.EXTERNAL_LINK_SERVICES:
return this.actions.getLinkAddons;
+ case AddonCategory.EXTERNAL_REDIRECT_SERVICES:
+ return this.actions.getRedirectAddons;
default:
return this.actions.getStorageAddons;
}
@@ -234,6 +242,8 @@ export class ProjectAddonsComponent implements OnInit {
return this.citationAddons();
case AddonCategory.EXTERNAL_LINK_SERVICES:
return this.linkAddons();
+ case AddonCategory.EXTERNAL_REDIRECT_SERVICES:
+ return this.redirectAddons();
default:
return this.storageAddons();
}
diff --git a/src/app/shared/components/addons/addon-terms/addon-terms.component.html b/src/app/shared/components/addons/addon-terms/addon-terms.component.html
index fb204c88f..80a0cfb42 100644
--- a/src/app/shared/components/addons/addon-terms/addon-terms.component.html
+++ b/src/app/shared/components/addons/addon-terms/addon-terms.component.html
@@ -1,24 +1,37 @@
-
-
-
- |
- {{ 'settings.addons.connectAddon.table.function' | translate }}
- |
-
- {{ 'settings.addons.connectAddon.table.status' | translate }}
- |
-
-
-
-
- | {{ term.function }} |
- {{ term.status }} |
-
-
-
+@if (isRedirectService()) {
+
+ {{ 'settings.addons.connectAddon.redirectAddons.terms' | translate }}
+
+
+} @else {
+
+
+
+ |
+ {{ 'settings.addons.connectAddon.table.function' | translate }}
+ |
+
+ {{ 'settings.addons.connectAddon.table.status' | translate }}
+ |
+
+
+
+
+ | {{ term.function }} |
+ {{ term.status }} |
+
+
+
+}
diff --git a/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts b/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts
index 60ded4b2f..5138bd785 100644
--- a/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts
+++ b/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts
@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ADDON_TERMS } from '@osf/shared/constants/addon-terms.const';
-import { isCitationAddon } from '@osf/shared/helpers/addon-type.helper';
+import { isCitationAddon, isRedirectAddon } from '@osf/shared/helpers/addon-type.helper';
import { AddonModel } from '@osf/shared/models/addons/addon.model';
import { AddonTerm } from '@osf/shared/models/addons/addon-utils.models';
@@ -12,12 +12,14 @@ import { OSFTestingModule } from '@testing/osf.testing.module';
jest.mock('@shared/helpers/addon-type.helper.ts', () => ({
isCitationAddon: jest.fn(),
+ isRedirectAddon: jest.fn(),
}));
describe('AddonTermsComponent', () => {
let component: AddonTermsComponent;
let fixture: ComponentFixture;
const mockIsCitationAddon = isCitationAddon as jest.MockedFunction;
+ const mockIsRedirectAddon = isRedirectAddon as jest.MockedFunction;
const mockAddon: AddonModel = MOCK_ADDON;
beforeEach(async () => {
@@ -29,6 +31,7 @@ describe('AddonTermsComponent', () => {
component = fixture.componentInstance;
mockIsCitationAddon.mockReturnValue(false);
+ mockIsRedirectAddon.mockReturnValue(false);
});
it('should create', () => {
@@ -212,4 +215,24 @@ describe('AddonTermsComponent', () => {
expect(hasInfoTerm || hasWarningTerm || hasDangerTerm).toBe(true);
});
+
+ it('should handle redirect terms correctly', () => {
+ const redirectAddon: AddonModel = {
+ ...mockAddon,
+ type: 'redirect',
+ };
+
+ mockIsRedirectAddon.mockReturnValue(true);
+ fixture.componentRef.setInput('addon', redirectAddon);
+ fixture.detectChanges();
+
+ const terms = component.terms();
+ expect(terms).toEqual([]);
+
+ const termsElement: HTMLElement = fixture.nativeElement;
+ expect(termsElement.querySelectorAll('tr').length).toBe(0);
+ expect(termsElement.querySelectorAll('p').length).toBe(1);
+ expect(termsElement.querySelectorAll('em').length).toBe(1);
+ expect(termsElement.textContent).toContain('settings.addons.connectAddon.redirectAddons.terms');
+ });
});
diff --git a/src/app/shared/components/addons/addon-terms/addon-terms.component.ts b/src/app/shared/components/addons/addon-terms/addon-terms.component.ts
index 5945f03dd..464a1f5b4 100644
--- a/src/app/shared/components/addons/addon-terms/addon-terms.component.ts
+++ b/src/app/shared/components/addons/addon-terms/addon-terms.component.ts
@@ -6,7 +6,7 @@ import { NgClass } from '@angular/common';
import { Component, computed, input } from '@angular/core';
import { ADDON_TERMS } from '@osf/shared/constants/addon-terms.const';
-import { isCitationAddon } from '@osf/shared/helpers/addon-type.helper';
+import { isCitationAddon, isRedirectAddon } from '@osf/shared/helpers/addon-type.helper';
import { AddonModel } from '@shared/models/addons/addon.model';
import { AddonTerm } from '@shared/models/addons/addon-utils.models';
import { AuthorizedAccountModel } from '@shared/models/addons/authorized-account.model';
@@ -19,6 +19,9 @@ import { AuthorizedAccountModel } from '@shared/models/addons/authorized-account
})
export class AddonTermsComponent {
addon = input(null);
+ redirectUrl = input(null);
+
+ isRedirectService = computed(() => isRedirectAddon(this.addon()));
terms = computed(() => {
const addon = this.addon();
if (!addon) {
@@ -31,6 +34,9 @@ export class AddonTermsComponent {
const supportedFeatures = addon.supportedFeatures || [];
const provider = addon.providerName;
const isCitationService = isCitationAddon(addon);
+ if (isRedirectAddon(addon)) {
+ return [];
+ }
const relevantTerms = isCitationService ? ADDON_TERMS.filter((term) => term.citation) : ADDON_TERMS;
diff --git a/src/app/shared/constants/addons-category-options.const.ts b/src/app/shared/constants/addons-category-options.const.ts
index 0db918573..42232392b 100644
--- a/src/app/shared/constants/addons-category-options.const.ts
+++ b/src/app/shared/constants/addons-category-options.const.ts
@@ -14,4 +14,8 @@ export const ADDON_CATEGORY_OPTIONS: SelectOption[] = [
label: 'settings.addons.categories.linkedServices',
value: AddonCategory.EXTERNAL_LINK_SERVICES,
},
+ {
+ label: 'settings.addons.categories.otherServices',
+ value: AddonCategory.EXTERNAL_REDIRECT_SERVICES,
+ },
];
diff --git a/src/app/shared/enums/addon-type.enum.ts b/src/app/shared/enums/addon-type.enum.ts
index 8b16ce88e..041fd556d 100644
--- a/src/app/shared/enums/addon-type.enum.ts
+++ b/src/app/shared/enums/addon-type.enum.ts
@@ -2,6 +2,7 @@ export enum AddonType {
STORAGE = 'storage',
CITATION = 'citation',
LINK = 'link',
+ REDIRECT = 'redirect', // Redirect addons will not have authorized accounts or configured addons
}
export enum AuthorizedAccountType {
diff --git a/src/app/shared/enums/addons-category.enum.ts b/src/app/shared/enums/addons-category.enum.ts
index c08bb9358..7965c3eb3 100644
--- a/src/app/shared/enums/addons-category.enum.ts
+++ b/src/app/shared/enums/addons-category.enum.ts
@@ -2,4 +2,5 @@ export enum AddonCategory {
EXTERNAL_STORAGE_SERVICES = 'external-storage-services',
EXTERNAL_CITATION_SERVICES = 'external-citation-services',
EXTERNAL_LINK_SERVICES = 'external-link-services',
+ EXTERNAL_REDIRECT_SERVICES = 'external-redirect-services',
}
diff --git a/src/app/shared/helpers/addon-type.helper.ts b/src/app/shared/helpers/addon-type.helper.ts
index 2b4c65f11..b3a684027 100644
--- a/src/app/shared/helpers/addon-type.helper.ts
+++ b/src/app/shared/helpers/addon-type.helper.ts
@@ -35,6 +35,12 @@ export function isLinkAddon(addon: AddonModel | AuthorizedAccountModel | Configu
);
}
+export function isRedirectAddon(addon: AddonModel | AuthorizedAccountModel | ConfiguredAddonModel | null): boolean {
+ if (!addon) return false;
+
+ return addon.type === AddonCategory.EXTERNAL_REDIRECT_SERVICES;
+}
+
export function getAddonTypeString(addon: AddonModel | AuthorizedAccountModel | ConfiguredAddonModel | null): string {
if (!addon) return '';
diff --git a/src/app/shared/mappers/addon.mapper.ts b/src/app/shared/mappers/addon.mapper.ts
index c3a70ed48..93d1027c0 100644
--- a/src/app/shared/mappers/addon.mapper.ts
+++ b/src/app/shared/mappers/addon.mapper.ts
@@ -29,6 +29,7 @@ export class AddonMapper {
credentialsFormat: response.attributes.credentials_format,
providerName: response.attributes.display_name,
iconUrl: response.attributes.icon_url,
+ redirectUrl: response.attributes.redirect_url,
configurableApiRoot: response.attributes.configurable_api_root,
};
}
diff --git a/src/app/shared/models/addons/addon-json-api.models.ts b/src/app/shared/models/addons/addon-json-api.models.ts
index 78c7a685f..4479322c1 100644
--- a/src/app/shared/models/addons/addon-json-api.models.ts
+++ b/src/app/shared/models/addons/addon-json-api.models.ts
@@ -10,6 +10,7 @@ export interface AddonGetResponseJsonApi {
credentials_format: string;
wb_key: string;
icon_url: string;
+ redirect_url?: string;
configurable_api_root: boolean;
[key: string]: unknown;
};
diff --git a/src/app/shared/models/addons/addon.model.ts b/src/app/shared/models/addons/addon.model.ts
index 4f6a7c458..44dc06871 100644
--- a/src/app/shared/models/addons/addon.model.ts
+++ b/src/app/shared/models/addons/addon.model.ts
@@ -14,4 +14,5 @@ export interface AddonModel {
credentialsAvailable?: boolean;
supportedResourceTypes?: string[];
wbKey?: string;
+ redirectUrl?: string;
}
diff --git a/src/app/shared/stores/addons/addons.actions.ts b/src/app/shared/stores/addons/addons.actions.ts
index bb37a0fd6..17a9c852a 100644
--- a/src/app/shared/stores/addons/addons.actions.ts
+++ b/src/app/shared/stores/addons/addons.actions.ts
@@ -16,6 +16,10 @@ export class GetLinkAddons {
static readonly type = '[Addons] Get Link Addons';
}
+export class GetRedirectAddons {
+ static readonly type = '[Addons] Get Other Addons';
+}
+
export class GetAuthorizedStorageAddons {
static readonly type = '[Addons] Get Authorized Storage Addons';
diff --git a/src/app/shared/stores/addons/addons.models.ts b/src/app/shared/stores/addons/addons.models.ts
index 4e795944f..082a40348 100644
--- a/src/app/shared/stores/addons/addons.models.ts
+++ b/src/app/shared/stores/addons/addons.models.ts
@@ -13,6 +13,7 @@ export interface AddonsStateModel {
storageAddons: AsyncStateModel;
citationAddons: AsyncStateModel;
linkAddons: AsyncStateModel;
+ redirectAddons: AsyncStateModel;
authorizedStorageAddons: AsyncStateModel;
authorizedCitationAddons: AsyncStateModel;
authorizedLinkAddons: AsyncStateModel;
@@ -44,6 +45,11 @@ export const ADDONS_DEFAULTS: AddonsStateModel = {
isLoading: false,
error: null,
},
+ redirectAddons: {
+ data: [],
+ isLoading: false,
+ error: null,
+ },
authorizedStorageAddons: {
data: [],
isLoading: false,
diff --git a/src/app/shared/stores/addons/addons.selectors.ts b/src/app/shared/stores/addons/addons.selectors.ts
index bd8158094..20247e7e7 100644
--- a/src/app/shared/stores/addons/addons.selectors.ts
+++ b/src/app/shared/stores/addons/addons.selectors.ts
@@ -51,6 +51,16 @@ export class AddonsSelectors {
return state.linkAddons.isLoading;
}
+ @Selector([AddonsState])
+ static getRedirectAddons(state: AddonsStateModel): AddonModel[] {
+ return state.redirectAddons.data;
+ }
+
+ @Selector([AddonsState])
+ static getRedirectAddonsLoading(state: AddonsStateModel): boolean {
+ return state.redirectAddons.isLoading;
+ }
+
@Selector([AddonsState])
static getAuthorizedStorageAddons(state: AddonsStateModel): AuthorizedAccountModel[] {
return state.authorizedStorageAddons.data;
diff --git a/src/app/shared/stores/addons/addons.state.ts b/src/app/shared/stores/addons/addons.state.ts
index 9f28071f5..b3866375f 100644
--- a/src/app/shared/stores/addons/addons.state.ts
+++ b/src/app/shared/stores/addons/addons.state.ts
@@ -30,6 +30,7 @@ import {
GetConfiguredLinkAddons,
GetConfiguredStorageAddons,
GetLinkAddons,
+ GetRedirectAddons,
GetStorageAddons,
UpdateAuthorizedAddon,
UpdateConfiguredAddon,
@@ -116,6 +117,30 @@ export class AddonsState {
);
}
+ @Action(GetRedirectAddons)
+ getRedirectAddons(ctx: StateContext) {
+ const state = ctx.getState();
+ ctx.patchState({
+ redirectAddons: {
+ ...state.redirectAddons,
+ isLoading: true,
+ },
+ });
+
+ return this.addonsService.getAddons(AddonType.REDIRECT).pipe(
+ tap((addons) => {
+ ctx.patchState({
+ redirectAddons: {
+ data: addons,
+ isLoading: false,
+ error: null,
+ },
+ });
+ }),
+ catchError((error) => handleSectionError(ctx, 'redirectAddons', error))
+ );
+ }
+
@Action(GetAuthorizedStorageAddons)
getAuthorizedStorageAddons(ctx: StateContext, action: GetAuthorizedStorageAddons) {
const state = ctx.getState();
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 095792668..fef2287cc 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -1693,7 +1693,8 @@
"categories": {
"additionalService": "Additional Storage",
"citationManager": "Citation Manager",
- "linkedServices": "Linked Services"
+ "linkedServices": "Linked Services",
+ "otherServices": "Other Services"
},
"toast": {
"updateSuccess": "Successfully updated {{addonName}} add-on configuration",
@@ -1733,6 +1734,12 @@
"function": "Function",
"status": "Status"
},
+ "redirectAddons": {
+ "terms": "Clicking the button below will redirect you outside of OSF. You will need to follow that service's permissions to continue.",
+ "tip": "Tip: if the page does not open, please disable your browser's pop-up blocker and try again. Or click on this link to go to {{serviceName}}.",
+ "popupError": "If you are having trouble with the pop-up window, please try clicking the link above to connect your account:",
+ "goToService": "Go to {{serviceName}}"
+ },
"confirmAccount": "Confirm account",
"connectAccount": "Connect following account: {{accountName}}",
"configure": "Configure",