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

CenterForOpenScience / ember-osf-web / 12283356371

11 Dec 2024 07:17PM UTC coverage: 64.785% (-0.1%) from 64.91%
12283356371

Pull #2421

github

web-flow
Merge 7c80ef4d1 into f41bc9ac2
Pull Request #2421: [ENG-6665] Add user messaging modal to user's tab on institutional dashboard

2738 of 4645 branches covered (58.95%)

Branch coverage included in aggregate %.

2 of 27 new or added lines in 2 files covered. (7.41%)

19 existing lines in 1 file now uncovered.

6970 of 10340 relevant lines covered (67.41%)

201.05 hits per line

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

49.02
/app/institutions/dashboard/-components/institutional-users-list/component.ts
1
import { task } from 'ember-concurrency';
2
import Component from '@glimmer/component';
3
import { tracked } from '@glimmer/tracking';
4
import { action } from '@ember/object';
5
import { inject as service } from '@ember/service';
6
import { waitFor } from '@ember/test-waiters';
7
import { restartableTask, timeout } from 'ember-concurrency';
8
import Intl from 'ember-intl/services/intl';
9

10
import InstitutionModel from 'ember-osf-web/models/institution';
11
import InstitutionDepartmentsModel from 'ember-osf-web/models/institution-department';
12
import Analytics from 'ember-osf-web/services/analytics';
13
import { RelationshipWithLinks } from 'osf-api';
14

15
interface Column {
16
    key: string;
17
    selected: boolean;
18
    label: string;
19
    sort_key: string | false;
20
    type: 'string' | 'date_by_month' | 'osf_link' | 'user_name' | 'orcid';
21
}
22

23
interface InstitutionalUsersListArgs {
24
    institution: InstitutionModel;
25
    departmentMetrics: InstitutionDepartmentsModel[];
26
}
27

