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

CenterForOpenScience / ember-osf-web / 12658699216

07 Jan 2025 08:20PM UTC coverage: 64.482%. First build
12658699216

Pull #2433

github

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

2738 of 4666 branches covered (58.68%)

Branch coverage included in aggregate %.

3 of 57 new or added lines in 5 files covered. (5.26%)

6973 of 10394 relevant lines covered (67.09%)

199.94 hits per line

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

34.13
/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 Toast from 'ember-toastr/services/toast';
15
import Intl from 'ember-intl/services/intl';
16
import Store from '@ember-data/store';
17
import CurrentUser from 'ember-osf-web/services/current-user';
18
import {MessageTypeChoices} from 'ember-osf-web/models/user-message';
19
import {RequestTypeChoices} from 'ember-osf-web/models/node-request';
20

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

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

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

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

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

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

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

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

75

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

193
    }
194

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

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

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

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

215

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

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

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

NEW
241
            if (errorCode === 400 && errorDetail.includes('does not have Access Requests enabled')) {
×
NEW
242
                setTimeout(() => {
×
NEW
243
                    this.showSendMessagePrompt = true; // Timeout to allow the modal to exit
×
244
                }, 200);
NEW
245
            } else if ([409, 400, 403].includes(errorCode)) {
×
246
                // Handle specific errors
NEW
247
                this.toast.error(errorDetail);
×
NEW
248
            } else if (errorDetail.includes('Request was throttled')) {
×
NEW
249
                this.toast.error(errorDetail);
×
NEW
250
            } else if (errorDetail === 'You cannot request access to a node you contribute to.') {
×
NEW
251
                this.toast.error(errorDetail);
×
252
            } else {
NEW
253
                this.toast.error(
×
254
                    this.intl.t('institutions.dashboard.object-list.request-project-message-modal.message_sent_failed'),
255
                );
256
            }
257
        } finally {
NEW
258
            this.projectRequestModalShown = false; // Close the main modal
×
259
        }
260
    }
261

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

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

288
    get selectedUserOsfGuid() {
NEW
289
        const url = new URL(this.selectedUserId);
×
NEW
290
        const pathSegments = url.pathname.split('/').filter(Boolean);
×
NEW
291
        return pathSegments[pathSegments.length - 1] || ''; // Last non-empty segment
×
292
    }
293

294
}
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