diff --git a/app/adapters/authorized-link-account.ts b/app/adapters/authorized-link-account.ts
new file mode 100644
index 0000000000..b90f11ff32
--- /dev/null
+++ b/app/adapters/authorized-link-account.ts
@@ -0,0 +1,10 @@
+import AddonServiceAdapter from './addon-service';
+
+export default class AuthorizedLinkAccountAdapter extends AddonServiceAdapter {
+}
+
+declare module 'ember-data/types/registries/adapter' {
+ export default interface AdapterRegistry {
+ 'authorized-link-account': AuthorizedLinkAccountAdapter;
+ } // eslint-disable-line semi
+}
diff --git a/app/adapters/configured-link-addon.ts b/app/adapters/configured-link-addon.ts
new file mode 100644
index 0000000000..4518c6d0f5
--- /dev/null
+++ b/app/adapters/configured-link-addon.ts
@@ -0,0 +1,10 @@
+import AddonServiceAdapter from './addon-service';
+
+export default class ConfiguredLinkAddonAdapter extends AddonServiceAdapter {
+}
+
+declare module 'ember-data/types/registries/adapter' {
+ export default interface AdapterRegistry {
+ 'configured-link-addon': ConfiguredLinkAddonAdapter;
+ } // eslint-disable-line semi
+}
diff --git a/app/adapters/external-link-service.ts b/app/adapters/external-link-service.ts
new file mode 100644
index 0000000000..36304069c7
--- /dev/null
+++ b/app/adapters/external-link-service.ts
@@ -0,0 +1,10 @@
+import AddonServiceAdapter from './addon-service';
+
+export default class ExternalLinkServiceAdapter extends AddonServiceAdapter {
+}
+
+declare module 'ember-data/types/registries/adapter' {
+ export default interface AdapterRegistry {
+ 'external-link-service': ExternalLinkServiceAdapter;
+ } // eslint-disable-line semi
+}
diff --git a/app/guid-node/addons/index/controller.ts b/app/guid-node/addons/index/controller.ts
index 5e176c24dc..36d1e19a7b 100644
--- a/app/guid-node/addons/index/controller.ts
+++ b/app/guid-node/addons/index/controller.ts
@@ -1,9 +1,20 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import Media from 'ember-responsive';
+import { tracked } from 'tracked-built-ins';
+enum FilterTypes {
+ STORAGE = 'additional-storage',
+ CITATION_MANAGER = 'citation-manager',
+ VERIFIED_LINK = 'verified-link',
+ // CLOUD_COMPUTING = 'cloud-computing', // disabled because BOA is down
+}
export default class GuidNodeAddonsController extends Controller {
@service media!: Media;
+ @tracked tabIndex = 0;
+ @tracked activeFilterType = FilterTypes.STORAGE;
+
+ queryParams = ['tabIndex', 'activeFilterType'];
get isMobile() {
return this.media.isMobile;
diff --git a/app/guid-node/addons/index/template.hbs b/app/guid-node/addons/index/template.hbs
index 005c1543a8..f3e88d7f28 100644
--- a/app/guid-node/addons/index/template.hbs
+++ b/app/guid-node/addons/index/template.hbs
@@ -3,6 +3,9 @@
{{#if manager.selectedProvider}}
@@ -274,6 +277,7 @@
@@ -282,7 +286,8 @@
{{/let}}
{{else}}
@@ -324,7 +329,7 @@
- {{#if this.requiresRootFolder }}
+ {{#if this.requiresFilesWidget }}
- {{t 'addons.configure.selected-folder'}}
+ {{#if this.isLinkAddon}}
+ {{t 'addons.configure.linked-item'}}
+ {{else}}
+ {{t 'addons.configure.selected-folder'}}
+ {{/if}}
{{#if this.selectedFolderDisplayName}}
{{this.selectedFolderDisplayName}}
+ {{else if this.selectedItemDisplayName}}
+ {{this.selectedItemDisplayName}}
{{else}}
{{t 'addons.configure.no-folder-selected'}}
{{/if}}
@@ -81,48 +87,48 @@
{{t 'addons.configure.error-loading-items'}} |
{{else}}
- {{#each fileManager.currentItems as |folder|}}
+ {{#each fileManager.currentItems as |item|}}
- {{#if folder.mayContainRootCandidates}}
+ {{#if (or item.mayContainRootCandidates fileManager.isLinkAddon)}}
- {{#if (or (eq folder.itemType 'FOLDER') (eq folder.itemType 'COLLECTION'))}}
+ {{#if (or (eq item.itemType 'FOLDER') (eq item.itemType 'COLLECTION'))}}
{{else}}
{{/if}}
- {{folder.itemName}}
+ {{item.itemName}}
{{else}}
- {{#if (or (eq folder.itemType 'FOLDER') (eq folder.itemType 'COLLECTION'))}}
+ {{#if (or (eq item.itemType 'FOLDER') (eq item.itemType 'COLLECTION'))}}
{{else}}
{{/if}}
- {{folder.itemName}}
+ {{item.itemName}}
{{/if}}
|
- {{#if folder.canBeRoot}}
+ {{#if (or item.canBeRoot fileManager.isLinkAddon)}}
{{/if}}
|
@@ -151,6 +157,20 @@
{{/if}}
+ {{#if this.isLinkAddon}}
+
+ {{/if}}
{
taskFor(this.getStartingFolder).lastPerformed?.error;
}
+ get isLinkAddon() {
+ return this.operationInvocableModel instanceof ConfiguredLinkAddonModel ||
+ this.operationInvocableModel instanceof AuthorizedLinkAccountModel ;
+ }
+
constructor(owner: unknown, args: Args) {
super(owner, args);
assert('Must provide a configuredAddon or authorizedAccount', args.configuredAddon || args.authorizedAccount);
diff --git a/lib/osf-components/addon/components/addons-service/file-manager/template.hbs b/lib/osf-components/addon/components/addons-service/file-manager/template.hbs
index 44054a036d..5781171a08 100644
--- a/lib/osf-components/addon/components/addons-service/file-manager/template.hbs
+++ b/lib/osf-components/addon/components/addons-service/file-manager/template.hbs
@@ -10,4 +10,5 @@
isLoading=this.isLoading
isError=this.isError
+ isLinkAddon=this.isLinkAddon
)}}
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 2e82a342fe..59540c7e4d 100644
--- a/lib/osf-components/addon/components/addons-service/manager/component.ts
+++ b/lib/osf-components/addon/components/addons-service/manager/component.ts
@@ -23,6 +23,7 @@ import { AccountCreationArgs} from 'ember-osf-web/models/authorized-account';
import AuthorizedStorageAccountModel from 'ember-osf-web/models/authorized-storage-account';
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';
interface FilterSpecificObject {
modelName: string;
@@ -44,11 +45,15 @@ enum PageMode {
export enum FilterTypes {
STORAGE = 'additional-storage',
CITATION_MANAGER = 'citation-manager',
+ VERIFIED_LINK = 'verified-link',
// CLOUD_COMPUTING = 'cloud-computing', // disabled because BOA is down
}
interface Args {
node: NodeModel;
+ activeFilterType: FilterTypes;
+ updateActiveFilterType: (type: string) => void;
+ updateTabIndex: (type: number) => void;
}
export default class AddonsServiceManagerComponent extends Component {
@@ -75,6 +80,12 @@ export default class AddonsServiceManagerComponent extends Component {
list: A([]),
configuredAddons: A([]),
},
+ [FilterTypes.VERIFIED_LINK]: {
+ modelName: 'external-link-service',
+ task: taskFor(this.getLinkAddonProviders),
+ list: A([]),
+ configuredAddons: A([]),
+ },
// [FilterTypes.CLOUD_COMPUTING]: {
// modelName: 'external-computing-service',
// task: taskFor(this.getComputingAddonProviders),
@@ -84,7 +95,6 @@ export default class AddonsServiceManagerComponent extends Component {
};
filterTypeMapper = new TrackedObject(this.mapper);
@tracked filterText = '';
- @tracked activeFilterType: FilterTypes = FilterTypes.STORAGE;
@tracked confirmRemoveConnectedLocation = false;
@tracked _pageMode?: PageMode;
@@ -95,10 +105,10 @@ export default class AddonsServiceManagerComponent extends Component {
@action
filterByAddonType(type: FilterTypes) {
- if (this.activeFilterType !== type) {
+ if (this.args.activeFilterType !== type) {
this.filterText = '';
}
- this.activeFilterType = type;
+ this.args.updateActiveFilterType(type);
const activeFilterObject = this.filterTypeMapper[type];
if (activeFilterObject.list.length === 0) {
activeFilterObject.task.perform();
@@ -116,7 +126,7 @@ export default class AddonsServiceManagerComponent extends Component {
}
get filteredConfiguredProviders() {
- const activeFilterObject = this.filterTypeMapper[this.activeFilterType];
+ const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
const possibleProviders = activeFilterObject.list;
const textFilteredAddons = possibleProviders.filter(
(provider: any) => provider.provider.displayName.toLowerCase().includes(this.filterText.toLowerCase()),
@@ -128,7 +138,7 @@ export default class AddonsServiceManagerComponent extends Component {
}
get filteredAddonProviders() {
- const activeFilterObject = this.filterTypeMapper[this.activeFilterType];
+ const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
const possibleProviders = activeFilterObject.list;
const textFilteredAddons = possibleProviders.filter(
(provider: any) => provider.provider.displayName.toLowerCase().includes(this.filterText.toLowerCase()),
@@ -138,7 +148,7 @@ export default class AddonsServiceManagerComponent extends Component {
}
get currentListIsLoading() {
- const activeFilterObject = this.filterTypeMapper[this.activeFilterType];
+ const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
return activeFilterObject.task.isRunning || taskFor(this.initialize).isRunning;
}
@@ -278,6 +288,7 @@ export default class AddonsServiceManagerComponent extends Component {
this.selectedConfiguration = undefined;
this.selectedAccount = undefined;
this.confirmRemoveConnectedLocation = false;
+ this.args.updateTabIndex(0);
}
@task
@@ -299,6 +310,10 @@ export default class AddonsServiceManagerComponent extends Component {
this.toast.success(this.intl.t('addons.configure.success', {
configurationName: this.selectedConfiguration.displayName,
}));
+ } else if (this.selectedConfiguration && this.selectedConfiguration instanceof ConfiguredLinkAddonModel) {
+ this.selectedConfiguration.targetId = args.targetId;
+ this.selectedConfiguration.resourceType = args.resourceType;
+ await this.selectedConfiguration.save();
}
this.cancelSetup();
} catch(e) {
@@ -333,6 +348,10 @@ export default class AddonsServiceManagerComponent extends Component {
taskFor(this.getServiceNode).perform(),
]);
await taskFor(this.getStorageAddonProviders).perform();
+ const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
+ if (activeFilterObject.list.length === 0) {
+ activeFilterObject.task.perform();
+ }
}
@task
@@ -396,6 +415,22 @@ export default class AddonsServiceManagerComponent extends Component {
activeFilterObject.list = serviceCitationProviders.sort(this.providerSorter);
}
+ @task
+ @waitFor
+ async getLinkAddonProviders() {
+ const activeFilterObject = this.filterTypeMapper[FilterTypes.VERIFIED_LINK];
+
+ if (this.addonServiceNode) {
+ const configuredAddons = await this.addonServiceNode.configuredLinkAddons;
+ activeFilterObject.configuredAddons = A(configuredAddons.toArray());
+ }
+
+ const serviceCitationProviders: Provider[] =
+ await taskFor(this.getExternalProviders)
+ .perform(activeFilterObject.modelName, activeFilterObject.configuredAddons);
+ activeFilterObject.list = serviceCitationProviders.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/terms-of-service/component.ts b/lib/osf-components/addon/components/addons-service/terms-of-service/component.ts
index f9acd68acf..a783dc3730 100644
--- a/lib/osf-components/addon/components/addons-service/terms-of-service/component.ts
+++ b/lib/osf-components/addon/components/addons-service/terms-of-service/component.ts
@@ -7,6 +7,7 @@ import { ExternalServiceCapabilities } from 'ember-osf-web/models/external-servi
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 ExternalLinkServiceModel from 'ember-osf-web/models/external-link-service';
interface Args {
provider: AllProviderTypes;
@@ -21,7 +22,7 @@ type CapabilityCategory =
ExternalServiceCapabilities.REGISTERING |
ExternalServiceCapabilities.FILE_VERSIONS;
-type ServiceTranslationKey = 'storage' | 'computing' | 'citation';
+type ServiceTranslationKey = 'storage' | 'computing' | 'citation' | 'link';
const capabilitiesToLabelKeyMap: Record = {
@@ -92,6 +93,11 @@ const capabilitiesToTextKeyMap: Record {
ExternalServiceCapabilities.REGISTERING,
];
this.baseTranslationKey = 'citation';
+ } else if (args.provider instanceof ExternalLinkServiceModel) {
+ this.applicableCapabilities = [
+ ExternalServiceCapabilities.FORKING,
+ ];
+ this.baseTranslationKey = 'link';
}
}
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 3be4f39fc2..3ba47e5d3f 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
@@ -25,6 +25,8 @@ import ExternalCitationServiceModel from 'ember-osf-web/models/external-citation
import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception';
import getHref from 'ember-osf-web/utils/get-href';
+import AuthorizedLinkAccountModel from 'ember-osf-web/models/authorized-link-account';
+import ExternalLinkServiceModel from 'ember-osf-web/models/external-link-service';
import { FilterTypes } from '../manager/component';
enum UserSettingPageModes {
@@ -65,6 +67,14 @@ export default class UserAddonManagerComponent extends Component {
authorizedAccounts: [] as AuthorizedCitationAccountModel[],
authorizedServiceIds: [] as string[],
},
+ [FilterTypes.VERIFIED_LINK]: {
+ modelName: 'external-link-service',
+ fetchProvidersTask: taskFor(this.getLinkAddonProviders),
+ list: A([]) as EmberArray,
+ getAuthorizedAccountsTask: taskFor(this.getAuthorizedLinkAccounts),
+ authorizedAccounts: [] as AuthorizedLinkAccountModel[],
+ authorizedServiceIds: [] as string[],
+ },
// [FilterTypes.CLOUD_COMPUTING]: {
// modelName: 'external-computing-service',
// fetchProvidersTask: taskFor(this.getComputingAddonProviders),
@@ -230,6 +240,17 @@ export default class UserAddonManagerComponent extends Component {
notifyPropertyChange(this, 'filterTypeMapper');
}
+ @task
+ @waitFor
+ async getAuthorizedLinkAccounts() {
+ const { userReference } = this;
+ const mappedObject = this.filterTypeMapper[FilterTypes.VERIFIED_LINK];
+ const accounts = (await userReference.authorizedLinkAccounts).toArray();
+ mappedObject.authorizedAccounts = accounts;
+ mappedObject.authorizedServiceIds = accounts.map(account => account.externalLinkService.get('id'));
+ notifyPropertyChange(this, 'filterTypeMapper');
+ }
+
@task
@waitFor
async getAuthorizedAccounts() {
@@ -289,6 +310,23 @@ export default class UserAddonManagerComponent extends Component {
));
}
+ @task
+ @waitFor
+ async getLinkAddonProviders() {
+ const activeFilterObject = this.filterTypeMapper[FilterTypes.VERIFIED_LINK];
+ const serviceCitationProviders = await taskFor(this.getExternalProviders)
+ .perform(activeFilterObject.modelName) as ExternalLinkServiceModel[];
+ activeFilterObject.list = serviceCitationProviders.sort(this.providerSorter)
+ .map(provider => new Provider(
+ provider,
+ this.currentUser,
+ undefined,
+ undefined,
+ undefined,
+ this.userReference,
+ ));
+ }
+
@task
@waitFor
async getAddonProviders() {
diff --git a/tests/acceptance/guid-node/addons-test.ts b/tests/acceptance/guid-node/addons-test.ts
index 85ff5f29a9..c9728956a8 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: 2 }, '2 addon filters are present');
+ assert.dom('[data-test-addon-list-filter]').exists({ count: 3 }, '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/tests/integration/components/addons-service/configured-addon-edit/component-test.ts b/tests/integration/components/addons-service/configured-addon-edit/component-test.ts
index c8f9455ae0..321e4d9624 100644
--- a/tests/integration/components/addons-service/configured-addon-edit/component-test.ts
+++ b/tests/integration/components/addons-service/configured-addon-edit/component-test.ts
@@ -92,8 +92,9 @@ module('Integration | Component | addons-service | configured-addon-edit', funct
const args = {
rootFolder: 'root-1-1',
displayName: 'My configured addon',
+ resourceType: '',
+ targetId: '',
};
-
assert.ok(this.onSave.calledOnceWith(args), 'Save action was called with selected folder id');
});
});
diff --git a/tests/integration/components/addons-service/manager/component-test.ts b/tests/integration/components/addons-service/manager/component-test.ts
index ad6e98295c..2f58463a26 100644
--- a/tests/integration/components/addons-service/manager/component-test.ts
+++ b/tests/integration/components/addons-service/manager/component-test.ts
@@ -27,9 +27,14 @@ module('Integration | Component | addons-service | manager', hooks => {
{ id: mirageNode.id, configuredStorageAddons: [] });
server.create('user-reference', { id: user.id });
this.set('node', node);
+ this.set('activeFilterType', 'additional-storage');
+ this.set('tabIndex', 0);
await render(hbs`
{{#if manager.currentListIsLoading}}
diff --git a/translations/en-us.yml b/translations/en-us.yml
index 78b043bdc6..e62ad177ba 100644
--- a/translations/en-us.yml
+++ b/translations/en-us.yml
@@ -257,6 +257,7 @@ addons:
additional-storage: 'Additional Storage'
citation-manager: 'Citation Manager'
cloud-computing: 'Cloud Computing'
+ verified-link: 'Linked Services'
no-results: 'No results found'
no-connected-accounts: 'No connected accounts'
connect: 'Connect'
@@ -359,8 +360,11 @@ addons:
authorizing: 'Connecting…'
configure:
heading: 'Configure {providerName}'
+ resource-type: 'Resource type'
+ resource-type-placeholder: 'Choose a resource type'
display-name: 'Display name'
selected-folder: 'Selected folder:'
+ linked-item: 'Linked item:'
no-folder-selected: 'No folder selected'
go-to-root: 'Go to home folder'
go-to-folder: 'Go to folder {folderName}'
@@ -372,7 +376,16 @@ addons:
error-loading-items: 'Error loading items'
success: 'Successfully updated {configurationName}'
error: 'Error updating {configurationName}'
-
+links:
+ linked-services: 'Linked Services'
+ linked-service: 'Linked Service'
+ display-name: 'Display Name'
+ resource-type: 'Resource Type'
+ empty-screen-message: 'This project has no configured linked services at a moment '
+ point-to-addons-message: 'In order to add linked service visit'
+ link: 'Link'
+ edit: 'Edit'
+ icon: '{addonName} icon'
metadata:
tab-title: 'Metadata'
main-tab: 'OSF'
@@ -1045,6 +1058,7 @@ node:
components: Components
add-ons: Add-ons
settings: Settings
+ links: Linked Services
projects:
search-placeholder: 'Find project by name'
select-placeholder: 'Click to select project'