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
),