• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CenterForOpenScience / ember-osf-web / 12717023113

10 Jan 2025 08:50PM UTC coverage: 64.499%. First build
12717023113

Pull #2433

github

web-flow
Merge 9a3e0567b into 5bbb5a3c5
Pull Request #2433: [ENG-6669] Institutional Access project Request Modal

2738 of 4664 branches covered (58.7%)

Branch coverage included in aggregate %.

3 of 55 new or added lines in 5 files covered. (5.45%)

6973 of 10392 relevant lines covered (67.1%)

199.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

35.25
/app/institutions/dashboard/-components/object-list/component.ts
1
import { action } from '@ember/object';
2
import { inject as service } from '@ember/service';
3
import Component from '@glimmer/component';
4
import { tracked } from '@glimmer/tracking';
5
import Features from 'ember-feature-flags/services/features';
6

7
import IndexCardSearchModel from 'ember-osf-web/models/index-card-search';
8
import InstitutionModel from 'ember-osf-web/models/institution';
9
import { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path';
10
import SearchResultModel from 'ember-osf-web/models/search-result';
11
import { Filter } from 'osf-components/components/search-page/component';
12
import { waitFor } from '@ember/test-waiters';
13
import { task } from 'ember-concurrency';
14
import { taskFor } from 'ember-concurrency-ts';
15
import Toast from 'ember-toastr/services/toast';
16
import Intl from 'ember-intl/services/intl';
17
import Store from '@ember-data/store';
18
import CurrentUser from 'ember-osf-web/services/current-user';
19
import {MessageTypeChoices} from 'ember-osf-web/models/user-message';
20
import {RequestTypeChoices} from 'ember-osf-web/models/node-request';
21

22
import config from 'ember-osf-web/config/environment';
23

24
const shareDownloadFlag = config.featureFlagNames.shareDownload;
1✔
25

26
interface Column {
27
    name: string;
28
    sortKey?: string;
29
    sortParam?: string;
30
}
31
interface ValueColumn extends Column {
32
    getValue(searchResult: SearchResultModel): string;
33
}
34

35
interface LinkColumn extends Column {
36
    getHref(searchResult: SearchResultModel): string;
37
    getLinkText(searchResult: SearchResultModel): string;
38
    type: 'link';
39
}
40

41
interface ComponentColumn extends Column {
42
    type: 'doi' | 'contributors';
43
}
44

45
export type ObjectListColumn = ValueColumn | LinkColumn | ComponentColumn;
46

47
interface InstitutionalObjectListArgs {
48
    institution: InstitutionModel;
49
    defaultQueryOptions: Record<'cardSearchFilter', Record<string, string[] | any>>;
50
    columns: ObjectListColumn[];
51
    objectType: string;
52
}
53

54
export default class InstitutionalObjectList extends Component<InstitutionalObjectListArgs> {
55
    @service features!: Features;
56
    @tracked activeFilters: Filter[] = [];
9✔
57
    @tracked page = '';
9✔
58
    @tracked sort = '-dateModified';
9✔
59
    @tracked sortParam?: string;
60
    @tracked visibleColumns = this.args.columns.map(column => column.name);
82✔
61
    @tracked dirtyVisibleColumns = [...this.visibleColumns]; // track changes to visible columns before they are saved
1✔
NEW
62
    @tracked selectedPermissions = 'write';
×
63
    @tracked projectRequestModalShown = false;
9✔
NEW
64
    @tracked activeTab = 'request-access'; // Default tab
×
NEW
65
    @tracked messageText = '';
×
NEW
66
    @tracked bccSender = false;
×
NEW
67
    @tracked replyTo = false;
×
NEW
68
    @tracked selectedUserId = '';
×
NEW
69
    @tracked selectedNodeId = '';
×
70
    @tracked showSendMessagePrompt = false;
9✔
71
    @service toast!: Toast;
72
    @service intl!: Intl;
73
    @service store!: Store;
74
    @service currentUser!: CurrentUser;
75

76

77
    get queryOptions() {
78
        const options = {
12✔
79
            cardSearchFilter: {
80
                ...this.args.defaultQueryOptions.cardSearchFilter,
81
            },
82
            'page[cursor]': this.page,
83
            'page[size]': 10,
84
            // sort can look like `sort=dateFieldName` or `sort[integer-value]=fieldName` if sortParam is provided
85
            sort: this.sortParam ? { [this.sortParam]: this.sort } : this.sort,
12!
86
        };
87
        const fullQueryOptions = this.activeFilters.reduce((acc, filter: Filter) => {
12✔
88
            if (filter.suggestedFilterOperator === SuggestedFilterOperators.IsPresent) {
1!
89
                acc.cardSearchFilter[filter.propertyPathKey] = {};
×
90
                acc.cardSearchFilter[filter.propertyPathKey][filter.value] = true;
×
91
                return acc;
×
92
            }
93
            const currentValue = acc.cardSearchFilter[filter.propertyPathKey];
1✔
94
            acc.cardSearchFilter[filter.propertyPathKey] =
1✔
95
                currentValue ? currentValue.concat(filter.value) : [filter.value];
1!
96
            return acc;
1✔
97
        }, options);
98
        return fullQueryOptions;
12✔
99
    }
100

101
    get valueSearchQueryOptions() {
102
        return {
1✔
103
            ...this.queryOptions.cardSearchFilter,
104
        };
105
    }
106

107
    get showDownloadButtons() {
108
        return this.features.isEnabled(shareDownloadFlag);
9✔
109
    }
110

111
    downloadUrl(cardSearch: IndexCardSearchModel, format: string) {
112
        if (!cardSearch.links.self) {
3!
113
            return '';
×
114
        }
115
        const cardSearchUrl = new URL((cardSearch.links.self as string));
3✔
116
        cardSearchUrl.searchParams.set('page[size]', '10000');
3✔
117
        cardSearchUrl.searchParams.set('acceptMediatype', format);
3✔
118
        cardSearchUrl.searchParams.set('withFileName', `${this.args.objectType}-search-results`);
3✔
119
        return cardSearchUrl.toString();
3✔
120
    }
121

122
    downloadCsvUrl(cardSearch: IndexCardSearchModel) {
123
        return this.downloadUrl(cardSearch, 'text/csv');
1✔
124
    }
125

126
    downloadTsvUrl(cardSearch: IndexCardSearchModel) {
127
        return this.downloadUrl(cardSearch, 'text/tab-separated-values');
1✔
128
    }
129

130
    downloadJsonUrl(cardSearch: IndexCardSearchModel) {
131
        return this.downloadUrl(cardSearch, 'application/json');
1✔
132
    }
133

134
    @action
135
    updateVisibleColumns() {
136
        this.visibleColumns = [...this.dirtyVisibleColumns];
2✔
137
    }
138

139
    @action
140
    resetColumns() {
141
        this.dirtyVisibleColumns = [...this.visibleColumns];
1✔
142
    }
143

144
    @action
145
    toggleColumnVisibility(columnName: string) {
146
        if (this.dirtyVisibleColumns.includes(columnName)) {
5✔
147
            this.dirtyVisibleColumns.removeObject(columnName);
4✔
148
        } else {
149
            this.dirtyVisibleColumns.pushObject(columnName);
1✔
150
        }
151
    }
152

153
    @action
154
    toggleFilter(property: Filter) {
155
        this.page = '';
2✔
156
        if (this.activeFilters.includes(property)) {
2✔
157
            this.activeFilters.removeObject(property);
1✔
158
        } else {
159
            this.activeFilters.pushObject(property);
1✔
160
        }
161
    }
162

163
    @action
164
    updateSortKey(newSortKey: string, newSortParam?: string) {
165
        this.sortParam = newSortParam;
×
166
        this.page = '';
×
167
        if (this.sort === newSortKey) {
×
168
            this.sort = '-' + newSortKey;
×
169
        } else {
170
            this.sort = newSortKey;
×
171
        }
172
    }
173

174
    @action
175
    updatePage(newPage: string) {
176
        this.page = newPage;
×
177
    }
178

179
    @action
180
    openProjectRequestModal(contributor: any) {
NEW
181
        this.selectedUserId = contributor.userId;
×
NEW
182
        this.selectedNodeId = contributor.nodeId;
×
NEW
183
        this.projectRequestModalShown = true;
×
184
    }
185

186
    @action
187
    handleBackToSendMessage() {
NEW
188
        this.activeTab = 'send-message';
×
NEW
189
        this.showSendMessagePrompt = false;
×
NEW
190
        setTimeout(() => {
×
NEW
191
            this.projectRequestModalShown = true; // Reopen the main modal
×
192
        }, 200);
193

194
    }
195

196
    @action
197
    closeSendMessagePrompt() {
NEW
198
        this.showSendMessagePrompt = false; // Hide confirmation modal without reopening
×
199
    }
200

201
    @action
202
    toggleProjectRequestModal() {
NEW
203
        this.projectRequestModalShown = !this.projectRequestModalShown;
×
204
    }
205

206
    @action
207
    updateselectedPermissions(permission: string) {
NEW
208
        this.selectedPermissions = permission;
×
209
    }
210

211
    @action
212
    setActiveTab(tabName: string) {
NEW
213
        this.activeTab = tabName;
×
214
    }
215

216

217
    @action
218
    resetFields() {
NEW
219
        this.selectedPermissions = 'write';
×
NEW
220
        this.bccSender = false;
×
NEW
221
        this.replyTo = false;
×
222
    }
223

224
    @task
225
    @waitFor
226
    async handleSend() {
NEW
227
        try {
×
NEW
228
            if (this.activeTab === 'send-message') {
×
NEW
229
                await taskFor(this._sendUserMessage).perform();
×
NEW
230
            } else if (this.activeTab === 'request-access') {
×
NEW
231
                await taskFor(this._sendNodeRequest).perform();
×
232
            }
233

NEW
234
            this.toast.success(
×
235
                this.intl.t('institutions.dashboard.object-list.request-project-message-modal.message_sent_success'),
236
            );
NEW
237
            this.resetFields();
×
238
        } catch (error) {
NEW
239
            const errorDetail = error?.errors?.[0]?.detail.user || error?.errors?.[0]?.detail || '';
×
NEW
240
            const errorCode = parseInt(error?.errors?.[0]?.status, 10);
×
241

NEW
242
            if (errorCode === 400 && errorDetail.includes('does not have Access Requests enabled')) {
×
243
                // Product wanted special handling for this error that involve a second pop-up modal
244
                // Timeout to allow the modal to exit, can't have two OSFDialogs open at same time
NEW
245
                setTimeout(() => {
×
NEW
246
                    this.showSendMessagePrompt = true; // Timeout to allow the modal to exit
×
247
                }, 200);
NEW
248
            } else if ([400, 403].includes(errorCode)) {
×
249
                // Handle more specific errors 403s could result due if a project quickly switches it's institution
NEW
250
                this.toast.error(errorDetail);
×
NEW
251
            } else if (errorDetail.includes('Request was throttled')) {  // 429 response not in JSON payload.
×
NEW
252
                this.toast.error(errorDetail);
×
253
            } else {
NEW
254
                this.toast.error(
×
255
                    this.intl.t('institutions.dashboard.object-list.request-project-message-modal.message_sent_failed'),
256
                );
257
            }
258
        } finally {
NEW
259
            this.projectRequestModalShown = false; // Close the main modal
×
260
        }
261
    }
262

263
    @task
264
    @waitFor
265
    async _sendUserMessage() {
NEW
266
        const userMessage = this.store.createRecord('user-message', {
×
267
            messageText: this.messageText.trim(),
268
            messageType: MessageTypeChoices.InstitutionalRequest,
269
            bccSender: this.bccSender,
270
            replyTo: this.replyTo,
271
            institution: this.args.institution,
272
            user: this.selectedUserOsfGuid,
273
        });
NEW
274
        await userMessage.save();
×
275
    }
276

277
    @task
278
    @waitFor
279
    async _sendNodeRequest() {
NEW
280
        const nodeRequest = this.store.createRecord('node-request', {
×
281
            comment: this.messageText.trim(),
282
            requestType: RequestTypeChoices.InstitutionalRequest,
283
            requestedPermissions: this.selectedPermissions,
284
            bccSender: this.bccSender,
285
            replyTo: this.replyTo,
286
            institution: this.args.institution,
287
            messageRecipient: this.selectedUserOsfGuid,
288
            target: this.selectedNodeId,
289
        });
NEW
290
        await nodeRequest.save();
×
291
    }
292

293
    get selectedUserOsfGuid() {
NEW
294
        const url = new URL(this.selectedUserId);
×
NEW
295
        const pathSegments = url.pathname.split('/').filter(Boolean);
×
NEW
296
        return pathSegments[pathSegments.length - 1] || ''; // Last non-empty segment
×
297
    }
298

299
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc