Skip to content

Commit 388d8ae

Browse files
authored
[ENG-6559] Download feature for SHARE data (#2415)
- Ticket: [ENG-6559] - Feature flag: n/a ## Purpose - Allow users to download projects/registrations/preprints info in institutional dashboard ## Summary of Changes - Add functionality to download from SHARE using the `acceptMediatype` and `withFileName` query-params
1 parent 8d77837 commit 388d8ae

File tree

9 files changed

+93
-9
lines changed

9 files changed

+93
-9
lines changed

app/institutions/dashboard/-components/object-list/component-test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ module('Integration | institutions | dashboard | -components | object-list', hoo
8181

8282
// The table data is not blatantly incorrect
8383
assert.dom('[data-test-object-table-body-row]').exists({ count: 10 }, 'There are 10 rows');
84+
85+
// Download buttons are present
86+
await click('[data-test-download-dropdown]');
87+
assert.dom('[data-test-download-csv-link]').exists('CSV download link exists');
88+
assert.dom('[data-test-download-tsv-link]').exists('TSV download link exists');
89+
assert.dom('[data-test-download-json-link]').exists('JSON download link exists');
8490
});
8591

8692
test('the table supports filtering', async function(assert) {

app/institutions/dashboard/-components/object-list/component.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { action } from '@ember/object';
22
import Component from '@glimmer/component';
33
import { tracked } from '@glimmer/tracking';
44

5+
import IndexCardSearchModel from 'ember-osf-web/models/index-card-search';
56
import InstitutionModel from 'ember-osf-web/models/institution';
67
import { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path';
78
import SearchResultModel from 'ember-osf-web/models/search-result';
@@ -73,6 +74,29 @@ export default class InstitutionalObjectList extends Component<InstitutionalObje
7374
};
7475
}
7576

77+
downloadUrl(cardSearch: IndexCardSearchModel, format: string) {
78+
if (!cardSearch.links.self) {
79+
return '';
80+
}
81+
const cardSearchUrl = new URL((cardSearch.links.self as string));
82+
cardSearchUrl.searchParams.set('page[size]', '10000');
83+
cardSearchUrl.searchParams.set('acceptMediatype', format);
84+
cardSearchUrl.searchParams.set('withFileName', `${this.args.objectType}-search-results`);
85+
return cardSearchUrl.toString();
86+
}
87+
88+
downloadCsvUrl(cardSearch: IndexCardSearchModel) {
89+
return this.downloadUrl(cardSearch, 'text/csv');
90+
}
91+
92+
downloadTsvUrl(cardSearch: IndexCardSearchModel) {
93+
return this.downloadUrl(cardSearch, 'text/tab-separated-values');
94+
}
95+
96+
downloadJsonUrl(cardSearch: IndexCardSearchModel) {
97+
return this.downloadUrl(cardSearch, 'application/json');
98+
}
99+
76100
@action
77101
updateVisibleColumns() {
78102
this.visibleColumns = [...this.dirtyVisibleColumns];

app/institutions/dashboard/-components/object-list/styles.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,15 @@
128128
display: inline-flex;
129129
padding-left: 10px;
130130
}
131+
132+
.download-dropdown-content {
133+
display: flex;
134+
flex-direction: column;
135+
align-items: flex-start;
136+
border: 1px solid $color-border-gray;
137+
width: max-content;
138+
}
139+
140+
.download-link {
141+
padding: 4px 8px;
142+
}

app/institutions/dashboard/-components/object-list/template.hbs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,36 @@ as |list|>
9595
<FaIcon @icon='download' />
9696
</dd.trigger>
9797
<dd.content local-class='download-dropdown-content'>
98-
<Button
99-
local-class='download-option'
100-
{{on 'click' (queue this.downloadCsv dd.close)}}
98+
<OsfLink
99+
data-test-download-csv-link
100+
data-analytics-name='Download CSV'
101+
local-class='download-link'
102+
{{on 'click' dd.close}}
103+
@href={{call (fn (action this.downloadCsvUrl list.latestIndexCardSearch))}}
104+
@target='_blank'
101105
>
102106
{{t 'institutions.dashboard.format_labels.csv'}}
103-
</Button>
104-
<Button
105-
local-class='download-option'
106-
{{on 'click' (queue this.downloadTsv dd.close)}}
107+
</OsfLink>
108+
<OsfLink
109+
data-test-download-tsv-link
110+
data-analytics-name='Download TSV'
111+
local-class='download-link'
112+
{{on 'click' dd.close}}
113+
@href={{call (fn (action this.downloadTsvUrl list.latestIndexCardSearch))}}
114+
@target='_blank'
107115
>
108116
{{t 'institutions.dashboard.format_labels.tsv'}}
109-
</Button>
117+
</OsfLink>
118+
<OsfLink
119+
data-test-download-json-link
120+
data-analytics-name='Download JSON'
121+
local-class='download-link'
122+
{{on 'click' dd.close}}
123+
@href={{call (fn (action this.downloadJsonUrl list.latestIndexCardSearch))}}
124+
@target='_blank'
125+
>
126+
{{t 'institutions.dashboard.format_labels.json'}}
127+
</OsfLink>
110128
</dd.content>
111129
</ResponsiveDropdown>
112130
</div>

app/models/index-card-search.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model';
2+
import {Links} from 'jsonapi-typescript';
23

34
import RelatedPropertyPathModel from './related-property-path';
45
import SearchResultModel from './search-result';
@@ -15,6 +16,7 @@ export default class IndexCardSearchModel extends Model {
1516
@attr('string') cardSearchText!: string;
1617
@attr('array') cardSearchFilters!: SearchFilter[];
1718
@attr('string') totalResultCount!: number | typeof ShareMoreThanTenThousand;
19+
@attr('object') links!: Links;
1820

1921
@hasMany('search-result', { inverse: null })
2022
searchResultPage!: AsyncHasMany<SearchResultModel> & SearchResultModel[];

app/serializers/index-card-search.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1+
import * as JSONAPI from 'jsonapi-typescript';
2+
13
import ShareSerializer from './share-serializer';
24

35
export default class IndexCardSearchSerializer extends ShareSerializer {
6+
// Taken from osf-serializer.ts
7+
_mergeLinks(resourceHash: JSONAPI.ResourceObject): Partial<JSONAPI.ResourceObject> {
8+
const links = { ...(resourceHash.links || {}) };
9+
return {
10+
attributes: { ...resourceHash.attributes, links: (links as any) },
11+
};
12+
}
13+
14+
extractAttributes(modelClass: any, resourceHash: JSONAPI.ResourceObject) {
15+
const attributeHash = this._mergeLinks(resourceHash);
16+
return super.extractAttributes(modelClass, attributeHash);
17+
}
418
}
519

620
declare module 'ember-data/types/registries/serializer' {

lib/osf-components/addon/components/index-card-searcher/component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Toast from 'ember-toastr/services/toast';
99
import SearchResultModel from 'ember-osf-web/models/search-result';
1010
import { taskFor } from 'ember-concurrency-ts';
1111
import RelatedPropertyPathModel, { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path';
12+
import IndexCardSearchModel from 'ember-osf-web/models/index-card-search';
1213

1314
interface IndexCardSearcherArgs {
1415
queryOptions: Record<string, any>;
@@ -27,6 +28,7 @@ export default class IndexCardSearcher extends Component<IndexCardSearcherArgs>
2728
@tracked relatedProperties?: RelatedPropertyPathModel[] = [];
2829
@tracked booleanFilters?: RelatedPropertyPathModel[] = [];
2930

31+
@tracked latestIndexCardSearch?: IndexCardSearchModel;
3032
@tracked firstPageCursor?: string;
3133
@tracked nextPageCursor?: string;
3234
@tracked prevPageCursor?: string;
@@ -64,6 +66,7 @@ export default class IndexCardSearcher extends Component<IndexCardSearcherArgs>
6466
(property: RelatedPropertyPathModel) =>
6567
property.suggestedFilterOperator !== SuggestedFilterOperators.IsPresent, // AnyOf or AtDate
6668
);
69+
this.latestIndexCardSearch = searchResult;
6770
this.firstPageCursor = searchResult.firstPageCursor;
6871
this.nextPageCursor = searchResult.nextPageCursor;
6972
this.prevPageCursor = searchResult.prevPageCursor;

lib/osf-components/addon/components/index-card-searcher/template.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
debouceSearchObjectsTask=this.debouceSearchObjectsTask
88
searchObjectsTask=this.searchObjectsTask
99

10+
latestIndexCardSearch=this.latestIndexCardSearch
1011
firstPageCursor=this.firstPageCursor
1112
nextPageCursor=this.nextPageCursor
1213
prevPageCursor=this.prevPageCursor

mirage/views/search.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,11 @@ export function cardSearch(_: Schema, request: Request) {
277277
requestedResourceTypes = Object.keys(resourceMetadataByType) as OsfmapResourceTypes[];
278278
}
279279

280+
const indexCardSearchId = faker.random.uuid();
280281
const indexCardSearch = {
281282
data: {
282283
type: 'index-card-search',
283-
id: faker.random.uuid(),
284+
id:indexCardSearchId,
284285
attributes: {
285286
cardSearchText: 'hello',
286287
cardSearchFilter: [
@@ -325,6 +326,9 @@ export function cardSearch(_: Schema, request: Request) {
325326
},
326327
searchResultPage: {},
327328
},
329+
links: {
330+
self: `https://share.osf.io/api/v2/index-card-search/${indexCardSearchId}`,
331+
},
328332
},
329333
included: [
330334
// Related properties

0 commit comments

Comments
 (0)