Skip to content

Commit 38a1a4c

Browse files
authored
fix(project-affiliation): updated logic for remove project affiliations (#834)
1 parent 756fafd commit 38a1a4c

File tree

6 files changed

+122
-24
lines changed

6 files changed

+122
-24
lines changed

src/app/features/analytics/analytics.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { SelectComponent } from '@osf/shared/components/select/select.component'
2727
import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component';
2828
import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component';
2929
import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens';
30+
import { replaceBadEncodedChars } from '@osf/shared/helpers/format-bad-encoding.helper';
3031
import { Primitive } from '@osf/shared/helpers/types.helper';
3132
import { DatasetInput } from '@osf/shared/models/charts/dataset-input';
3233
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
@@ -177,7 +178,7 @@ export class AnalyticsComponent implements OnInit {
177178
const parts = item.path.split('/').filter(Boolean);
178179
const resource = parts[1]?.replace('-', ' ') || 'overview';
179180
let cleanTitle = item.title === 'OSF' ? item.title : item.title.replace(/^OSF \| /, '');
180-
cleanTitle = cleanTitle.replace(/&amp;/gi, '&').replace(/&lt;/gi, '<').replace(/&gt;/gi, '>');
181+
cleanTitle = replaceBadEncodedChars(cleanTitle);
181182
return cleanTitle.endsWith(resource) ? cleanTitle : `${cleanTitle} | ${resource}`;
182183
});
183184

src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,12 @@ <h3 class="text-xl pb-4">{{ 'myProjects.settings.projectAffiliation' | translate
2626
<p>{{ affiliation.name }}</p>
2727
</div>
2828

29-
@if (canEdit()) {
29+
@if (canRemoveAffiliation(affiliation)) {
3030
<p-button
3131
class="danger-icon-btn"
3232
icon="fas fa-trash"
3333
severity="danger"
3434
text
35-
[disabled]="!removeAffiliationPermission().get(affiliation.id)"
3635
[ariaLabel]="'common.buttons.delete' | translate"
3736
(onClick)="removeAffiliation(affiliation)"
3837
></p-button>

src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,108 @@ describe('SettingsProjectAffiliationComponent', () => {
5858

5959
expect(component.removed.emit).toHaveBeenCalledWith(MOCK_INSTITUTION);
6060
});
61+
62+
describe('canRemoveAffiliation', () => {
63+
const affiliatedInstitution = { ...MOCK_INSTITUTION, id: 'affiliated-id' };
64+
const nonAffiliatedInstitution = { ...MOCK_INSTITUTION, id: 'non-affiliated-id' };
65+
const userInstitutions = [{ ...MOCK_INSTITUTION, id: 'affiliated-id' }];
66+
67+
beforeEach(() => {
68+
TestBed.resetTestingModule();
69+
TestBed.configureTestingModule({
70+
imports: [SettingsProjectAffiliationComponent, OSFTestingModule],
71+
providers: [
72+
provideMockStore({
73+
signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: userInstitutions }],
74+
}),
75+
],
76+
}).compileComponents();
77+
78+
fixture = TestBed.createComponent(SettingsProjectAffiliationComponent);
79+
component = fixture.componentInstance;
80+
});
81+
82+
it('should return true when canRemove is true', () => {
83+
fixture.componentRef.setInput('canRemove', true);
84+
fixture.componentRef.setInput('canEdit', false);
85+
fixture.detectChanges();
86+
87+
expect(component.canRemoveAffiliation(affiliatedInstitution)).toBe(true);
88+
expect(component.canRemoveAffiliation(nonAffiliatedInstitution)).toBe(true);
89+
});
90+
91+
it('should return true when canEdit is true and user is affiliated with institution', () => {
92+
fixture.componentRef.setInput('canRemove', false);
93+
fixture.componentRef.setInput('canEdit', true);
94+
fixture.detectChanges();
95+
96+
expect(component.canRemoveAffiliation(affiliatedInstitution)).toBe(true);
97+
});
98+
99+
it('should return false when canEdit is true but user is not affiliated with institution', () => {
100+
fixture.componentRef.setInput('canRemove', false);
101+
fixture.componentRef.setInput('canEdit', true);
102+
fixture.detectChanges();
103+
104+
expect(component.canRemoveAffiliation(nonAffiliatedInstitution)).toBe(false);
105+
});
106+
107+
it('should return false when both canRemove and canEdit are false', () => {
108+
fixture.componentRef.setInput('canRemove', false);
109+
fixture.componentRef.setInput('canEdit', false);
110+
fixture.detectChanges();
111+
112+
expect(component.canRemoveAffiliation(affiliatedInstitution)).toBe(false);
113+
expect(component.canRemoveAffiliation(nonAffiliatedInstitution)).toBe(false);
114+
});
115+
});
116+
117+
describe('userInstitutionIds', () => {
118+
it('should create a Set of user institution IDs', () => {
119+
const userInstitutions = [
120+
{ ...MOCK_INSTITUTION, id: 'id1' },
121+
{ ...MOCK_INSTITUTION, id: 'id2' },
122+
];
123+
124+
TestBed.resetTestingModule();
125+
TestBed.configureTestingModule({
126+
imports: [SettingsProjectAffiliationComponent, OSFTestingModule],
127+
providers: [
128+
provideMockStore({
129+
signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: userInstitutions }],
130+
}),
131+
],
132+
}).compileComponents();
133+
134+
fixture = TestBed.createComponent(SettingsProjectAffiliationComponent);
135+
component = fixture.componentInstance;
136+
fixture.detectChanges();
137+
138+
const result = component.userInstitutionIds();
139+
expect(result).toBeInstanceOf(Set);
140+
expect(result.has('id1')).toBe(true);
141+
expect(result.has('id2')).toBe(true);
142+
expect(result.has('id3')).toBe(false);
143+
});
144+
145+
it('should return empty Set when no user institutions', () => {
146+
TestBed.resetTestingModule();
147+
TestBed.configureTestingModule({
148+
imports: [SettingsProjectAffiliationComponent, OSFTestingModule],
149+
providers: [
150+
provideMockStore({
151+
signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: [] }],
152+
}),
153+
],
154+
}).compileComponents();
155+
156+
fixture = TestBed.createComponent(SettingsProjectAffiliationComponent);
157+
component = fixture.componentInstance;
158+
fixture.detectChanges();
159+
160+
const result = component.userInstitutionIds();
161+
expect(result).toBeInstanceOf(Set);
162+
expect(result.size).toBe(0);
163+
});
164+
});
61165
});

