From 2ab7b2180bdf1fab536ca8f0f627d04027856f0a Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 18 Dec 2025 11:53:18 +0200 Subject: [PATCH] fix(project-affiliation): updated logic for remove project affiliations --- .../features/analytics/analytics.component.ts | 3 +- ...ettings-project-affiliation.component.html | 3 +- ...ings-project-affiliation.component.spec.ts | 104 ++++++++++++++++++ .../settings-project-affiliation.component.ts | 32 ++---- .../project/settings/settings.component.html | 1 + .../shared/mappers/view-only-links.mapper.ts | 3 +- 6 files changed, 122 insertions(+), 24 deletions(-) diff --git a/src/app/features/analytics/analytics.component.ts b/src/app/features/analytics/analytics.component.ts index 29494fd25..31d130d34 100644 --- a/src/app/features/analytics/analytics.component.ts +++ b/src/app/features/analytics/analytics.component.ts @@ -27,6 +27,7 @@ import { SelectComponent } from '@osf/shared/components/select/select.component' import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; +import { replaceBadEncodedChars } from '@osf/shared/helpers/format-bad-encoding.helper'; import { Primitive } from '@osf/shared/helpers/types.helper'; import { DatasetInput } from '@osf/shared/models/charts/dataset-input'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; @@ -177,7 +178,7 @@ export class AnalyticsComponent implements OnInit { const parts = item.path.split('/').filter(Boolean); const resource = parts[1]?.replace('-', ' ') || 'overview'; let cleanTitle = item.title === 'OSF' ? item.title : item.title.replace(/^OSF \| /, ''); - cleanTitle = cleanTitle.replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>'); + cleanTitle = replaceBadEncodedChars(cleanTitle); return cleanTitle.endsWith(resource) ? cleanTitle : `${cleanTitle} | ${resource}`; }); diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html index 2c2bdcab4..9c277665e 100644 --- a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html @@ -26,13 +26,12 @@

{{ 'myProjects.settings.projectAffiliation' | translate

{{ affiliation.name }}

- @if (canEdit()) { + @if (canRemoveAffiliation(affiliation)) { diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts index ab05020fb..6e12a2741 100644 --- a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts @@ -58,4 +58,108 @@ describe('SettingsProjectAffiliationComponent', () => { expect(component.removed.emit).toHaveBeenCalledWith(MOCK_INSTITUTION); }); + + describe('canRemoveAffiliation', () => { + const affiliatedInstitution = { ...MOCK_INSTITUTION, id: 'affiliated-id' }; + const nonAffiliatedInstitution = { ...MOCK_INSTITUTION, id: 'non-affiliated-id' }; + const userInstitutions = [{ ...MOCK_INSTITUTION, id: 'affiliated-id' }]; + + beforeEach(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [SettingsProjectAffiliationComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: userInstitutions }], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SettingsProjectAffiliationComponent); + component = fixture.componentInstance; + }); + + it('should return true when canRemove is true', () => { + fixture.componentRef.setInput('canRemove', true); + fixture.componentRef.setInput('canEdit', false); + fixture.detectChanges(); + + expect(component.canRemoveAffiliation(affiliatedInstitution)).toBe(true); + expect(component.canRemoveAffiliation(nonAffiliatedInstitution)).toBe(true); + }); + + it('should return true when canEdit is true and user is affiliated with institution', () => { + fixture.componentRef.setInput('canRemove', false); + fixture.componentRef.setInput('canEdit', true); + fixture.detectChanges(); + + expect(component.canRemoveAffiliation(affiliatedInstitution)).toBe(true); + }); + + it('should return false when canEdit is true but user is not affiliated with institution', () => { + fixture.componentRef.setInput('canRemove', false); + fixture.componentRef.setInput('canEdit', true); + fixture.detectChanges(); + + expect(component.canRemoveAffiliation(nonAffiliatedInstitution)).toBe(false); + }); + + it('should return false when both canRemove and canEdit are false', () => { + fixture.componentRef.setInput('canRemove', false); + fixture.componentRef.setInput('canEdit', false); + fixture.detectChanges(); + + expect(component.canRemoveAffiliation(affiliatedInstitution)).toBe(false); + expect(component.canRemoveAffiliation(nonAffiliatedInstitution)).toBe(false); + }); + }); + + describe('userInstitutionIds', () => { + it('should create a Set of user institution IDs', () => { + const userInstitutions = [ + { ...MOCK_INSTITUTION, id: 'id1' }, + { ...MOCK_INSTITUTION, id: 'id2' }, + ]; + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [SettingsProjectAffiliationComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: userInstitutions }], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SettingsProjectAffiliationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + const result = component.userInstitutionIds(); + expect(result).toBeInstanceOf(Set); + expect(result.has('id1')).toBe(true); + expect(result.has('id2')).toBe(true); + expect(result.has('id3')).toBe(false); + }); + + it('should return empty Set when no user institutions', () => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [SettingsProjectAffiliationComponent, OSFTestingModule], + providers: [ + provideMockStore({ + signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: [] }], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SettingsProjectAffiliationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + const result = component.userInstitutionIds(); + expect(result).toBeInstanceOf(Set); + expect(result.size).toBe(0); + }); + }); }); diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts index cb57d1a0f..de8f86c66 100644 --- a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts @@ -20,32 +20,24 @@ import { FetchUserInstitutions, InstitutionsSelectors } from '@shared/stores/ins }) export class SettingsProjectAffiliationComponent implements OnInit { affiliations = input([]); - userInstitutions = select(InstitutionsSelectors.getUserInstitutions); - removed = output(); canEdit = input(false); + canRemove = input(false); + removed = output(); - removeAffiliationPermission = computed(() => { - const affiliatedInstitutions = this.affiliations(); - const userInstitutions = this.userInstitutions(); - - const result = new Map(); - - for (const institution of affiliatedInstitutions) { - const isUserAffiliatedWithCurrentInstitution = userInstitutions.some( - (userInstitution) => userInstitution.id === institution.id - ); - result.set(institution.id, isUserAffiliatedWithCurrentInstitution); - } + userInstitutions = select(InstitutionsSelectors.getUserInstitutions); - return result; - }); + readonly userInstitutionIds = computed(() => new Set(this.userInstitutions().map((inst) => inst.id))); - private readonly actions = createDispatchMap({ - fetchUserInstitutions: FetchUserInstitutions, - }); + private readonly actions = createDispatchMap({ fetchUserInstitutions: FetchUserInstitutions }); ngOnInit() { - this.actions.fetchUserInstitutions(); + if (this.canEdit()) { + this.actions.fetchUserInstitutions(); + } + } + + canRemoveAffiliation(institution: Institution): boolean { + return this.canRemove() || (this.canEdit() && this.userInstitutionIds().has(institution.id)); } removeAffiliation(affiliation: Institution) { diff --git a/src/app/features/project/settings/settings.component.html b/src/app/features/project/settings/settings.component.html index 2aecf4770..a464558dd 100644 --- a/src/app/features/project/settings/settings.component.html +++ b/src/app/features/project/settings/settings.component.html @@ -50,6 +50,7 @@ diff --git a/src/app/shared/mappers/view-only-links.mapper.ts b/src/app/shared/mappers/view-only-links.mapper.ts index 354649c88..bbd57b0cc 100644 --- a/src/app/shared/mappers/view-only-links.mapper.ts +++ b/src/app/shared/mappers/view-only-links.mapper.ts @@ -1,3 +1,4 @@ +import { replaceBadEncodedChars } from '../helpers/format-bad-encoding.helper'; import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel, @@ -34,7 +35,7 @@ export class ViewOnlyLinksMapper { (node) => ({ id: node.id, - title: node.attributes.title, + title: replaceBadEncodedChars(node.attributes.title), category: node.attributes.category, }) as ViewOnlyLinkNodeModel ),