diff --git a/app/adapters/external-redirect-service.ts b/app/adapters/external-redirect-service.ts new file mode 100644 index 0000000000..dd26a4a91a --- /dev/null +++ b/app/adapters/external-redirect-service.ts @@ -0,0 +1,10 @@ +import AddonServiceAdapter from './addon-service'; + +export default class ExternalRedirectServiceAdapter extends AddonServiceAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'external-redirect-service': ExternalRedirectServiceAdapter; + } // eslint-disable-line semi +} diff --git a/app/guid-node/addons/index/template.hbs b/app/guid-node/addons/index/template.hbs index 9705bfc09f..401e8a606a 100644 --- a/app/guid-node/addons/index/template.hbs +++ b/app/guid-node/addons/index/template.hbs @@ -27,9 +27,20 @@ data-analytics-scope='Addon - {{provider.provider.displayName}}' > {{#if (eq manager.pageMode 'terms')}} - + {{#if manager.selectedProviderIsRedirectService}} +

+ {{t 'addons.terms.redirect.disclaimer'}} +

+

+ + {{t 'addons.terms.redirect.pop-up-tooltip'}} + +

+ {{else}} + + {{/if}}
diff --git a/app/models/external-redirect-service.ts b/app/models/external-redirect-service.ts new file mode 100644 index 0000000000..06e8c280b7 --- /dev/null +++ b/app/models/external-redirect-service.ts @@ -0,0 +1,14 @@ +import { attr } from '@ember-data/model'; + +import ExternalServiceModel from './external-service'; + +export default class ExternalRedirectServiceModel extends ExternalServiceModel { + @attr('string') redirectUrl!: string; + // TODO: actually need some attrs here for redirect options +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'external-redirect-service': ExternalRedirectServiceModel; + } // eslint-disable-line semi +} diff --git a/app/packages/addons-service/provider.ts b/app/packages/addons-service/provider.ts index 25e07b6d88..b7f7f23d84 100644 --- a/app/packages/addons-service/provider.ts +++ b/app/packages/addons-service/provider.ts @@ -1,5 +1,6 @@ import { getOwner, setOwner } from '@ember/application'; import EmberArray from '@ember/array'; +import { notifyPropertyChange } from '@ember/object'; import { inject as service } from '@ember/service'; import { waitFor } from '@ember/test-waiters'; import Store from '@ember-data/store'; @@ -23,7 +24,7 @@ import AuthorizedComputingAccountModel from 'ember-osf-web/models/authorized-com import ExternalStorageServiceModel from 'ember-osf-web/models/external-storage-service'; import ExternalComputingServiceModel from 'ember-osf-web/models/external-computing-service'; import ExternalCitationServiceModel from 'ember-osf-web/models/external-citation-service'; -import { notifyPropertyChange } from '@ember/object'; +import ExternalRedirectServiceModel from 'ember-osf-web/models/external-redirect-service'; import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; import ExternalLinkServiceModel from 'ember-osf-web/models/external-link-service'; import AuthorizedLinkAccountModel from 'ember-osf-web/models/authorized-link-account'; @@ -33,7 +34,8 @@ export type AllProviderTypes = ExternalStorageServiceModel | ExternalComputingServiceModel | ExternalCitationServiceModel | - ExternalLinkServiceModel; + ExternalLinkServiceModel | + ExternalRedirectServiceModel; export type AllAuthorizedAccountTypes = AuthorizedStorageAccountModel | AuthorizedCitationAccountModel | diff --git a/app/serializers/external-redirect-service.ts b/app/serializers/external-redirect-service.ts new file mode 100644 index 0000000000..cbef16ffcd --- /dev/null +++ b/app/serializers/external-redirect-service.ts @@ -0,0 +1,10 @@ +import GravyValetSerializer from './gravy-valet-serializer'; + +export default class ExternalRedirectServiceSerializer extends GravyValetSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'external-redirect-service': ExternalRedirectServiceSerializer; + } // eslint-disable-line semi +} diff --git a/app/settings/addons/styles.scss b/app/settings/addons/styles.scss index 2dac0a7a54..cde6d0160c 100644 --- a/app/settings/addons/styles.scss +++ b/app/settings/addons/styles.scss @@ -120,4 +120,9 @@ background-color: $bg-light; color: var(--primary-color); } + + .disabled { + opacity: 0.5; + pointer-events: none; + } } diff --git a/app/settings/addons/template.hbs b/app/settings/addons/template.hbs index d7a93cd4a6..789d5cbd5b 100644 --- a/app/settings/addons/template.hbs +++ b/app/settings/addons/template.hbs @@ -23,9 +23,20 @@

{{t 'addons.terms.heading' providerName=manager.selectedProvider.provider.displayName}}

- + {{#if manager.selectedProviderIsRedirectService}} +

+ {{t 'addons.terms.redirect.disclaimer'}} +

+

+ + {{t 'addons.terms.redirect.pop-up-tooltip'}} + +

+ {{else}} + + {{/if}}
@@ -94,14 +109,14 @@ {{/each}} - @@ -110,6 +125,8 @@ {{t 'addons.list.connected-accounts'}} diff --git a/lib/osf-components/addon/components/addons-service/manager/component.ts b/lib/osf-components/addon/components/addons-service/manager/component.ts index 4bbb936617..f1dec99a29 100644 --- a/lib/osf-components/addon/components/addons-service/manager/component.ts +++ b/lib/osf-components/addon/components/addons-service/manager/component.ts @@ -24,6 +24,7 @@ import AuthorizedStorageAccountModel from 'ember-osf-web/models/authorized-stora import ConfiguredCitationAddonModel from 'ember-osf-web/models/configured-citation-addon'; import UserReferenceModel from 'ember-osf-web/models/user-reference'; import ConfiguredLinkAddonModel from 'ember-osf-web/models/configured-link-addon'; +import ExternalRedirectServiceModel from 'ember-osf-web/models/external-redirect-service'; interface FilterSpecificObject { modelName: string; @@ -47,6 +48,7 @@ export enum FilterTypes { CITATION_MANAGER = 'citation-manager', VERIFIED_LINK = 'verified-link', // CLOUD_COMPUTING = 'cloud-computing', // disabled because BOA is down + REDIRECT_SERVICE = 'redirect-service', } interface Args { @@ -92,6 +94,12 @@ export default class AddonsServiceManagerComponent extends Component { // list: A([]), // configuredAddons: A([]), // }, + [FilterTypes.REDIRECT_SERVICE]: { + modelName: 'external-redirect-service', + task: taskFor(this.getRedirectAddonProviders), + list: A([]), + // configuredAddons: A([]), + }, }; filterTypeMapper = new TrackedObject(this.mapper); @tracked filterText = ''; @@ -152,6 +160,10 @@ export default class AddonsServiceManagerComponent extends Component { return activeFilterObject.task.isRunning || taskFor(this.initialize).isRunning; } + get selectedProviderIsRedirectService() { + return this.selectedProvider?.provider instanceof ExternalRedirectServiceModel; + } + @action async configureProvider(provider: Provider, configuredAddon: AllConfiguredAddonTypes) { this.cancelSetup(); @@ -204,6 +216,23 @@ export default class AddonsServiceManagerComponent extends Component { @action async acceptTerms() { + if (this.selectedProviderIsRedirectService) { + const openURL = new URL((this.selectedProvider!.provider as ExternalRedirectServiceModel).redirectUrl); + openURL.searchParams.set('nodeIri', this.node.links.iri!.toString()); + openURL.searchParams.set('userIri', this.userReference!.userUri); + const newWindow = window.open( + openURL.toString(), + '_blank', 'popup,width=600,height=600,scrollbars=yes,resizable=yes', + ); + if (newWindow) { + newWindow.focus(); + } else { + this.toast.error(this.intl.t('addons.redirect.pop-up-error')); + } + + this.cancelSetup(); + return; + } await taskFor(this.selectedProvider!.getAuthorizedAccounts).perform(); if(this.selectedProvider!.authorizedAccounts!.length > 0){ this.pageMode = PageMode.NEW_OR_EXISTING_ACCOUNT; @@ -431,6 +460,17 @@ export default class AddonsServiceManagerComponent extends Component { activeFilterObject.list = serviceCitationProviders.sort(this.providerSorter); } + @task + @waitFor + async getRedirectAddonProviders() { + const activeFilterObject = this.filterTypeMapper[FilterTypes.REDIRECT_SERVICE]; + + const serviceRedirectProviders: Provider[] = + await taskFor(this.getExternalProviders) + .perform(activeFilterObject.modelName, activeFilterObject.configuredAddons); + activeFilterObject.list = serviceRedirectProviders.sort(this.providerSorter); + } + providerSorter(a: Provider, b: Provider) { return a.provider.displayName.localeCompare(b.provider.displayName); } diff --git a/lib/osf-components/addon/components/addons-service/manager/template.hbs b/lib/osf-components/addon/components/addons-service/manager/template.hbs index ab527a5258..6ebdb63255 100644 --- a/lib/osf-components/addon/components/addons-service/manager/template.hbs +++ b/lib/osf-components/addon/components/addons-service/manager/template.hbs @@ -10,6 +10,7 @@ currentListIsLoading=this.currentListIsLoading projectEnabledAddons=this.projectEnabledAddons selectedProvider=this.selectedProvider + selectedProviderIsRedirectService=this.selectedProviderIsRedirectService configureProvider=this.configureProvider beginAccountSetup=this.beginAccountSetup acceptTerms=this.acceptTerms diff --git a/lib/osf-components/addon/components/addons-service/user-addons-manager/component.ts b/lib/osf-components/addon/components/addons-service/user-addons-manager/component.ts index fd8d630934..7ac40aa05c 100644 --- a/lib/osf-components/addon/components/addons-service/user-addons-manager/component.ts +++ b/lib/osf-components/addon/components/addons-service/user-addons-manager/component.ts @@ -20,6 +20,7 @@ import ExternalStorageServiceModel from 'ember-osf-web/models/external-storage-s import ExternalComputingServiceModel from 'ember-osf-web/models/external-computing-service'; import ExternalCitationServiceModel from 'ember-osf-web/models/external-citation-service'; import ExternalLinkServiceModel from 'ember-osf-web/models/external-link-service'; +import ExternalRedirectServiceModel from 'ember-osf-web/models/external-redirect-service'; import UserModel from 'ember-osf-web/models/user'; import UserReferenceModel from 'ember-osf-web/models/user-reference'; @@ -52,6 +53,7 @@ export default class UserAddonManagerComponent extends Component { user = this.args.user; @tracked userReference!: UserReferenceModel; @tracked tabIndex = 0; + @tracked connectAccountsTabDisabled = false; possibleFilterTypes = Object.values(FilterTypes); @tracked filterTypeMapper = { @@ -87,6 +89,14 @@ export default class UserAddonManagerComponent extends Component { // authorizedAccounts: [] as AuthorizedComputingAccountModel[], // authorizedServiceIds: [] as string[], // }, + [FilterTypes.REDIRECT_SERVICE]: { + modelName: 'external-redirect-service', + fetchProvidersTask: taskFor(this.getRedirectAddonProviders), + list: A([]) as EmberArray, + getAuthorizedAccountsTask: taskFor(this.getAuthorizedRedirectAccounts), + authorizedAccounts: [] as AllAuthorizedAccountTypes[], + authorizedServiceIds: [] as string[], + }, }; @tracked filterText = ''; @tracked activeFilterType = FilterTypes.STORAGE; @@ -107,6 +117,11 @@ export default class UserAddonManagerComponent extends Component { this.filterText = ''; } this.activeFilterType = type; + this.connectAccountsTabDisabled = false; + if (this.activeFilterType === FilterTypes.REDIRECT_SERVICE) { + this.changeTab(0); + this.connectAccountsTabDisabled = true; + } const activeFilterObject = this.filterTypeMapper[type]; if (activeFilterObject.list.length === 0) { activeFilterObject.fetchProvidersTask.perform(); @@ -143,6 +158,10 @@ export default class UserAddonManagerComponent extends Component { return activeFilterObject.fetchProvidersTask.isRunning; } + get selectedProviderIsRedirectService() { + return this.selectedProvider?.provider instanceof ExternalRedirectServiceModel; + } + @action connectNewProviderAccount(provider: Provider) { this.pageMode = UserSettingPageModes.TERMS; @@ -151,6 +170,22 @@ export default class UserAddonManagerComponent extends Component { @action acceptProviderTerms() { + if (this.selectedProviderIsRedirectService) { + const openURL = new URL((this.selectedProvider!.provider as ExternalRedirectServiceModel).redirectUrl); + openURL.searchParams.set('userIri', this.userReference!.userUri); + const newWindow = window.open( + openURL.toString(), + '_blank', 'popup,width=600,height=600,scrollbars=yes,resizable=yes', + ); + if (newWindow) { + newWindow.focus(); + } else { + this.toast.error(this.intl.t('addons.redirect.pop-up-error')); + } + + this.cancelSetup(); + return; + } this.pageMode = UserSettingPageModes.ACCOUNT_CREATE; } @@ -258,6 +293,16 @@ export default class UserAddonManagerComponent extends Component { notifyPropertyChange(this, 'filterTypeMapper'); } + @task + @waitFor + async getAuthorizedRedirectAccounts() { + // Redirect services do not have authorized accounts + const mappedObject = this.filterTypeMapper[FilterTypes.REDIRECT_SERVICE]; + mappedObject.authorizedAccounts = [] as AllAuthorizedAccountTypes[]; + mappedObject.authorizedServiceIds = []; + notifyPropertyChange(this, 'filterTypeMapper'); + } + @task @waitFor async getAuthorizedAccounts() { @@ -334,6 +379,23 @@ export default class UserAddonManagerComponent extends Component { )); } + @task + @waitFor + async getRedirectAddonProviders() { + const activeFilterObject = this.filterTypeMapper[FilterTypes.REDIRECT_SERVICE]; + const serviceRedirectProviders = await taskFor(this.getExternalProviders) + .perform(activeFilterObject.modelName) as ExternalRedirectServiceModel[]; + activeFilterObject.list = serviceRedirectProviders.sort(this.providerSorter) + .map(provider => new Provider( + provider, + this.currentUser, + undefined, + undefined, + undefined, + this.userReference, + )); + } + @task @waitFor async getAddonProviders() { diff --git a/lib/osf-components/addon/components/addons-service/user-addons-manager/template.hbs b/lib/osf-components/addon/components/addons-service/user-addons-manager/template.hbs index 7f1888a41b..922637d64c 100644 --- a/lib/osf-components/addon/components/addons-service/user-addons-manager/template.hbs +++ b/lib/osf-components/addon/components/addons-service/user-addons-manager/template.hbs @@ -10,8 +10,10 @@ currentTypeAuthorizedServiceIds=this.currentTypeAuthorizedServiceIds pageMode=this.pageMode tabIndex=this.tabIndex + connectAccountsTabDisabled=this.connectAccountsTabDisabled changeTab=this.changeTab selectedProvider=this.selectedProvider + selectedProviderIsRedirectService=this.selectedProviderIsRedirectService selectedAccount=this.selectedAccount connectNewProviderAccount=this.connectNewProviderAccount acceptProviderTerms=this.acceptProviderTerms diff --git a/mirage/config.ts b/mirage/config.ts index 5ae2c92c20..b46133b243 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -572,6 +572,8 @@ export default function(this: Server) { this.resource('external-storage-services', { only: ['index', 'show'] }); this.resource('external-citation-services', { only: ['index', 'show'] }); this.resource('external-computing-services', { only: ['index', 'show'] }); + this.resource('external-redirect-services', { only: ['index', 'show'] }); + this.resource('external-link-services', { only: ['index', 'show'] }); this.resource('user-references', { only: ['index', 'show'] }); this.get('/user-references/:userGuid/authorized_storage_accounts/', addons.userReferenceAuthorizedStorageAccountList); diff --git a/mirage/fixtures/external-link-service.ts b/mirage/fixtures/external-link-service.ts new file mode 100644 index 0000000000..fa3b10baf7 --- /dev/null +++ b/mirage/fixtures/external-link-service.ts @@ -0,0 +1,9 @@ +import { CredentialsFormat } from 'ember-osf-web/models/external-service'; +export default [ + { + id: 'dataverse', + displayName: 'Dataverse', + credentialsFormat: CredentialsFormat.OAUTH2, + iconUrl: 'https://dataverse.org/logo.png', + }, +]; diff --git a/mirage/fixtures/external-redirect-service.ts b/mirage/fixtures/external-redirect-service.ts new file mode 100644 index 0000000000..0da2f3c870 --- /dev/null +++ b/mirage/fixtures/external-redirect-service.ts @@ -0,0 +1,11 @@ +/* eslint-disable max-len */ +import { CredentialsFormat } from 'ember-osf-web/models/external-service'; +export default [ + { + id: 'datapipe', + displayName: 'DataPipe', + credentialsFormat: CredentialsFormat.OAUTH2, + redirectUrl: 'https://pipe.jspsych.org/', + iconUrl: 'https://pipe.jspsych.org/logo.png', + }, +]; diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index db16a251b8..d2f43e1470 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -31,6 +31,8 @@ export default function(server: Server) { server.loadFixtures('external-storage-services'); server.loadFixtures('external-computing-services'); server.loadFixtures('external-citation-services'); + // server.loadFixtures('external-link-services'); + server.loadFixtures('external-redirect-services'); // server.loadFixtures('registration-providers'); // load citations for preprints, registrations, or manyProjectRegistrations diff --git a/mirage/serializers/external-redirect-service.ts b/mirage/serializers/external-redirect-service.ts new file mode 100644 index 0000000000..15a1e342eb --- /dev/null +++ b/mirage/serializers/external-redirect-service.ts @@ -0,0 +1,6 @@ +import ExternalRedirectServiceModel from 'ember-osf-web/models/external-redirect-service'; + +import AddonServiceSerializer from './addon-service'; + +export default class ConfiguredRedirectAddonSerializer extends AddonServiceSerializer { +} diff --git a/tests/acceptance/guid-node/addons-test.ts b/tests/acceptance/guid-node/addons-test.ts index c9728956a8..808dcfd0dc 100644 --- a/tests/acceptance/guid-node/addons-test.ts +++ b/tests/acceptance/guid-node/addons-test.ts @@ -45,7 +45,7 @@ module('Acceptance | guid-node/addons', hooks => { .hasAttribute('aria-selected', 'true', 'All addons tab is selected'); // check additonal storage providers - assert.dom('[data-test-addon-list-filter]').exists({ count: 3 }, '3 addon filters are present'); + assert.dom('[data-test-addon-list-filter]').exists({ count: 4 }, '3 addon filters are present'); assert.dom('[data-test-addon-list-filter="additional-storage"]') .hasClass(styles.active, 'Additional storage filter is active'); assert.dom('[data-test-addon-card-connect]').exists({ count: 9 }, '9 storage addons are present'); diff --git a/translations/en-us.yml b/translations/en-us.yml index e568ed6c76..72e3979a1f 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -259,6 +259,7 @@ addons: citation-manager: 'Citation Manager' cloud-computing: 'Cloud Computing' verified-link: 'Linked Services' + redirect-service: 'Other Services' no-results: 'No results found' no-connected-accounts: 'No connected accounts' connect: 'Connect' @@ -271,6 +272,7 @@ addons: terms: heading: '{providerName} Terms' accepting: 'Accepting terms…' + go-to-service: 'Go to {providerName}' table-headings: function: 'Function' status: 'Status' @@ -318,6 +320,10 @@ addons: permissions-true: 'The OSF does not change permissions for linked {provider} files. Privacy changes made to an OSF project or component will not affect those set in {provider}.' registering-false: '{provider} content will not be registered.' file-versions-false: '{provider} files can be viewed/downloaded outside OSF only. Version history is not supported.' + redirect: + disclaimer: "Clicking the button below will redirect you outside of OSF. You will need to follow that service's permissions to continue." + pop-up-tooltip: 'Tip: If the page does not open, try disabling your pop-up blocker.' + pop-up-error: 'Unable to open pop-up window. Please disable your pop-up blocker and try again.' accountSelect: heading: 'Log in to {providerName} or select an account' no-accounts: 'No accounts available' @@ -368,7 +374,7 @@ addons: verify: 'Connect the following account:' authorizing: 'Connecting…' configure: - google-file-picker: + google-file-picker: select-root-folder: 'Select Root Folder' selected-folder: 'Selected Folder' root-folder-title: 'Select a root folder'