src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,24 @@ import { FetchUserInstitutions, InstitutionsSelectors } from '@shared/stores/ins
2020
})
2121
export class SettingsProjectAffiliationComponent implements OnInit {
2222
affiliations = input<Institution[]>([]);
23-
userInstitutions = select(InstitutionsSelectors.getUserInstitutions);
24-
removed = output<Institution>();
2523
canEdit = input<boolean>(false);
24+
canRemove = input<boolean>(false);
25+
removed = output<Institution>();
2626

27-
removeAffiliationPermission = computed(() => {
28-
const affiliatedInstitutions = this.affiliations();
29-
const userInstitutions = this.userInstitutions();
30-
31-
const result = new Map<string, boolean>();
32-
33-
for (const institution of affiliatedInstitutions) {
34-
const isUserAffiliatedWithCurrentInstitution = userInstitutions.some(
35-
(userInstitution) => userInstitution.id === institution.id
36-
);
37-
result.set(institution.id, isUserAffiliatedWithCurrentInstitution);
38-
}
27+
userInstitutions = select(InstitutionsSelectors.getUserInstitutions);
3928

40-
return result;
41-
});
29+
readonly userInstitutionIds = computed(() => new Set(this.userInstitutions().map((inst) => inst.id)));
4230

43-
private readonly actions = createDispatchMap({
44-
fetchUserInstitutions: FetchUserInstitutions,
45-
});
31+
private readonly actions = createDispatchMap({ fetchUserInstitutions: FetchUserInstitutions });
4632

4733
ngOnInit() {
48-
this.actions.fetchUserInstitutions();
34+
if (this.canEdit()) {
35+
this.actions.fetchUserInstitutions();
36+
}
37+
}
38+
39+
canRemoveAffiliation(institution: Institution): boolean {
40+
return this.canRemove() || (this.canEdit() && this.userInstitutionIds().has(institution.id));
4941
}
5042

5143
removeAffiliation(affiliation: Institution) {

src/app/features/project/settings/settings.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
<osf-settings-project-affiliation
5252
[canEdit]="hasWriteAccess()"
53+
[canRemove]="hasAdminAccess()"
5354
[affiliations]="projectDetails().affiliatedInstitutions"
5455
(removed)="removeAffiliation($event)"
5556
></osf-settings-project-affiliation>

src/app/shared/mappers/view-only-links.mapper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { replaceBadEncodedChars } from '../helpers/format-bad-encoding.helper';
12
import {
23
PaginatedViewOnlyLinksModel,
34
ViewOnlyLinkModel,
@@ -34,7 +35,7 @@ export class ViewOnlyLinksMapper {
3435
(node) =>
3536
({
3637
id: node.id,
37-
title: node.attributes.title,
38+
title: replaceBadEncodedChars(node.attributes.title),
3839
category: node.attributes.category,
3940
}) as ViewOnlyLinkNodeModel
4041
),

0 commit comments

Comments
 (0)