Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-4671] Implement submit-to-boa flow for file list and file detail page #2034

Merged
1 change: 1 addition & 0 deletions app/guid-file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default class GuidFile extends Route {
};
this.set('headTags', this.metaTags.getHeadTags(metaTagsData));
this.headTagsService.collectHeadTags();
await taskFor(model.target.get('getEnabledAddons')).perform();
blocker.done();
}

Expand Down
7 changes: 6 additions & 1 deletion app/guid-file/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
({{t 'general.version'}}: {{this.viewedVersion}})
{{/if}}
</h2>
<FileActionsMenu @item={{this.model}} @onDelete={{this.onDelete}} @allowRename={{false}} />
<FileActionsMenu
@item={{this.model}}
@onDelete={{this.onDelete}}
@allowRename={{false}}
@addonsEnabled={{this.model.fileModel.target.addonsEnabled}}
/>
</div>
</:header>
<:body>
Expand Down
1 change: 1 addition & 0 deletions app/guid-node/files/provider/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class GuidNodeFilesProviderRoute extends Route.extend({}) {
@waitFor
async fileProviderTask(guidRouteModel: GuidRouteModel<NodeModel>, fileProviderId: string) {
const node = await guidRouteModel.taskInstance;
await taskFor(node.getEnabledAddons).perform();
try {
const fileProviders = await node.queryHasMany(
'files',
Expand Down
36 changes: 36 additions & 0 deletions app/models/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import { computed } from '@ember/object';
import { alias, bool, equal, not } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { htmlSafe } from '@ember/template';
import { tracked } from '@glimmer/tracking';
import { buildValidations, validator } from 'ember-cp-validations';
import Intl from 'ember-intl/services/intl';

import config from 'ember-osf-web/config/environment';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';

import getRelatedHref from 'ember-osf-web/utils/get-related-href';

import AbstractNodeModel from 'ember-osf-web/models/abstract-node';
Expand All @@ -25,6 +30,13 @@ import RegistrationModel from './registration';
import SubjectModel from './subject';
import WikiModel from './wiki';

const {
OSF: {
apiUrl,
apiNamespace,
},
} = config;

const Validations = buildValidations({
title: [
validator('presence', true),
Expand Down Expand Up @@ -108,6 +120,10 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col
@attr('boolean') currentUserCanComment!: boolean;
@attr('boolean') wikiEnabled!: boolean;

// FE-only property to check enabled addons.
// null until getEnabledAddons has been called
@tracked addonsEnabled?: string[];

@hasMany('contributor', { inverse: 'node' })
contributors!: AsyncHasMany<ContributorModel> & ContributorModel[];

Expand Down Expand Up @@ -311,6 +327,26 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col

this.set('nodeLicense', props);
}

@task
@waitFor
async getEnabledAddons() {
const endpoint = `${apiUrl}/${apiNamespace}/nodes/${this.id}/addons/`;
const response = await this.currentUser.authenticatedAJAX({
url: endpoint,
type: 'GET',
headers: {
'Content-Type': 'application/json',
},
xhrFields: { withCredentials: true },
});
if (response.data) {
const addonList = response.data
.filter((addon: any) => addon.attributes.node_has_auth)
.map((addon: any) => addon.id);
this.set('addonsEnabled', addonList);
}
}
}

declare module 'ember-data/types/registries/model' {
Expand Down
8 changes: 8 additions & 0 deletions app/packages/files/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ export default abstract class File {
);
}

get isBoaFile() {
return this.fileModel.name.endsWith('.boa');
}

get providerIsOsfstorage() {
return this.fileModel.provider === 'osfstorage';
}

async createFolder(newFolderName: string) {
if (this.fileModel.isFolder) {
await this.fileModel.createFolder(newFolderName);
Expand Down
16 changes: 16 additions & 0 deletions lib/osf-components/addon/components/file-actions-menu/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface Args {
item: File;
onDelete: () => void;
manager?: StorageManager; // No manager for file-detail page
addonsEnabled? : string[];
}

export default class FileActionsMenu extends Component<Args> {
Expand All @@ -19,6 +20,7 @@ export default class FileActionsMenu extends Component<Args> {
@tracked moveModalOpen = false;
@tracked useCopyModal = false;
@tracked renameModalOpen = false;
@tracked isSubmitToBoaModalOpen = false;

@action
closeDeleteModal() {
Expand All @@ -34,4 +36,18 @@ export default class FileActionsMenu extends Component<Args> {
openRenameModal() {
this.renameModalOpen = true;
}

@action
closeSubmitToBoaModal() {
this.isSubmitToBoaModalOpen = false;
}

@action
openSubmitToBoaModal() {
this.isSubmitToBoaModalOpen = true;
}

get isBoaEnabled() {
return this.args.addonsEnabled?.includes('boa');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { inject as service } from '@ember/service';
import { waitFor } from '@ember/test-waiters';
import Component from '@glimmer/component';
import { task } from 'ember-concurrency';
import IntlService from 'ember-intl/services/intl';
import File from 'ember-osf-web/packages/files/file';
import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception';
import config from 'ember-osf-web/config/environment';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

interface Args {
file: File;
isOpen: boolean;
closeModal: () => {};
}

export default class SubmitToBoaModal extends Component<Args> {
@service toast!: Toastr;
@service intl!: IntlService;
datasets?: string[];
@tracked selectedDataset?: string;

datasets = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will hard-code this list for now, and if/when Boa adds anything to this list, we will update it or fetch it from somewhere.

'2022 Jan/Java',
'2022 Feb/Python',
'2021 Method Chains',
'2021 Aug/Python',
'2021 Aug/Kotlin (small)',
'2021 Aug/Kotlin',
'2021 Jan/ML-Verse',
'2020 August/Python-DS',
'2019 October/GitHub (small)',
'2019 October/GitHub (medium)',
'2019 October/GitHub',
'2015 September/GitHub',
'2013 September/SF (small)',
'2013 September/SF (medium)',
'2013 September/SF',
'2013 May/SF',
'2013 February/SF',
'2012 July/SF',
];
Comment on lines +24 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is less than ideal, but I see it's been discussed already as part of the scope. Let's never update this list, and if it ever needs to change, we should use an endpoint to get the current list of datasets.


@action
onDatasetChange(newDataset: string) {
this.selectedDataset = newDataset;
}

@task
@waitFor
async confirmSubmitToBoa() {
try {
const file = this.args.file;
const fileModel = file.fileModel;
const parentFolder = await fileModel.get('parentFolder');
const grandparentFolder = await parentFolder.get('parentFolder');
const endpoint = config.OSF.url + 'api/v1/project/' + fileModel.target.get('id') + '/boa/submit-job/';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/v1 url? Yuck!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sadly true, it uses the existing add-on routes, might be something that will be totally refactored in the new add-on service?

const uploadLink = new URL(parentFolder.get('links').upload as string);
uploadLink.searchParams.set('kind', 'file');
const payload = {
data: {
nodeId: fileModel.target.get('id'),
name: file.name,
materialized: fileModel.materializedPath,
links: {
download: file.links.download,
upload: file.links.upload,
},
},
parent: {
links: {
upload: uploadLink.toString(),
},
isAddonRoot: !grandparentFolder,
},
dataset: this.selectedDataset,
};
await this.args.file.currentUser.authenticatedAJAX({
url: endpoint,
type: 'POST',
data: JSON.stringify(payload),
xhrFields: { withCredentials: true },
headers: {
'Content-Type': 'application/json',
},
});

this.args.closeModal();
} catch (e) {
captureException(e);
const errorMessageKey = this.intl.t('osf-components.file-browser.submit_to_boa_fail',
{ fileName: this.args.file.name, htmlSafe: true }) as string;
this.toast.error(getApiErrorMessage(e), errorMessageKey);
return;
}

this.toast.success(
this.intl.t('osf-components.file-browser.submit_to_boa_success', { fileName: this.args.file.name }),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<OsfDialog @isOpen={{@isOpen}} @onClose={{@closeModal}} as |dialog|>
<dialog.heading>
{{t 'osf-components.file-browser.submit_to_boa'}}
</dialog.heading>
<dialog.main>

<p>{{t 'osf-components.file-browser.boa_dataset_spiel'}}</p>
<PowerSelect
@options={{this.datasets}}
@selected={{this.selectedDataset}}
@placeholder={{t 'osf-components.file-browser.boa_dataset_select_placeholder'}}
@onChange={{this.onDatasetChange}}
as |dataset|
>
{{dataset}}
</PowerSelect>
<p>{{t 'osf-components.file-browser.confirm_submit_to_boa' fileName=@file.name}}</p>

</dialog.main>
<dialog.footer>
<Button
{{on 'click' (fn (mut @isOpen) false)}}
>
{{t 'general.cancel'}}
</Button>
<Button
@type='primary'
disabled={{or this.confirmSubmitToBoa.isRunning (not this.selectedDataset)}}
{{on 'click' (perform this.confirmSubmitToBoa)}}
>
{{t 'osf-components.file-browser.confirm_submit_to_boa_yes'}}
</Button>
</dialog.footer>
</OsfDialog>
26 changes: 26 additions & 0 deletions lib/osf-components/addon/components/file-actions-menu/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,35 @@
</Button>
{{/if}}
{{/if}}
{{#if @item.currentUserCanDelete}}
{{#if @item.providerIsOsfstorage}}
{{#if @item.isBoaFile}}
{{#if this.isBoaEnabled}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of nested ifs. Probably fine, though might be better as a getter in the component ts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I will distill this down to a getter on the component

<Button
@layout='fake-link'
data-test-submit-to-boa
data-analytics-name='Submit to Boa'
local-class='DropdownItem'
{{on 'click' (queue
dropdown.close
this.openSubmitToBoaModal
)}}
>
<FaIcon @icon='upload' />
{{t 'file_actions_menu.submit_to_boa'}}
</Button>
{{/if}}
{{/if}}
{{/if}}
{{/if}}
</dropdown.content>
</ResponsiveDropdown>
<FileActionsMenu::DeleteModal @file={{@item}} @isOpen={{this.isDeleteModalOpen}} @closeModal={{this.closeDeleteModal}} @onDelete={{@onDelete}} />
<FileActionsMenu::SubmitToBoaModal
@file={{@item}}
@isOpen={{this.isSubmitToBoaModalOpen}}
@closeModal={{this.closeSubmitToBoaModal}}
/>
{{#if @manager}}
<MoveFileModal
@isOpen={{this.moveModalOpen}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@
local-class='FileList__item__options'
>
{{#unless @manager.selectedFiles}}
<FileActionsMenu @item={{@item}} @onDelete={{@manager.reload}} @manager={{@manager}} @allowRename={{true}} />
<FileActionsMenu
@item={{@item}}
@onDelete={{@manager.reload}}
@manager={{@manager}}
@allowRename={{true}}
@addonsEnabled={{@manager.targetNode.addonsEnabled}}
/>
{{/unless}}
</div>
</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'osf-components/components/file-actions-menu/submit-to-boa-modal/component';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'osf-components/components/file-actions-menu/submit-to-boa-modal/template';
8 changes: 7 additions & 1 deletion mirage/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import config from 'ember-osf-web/config/environment';

import { createReviewAction } from 'ember-osf-web/mirage/views/review-action';
import { createResource, updateResource } from 'ember-osf-web/mirage/views/resource';
import { addonsList } from './views/addons';
import { getCitation } from './views/citation';
import { createCollectionSubmission, getCollectionSubmissions } from './views/collection-submission';
import { createSubmissionAction } from './views/collection-submission-action';
Expand Down Expand Up @@ -49,7 +50,7 @@ import { updatePassword } from './views/user-password';
import * as userSettings from './views/user-setting';
import * as wb from './views/wb';

const { OSF: { apiUrl, shareBaseUrl } } = config;
const { OSF: { apiUrl, shareBaseUrl, url: osfUrl } } = config;

export default function(this: Server) {
this.passthrough(); // pass through all requests on currrent domain
Expand All @@ -67,6 +68,10 @@ export default function(this: Server) {
this.get('/index-value-search', valueSearch);
// this.get('/index-card/:id', Detail);

this.urlPrefix = osfUrl;
this.namespace = '/api/v1/';
this.post('project/:id/boa/submit-job/', () => ({})); // submissions to BoA

this.urlPrefix = apiUrl;
this.namespace = '/v2';

Expand Down Expand Up @@ -111,6 +116,7 @@ export default function(this: Server) {
onCreate: createBibliographicContributor,
});

this.get('/nodes/:parentID/addons', addonsList);
this.get('/nodes/:parentID/files', nodeFileProviderList); // Node file providers list
this.get('/nodes/:parentID/files/:fileProviderId', nodeFilesListForProvider); // Node files list for file provider
this.get('/nodes/:parentID/files/:fileProviderId/:folderId', folderFilesList); // Node folder detail view
Expand Down
Loading
Loading