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

CenterForOpenScience / ember-osf-web / 13033595221

29 Jan 2025 02:33PM UTC coverage: 66.835% (-1.2%) from 68.015%
13033595221

push

github

web-flow
Merge Feature/addon services into develop (#2492)

* Add addon service routes (#2042)

## Purpose
- Add routes and placeholder templates for new addon service routes

## Summary of Changes
- Update app/router.ts
- Add route and template file for new routes
- Add unit tests for new routes

* [ENG-4687] Add v2 models, adapters, and serializers (#2045)

Purpose

Add models, adapters, and serializers for various v2 endpoints we'll need for addons. Note that some of this does not reflect the BE as it exists today, but is a bit aspirational. That being said, it does not include all the changes we ultimately want, just the ones that are necessary to make this function in a sane manner.

Summary of Changes

Add addon, external-account, node-addon, and user-addon models, adapters, and serializers

* [ENG-4688] Addon service models (#2048)

* Add config variable for new addon service

* WIP models for addon service

* More WIP models

* WIP

* Remove unneeded model

* [ENG-4681] Add mirage for v2 api (#2051)

## Purpose

Make the v2 endpoints work with mirage. This includes a lot of normalization of the API but not the extra features such as extended attributes for providers nor getting the folder lists.

## Summary of Changes

1. Add mirage
2. Adjust models

* [ENG-4682] Mirage for addons (#2062)

* Implement basic management component and providers (#2084)

Purpose

Start the development of the management component. This includes the mixing and sorting of the various provider types, creating abstracted providers for each api, and filling out some of the v2 api functionality.

Summary of Changes

Create management component
Create Provider and LegacyProvider
Add parent relationships to some models
Modify the addon model for the new way of serializing the addon category

* [ENG-4964] Addon cards (#2092)

## Purpose
- Add addon-cards component to configure, enable, and disable addons

## Summary of C... (continued)

3106 of 5066 branches covered (61.31%)

Branch coverage included in aggregate %.

453 of 809 new or added lines in 38 files covered. (56.0%)

9 existing lines in 3 files now uncovered.

7899 of 11400 relevant lines covered (69.29%)

189.68 hits per line

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

81.44
/lib/osf-components/addon/components/node-metadata-manager/component.ts
1
import Store from '@ember-data/store';
2
import { assert } from '@ember/debug';
3
import { action, notifyPropertyChange } from '@ember/object';
4
import { alias, or } from '@ember/object/computed';
5
import { inject as service } from '@ember/service';
6
import { waitFor } from '@ember/test-waiters';
7
import Component from '@glimmer/component';
8
import Intl from 'ember-intl/services/intl';
9
import Toast from 'ember-toastr/services/toast';
10
import { BufferedChangeset } from 'ember-changeset/types';
11
import { restartableTask, task } from 'ember-concurrency';
12
import { taskFor } from 'ember-concurrency-ts';
13

14
import NodeModel from 'ember-osf-web/models/node';
15
import CustomItemMetadataRecordModel from 'ember-osf-web/models/custom-item-metadata-record';
16
import { resourceTypeGeneralOptions } from 'ember-osf-web/models/custom-metadata';
17
import { Permission } from 'ember-osf-web/models/osf-model';
18
import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception';
19
import { languageCodes, LanguageCode } from 'ember-osf-web/utils/languages';
20
import buildChangeset from 'ember-osf-web/utils/build-changeset';
21
import { tracked } from '@glimmer/tracking';
22

23
import { languageFromLanguageCode } from 'osf-components/components/file-metadata-manager/component';
24
import InstitutionModel from 'ember-osf-web/models/institution';
25
import LicenseModel from 'ember-osf-web/models/license';
26
import IdentifierModel from 'ember-osf-web/models/identifier';
27
import CurrentUser from 'ember-osf-web/services/current-user';
28
import UserModel from 'ember-osf-web/models/user';
29

30
interface Args {
31
    node: (NodeModel);
32
}
33

34
export interface NodeMetadataManager {
35
    editDescription: () => void;
36
    editFunding: () => void;
37
    editInstitutions: () => void;
38
    editResources: () => void;
39
    saveMetadata: () => void;
40
    saveNode: () => void;
41
    saveInstitutions: () => void;
42
    cancelMetadata: () => void;
43
    cancelNode: () => void;
44
    cancelInstitutions: () => void;
45
    toggleInstitution: () => void;
46
    metadata: CustomItemMetadataRecordModel;
47
    node: (NodeModel);
48
    changeset: BufferedChangeset;
49
    inEditMode: boolean;
50
    isSaving: boolean;
51
    userCanEdit: boolean;
52
    isDirty: boolean;
53
    isGatheringData: boolean;
54
    license: LicenseModel;
55
    identifiers: IdentifierModel[];
56
    isEditingInstitutions: boolean;
57
    isSavingInstitutions: boolean;
58
    affiliatedList: InstitutionModel[];
59
    user: UserModel;
60
}
61

62
export default class NodeMetadataManagerComponent extends Component<Args> {
63
    @service store!: Store;
64
    @service intl!: Intl;
65
    @service toast!: Toast;
66
    @service currentUser!: CurrentUser;
67

68
    @tracked metadata!: CustomItemMetadataRecordModel;
69
    node: (NodeModel) = this.args.node;
8✔
70
    @tracked changeset!: BufferedChangeset;
71
    @tracked nodeChangeset!: BufferedChangeset;
72
    @or(
73
        'isEditingDescription',
74
        'isEditingFunding',
75
        'isEditingResources',
76
        'isEditingInstitutions',
77
    ) inEditMode!: boolean;
78
    @tracked isEditingDescription = false;
8✔
79
    @tracked isEditingFunding = false;
7✔
80
    @tracked isEditingResources = false;
8✔
81
    @tracked isEditingInstitutions = false;
7✔
82
    @tracked userCanEdit!: boolean;
83
    @or(
84
        'getGuidMetadata.isRunning',
85
        'cancelMetadata.isRunning',
86
        'cancelNode.isRunning',
87
    ) isGatheringData!: boolean;
88
    @or('saveNode.isRunning', 'saveMetadata.isRunning', 'saveInstitutions.isRunning') isSaving!: boolean;
89
    @alias('changeset.isDirty') isDirty!: boolean;
90
    @alias('node.id') nodeId!: string;
91
    @tracked license!: LicenseModel;
92
    @tracked identifiers!: IdentifierModel[];
93
    @tracked guidType!: string | undefined;
94
    @tracked affiliatedList!: InstitutionModel[];
95
    @tracked currentAffiliatedList!: InstitutionModel[];
96
    resourceTypeGeneralOptions: string[] = resourceTypeGeneralOptions;
8✔
97
    languageCodes: LanguageCode[] = languageCodes;
8✔
98
    saveErrored = false;
8✔
99
    saveNodeErrored = false;
8✔
100

101
    constructor(owner: unknown, args: Args) {
102
        super(owner, args);
8✔
103
        assert(
8✔
104
            'You will need pass in a node or registration object to the NodeMetadataManager component to get metadata',
105
            Boolean(args.node),
106
        );
107
        try {
8✔
108
            taskFor(this.getGuidMetadata).perform();
8✔
109
            this.userCanEdit = this.node.currentUserPermissions.includes(Permission.Write);
8✔
110
        } catch (e) {
111
            const errorTitle = this.intl.t('osf-components.item-metadata-manager.error-getting-metadata');
×
112
            this.toast.error(getApiErrorMessage(e), errorTitle);
×
113
        }
114
    }
115

116
    @task
117
    @waitFor
118
    async getGuidMetadata() {
119
        if (this.node) {
8!
120
            const guidRecord = await this.store.findRecord('guid', this.node.id, {
8✔
121
                include: 'custom_metadata',
122
                resolve: false,
123
            });
124
            this.guidType = guidRecord.referentType;
8✔
125
            this.metadata = await guidRecord.customMetadata as CustomItemMetadataRecordModel;
8✔
126
            notifyPropertyChange(this, 'metadata');
8✔
127
            const node = this.node as NodeModel;
8✔
128
            this.affiliatedList = await node.queryHasMany(
8✔
129
                'affiliatedInstitutions', {
130
                    pageSize: 100,
131
                },
132
            );
133
            this.currentAffiliatedList = [...this.affiliatedList];
8✔
134
            this.license = await node.license;
8✔
135
            this.identifiers = await node.queryHasMany('identifiers');
8✔
136
            this.changeset = buildChangeset(this.metadata, null);
8✔
137
            this.changeset.languageObject = {
8✔
138
                code: this.metadata.language,
139
                name: this.languageFromCode,
140
            };
141
            this.changeset.execute();
8✔
142
            this.nodeChangeset = buildChangeset(this.node, null);
8✔
143
        }
144
    }
145

146
    get languageFromCode(){
147
        const languageCode = this.metadata?.language || '';
19!
148
        return languageFromLanguageCode(languageCode);
19✔
149
    }
150

151
    @action
152
    editDescription(){
153
        this.isEditingDescription = true;
3✔
154
    }
155

156
    @action
157
    editResources(){
158
        this.isEditingResources = true;
2✔
159
    }
160

161
    @action
162
    editFunding(){
163
        this.isEditingFunding = true;
1✔
164
    }
165

166
    @action
167
    editInstitutions(){
168
        this.isEditingInstitutions = true;
3✔
169
    }
170

171
    @action
172
    toggleInstitution(institution: InstitutionModel) {
173
        if (this.currentAffiliatedList.includes(institution)) {
2!
174
            this.currentAffiliatedList.removeObject(institution);
×
175
        } else {
176
            this.currentAffiliatedList.pushObject(institution);
2✔
177
        }
178
    }
179

180
    @task
181
    @waitFor
182
    async cancelMetadata(){
183
        if (this.saveErrored) {
2!
UNCOV
184
            await this.metadata.reload();
×
UNCOV
185
            this.metadata.rollbackAttributes();
×
UNCOV
186
            this.saveErrored = false;
×
187
        }
188
        this.changeset.rollback();
2✔
189
        this.changeset.languageObject = {
2✔
190
            code: this.metadata.language,
191
            name: this.languageFromCode,
192
        };
193
        this.changeset.execute();
2✔
194
        this.isEditingFunding = false;
2✔
195
        this.isEditingResources = false;
2✔
196
    }
197

198
    @task
199
    @waitFor
200
    async cancelNode(){
201
        if (this.saveNodeErrored){
2✔
202
            await this.node.reload();
1✔
203
            this.saveNodeErrored = false;
1✔
204
        }
205
        this.nodeChangeset.rollback();
2✔
206
        this.isEditingDescription = false;
2✔
207
    }
208

209
    @action
210
    cancelInstitutions(){
211
        this.currentAffiliatedList = [...this.affiliatedList];
2✔
212
        this.isEditingInstitutions = false;
2✔
213
    }
214

215
    @action
216
    changeLanguage(selected: LanguageCode) {
217
        const language = selected ? selected.code : '';
2!
218
        this.changeset.set('language', language);
2✔
219
    }
220

221
    @restartableTask
222
    @waitFor
223
    async saveMetadata(){
224
        try {
1✔
225
            await this.changeset.save();
1✔
226
            this.isEditingFunding = false;
1✔
227
            this.isEditingResources = false;
1✔
228
            this.saveErrored = false;
1✔
229
        } catch (e) {
UNCOV
230
            this.saveErrored = true;
×
UNCOV
231
            if (e.errors[0].source.pointer === '/data/attributes/funders/award_uri') {
×
232
                this.toast.error(this.intl.t('osf-components.funding-metadata.api_uri_validation_error'));
×
233
            } else {
UNCOV
234
                const errorTitle = this.intl.t('osf-components.item-metadata-manager.error-saving-metadata');
×
UNCOV
235
                this.toast.error(getApiErrorMessage(e), errorTitle);
×
236
            }
237
        }
238
    }
239

240
    @restartableTask
241
    @waitFor
242
    async saveNode(){
243
        try {
2✔
244
            await this.nodeChangeset.save();
2✔
245
            this.isEditingDescription = false;
1✔
246
            this.saveNodeErrored = false;
1✔
247
        } catch (e) {
248
            this.saveNodeErrored = true;
1✔
249
            const errorTitle = this.intl.t('osf-components.item-metadata-manager.error-saving-metadata');
1✔
250
            this.toast.error(getApiErrorMessage(e), errorTitle);
1✔
251
        }
252
    }
253

254
    @restartableTask
255
    @waitFor
256
    async saveInstitutions() {
257
        try {
2✔
258
            await this.node.updateM2MRelationship('affiliatedInstitutions', this.currentAffiliatedList);
2✔
259
            await this.node.reload();
1✔
260
            this.affiliatedList = [...this.currentAffiliatedList];
1✔
261
            this.isEditingInstitutions = false;
1✔
262
        } catch (e) {
263
            const errorMessage = this.intl.t('registries.drafts.draft.metadata.save_institutions_error');
1✔
264
            captureException(e, { errorMessage });
1✔
265
            this.toast.error(getApiErrorMessage(e), errorMessage);
1✔
266
            throw e;
1✔
267
        }
268
    }
269
}
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

© 2026 Coveralls, Inc