28
export default class InstitutionalUsersList extends Component<InstitutionalUsersListArgs> {
29
    @service analytics!: Analytics;
30
    @service intl!: Intl;
31
    @service store;
32
    @service currentUser!: CurrentUser;
33

34
    institution?: InstitutionModel;
35

36
    departmentMetrics?: InstitutionDepartmentsModel[];
37

38
    // Properties
39
    @tracked department = this.defaultDepartment;
3✔
40
    @tracked sort = 'user_name';
3✔
UNCOV
41
    @tracked selectedDepartments: string[] = [];
×
42
    @tracked filteredUsers = [];
×
43
    @tracked messageModalShown = false;
3✔
NEW
44
    @tracked messageText = '';
×
NEW
45
    @tracked selectedUserId = null;
×
46
    @service toast!: Toast;
47

48
    @tracked columns: Column[] = [
3✔
49
        {
50
            key: 'user_name',
51
            sort_key: 'user_name',
52
            label: this.intl.t('institutions.dashboard.users_list.name'),
53
            selected: true,
54
            type: 'user_name',
55
        },
56
        {
57
            key: 'department',
58
            sort_key: 'department',
59
            label: this.intl.t('institutions.dashboard.users_list.department'),
60
            selected: true,
61
            type: 'string',
62
        },
63
        {
64
            key: 'osf_link',
65
            sort_key: false,
66
            label: this.intl.t('institutions.dashboard.users_list.osf_link'),
67
            selected: true,
68
            type: 'osf_link',
69
        },
70
        {
71
            key: 'orcid',
72
            sort_key: false,
73
            label: this.intl.t('institutions.dashboard.users_list.orcid'),
74
            selected: true,
75
            type: 'orcid',
76
        },
77
        {
78
            key: 'publicProjects',
79
            sort_key: 'public_projects',
80
            label: this.intl.t('institutions.dashboard.users_list.public_projects'),
81
            selected: true,
82
            type: 'string',
83
        },
84
        {
85
            key: 'privateProjects',
86
            sort_key: 'private_projects',
87
            label: this.intl.t('institutions.dashboard.users_list.private_projects'),
88
            selected: true,
89
            type: 'string',
90
        },
91
        {
92
            key: 'publicRegistrationCount',
93
            sort_key: 'public_registration_count',
94
            label: this.intl.t('institutions.dashboard.users_list.public_registration_count'),
95
            selected: true,
96
            type: 'string',
97
        },
98
        {
99
            key: 'embargoedRegistrationCount',
100
            sort_key: 'embargoed_registration_count',
101
            label: this.intl.t('institutions.dashboard.users_list.embargoed_registration_count'),
102
            selected: true,
103
            type: 'string',
104
        },
105
        {
106
            key: 'publishedPreprintCount',
107
            sort_key: 'published_preprint_count',
108
            label: this.intl.t('institutions.dashboard.users_list.published_preprint_count'),
109
            selected: true,
110
            type: 'string',
111
        },
112
        {
113
            key: 'publicFileCount',
114
            sort_key: 'public_file_count',
115
            label: this.intl.t('institutions.dashboard.users_list.public_file_count'),
116
            selected: false,
117
            type: 'string',
118
        },
119
        {
120
            key: 'userDataUsage',
121
            sort_key: 'storage_byte_count',
122
            label: this.intl.t('institutions.dashboard.users_list.storage_byte_count'),
123
            selected: false,
124
            type: 'string',
125
        },
126
        {
127
            key: 'accountCreationDate',
128
            sort_key: 'account_creation_date',
129
            label: this.intl.t('institutions.dashboard.users_list.account_created'),
130
            selected: false,
131
            type: 'date_by_month',
132
        },
133
        {
134
            key: 'monthLastLogin',
135
            sort_key: 'month_last_login',
136
            label: this.intl.t('institutions.dashboard.users_list.month_last_login'),
137
            selected: false,
138
            type: 'date_by_month',
139
        },
140
        {
141
            key: 'monthLastActive',
142
            sort_key: 'month_last_active',
143
            label: this.intl.t('institutions.dashboard.users_list.month_last_active'),
144
            selected: false,
145
            type: 'date_by_month',
146
        },
147
    ];
148

149
    @tracked selectedColumns: string[] = this.columns.filter(col => col.selected).map(col => col.key);
42✔
150
    // Private properties
151
    @tracked hasOrcid = false;
3✔
152
    @tracked totalUsers = 0;
3✔
153
    orcidUrlPrefix = 'https://orcid.org/';
3✔
154

155
    @action
156
    toggleColumnSelection(columnKey: string) {
UNCOV
157
        const column = this.columns.find(col => col.key === columnKey);
×
UNCOV
158
        if (column) {
×
UNCOV
159
            column.selected = !column.selected;
×
160
        }
161
    }
162

163
    get defaultDepartment() {
164
        return this.intl.t('institutions.dashboard.select_default');
15✔
165
    }
166

167
    get departments() {
168
        let departments = [this.defaultDepartment];
3✔
169

170
        if (this.args.institution && this.args.departmentMetrics) {
3✔
171
            const institutionDepartments = this.args.departmentMetrics.map((x: InstitutionDepartmentsModel) => x.name);
3✔
172
            departments = departments.concat(institutionDepartments);
1✔
173
        }
174
        return departments;
3✔
175
    }
176

177
    get isDefaultDepartment() {
178
        return this.department === this.defaultDepartment;
9✔
179
    }
180

181
    get queryUsers() {
182
        const query = {} as Record<string, string>;
9✔
183
        if (this.department && !this.isDefaultDepartment) {
9!
UNCOV
184
            query['filter[department]'] = this.department;
×
185
        }
186
        if (this.hasOrcid) {
9!
187
            query['filter[orcid_id][ne]'] = '';
×
188
        }
189
        if (this.sort) {
9!
190
            query.sort = this.sort;
9✔
191
        }
192
        return query;
9✔
193
    }
194

195
    downloadUrl(format: string) {
196
        const institutionRelationships = this.args.institution.links.relationships;
3✔
197
        const usersLink = (institutionRelationships!.user_metrics as RelationshipWithLinks).links.related.href;
3✔
198
        const userURL = new URL(usersLink!);
3✔
199
        userURL.searchParams.set('format', format);
3✔
200
        userURL.searchParams.set('page[size]', '10000');
3✔
201
        Object.entries(this.queryUsers).forEach(([key, value]) => {
3✔
202
            userURL.searchParams.set(key, value);
3✔
203
        });
204
        return userURL.toString();
3✔
205
    }
206

207
    get downloadCsvUrl() {
208
        return this.downloadUrl('csv');
1✔
209
    }
210

211
    get downloadTsvUrl() {
212
        return this.downloadUrl('tsv');
1✔
213
    }
214

215
    get downloadJsonUrl() {
216
        return this.downloadUrl('json_report');
1✔
217
    }
218

219
    @restartableTask
220
    @waitFor
221
    async searchDepartment(name: string) {
UNCOV
222
        await timeout(500);
×
UNCOV
223
        if (this.institution) {
×
UNCOV
224
            const depts: InstitutionDepartmentsModel[] = await this.args.institution.queryHasMany('departmentMetrics', {
×
225
                filter: {
226
                    name,
227
                },
228
            });
UNCOV
229
            return depts.map(dept => dept.name);
×
230
        }
UNCOV
231
        return [];
×
232
    }
233

234
    @action
235
    onSelectChange(department: string) {
UNCOV
236
        this.department = department;
×
237
    }
238

239
    @action
240
    sortInstitutionalUsers(sortBy: string) {
241
        if (this.sort === sortBy) {
3✔
242
            // If the current sort is ascending, toggle to descending
243
            this.sort = `-${sortBy}`;
1✔
244
        } else if (this.sort === `-${sortBy}`) {
2✔
245
            // If the current sort is descending, toggle to ascending
246
            this.sort = sortBy;
1✔
247
        } else {
248
            // Set to descending if it's a new sort field
249
            this.sort = `-${sortBy}`;
1✔
250
        }
251
    }
252

253
    @action
254
    cancelSelection() {
UNCOV
255
        this.selectedDepartments = [];
×
256
    }
257

258
    @action
259
    applyColumnSelection() {
UNCOV
260
        this.selectedColumns = this.columns.filter(col => col.selected).map(col => col.key);
×
261
    }
262

263
    @action
264
    toggleOrcidFilter(hasOrcid: boolean) {
UNCOV
265
        this.hasOrcid = hasOrcid;
×
266
    }
267

268
    @action
269
    clickToggleOrcidFilter(hasOrcid: boolean) {
UNCOV
270
        this.hasOrcid = !hasOrcid;
×
271
    }
272

273
    @action
274
    openMessageModal(userId: string) {
NEW
275
        this.selectedUserId = userId;
×
NEW
276
        this.messageModalShown = true;
×
277
    }
278

279
    @action
280
    toggleMessageModal(userId: string | null = null) {
×
NEW
281
        this.messageModalShown = !this.messageModalShown;
×
NEW
282
        this.selectedUserId = userId;
×
NEW
283
        if (!this.messageModalShown) {
×
NEW
284
            this.resetModalFields();
×
285
        }
286
    }
287

288
    resetModalFields() {
NEW
289
        this.messageText = '';
×
NEW
290
        this.cc = false;
×
NEW
291
        this.replyTo = false;
×
292
    }
293

294
    @action
295
    updateMessageText(event: Event) {
NEW
296
        this.messageText = (event.target as HTMLTextAreaElement).value;
×
297
    }
298

299
    @action
300
    toggleCc() {
NEW
301
        this.cc = !this.cc;
×
302
    }
303

304
    @action
305
    toggleReplyTo() {
NEW
306
        this.replyTo = !this.replyTo;
×
307
    }
308

309
    @task
310
    @waitFor
311
    async sendMessage() {
NEW
312
        if (!this.messageText.trim()) {
×
NEW
313
            this.toast.error(this.intl.t('error.empty_message'));
×
NEW
314
            return;
×
315
        }
316

NEW
317
        try {
×
NEW
318
            const userMessage = this.store.createRecord('user-message', {
×
319
                messageText: this.messageText.trim(),
320
                messageType: 'institutional_request',
321
                cc: this.cc,
322
                replyTo: this.replyTo,
323
                institution: this.args.institution,
324
                user: this.selectedUserId,
325
            });
326

NEW
UNCOV
327
            await userMessage.save();
×
NEW
UNCOV
328
            this.toast.success(this.intl.t('success.message_sent'));
×
329
        } catch (error) {
NEW
UNCOV
330
            this.toast.error(this.intl.t('error.message_failed'));
×
331
        } finally {
NEW
UNCOV
332
            this.messageModalShown = false;
×
333
        }
334
    }
335
}
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