diff --git a/app/guid-node/addons/index/controller.ts b/app/guid-node/addons/index/controller.ts index a545b79f61..36d1e19a7b 100644 --- a/app/guid-node/addons/index/controller.ts +++ b/app/guid-node/addons/index/controller.ts @@ -6,6 +6,7 @@ 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 { diff --git a/app/guid-node/addons/index/template.hbs b/app/guid-node/addons/index/template.hbs index c18dcc59d0..f3e88d7f28 100644 --- a/app/guid-node/addons/index/template.hbs +++ b/app/guid-node/addons/index/template.hbs @@ -277,6 +277,7 @@ diff --git a/app/models/configured-addon.ts b/app/models/configured-addon.ts index a0e9d73872..354cd3cce4 100644 --- a/app/models/configured-addon.ts +++ b/app/models/configured-addon.ts @@ -5,6 +5,7 @@ import { task } from 'ember-concurrency'; import UserReferenceModel from 'ember-osf-web/models/user-reference'; import { tracked } from 'tracked-built-ins'; import { taskFor } from 'ember-concurrency-ts'; +import { SupportedResourceTypes } from 'ember-osf-web/models/external-link-service'; import { ConnectedStorageOperationNames, OperationKwargs } from './addon-operation-invocation'; import { ConnectedCapabilities } from './authorized-account'; @@ -12,6 +13,8 @@ import { ConnectedCapabilities } from './authorized-account'; export interface ConfiguredAddonEditableAttrs { displayName: string; rootFolder: string; + targetId: string; + resourceType: SupportedResourceTypes; } export default class ConfiguredAddonModel extends Model { diff --git a/app/models/configured-link-addon.ts b/app/models/configured-link-addon.ts index 467e5a1edf..1ac7750ba1 100644 --- a/app/models/configured-link-addon.ts +++ b/app/models/configured-link-addon.ts @@ -1,21 +1,24 @@ import { AsyncBelongsTo, attr, belongsTo } from '@ember-data/model'; import { waitFor } from '@ember/test-waiters'; import { task } from 'ember-concurrency'; -import { ConnectedLinkOperationNames, OperationKwargs } from 'ember-osf-web/models/addon-operation-invocation'; +import { ConnectedLinkOperationNames, Item, OperationKwargs } from 'ember-osf-web/models/addon-operation-invocation'; import ResourceReferenceModel from 'ember-osf-web/models/resource-reference'; -import ExternalLinkServiceModel from 'ember-osf-web/models/external-link-service'; +import ExternalLinkServiceModel, { SupportedResourceTypes } from 'ember-osf-web/models/external-link-service'; import AuthorizedLinkAccountModel from 'ember-osf-web/models/authorized-link-account'; +import { tracked } from 'tracked-built-ins'; +import { taskFor } from 'ember-concurrency-ts'; import ConfiguredAddonModel from './configured-addon'; export default class ConfiguredLinkAddonModel extends ConfiguredAddonModel { - @attr('number') concurrentUploads!: number; + @attr('string') targetId!: string; + @attr('string') resourceType!: SupportedResourceTypes; @belongsTo('external-link-service', { inverse: null }) externalLinkService!: AsyncBelongsTo & ExternalLinkServiceModel; - @belongsTo('authorized-storage-account') + @belongsTo('authorized-link-account') baseAccount!: AsyncBelongsTo & AuthorizedLinkAccountModel; @belongsTo('resource-reference', { inverse: 'configuredLinkAddons' }) @@ -25,6 +28,10 @@ export default class ConfiguredLinkAddonModel extends ConfiguredAddonModel { return (this as ConfiguredLinkAddonModel).belongsTo('externalLinkService').id(); } + get hasRootFolder() { + return false; + } + @task @waitFor async getFolderItems(this: ConfiguredAddonModel, kwargs?: OperationKwargs) { @@ -49,6 +56,15 @@ export default class ConfiguredLinkAddonModel extends ConfiguredAddonModel { }); return await newInvocation.save(); } + + @tracked targetItemName = ''; + + @task + @waitFor + async getTargetItemName(this: ConfiguredLinkAddonModel) { + const response = await taskFor(this.getItemInfo).perform(this.targetId); + this.targetItemName = (response.operationResult as Item).itemName; + } } declare module 'ember-data/types/registries/model' { diff --git a/app/packages/addons-service/provider.ts b/app/packages/addons-service/provider.ts index 602aa683fb..69ba9a72c4 100644 --- a/app/packages/addons-service/provider.ts +++ b/app/packages/addons-service/provider.ts @@ -25,19 +25,25 @@ import ExternalComputingServiceModel from 'ember-osf-web/models/external-computi import ExternalCitationServiceModel from 'ember-osf-web/models/external-citation-service'; import { notifyPropertyChange } from '@ember/object'; 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'; +import ConfiguredLinkAddonModel from 'ember-osf-web/models/configured-link-addon'; export type AllProviderTypes = ExternalStorageServiceModel | ExternalComputingServiceModel | - ExternalCitationServiceModel; + ExternalCitationServiceModel | + ExternalLinkServiceModel; export type AllAuthorizedAccountTypes = AuthorizedStorageAccountModel | AuthorizedCitationAccountModel | - AuthorizedComputingAccountModel; + AuthorizedComputingAccountModel | + AuthorizedLinkAccountModel; export type AllConfiguredAddonTypes = ConfiguredStorageAddonModel | ConfiguredCitationAddonModel | - ConfiguredComputingAddonModel; + ConfiguredComputingAddonModel | + ConfiguredLinkAddonModel; interface ProviderTypeMapper { getAuthorizedAccounts: Task; @@ -82,6 +88,11 @@ export default class Provider { createAuthorizedAccount: taskFor(this.createAuthorizedCitationAccount), createConfiguredAddon: taskFor(this.createConfiguredCitationAddon), }, + externalLinkService: { + getAuthorizedAccounts: taskFor(this.getAuthorizedLinkAccounts), + createAuthorizedAccount: taskFor(this.createAuthorizedLinkAccount), + createConfiguredAddon: taskFor(this.createConfiguredLinkAddon), + }, }; @tracked configuredAddon?: AllConfiguredAddonTypes; @@ -134,6 +145,8 @@ export default class Provider { this.providerMap = this.providerTypeMapper.externalComputingService; } else if (provider instanceof ExternalCitationServiceModel) { this.providerMap = this.providerTypeMapper.externalCitationService; + } else if (provider instanceof ExternalLinkServiceModel) { + this.providerMap = this.providerTypeMapper.externalLinkService; } taskFor(this.initialize).perform(); } @@ -211,6 +224,14 @@ export default class Provider { .filterBy('externalComputingService.id', this.provider.id).toArray(); } + @task + @waitFor + async getAuthorizedLinkAccounts() { + const authorizedLinkAccounts = await this.userReference.authorizedLinkAccounts; + this.authorizedAccounts = authorizedLinkAccounts + .filterBy('externalLinkService.id', this.provider.id).toArray(); + } + @task @waitFor async getAuthorizedAccounts() { @@ -278,6 +299,25 @@ export default class Provider { return newAccount; } + @task + @waitFor + private async createAuthorizedLinkAccount(arg: AccountCreationArgs) { + const { credentials, apiBaseUrl, displayName, initiateOauth } = arg; + const newAccount = this.store.createRecord('authorized-link-account', { + credentials, + apiBaseUrl, + initiateOauth, + externalUserId: this.currentUser.user?.id, + authorizedCapabilities: ['ACCESS', 'UPDATE'], + scopes: [], + externalLinkService: this.provider, + accountOwner: this.userReference, + displayName, + }); + await newAccount.save(); + return newAccount; + } + @task @waitFor public async createAuthorizedAccount(arg: AccountCreationArgs) { @@ -339,6 +379,20 @@ export default class Provider { return await configuredComputingAddon.save(); } + @task + @waitFor + private async createConfiguredLinkAddon(account: AuthorizedComputingAccountModel) { + const configuredLinkAddon = this.store.createRecord('configured-link-addon', { + externalLinkService: this.provider, + accountOwner: this.userReference, + authorizedResourceUri: this.node!.links.iri, + baseAccount: account, + connectedCapabilities: ['ACCESS', 'UPDATE'], + }); + return await configuredLinkAddon.save(); + } + + @task @waitFor public async createConfiguredAddon(account: AllAuthorizedAccountTypes) { diff --git a/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts b/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts index 6b55e01e76..6564d445e3 100644 --- a/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts +++ b/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts @@ -7,10 +7,12 @@ import { Item, ItemType } from 'ember-osf-web/models/addon-operation-invocation' import AuthorizedAccountModel from 'ember-osf-web/models/authorized-account'; import AuthorizedCitationAccountModel from 'ember-osf-web/models/authorized-citation-account'; import AuthorizedComputingAccountModel from 'ember-osf-web/models/authorized-computing-account'; +import AuthorizedLinkAccountModel from 'ember-osf-web/models/authorized-link-account'; import AuthorizedStorageAccountModel from 'ember-osf-web/models/authorized-storage-account'; import ConfiguredAddonModel from 'ember-osf-web/models/configured-addon'; import ConfiguredCitationAddonModel from 'ember-osf-web/models/configured-citation-addon'; import ConfiguredComputingAddonModel from 'ember-osf-web/models/configured-computing-addon'; +import ConfiguredLinkAddonModel from 'ember-osf-web/models/configured-link-addon'; import ConfiguredStorageAddonModel from 'ember-osf-web/models/configured-storage-addon'; @@ -25,9 +27,15 @@ export default class ConfiguredAddonEdit extends Component { @tracked selectedFolder = this.args.configuredAddon?.rootFolder; @tracked selectedFolderDisplayName = this.args.configuredAddon?.rootFolderName; @tracked currentItems: Item[] = []; + @tracked selectedItem = (this.args.configuredAddon as ConfiguredLinkAddonModel).targetId; + @tracked selectedItemDisplayName = (this.args.configuredAddon as ConfiguredLinkAddonModel).targetItemName; + @tracked selectedResourceType = (this.args.configuredAddon as ConfiguredLinkAddonModel).resourceType; originalName = this.displayName; originalRootFolder = this.selectedFolder; + originalSelectedItem = this.selectedItem; + originalResourceType = this.selectedResourceType; + defaultKwargs: any = {}; constructor(owner: unknown, args: Args) { @@ -39,6 +47,9 @@ export default class ConfiguredAddonEdit extends Component { if (this.args.configuredAddon instanceof ConfiguredCitationAddonModel) { this.defaultKwargs['filterItems'] = ItemType.Collection; } + if (this.args.configuredAddon instanceof ConfiguredLinkAddonModel) { + // noop + } } if (this.args.authorizedAccount) { if (this.args.authorizedAccount instanceof AuthorizedStorageAccountModel) { @@ -47,10 +58,17 @@ export default class ConfiguredAddonEdit extends Component { if (this.args.authorizedAccount instanceof AuthorizedCitationAccountModel) { this.defaultKwargs['filterItems'] = ItemType.Collection; } + if (this.args.authorizedAccount instanceof AuthorizedLinkAccountModel) { + // noop + } } } - get requiresRootFolder() { + get isLinkAddon() { + return this.args.configuredAddon instanceof ConfiguredLinkAddonModel; + } + + get requiresFilesWidget() { return !( this.args.authorizedAccount instanceof AuthorizedComputingAccountModel || @@ -64,33 +82,40 @@ export default class ConfiguredAddonEdit extends Component { get disableSave() { const displayNameUnchanged = this.displayName === this.originalName; - const rootFolderUnchanged = this.requiresRootFolder && this.selectedFolder === this.originalRootFolder; - const needsRootFolder = this.requiresRootFolder && !this.selectedFolder; + const rootFolderUnchanged = this.requiresFilesWidget && !this.isLinkAddon + && this.selectedFolder === this.originalRootFolder; + const targetIdUnchanged = this.isLinkAddon && this.originalSelectedItem === this.selectedItem; + const resourceTypeUnchanged = this.originalResourceType === this.selectedResourceType; + const needsResourceType = this.isLinkAddon && !this.selectedResourceType; + const needsTargetId = this.isLinkAddon && !this.selectedItem; + const needsRootFolder = this.requiresFilesWidget && !this.isLinkAddon && !this.selectedFolder; - if (this.invalidDisplayName || needsRootFolder) { + if (this.invalidDisplayName || needsRootFolder || needsResourceType || needsTargetId) { return true; } + if (this.isLinkAddon) { + return targetIdUnchanged && resourceTypeUnchanged || this.args.onSave.isRunning; + } return (rootFolderUnchanged && displayNameUnchanged) || this.args.onSave.isRunning; } - get nameValid() { - return !this.invalidDisplayName && this.displayName !== this.originalName; - } - - get folderValid() { - return !this.requiresRootFolder && this.selectedFolder && this.selectedFolder !== this.originalRootFolder; - } - get onSaveArgs() { return { displayName: this.displayName, rootFolder: this.selectedFolder, + targetId: this.selectedItem, + resourceType: this.selectedResourceType, }; } @action - selectFolder(folder: Item) { - this.selectedFolder = folder.itemId; - this.selectedFolderDisplayName = folder.itemName; + selectItem(item: Item) { + if (this.isLinkAddon) { + this.selectedItem = item.itemId; + this.selectedItemDisplayName = item.itemName; + } else { + this.selectedFolder = item.itemId; + this.selectedFolderDisplayName = item.itemName; + } } } diff --git a/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss b/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss index 3dc6c476d5..6274c4e265 100644 --- a/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss +++ b/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss @@ -19,6 +19,7 @@ width: 100%; border: solid 1px $color-border-gray; border-collapse: collapse; + margin-bottom: 1rem; thead { border-bottom: solid 1px $color-border-gray; diff --git a/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs b/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs index 4d40bc76e4..28ff4d060c 100644 --- a/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs +++ b/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs @@ -21,14 +21,20 @@ {{/if}} - {{#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)}} {{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}}