From 1e73d6a3efc45904bd04f82f47ce689fa4e6aa2e Mon Sep 17 00:00:00 2001 From: mkovalua Date: Tue, 11 Nov 2025 19:07:03 +0200 Subject: [PATCH 01/74] not show osfstorage/addons dropdown for registration file tab --- .../files/pages/files/files.component.html | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 631eceea2..79c645900 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -9,21 +9,23 @@ }
- - -

{{ selectedOption.label }}

-
- -

{{ option.label }}

-
-
+ @if (!isRegistration()) { + + +

{{ selectedOption.label }}

+
+ +

{{ option.label }}

+
+
+ }
@if (filesSelection.length) { @if (!isMoveDialogOpened()) { From 92c8ed868c632f18dad88c022597bf41809aae0c Mon Sep 17 00:00:00 2001 From: Bohdan Odintsov Date: Wed, 12 Nov 2025 15:21:09 +0200 Subject: [PATCH 02/74] fix(files): Fix empty when error from files provider --- src/app/features/files/store/files.state.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts index b9d718298..7a6ca3169 100644 --- a/src/app/features/files/store/files.state.ts +++ b/src/app/features/files/store/files.state.ts @@ -64,7 +64,15 @@ export class FilesState { isAnonymous: response.meta?.anonymous ?? false, }); }), - catchError((error) => handleSectionError(ctx, 'files', error)) + catchError((error) => + of([]).pipe( + tap(() => + ctx.patchState({ + files: { data: [], isLoading: false, error, totalCount: 0 }, + }) + ) + ) + ) ); } From 34b06416860c959ba536db5dfea04dabe1fb0844 Mon Sep 17 00:00:00 2001 From: Bohdan Odintsov Date: Wed, 12 Nov 2025 17:06:37 +0200 Subject: [PATCH 03/74] fix(files): Set to default state of files --- src/app/features/files/store/files.state.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts index 7a6ca3169..306c69598 100644 --- a/src/app/features/files/store/files.state.ts +++ b/src/app/features/files/store/files.state.ts @@ -64,15 +64,10 @@ export class FilesState { isAnonymous: response.meta?.anonymous ?? false, }); }), - catchError((error) => - of([]).pipe( - tap(() => - ctx.patchState({ - files: { data: [], isLoading: false, error, totalCount: 0 }, - }) - ) - ) - ) + catchError((error) => { + ctx.patchState({ files: FILES_STATE_DEFAULTS.files }); + return handleSectionError(ctx, 'files', error); + }) ); } From 1d2643385711e255ce4f4ca04ffb263426cca40a Mon Sep 17 00:00:00 2001 From: Bohdan Odintsov Date: Fri, 14 Nov 2025 16:44:22 +0200 Subject: [PATCH 04/74] fix(providers): Make icon of provider be on center --- .../collections-discover.component.html | 6 ++++-- .../preprint-provider-hero.component.html | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.html b/src/app/features/collections/components/collections-discover/collections-discover.component.html index 875329e71..1e9261f03 100644 --- a/src/app/features/collections/components/collections-discover/collections-discover.component.html +++ b/src/app/features/collections/components/collections-discover/collections-discover.component.html @@ -4,6 +4,10 @@ class="collections-sub-header flex justify-content-between flex-column gap-4 mb-4 sm:mb-6 sm:gap-0 sm:flex-row" >
+

{{ collectionProvider()?.name }}

+
+ +
@let provider = collectionProvider(); @if (provider && provider.assets) { @@ -14,8 +18,6 @@ [src]="provider.assets.squareColorNoTransparent" /> } - -

{{ collectionProvider()?.name }}

diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html index 8754ee628..7339576ee 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html @@ -3,6 +3,12 @@
@if (isPreprintProviderLoading()) { + } @else { +

{{ preprintProvider()!.name }}

+ } +
+
+ @if (isPreprintProviderLoading()) { } @else { -

{{ preprintProvider()!.name }}

}
From a2730423cc2a4542105d0ce7875c4e70ad4fc934 Mon Sep 17 00:00:00 2001 From: futa-ikeda Date: Fri, 14 Nov 2025 14:57:37 -0500 Subject: [PATCH 05/74] feat(login): Add next param to login URL redirect --- src/app/core/services/auth.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index c07b1e10d..871fa81b5 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -35,14 +35,15 @@ export class AuthService { navigateToSignIn(): void { this.loaderService.show(); - const loginUrl = `${this.casUrl}/login?${urlParam({ service: `${this.webUrl}/login` })}`; + const loginUrl = `${this.casUrl}/login?${urlParam({ service: `${this.webUrl}/login`, next: window.location.href })}`; window.location.href = loginUrl; } navigateToOrcidSignIn(): void { const loginUrl = `${this.casUrl}/login?${urlParam({ redirectOrcid: 'true', - service: `${this.webUrl}/login/?next=${encodeURIComponent(this.webUrl)}`, + service: `${this.webUrl}/login`, + next: window.location.href, })}`; window.location.href = loginUrl; } @@ -50,7 +51,8 @@ export class AuthService { navigateToInstitutionSignIn(): void { const loginUrl = `${this.casUrl}/login?${urlParam({ campaign: 'institution', - service: `${this.webUrl}/login/?next=${encodeURIComponent(this.webUrl)}`, + service: `${this.webUrl}/login`, + next: window.location.href, })}`; window.location.href = loginUrl; } From de0624182376857771be0a300c05ab29e7b8b038 Mon Sep 17 00:00:00 2001 From: futa-ikeda Date: Mon, 17 Nov 2025 15:24:38 -0500 Subject: [PATCH 06/74] fix(addons): Show addon icon in User Connected Addons page --- src/app/shared/mappers/addon.mapper.ts | 2 ++ .../services/addons/addons.service.spec.ts | 6 ++++-- .../shared/services/addons/addons.service.ts | 2 +- .../shared/stores/addons/addons.state.spec.ts | 17 ++++++++++------- .../addons/addons.authorized-storage.data.ts | 2 ++ 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/shared/mappers/addon.mapper.ts b/src/app/shared/mappers/addon.mapper.ts index c3a70ed48..bb6ab0331 100644 --- a/src/app/shared/mappers/addon.mapper.ts +++ b/src/app/shared/mappers/addon.mapper.ts @@ -56,6 +56,7 @@ export class AddonMapper { const displayName = (matchingService?.['display_name'] as string) || ''; const credentialsFormat = (matchingService?.['credentials_format'] as string) || ''; const supportedFeatures = (matchingService?.['supported_features'] as string[]) || []; + const iconUrl = (matchingService?.['icon_url'] as string) || ''; return { type: response.type, @@ -73,6 +74,7 @@ export class AddonMapper { externalServiceName, supportedFeatures, credentialsFormat, + iconUrl, providerName: displayName, }; } diff --git a/src/app/shared/services/addons/addons.service.spec.ts b/src/app/shared/services/addons/addons.service.spec.ts index 45355d7ba..5c4b05dcc 100644 --- a/src/app/shared/services/addons/addons.service.spec.ts +++ b/src/app/shared/services/addons/addons.service.spec.ts @@ -90,7 +90,7 @@ describe('Service: Addons', () => { }); const request = httpMock.expectOne( - 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' + 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format,icon_url' ); expect(request.request.method).toBe('GET'); request.flush(getAddonsAuthorizedStorageData()); @@ -103,7 +103,8 @@ describe('Service: Addons', () => { authorizedCapabilities: ['ACCESS', 'UPDATE'], authorizedOperationNames: ['list_root_items', 'get_item_info', 'list_child_items'], credentialsAvailable: true, - credentialsFormat: '', + credentialsFormat: 'OAUTH2', + iconUrl: 'https://osf.io/assets/images/logo.svg', defaultRootFolder: '', displayName: 'Google Drive', externalServiceName: 'googledrive', @@ -151,6 +152,7 @@ describe('Service: Addons', () => { authorizedOperationNames: ['list_root_items', 'get_item_info', 'list_child_items'], credentialsAvailable: true, credentialsFormat: '', + iconUrl: '', defaultRootFolder: '', displayName: 'Google Drive', externalServiceName: '', diff --git a/src/app/shared/services/addons/addons.service.ts b/src/app/shared/services/addons/addons.service.ts index d2e5c6ef7..dadc7d3c2 100644 --- a/src/app/shared/services/addons/addons.service.ts +++ b/src/app/shared/services/addons/addons.service.ts @@ -79,7 +79,7 @@ export class AddonsService { getAuthorizedAddons(addonType: string, referenceId: string): Observable { const params = { - [`fields[external-${addonType}-services]`]: 'external_service_name,credentials_format', + [`fields[external-${addonType}-services]`]: 'external_service_name,credentials_format,icon_url', }; return this.jsonApiService .get< diff --git a/src/app/shared/stores/addons/addons.state.spec.ts b/src/app/shared/stores/addons/addons.state.spec.ts index f86ead712..b01e35596 100644 --- a/src/app/shared/stores/addons/addons.state.spec.ts +++ b/src/app/shared/stores/addons/addons.state.spec.ts @@ -210,7 +210,7 @@ describe('State: Addons', () => { expect(loading()).toBeTruthy(); const request = httpMock.expectOne( - 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' + 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format,icon_url' ); expect(request.request.method).toBe('GET'); request.flush(getAddonsAuthorizedStorageData()); @@ -223,7 +223,7 @@ describe('State: Addons', () => { authorizedCapabilities: ['ACCESS', 'UPDATE'], authorizedOperationNames: ['list_root_items', 'get_item_info', 'list_child_items'], credentialsAvailable: true, - credentialsFormat: '', + credentialsFormat: 'OAUTH2', defaultRootFolder: '', displayName: 'Google Drive', externalServiceName: 'googledrive', @@ -233,6 +233,7 @@ describe('State: Addons', () => { providerName: '', supportedFeatures: [], type: 'authorized-storage-accounts', + iconUrl: 'https://osf.io/assets/images/logo.svg', }) ); @@ -259,7 +260,7 @@ describe('State: Addons', () => { expect(loading()).toBeTruthy(); const request = httpMock.expectOne( - 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' + 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format,icon_url' ); expect(request.request.method).toBe('GET'); @@ -268,7 +269,7 @@ describe('State: Addons', () => { expect(result).toEqual({ data: [], error: - 'Http failure response for http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format: 500 Server Error', + 'Http failure response for http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format,icon_url: 500 Server Error', isLoading: false, isSubmitting: false, }); @@ -303,7 +304,6 @@ describe('State: Addons', () => { authorizedCapabilities: ['ACCESS', 'UPDATE'], authorizedOperationNames: ['list_root_items', 'get_item_info', 'list_child_items'], credentialsAvailable: true, - credentialsFormat: '', defaultRootFolder: '', displayName: 'Google Drive', externalServiceName: '', @@ -313,6 +313,8 @@ describe('State: Addons', () => { providerName: '', supportedFeatures: [], type: 'authorized-storage-accounts', + credentialsFormat: '', // No credentialsFormat in a PATCH response + iconUrl: '', // No iconUrl in a PATCH response }) ); @@ -339,7 +341,7 @@ describe('State: Addons', () => { expect(loading()).toBeTruthy(); let request = httpMock.expectOne( - 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format' + 'http://addons.localhost:8000/user-references/reference-id/authorized_storage_accounts/?include=external-storage-service&fields%5Bexternal-storage-services%5D=external_service_name,credentials_format,icon_url' ); expect(request.request.method).toBe('GET'); request.flush(getAddonsAuthorizedStorageData()); @@ -358,7 +360,6 @@ describe('State: Addons', () => { authorizedCapabilities: ['ACCESS', 'UPDATE'], authorizedOperationNames: ['list_root_items', 'get_item_info', 'list_child_items'], credentialsAvailable: true, - credentialsFormat: '', defaultRootFolder: '', displayName: 'Google Drive', externalServiceName: '', @@ -368,6 +369,8 @@ describe('State: Addons', () => { providerName: '', supportedFeatures: [], type: 'authorized-storage-accounts', + credentialsFormat: '', // No credentialsFormat in a PATCH response + iconUrl: '', // No iconUrl in a PATCH response }) ); diff --git a/src/testing/data/addons/addons.authorized-storage.data.ts b/src/testing/data/addons/addons.authorized-storage.data.ts index 4365c4574..2a7ea5d94 100644 --- a/src/testing/data/addons/addons.authorized-storage.data.ts +++ b/src/testing/data/addons/addons.authorized-storage.data.ts @@ -110,6 +110,8 @@ const AuthorizedStorage = { id: '8aeb85e9-3a73-426f-a89b-5624b4b9d418', attributes: { external_service_name: 'googledrive', + credentials_format: 'OAUTH2', + icon_url: 'https://osf.io/assets/images/logo.svg', }, links: { self: 'https://addons.staging4.osf.io/v1/external-storage-services/8aeb85e9-3a73-426f-a89b-5624b4b9d418', From 76d31d79f890b5a79ad1360079b9f74393a654d2 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 14 Nov 2025 13:57:39 +0200 Subject: [PATCH 07/74] add pagination for registration files retrieval --- .../features/registries/store/handlers/files.handlers.ts | 4 ++-- src/app/shared/services/files.service.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/features/registries/store/handlers/files.handlers.ts b/src/app/features/registries/store/handlers/files.handlers.ts index 478dd1e8e..f852f6be4 100644 --- a/src/app/features/registries/store/handlers/files.handlers.ts +++ b/src/app/features/registries/store/handlers/files.handlers.ts @@ -34,7 +34,7 @@ export class FilesHandlers { ); } - getProjectFiles(ctx: StateContext, { filesLink }: GetFiles) { + getProjectFiles(ctx: StateContext, { filesLink, page }: GetFiles) { const state = ctx.getState(); ctx.patchState({ files: { @@ -43,7 +43,7 @@ export class FilesHandlers { }, }); - return this.filesService.getFilesWithoutFiltering(filesLink).pipe( + return this.filesService.getFilesWithoutFiltering(filesLink, page).pipe( tap((response) => { ctx.patchState({ files: { diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 988adbcc1..57640249d 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -85,9 +85,12 @@ export class FilesService { .pipe(map((response) => ({ files: FilesMapper.getFileFolders(response.data), meta: response.meta }))); } - getFilesWithoutFiltering(filesLink: string): Observable { + getFilesWithoutFiltering(filesLink: string, page = 1): Observable { + const params: Record = { + page: page.toString(), + } return this.jsonApiService - .get(filesLink) + .get(filesLink, params) .pipe(map((response) => FilesMapper.getFiles(response.data))); } From 97333eec6ec8213d3625811376c4833dea72e3e7 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 14 Nov 2025 14:01:10 +0200 Subject: [PATCH 08/74] trigger scroll action for registration files to call pagination --- .../components/files-control/files-control.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/features/registries/components/files-control/files-control.component.html b/src/app/features/registries/components/files-control/files-control.component.html index f2a233060..93fd6d2fa 100644 --- a/src/app/features/registries/components/files-control/files-control.component.html +++ b/src/app/features/registries/components/files-control/files-control.component.html @@ -46,6 +46,7 @@ [storage]="null" [currentFolder]="currentFolder()!" [isLoading]="isFilesLoading()" + [scrollHeight]="'200px'" [viewOnly]="filesViewOnly()" [resourceId]="projectId()" [provider]="provider()" From e377b454e39a8445bbd7200f037d1603a217d585 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 14 Nov 2025 14:38:46 +0200 Subject: [PATCH 09/74] take total files count from metadata --- .../store/preprint-stepper/preprint-stepper.state.ts | 6 +++--- .../features/registries/store/handlers/files.handlers.ts | 4 ++-- src/app/shared/services/files.service.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts index f1c9bbeb8..1aca6708c 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts @@ -313,12 +313,12 @@ export class PreprintStepperState { getProjectFilesByLink(ctx: StateContext, action: FetchProjectFilesByLink) { ctx.setState(patch({ projectFiles: patch({ isLoading: true }) })); - return this.fileService.getFilesWithoutFiltering(action.filesLink).pipe( - tap((files: FileModel[]) => { + return this.fileService.getFilesWithoutFiltering(action.filesLink, 1).pipe( + tap((response) => { ctx.setState( patch({ projectFiles: patch({ - data: files, + data: response.files, isLoading: false, }), }) diff --git a/src/app/features/registries/store/handlers/files.handlers.ts b/src/app/features/registries/store/handlers/files.handlers.ts index f852f6be4..0fa48c323 100644 --- a/src/app/features/registries/store/handlers/files.handlers.ts +++ b/src/app/features/registries/store/handlers/files.handlers.ts @@ -47,10 +47,10 @@ export class FilesHandlers { tap((response) => { ctx.patchState({ files: { - data: response, + data: response.files, isLoading: false, error: null, - totalCount: response.length, + totalCount: response.meta?.total ?? 0, }, }); }), diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 57640249d..ca6eba59c 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -85,13 +85,13 @@ export class FilesService { .pipe(map((response) => ({ files: FilesMapper.getFileFolders(response.data), meta: response.meta }))); } - getFilesWithoutFiltering(filesLink: string, page = 1): Observable { + getFilesWithoutFiltering(filesLink: string, page = 1): Observable<{ files: FileModel[]; meta?: MetaJsonApi }> { const params: Record = { page: page.toString(), } return this.jsonApiService .get(filesLink, params) - .pipe(map((response) => FilesMapper.getFiles(response.data))); + .pipe(map((response) => ({ files: FilesMapper.getFiles(response.data), meta: response.meta }))); } uploadFile( From 98fb3c921884cd0b5e51099df0fff830a7cb0676 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 14 Nov 2025 22:39:12 +0200 Subject: [PATCH 10/74] show all available files of project when scrolling on registration creation --- .../components/files-control/files-control.component.html | 2 +- .../features/registries/store/handlers/files.handlers.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/features/registries/components/files-control/files-control.component.html b/src/app/features/registries/components/files-control/files-control.component.html index 93fd6d2fa..bf53c0a2f 100644 --- a/src/app/features/registries/components/files-control/files-control.component.html +++ b/src/app/features/registries/components/files-control/files-control.component.html @@ -46,7 +46,7 @@ [storage]="null" [currentFolder]="currentFolder()!" [isLoading]="isFilesLoading()" - [scrollHeight]="'200px'" + [scrollHeight]="'500px'" [viewOnly]="filesViewOnly()" [resourceId]="projectId()" [provider]="provider()" diff --git a/src/app/features/registries/store/handlers/files.handlers.ts b/src/app/features/registries/store/handlers/files.handlers.ts index 0fa48c323..4e793cd03 100644 --- a/src/app/features/registries/store/handlers/files.handlers.ts +++ b/src/app/features/registries/store/handlers/files.handlers.ts @@ -40,14 +40,18 @@ export class FilesHandlers { files: { ...state.files, isLoading: true, + error: null, + totalCount: 0, }, }); return this.filesService.getFilesWithoutFiltering(filesLink, page).pipe( tap((response) => { + const newData = page === 1 ? response.files : [...(state.files.data ?? []), ...response.files]; + ctx.patchState({ files: { - data: response.files, + data: newData, isLoading: false, error: null, totalCount: response.meta?.total ?? 0, From b0cb0bf18ce6c1aeee8f8c96dee845b64fc5b856 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 17 Nov 2025 14:23:04 +0200 Subject: [PATCH 11/74] fix linter issues --- src/app/shared/services/files.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index ca6eba59c..363009762 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -88,7 +88,7 @@ export class FilesService { getFilesWithoutFiltering(filesLink: string, page = 1): Observable<{ files: FileModel[]; meta?: MetaJsonApi }> { const params: Record = { page: page.toString(), - } + }; return this.jsonApiService .get(filesLink, params) .pipe(map((response) => ({ files: FilesMapper.getFiles(response.data), meta: response.meta }))); From 72492639cd2e99887afe096bf86982da3c6ebb87 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 17 Nov 2025 16:02:53 +0200 Subject: [PATCH 12/74] resolve CR comments --- .../preprint-stepper/preprint-stepper.state.ts | 2 +- .../registries/store/handlers/files.handlers.ts | 4 ++-- src/app/shared/services/files.service.ts | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts index 1aca6708c..194489d77 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts @@ -318,7 +318,7 @@ export class PreprintStepperState { ctx.setState( patch({ projectFiles: patch({ - data: response.files, + data: response.data, isLoading: false, }), }) diff --git a/src/app/features/registries/store/handlers/files.handlers.ts b/src/app/features/registries/store/handlers/files.handlers.ts index 4e793cd03..e6b5d4f38 100644 --- a/src/app/features/registries/store/handlers/files.handlers.ts +++ b/src/app/features/registries/store/handlers/files.handlers.ts @@ -47,14 +47,14 @@ export class FilesHandlers { return this.filesService.getFilesWithoutFiltering(filesLink, page).pipe( tap((response) => { - const newData = page === 1 ? response.files : [...(state.files.data ?? []), ...response.files]; + const newData = page === 1 ? response.data : [...(state.files.data ?? []), ...response.data]; ctx.patchState({ files: { data: newData, isLoading: false, error: null, - totalCount: response.meta?.total ?? 0, + totalCount: response.totalCount, }, }); }), diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 363009762..0fd3306e0 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -16,6 +16,7 @@ import { OsfFileRevision, PatchFileMetadata, } from '@osf/features/files/models'; +import { PaginatedData } from '@osf/shared/models/paginated-data.model'; import { FileKind } from '../enums/file-kind.enum'; import { AddonMapper } from '../mappers/addon.mapper'; @@ -85,13 +86,17 @@ export class FilesService { .pipe(map((response) => ({ files: FilesMapper.getFileFolders(response.data), meta: response.meta }))); } - getFilesWithoutFiltering(filesLink: string, page = 1): Observable<{ files: FileModel[]; meta?: MetaJsonApi }> { + getFilesWithoutFiltering(filesLink: string, page = 1): Observable> { const params: Record = { page: page.toString(), }; - return this.jsonApiService - .get(filesLink, params) - .pipe(map((response) => ({ files: FilesMapper.getFiles(response.data), meta: response.meta }))); + return this.jsonApiService.get(filesLink, params).pipe( + map((response) => ({ + data: FilesMapper.getFiles(response.data), + totalCount: response.meta.total, + pageSize: response.meta.per_page, + })) + ); } uploadFile( From 996d5dd2175f8af25a22d5f76a1eebb7ed76ed63 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 17 Nov 2025 20:01:42 +0200 Subject: [PATCH 13/74] link user names on the contributor pages to OSF Profiles --- .../contributors-table/contributors-table.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/contributors/contributors-table/contributors-table.component.html b/src/app/shared/components/contributors/contributors-table/contributors-table.component.html index 9d8f37d3f..daaa9ba85 100644 --- a/src/app/shared/components/contributors/contributors-table/contributors-table.component.html +++ b/src/app/shared/components/contributors/contributors-table/contributors-table.component.html @@ -68,7 +68,7 @@

- {{ contributor.fullName }} + {{ contributor.fullName }}

From 03ec15931f14b0f48ab7d2c94aab42feb083bede Mon Sep 17 00:00:00 2001 From: Oleh Paduchak Date: Mon, 3 Nov 2025 15:35:37 +0200 Subject: [PATCH 14/74] fix(storage-item-selector): allow users to add files from google drive on addon setup --- .../storage-item-selector.component.html | 9 +++++++++ .../storage-item-selector.component.ts | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html index 0d860844d..d2f04b986 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html @@ -44,6 +44,15 @@

[handleFolderSelection]="handleFolderSelection" [rootFolder]="selectedStorageItem()" > + @if (selectedStorageItem() !== null) { + + } } @else {
diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts index b5fb14896..79ea944c5 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts @@ -22,6 +22,7 @@ import { OnInit, output, signal, + viewChild, } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -60,6 +61,8 @@ import { ResourceTypeInfoDialogComponent } from '../resource-type-info-dialog/re changeDetection: ChangeDetectionStrategy.OnPush, }) export class StorageItemSelectorComponent implements OnInit { + addFilesPicker = viewChild('filePicker'); + private destroyRef = inject(DestroyRef); private customDialogService = inject(CustomDialogService); private translateService = inject(TranslateService); @@ -236,6 +239,11 @@ export class StorageItemSelectorComponent implements OnInit { handleFolderSelection = (folder: StorageItem): void => { this.selectedStorageItem.set(folder); this.hasFolderChanged.set(folder?.itemId !== this.initiallySelectedStorageItem()?.itemId); + if (this.isGoogleFilePicker()) { + setTimeout(() => { + this.addFilesPicker()?.createPicker(); + }, 1000); + } }; private updateBreadcrumbs( From 7b97b3ac12dc78f0037d9229011781937e90cb53 Mon Sep 17 00:00:00 2001 From: Oleh Paduchak Date: Tue, 4 Nov 2025 15:56:46 +0200 Subject: [PATCH 15/74] fix(storage-item-selector): replase set timeout with timer --- .../storage-item-selector.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts index 79ea944c5..597e887e2 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts @@ -10,6 +10,8 @@ import { InputText } from 'primeng/inputtext'; import { RadioButton } from 'primeng/radiobutton'; import { Skeleton } from 'primeng/skeleton'; +import { timer } from 'rxjs'; + import { ChangeDetectionStrategy, Component, @@ -240,9 +242,9 @@ export class StorageItemSelectorComponent implements OnInit { this.selectedStorageItem.set(folder); this.hasFolderChanged.set(folder?.itemId !== this.initiallySelectedStorageItem()?.itemId); if (this.isGoogleFilePicker()) { - setTimeout(() => { - this.addFilesPicker()?.createPicker(); - }, 1000); + timer(1000) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.addFilesPicker()?.createPicker()); } }; From 5d9475c56f20e7aee38084ab55c118082814dd2c Mon Sep 17 00:00:00 2001 From: Bohdan Odintsov Date: Tue, 18 Nov 2025 16:41:08 +0200 Subject: [PATCH 16/74] fix(mappers): make mappers use format bad encoding --- .../mappers/file-custom-metadata.mapper.ts | 5 +++-- .../files/mappers/resource-metadata.mapper.ts | 5 +++-- .../meetings/mappers/meetings.mapper.ts | 3 ++- .../metadata-description.component.html | 2 +- .../metadata-description.component.ts | 4 +--- .../metadata-title.component.html | 2 +- .../metadata-title.component.ts | 4 +--- .../metadata/mappers/metadata.mapper.ts | 5 +++-- .../mappers/preprint-moderation.mapper.ts | 5 +++-- .../mappers/registry-moderation.mapper.ts | 3 ++- .../preprints/mappers/preprints.mapper.ts | 9 +++++---- .../settings/mappers/settings.mapper.ts | 5 +++-- .../registry/mappers/linked-nodes.mapper.ts | 5 +++-- .../mappers/linked-registrations.mapper.ts | 5 +++-- .../mappers/registry-components.mapper.ts | 5 +++-- .../shared/mappers/activity-logs.mapper.ts | 16 +++++++++------- .../mappers/collections/collections.mapper.ts | 19 ++++++++++--------- .../institutions/institutions.mapper.ts | 5 +++-- src/app/shared/mappers/my-resources.mapper.ts | 4 +++- .../shared/mappers/nodes/base-node.mapper.ts | 7 ++++--- .../mappers/nodes/node-preprint.mapper.ts | 3 ++- .../mappers/projects/projects.mapper.ts | 9 +++++---- .../registration/page-schema.mapper.ts | 5 +++-- .../registration/registration-node.mapper.ts | 5 +++-- .../registration/registration.mapper.ts | 17 +++++++++-------- .../shared/mappers/search/search.mapper.ts | 3 ++- src/app/shared/mappers/wiki/wiki.mapper.ts | 3 ++- 27 files changed, 92 insertions(+), 71 deletions(-) diff --git a/src/app/features/files/mappers/file-custom-metadata.mapper.ts b/src/app/features/files/mappers/file-custom-metadata.mapper.ts index a347b32a4..3af893193 100644 --- a/src/app/features/files/mappers/file-custom-metadata.mapper.ts +++ b/src/app/features/files/mappers/file-custom-metadata.mapper.ts @@ -1,13 +1,14 @@ import { ApiData } from '@osf/shared/models/common/json-api.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { FileCustomMetadata, OsfFileCustomMetadata } from '../models'; export function MapFileCustomMetadata(data: ApiData): OsfFileCustomMetadata { return { id: data.id, - description: data.attributes.description, + description: replaceBadEncodedChars(data.attributes.description), language: data.attributes.language, resourceTypeGeneral: data.attributes.resource_type_general, - title: data.attributes.title, + title: replaceBadEncodedChars(data.attributes.title), }; } diff --git a/src/app/features/files/mappers/resource-metadata.mapper.ts b/src/app/features/files/mappers/resource-metadata.mapper.ts index cf89e485d..5fc0e6fc0 100644 --- a/src/app/features/files/mappers/resource-metadata.mapper.ts +++ b/src/app/features/files/mappers/resource-metadata.mapper.ts @@ -1,5 +1,6 @@ import { IdentifiersMapper } from '@osf/shared/mappers/identifiers.mapper'; import { ResourceMetadata } from '@osf/shared/models/resource-metadata.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { GetResourceCustomMetadataResponse } from '../models/get-resource-custom-metadata-response.model'; import { GetResourceShortInfoResponse } from '../models/get-resource-short-info-response.model'; @@ -9,8 +10,8 @@ export function MapResourceMetadata( customMetadata: GetResourceCustomMetadataResponse ): ResourceMetadata { return { - title: shortInfo.data.attributes.title, - description: shortInfo.data.attributes.description, + title: replaceBadEncodedChars(shortInfo.data.attributes.title), + description: replaceBadEncodedChars(shortInfo.data.attributes.description), dateCreated: new Date(shortInfo.data.attributes.date_created), dateModified: new Date(shortInfo.data.attributes.date_modified), funders: diff --git a/src/app/features/meetings/mappers/meetings.mapper.ts b/src/app/features/meetings/mappers/meetings.mapper.ts index 4807257e6..f967aa4bb 100644 --- a/src/app/features/meetings/mappers/meetings.mapper.ts +++ b/src/app/features/meetings/mappers/meetings.mapper.ts @@ -1,4 +1,5 @@ import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { MeetingGetResponseJsonApi, @@ -28,7 +29,7 @@ export class MeetingsMapper { return { data: response.data.map((item) => ({ id: item.id, - title: item.attributes.title, + title: replaceBadEncodedChars(item.attributes.title), dateCreated: item.attributes.date_created, authorName: item.attributes.author_name, downloadCount: item.attributes.download_count || 0, diff --git a/src/app/features/metadata/components/metadata-description/metadata-description.component.html b/src/app/features/metadata/components/metadata-description/metadata-description.component.html index f74c20da0..b1dd3143b 100644 --- a/src/app/features/metadata/components/metadata-description/metadata-description.component.html +++ b/src/app/features/metadata/components/metadata-description/metadata-description.component.html @@ -13,6 +13,6 @@

{{ 'project.overview.metadata.description' | translate }}

- {{ (description() | fixSpecialChar) || ('project.overview.metadata.noDescription' | translate) }} + {{ description() || ('project.overview.metadata.noDescription' | translate) }}

diff --git a/src/app/features/metadata/components/metadata-description/metadata-description.component.ts b/src/app/features/metadata/components/metadata-description/metadata-description.component.ts index e7cd92173..27a06c164 100644 --- a/src/app/features/metadata/components/metadata-description/metadata-description.component.ts +++ b/src/app/features/metadata/components/metadata-description/metadata-description.component.ts @@ -5,11 +5,9 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { FixSpecialCharPipe } from '@osf/shared/pipes/fix-special-char.pipe'; - @Component({ selector: 'osf-metadata-description', - imports: [Card, Button, FixSpecialCharPipe, TranslatePipe], + imports: [Card, Button, TranslatePipe], templateUrl: './metadata-description.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/src/app/features/metadata/components/metadata-title/metadata-title.component.html b/src/app/features/metadata/components/metadata-title/metadata-title.component.html index 91a677fd7..9f1e06f1a 100644 --- a/src/app/features/metadata/components/metadata-title/metadata-title.component.html +++ b/src/app/features/metadata/components/metadata-title/metadata-title.component.html @@ -13,6 +13,6 @@

{{ 'common.labels.title' | translate }}

- {{ title() | fixSpecialChar }} + {{ title() }}

diff --git a/src/app/features/metadata/components/metadata-title/metadata-title.component.ts b/src/app/features/metadata/components/metadata-title/metadata-title.component.ts index 5ab4d32ba..b1864575c 100644 --- a/src/app/features/metadata/components/metadata-title/metadata-title.component.ts +++ b/src/app/features/metadata/components/metadata-title/metadata-title.component.ts @@ -5,11 +5,9 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { FixSpecialCharPipe } from '@osf/shared/pipes/fix-special-char.pipe'; - @Component({ selector: 'osf-metadata-title', - imports: [Card, Button, FixSpecialCharPipe, TranslatePipe], + imports: [Card, Button, TranslatePipe], templateUrl: './metadata-title.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/src/app/features/metadata/mappers/metadata.mapper.ts b/src/app/features/metadata/mappers/metadata.mapper.ts index c55d1f5bd..5c166574d 100644 --- a/src/app/features/metadata/mappers/metadata.mapper.ts +++ b/src/app/features/metadata/mappers/metadata.mapper.ts @@ -1,5 +1,6 @@ import { IdentifiersMapper } from '@osf/shared/mappers/identifiers.mapper'; import { LicensesMapper } from '@osf/shared/mappers/licenses.mapper'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { CustomItemMetadataRecord, CustomMetadataJsonApi, MetadataJsonApi, MetadataModel } from '../models'; @@ -7,8 +8,8 @@ export class MetadataMapper { static fromMetadataApiResponse(response: MetadataJsonApi): MetadataModel { return { id: response.id, - title: response.attributes.title, - description: response.attributes.description, + title: replaceBadEncodedChars(response.attributes.title), + description: replaceBadEncodedChars(response.attributes.description), tags: response.attributes.tags, dateCreated: response.attributes.date_created, dateModified: response.attributes.date_modified, diff --git a/src/app/features/moderation/mappers/preprint-moderation.mapper.ts b/src/app/features/moderation/mappers/preprint-moderation.mapper.ts index b87c70438..5fb0cc752 100644 --- a/src/app/features/moderation/mappers/preprint-moderation.mapper.ts +++ b/src/app/features/moderation/mappers/preprint-moderation.mapper.ts @@ -1,6 +1,7 @@ import { UserMapper } from '@osf/shared/mappers/user'; import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { PreprintProviderModerationInfo, @@ -64,7 +65,7 @@ export class PreprintModerationMapper { return { data: response.data.map((x) => ({ id: x.id, - title: x.attributes.title, + title: replaceBadEncodedChars(x.attributes.title), public: x.attributes.public, reviewsState: x.attributes.reviews_state, actions: [], @@ -86,7 +87,7 @@ export class PreprintModerationMapper { return { data: response.data.map((x) => ({ id: x.id, - title: x.embeds.target.data.attributes.title, + title: replaceBadEncodedChars(x.embeds.target.data.attributes.title), preprintId: x.embeds.target.data.id, actions: [], contributors: [], diff --git a/src/app/features/moderation/mappers/registry-moderation.mapper.ts b/src/app/features/moderation/mappers/registry-moderation.mapper.ts index 8c7113170..70ab5fdb4 100644 --- a/src/app/features/moderation/mappers/registry-moderation.mapper.ts +++ b/src/app/features/moderation/mappers/registry-moderation.mapper.ts @@ -1,5 +1,6 @@ import { UserMapper } from '@osf/shared/mappers/user'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { RegistryDataJsonApi, @@ -13,7 +14,7 @@ export class RegistryModerationMapper { static fromResponse(response: RegistryDataJsonApi): RegistryModeration { return { id: response.id, - title: response.attributes.title, + title: replaceBadEncodedChars(response.attributes.title), reviewsState: response.attributes.reviews_state, revisionStatus: response.attributes.revision_state, public: response.attributes.public, diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index 283704403..51ead14da 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -2,6 +2,7 @@ import { StringOrNull } from '@osf/shared/helpers/types.helper'; import { IdentifiersMapper } from '@osf/shared/mappers/identifiers.mapper'; import { LicensesMapper } from '@osf/shared/mappers/licenses.mapper'; import { ApiData, JsonApiResponseWithMeta, ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { PreprintAttributesJsonApi, @@ -18,7 +19,7 @@ export class PreprintsMapper { return { data: { attributes: { - title: title, + title: replaceBadEncodedChars(title), description: abstract, }, relationships: { @@ -44,7 +45,7 @@ export class PreprintsMapper { dateWithdrawn: response.attributes.date_withdrawn, datePublished: response.attributes.date_published, dateLastTransitioned: response.attributes.date_last_transitioned, - title: response.attributes.title, + title: replaceBadEncodedChars(response.attributes.title), description: response.attributes.description, reviewsState: response.attributes.reviews_state, preprintDoiCreated: response.attributes.preprint_doi_created, @@ -102,7 +103,7 @@ export class PreprintsMapper { dateWithdrawn: data.attributes.date_withdrawn, datePublished: data.attributes.date_published, dateLastTransitioned: data.attributes.date_last_transitioned, - title: data.attributes.title, + title: replaceBadEncodedChars(data.attributes.title), description: data.attributes.description, reviewsState: data.attributes.reviews_state, preprintDoiCreated: data.attributes.preprint_doi_created, @@ -172,7 +173,7 @@ export class PreprintsMapper { data: response.data.map((preprintData) => { return { id: preprintData.id, - title: preprintData.attributes.title, + title: replaceBadEncodedChars(preprintData.attributes.title), dateModified: preprintData.attributes.date_modified, contributors: preprintData.embeds.bibliographic_contributors.data.map((contrData) => { return { diff --git a/src/app/features/project/settings/mappers/settings.mapper.ts b/src/app/features/project/settings/mappers/settings.mapper.ts index 3dec5f603..d2eef0d1d 100644 --- a/src/app/features/project/settings/mappers/settings.mapper.ts +++ b/src/app/features/project/settings/mappers/settings.mapper.ts @@ -1,6 +1,7 @@ import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; import { InstitutionsMapper } from '@osf/shared/mappers/institutions'; import { RegionsMapper } from '@osf/shared/mappers/regions'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { BaseNodeDataJsonApi } from '@shared/models/nodes/base-node-data-json-api.model'; import { @@ -30,8 +31,8 @@ export class SettingsMapper { static fromNodeResponse(data: BaseNodeDataJsonApi): NodeDetailsModel { return { id: data.id, - title: data.attributes.title, - description: data.attributes.description, + title: replaceBadEncodedChars(data.attributes.title), + description: replaceBadEncodedChars(data.attributes.description), isPublic: data.attributes.public, region: data.embeds?.region ? RegionsMapper.getRegion(data?.embeds?.region?.data) : null, affiliatedInstitutions: data.embeds?.affiliated_institutions diff --git a/src/app/features/registry/mappers/linked-nodes.mapper.ts b/src/app/features/registry/mappers/linked-nodes.mapper.ts index 441996057..372257e1e 100644 --- a/src/app/features/registry/mappers/linked-nodes.mapper.ts +++ b/src/app/features/registry/mappers/linked-nodes.mapper.ts @@ -1,4 +1,5 @@ import { ContributorsMapper } from '@osf/shared/mappers/contributors'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { LinkedNode, LinkedNodeJsonApi } from '../models'; @@ -6,8 +7,8 @@ export class LinkedNodesMapper { static fromApiResponse(apiNode: LinkedNodeJsonApi): LinkedNode { return { id: apiNode.id, - title: apiNode.attributes.title, - description: apiNode.attributes.description, + title: replaceBadEncodedChars(apiNode.attributes.title), + description: replaceBadEncodedChars(apiNode.attributes.description), category: apiNode.attributes.category, dateCreated: apiNode.attributes.date_created, dateModified: apiNode.attributes.date_modified, diff --git a/src/app/features/registry/mappers/linked-registrations.mapper.ts b/src/app/features/registry/mappers/linked-registrations.mapper.ts index c61b4e7da..72b763e19 100644 --- a/src/app/features/registry/mappers/linked-registrations.mapper.ts +++ b/src/app/features/registry/mappers/linked-registrations.mapper.ts @@ -1,4 +1,5 @@ import { ContributorsMapper } from '@osf/shared/mappers/contributors'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { LinkedRegistration, LinkedRegistrationJsonApi } from '../models'; @@ -6,8 +7,8 @@ export class LinkedRegistrationsMapper { static fromApiResponse(apiRegistration: LinkedRegistrationJsonApi): LinkedRegistration { return { id: apiRegistration.id, - title: apiRegistration.attributes.title, - description: apiRegistration.attributes.description, + title: replaceBadEncodedChars(apiRegistration.attributes.title), + description: replaceBadEncodedChars(apiRegistration.attributes.description), category: apiRegistration.attributes.category, dateCreated: apiRegistration.attributes.date_created, dateModified: apiRegistration.attributes.date_modified, diff --git a/src/app/features/registry/mappers/registry-components.mapper.ts b/src/app/features/registry/mappers/registry-components.mapper.ts index 5e8339323..55ac8b2f6 100644 --- a/src/app/features/registry/mappers/registry-components.mapper.ts +++ b/src/app/features/registry/mappers/registry-components.mapper.ts @@ -1,4 +1,5 @@ import { ContributorsMapper } from '@osf/shared/mappers/contributors'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { RegistryComponentJsonApi, RegistryComponentModel } from '../models'; @@ -6,8 +7,8 @@ export class RegistryComponentsMapper { static fromApiResponse(apiComponent: RegistryComponentJsonApi): RegistryComponentModel { return { id: apiComponent.id, - title: apiComponent.attributes.title, - description: apiComponent.attributes.description, + title: replaceBadEncodedChars(apiComponent.attributes.title), + description: replaceBadEncodedChars(apiComponent.attributes.description), category: apiComponent.attributes.category, dateCreated: apiComponent.attributes.date_created, dateModified: apiComponent.attributes.date_modified, diff --git a/src/app/shared/mappers/activity-logs.mapper.ts b/src/app/shared/mappers/activity-logs.mapper.ts index d0b7877a6..a4c1fc1c6 100644 --- a/src/app/shared/mappers/activity-logs.mapper.ts +++ b/src/app/shared/mappers/activity-logs.mapper.ts @@ -1,3 +1,5 @@ +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; + import { DEFAULT_TABLE_PARAMS } from '../constants/default-table-params.constants'; import { ActivityLog, LogContributor } from '../models/activity-logs/activity-logs.model'; import { ActivityLogJsonApi, LogContributorJsonApi } from '../models/activity-logs/activity-logs-json-api.model'; @@ -24,7 +26,7 @@ export class ActivityLogsMapper { paramsNode: params.params_node ? { id: params.params_node.id, - title: params.params_node.title, + title: replaceBadEncodedChars(params.params_node.title), } : { id: '', title: '' }, paramsProject: params.params_project, @@ -32,14 +34,14 @@ export class ActivityLogsMapper { ? { id: params.template_node.id, url: params.template_node.url, - title: params.template_node.title, + title: replaceBadEncodedChars(params.template_node.title), } : null, pointer: params.pointer ? { category: params.pointer.category, id: params.pointer.id, - title: params.pointer.title, + title: replaceBadEncodedChars(params.pointer.title), url: params.pointer.url, } : null, @@ -72,8 +74,8 @@ export class ActivityLogsMapper { ? { id: log.embeds.original_node.data.id, type: log.embeds.original_node.data.type, - title: log.embeds.original_node.data.attributes.title, - description: log.embeds.original_node.data.attributes.description, + title: replaceBadEncodedChars(log.embeds.original_node.data.attributes.title), + description: replaceBadEncodedChars(log.embeds.original_node.data.attributes.description), category: log.embeds.original_node.data.attributes.category, customCitation: log.embeds.original_node.data.attributes.custom_citation, dateCreated: log.embeds.original_node.data.attributes.date_created, @@ -119,8 +121,8 @@ export class ActivityLogsMapper { ? { id: log.embeds.linked_node.data.id, type: log.embeds.linked_node.data.type, - title: log.embeds.linked_node.data.attributes.title, - description: log.embeds.linked_node.data.attributes.description, + title: replaceBadEncodedChars(log.embeds.linked_node.data.attributes.title), + description: replaceBadEncodedChars(log.embeds.linked_node.data.attributes.description), category: log.embeds.linked_node.data.attributes.category, customCitation: log.embeds.linked_node.data.attributes.custom_citation, dateCreated: log.embeds.linked_node.data.attributes.date_created, diff --git a/src/app/shared/mappers/collections/collections.mapper.ts b/src/app/shared/mappers/collections/collections.mapper.ts index b0fb73392..88aa46f12 100644 --- a/src/app/shared/mappers/collections/collections.mapper.ts +++ b/src/app/shared/mappers/collections/collections.mapper.ts @@ -20,6 +20,7 @@ import { import { ResponseJsonApi } from '@osf/shared/models/common/json-api.model'; import { ContributorModel } from '@osf/shared/models/contributors/contributor.model'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { UserMapper } from '../user'; @@ -28,8 +29,8 @@ export class CollectionsMapper { return { id: response.id, type: response.type, - name: response.attributes.name, - description: response.attributes.description, + name: replaceBadEncodedChars(response.attributes.name), + description: replaceBadEncodedChars(response.attributes.description), advisoryBoard: response.attributes.advisory_board, example: response.attributes.example, domain: response.attributes.domain, @@ -58,7 +59,7 @@ export class CollectionsMapper { brand: response.embeds.brand.data ? { id: response.embeds.brand.data.id, - name: response.embeds.brand.data.attributes.name, + name: replaceBadEncodedChars(response.embeds.brand.data.attributes.name), heroLogoImageUrl: response.embeds.brand.data.attributes.hero_logo_image, topNavLogoImageUrl: response.embeds.brand.data.attributes.topnav_logo_image, heroBackgroundImageUrl: response.embeds.brand.data.attributes.hero_background_image, @@ -74,7 +75,7 @@ export class CollectionsMapper { return { id: response.id, type: response.type, - title: response.attributes.title, + title: replaceBadEncodedChars(response.attributes.title), dateCreated: response.attributes.date_created, dateModified: response.attributes.date_modified, bookmarks: response.attributes.bookmarks, @@ -110,7 +111,7 @@ export class CollectionsMapper { dataType: submission.attributes.data_type, disease: submission.attributes.disease, gradeLevels: submission.attributes.grade_levels, - collectionTitle: submission.embeds.collection.data.attributes.title, + collectionTitle: replaceBadEncodedChars(submission.embeds.collection.data.attributes.title), collectionId: submission.embeds.collection.data.relationships.provider.data.id, }; } @@ -127,8 +128,8 @@ export class CollectionsMapper { type: submission.type, nodeId: submission.embeds.guid.data.id, nodeUrl: submission.embeds.guid.data.links.html, - title: submission.embeds.guid.data.attributes.title, - description: submission.embeds.guid.data.attributes.description, + title: replaceBadEncodedChars(submission.embeds.guid.data.attributes.title), + description: replaceBadEncodedChars(submission.embeds.guid.data.attributes.description), category: submission.embeds.guid.data.attributes.category, dateCreated: submission.embeds.guid.data.attributes.date_created, dateModified: submission.embeds.guid.data.attributes.date_modified, @@ -183,8 +184,8 @@ export class CollectionsMapper { type: submission.type, nodeId: submission.embeds.guid.data.id, nodeUrl: submission.embeds.guid.data.links.html, - title: submission.embeds.guid.data.attributes.title, - description: submission.embeds.guid.data.attributes.description, + title: replaceBadEncodedChars(submission.embeds.guid.data.attributes.title), + description: replaceBadEncodedChars(submission.embeds.guid.data.attributes.description), category: submission.embeds.guid.data.attributes.category, dateCreated: submission.embeds.guid.data.attributes.date_created, dateModified: submission.embeds.guid.data.attributes.date_modified, diff --git a/src/app/shared/mappers/institutions/institutions.mapper.ts b/src/app/shared/mappers/institutions/institutions.mapper.ts index 56cb294f4..66642d805 100644 --- a/src/app/shared/mappers/institutions/institutions.mapper.ts +++ b/src/app/shared/mappers/institutions/institutions.mapper.ts @@ -4,6 +4,7 @@ import { InstitutionsWithMetaJsonApiResponse, } from '@osf/shared/models/institutions/institution-json-api.model'; import { Institution, InstitutionsWithTotalCount } from '@osf/shared/models/institutions/institutions.models'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; export class InstitutionsMapper { static fromInstitutionsResponse(response: InstitutionsJsonApiResponse): Institution[] { @@ -14,8 +15,8 @@ export class InstitutionsMapper { return { id: data.id, type: data.type, - name: data.attributes.name, - description: data.attributes.description, + name: replaceBadEncodedChars(data.attributes.name), + description: replaceBadEncodedChars(data.attributes.description), iri: data.links.iri, rorIri: data.attributes.ror_iri, iris: data.attributes.iris, diff --git a/src/app/shared/mappers/my-resources.mapper.ts b/src/app/shared/mappers/my-resources.mapper.ts index b924d9a5c..7a2a5d5d2 100644 --- a/src/app/shared/mappers/my-resources.mapper.ts +++ b/src/app/shared/mappers/my-resources.mapper.ts @@ -1,3 +1,5 @@ +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; + import { MyResourcesItem, MyResourcesItemGetResponseJsonApi } from '../models/my-resources/my-resources.models'; import { ContributorsMapper } from './contributors'; @@ -7,7 +9,7 @@ export class MyResourcesMapper { return { id: response.id, type: response.type, - title: response.attributes.title, + title: replaceBadEncodedChars(response.attributes.title), dateCreated: response.attributes.date_created, dateModified: response.attributes.date_modified, isPublic: response.attributes.public, diff --git a/src/app/shared/mappers/nodes/base-node.mapper.ts b/src/app/shared/mappers/nodes/base-node.mapper.ts index e839fcf0f..fa0645bac 100644 --- a/src/app/shared/mappers/nodes/base-node.mapper.ts +++ b/src/app/shared/mappers/nodes/base-node.mapper.ts @@ -3,6 +3,7 @@ import { BaseNodeModel, NodeModel } from '@osf/shared/models/nodes/base-node.mod import { BaseNodeDataJsonApi } from '@osf/shared/models/nodes/base-node-data-json-api.model'; import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { ContributorsMapper } from '../contributors'; @@ -22,7 +23,7 @@ export class BaseNodeMapper { static getNodesWithChildren(data: BaseNodeDataJsonApi[], parentId: string): NodeShortInfoModel[] { return this.getAllDescendants(data, parentId).map((item) => ({ id: item.id, - title: item.attributes.title, + title: replaceBadEncodedChars(item.attributes.title), isPublic: item.attributes.public, permissions: item.attributes.current_user_permissions, parentId: item.relationships.parent?.data?.id, @@ -33,8 +34,8 @@ export class BaseNodeMapper { return { id: data.id, type: data.type, - title: data.attributes.title, - description: data.attributes.description, + title: replaceBadEncodedChars(data.attributes.title), + description: replaceBadEncodedChars(data.attributes.description), category: data.attributes.category, dateCreated: data.attributes.date_created, dateModified: data.attributes.date_modified, diff --git a/src/app/shared/mappers/nodes/node-preprint.mapper.ts b/src/app/shared/mappers/nodes/node-preprint.mapper.ts index da9df54bf..700581b8c 100644 --- a/src/app/shared/mappers/nodes/node-preprint.mapper.ts +++ b/src/app/shared/mappers/nodes/node-preprint.mapper.ts @@ -1,11 +1,12 @@ import { NodePreprintModel } from '@osf/shared/models/nodes/node-preprint.model'; import { NodePreprintDataJsonApi } from '@osf/shared/models/nodes/node-preprint-json-api.model'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; export class NodePreprintMapper { static getNodePreprint(data: NodePreprintDataJsonApi): NodePreprintModel { return { id: data.id, - title: data.attributes.title, + title: replaceBadEncodedChars(data.attributes.title), dateCreated: data.attributes.date_created, dateModified: data.attributes.date_modified, datePublished: data.attributes.date_published, diff --git a/src/app/shared/mappers/projects/projects.mapper.ts b/src/app/shared/mappers/projects/projects.mapper.ts index 51cbfa73e..cfe5faf1a 100644 --- a/src/app/shared/mappers/projects/projects.mapper.ts +++ b/src/app/shared/mappers/projects/projects.mapper.ts @@ -2,6 +2,7 @@ import { CollectionSubmissionMetadataPayloadJsonApi } from '@osf/features/collec import { ProjectMetadataUpdatePayload } from '@osf/shared/models/project-metadata-update-payload.model'; import { ProjectModel } from '@osf/shared/models/projects/projects.models'; import { ProjectJsonApi, ProjectsResponseJsonApi } from '@osf/shared/models/projects/projects-json-api.models'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; export class ProjectsMapper { static fromGetAllProjectsResponse(response: ProjectsResponseJsonApi): ProjectModel[] { @@ -12,7 +13,7 @@ export class ProjectsMapper { return { id: project.id, type: project.type, - title: project.attributes.title, + title: replaceBadEncodedChars(project.attributes.title), dateModified: project.attributes.date_modified, isPublic: project.attributes.public, licenseId: project.relationships.license?.data?.id || null, @@ -22,7 +23,7 @@ export class ProjectsMapper { copyrightHolders: project.attributes.node_license.copyright_holders.join(','), } : null, - description: project.attributes.description, + description: replaceBadEncodedChars(project.attributes.description), tags: project.attributes.tags || [], }; } @@ -41,8 +42,8 @@ export class ProjectsMapper { }, }, attributes: { - title: metadata.title, - description: metadata.description, + title: replaceBadEncodedChars(metadata.title), + description: replaceBadEncodedChars(metadata.description), tags: metadata.tags, ...(metadata.licenseOptions && { node_license: { diff --git a/src/app/shared/mappers/registration/page-schema.mapper.ts b/src/app/shared/mappers/registration/page-schema.mapper.ts index 84c57a97f..c42851d43 100644 --- a/src/app/shared/mappers/registration/page-schema.mapper.ts +++ b/src/app/shared/mappers/registration/page-schema.mapper.ts @@ -1,5 +1,6 @@ import { BlockType } from '@osf/shared/enums/block-type.enum'; import { FieldType } from '@osf/shared/enums/field-type.enum'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { PageSchema, Question, Section } from '@shared/models/registration/page-schema.model'; import { SchemaBlocksResponseJsonApi } from '@shared/models/registration/schema-blocks-json-api.model'; @@ -19,7 +20,7 @@ export class PageSchemaMapper { case BlockType.PageHeading: currentPage = { id: item.id, - title: item.attributes.display_text, + title: replaceBadEncodedChars(item.attributes.display_text), helpText: item.attributes.help_text, questions: [], }; @@ -31,7 +32,7 @@ export class PageSchemaMapper { if (currentPage) { currentSection = { id: item.id, - title: item.attributes.display_text, + title: replaceBadEncodedChars(item.attributes.display_text), helpText: item.attributes.help_text, questions: [], }; diff --git a/src/app/shared/mappers/registration/registration-node.mapper.ts b/src/app/shared/mappers/registration/registration-node.mapper.ts index fda7ddd86..9dcafab4d 100644 --- a/src/app/shared/mappers/registration/registration-node.mapper.ts +++ b/src/app/shared/mappers/registration/registration-node.mapper.ts @@ -1,4 +1,5 @@ import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { ProviderShortInfoModel } from '@shared/models/provider/provider.model'; import { RegistryProviderDetailsJsonApi } from '@shared/models/provider/registration-provider-json-api.model'; import { RegistrationNodeModel, RegistrationResponses } from '@shared/models/registration/registration-node.model'; @@ -26,7 +27,7 @@ export class RegistrationNodeMapper { dateModified: attributes.date_modified, dateRegistered: attributes.date_registered, dateWithdrawn: attributes.date_withdrawn, - description: attributes.description, + description: replaceBadEncodedChars(attributes.description), embargoed: attributes.embargoed, embargoEndDate: attributes.embargo_end_date, hasAnalyticCode: attributes.has_analytic_code, @@ -56,7 +57,7 @@ export class RegistrationNodeMapper { reviewsState: attributes.reviews_state, revisionState: attributes.revision_state, tags: attributes.tags || [], - title: attributes.title, + title: replaceBadEncodedChars(attributes.title), wikiEnabled: attributes.wiki_enabled, withdrawalJustification: attributes.withdrawal_justification, withdrawn: attributes.withdrawn, diff --git a/src/app/shared/mappers/registration/registration.mapper.ts b/src/app/shared/mappers/registration/registration.mapper.ts index 966b9fafa..43040ec85 100644 --- a/src/app/shared/mappers/registration/registration.mapper.ts +++ b/src/app/shared/mappers/registration/registration.mapper.ts @@ -1,4 +1,5 @@ import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { DraftRegistrationModel } from '@shared/models/registration/draft-registration.model'; import { RegistrationModel } from '@shared/models/registration/registration.model'; import { RegistrationCard } from '@shared/models/registration/registration-card.model'; @@ -17,8 +18,8 @@ export class RegistrationMapper { static fromDraftRegistrationResponse(response: DraftRegistrationDataJsonApi): DraftRegistrationModel { return { id: response.id, - title: response.attributes.title, - description: response.attributes.description, + title: replaceBadEncodedChars(response.attributes.title), + description: replaceBadEncodedChars(response.attributes.description), registrationSchemaId: response.relationships.registration_schema?.data?.id || '', license: { id: response.relationships.license?.data?.id || '', @@ -34,13 +35,13 @@ export class RegistrationMapper { branchedFrom: response.embeds?.branched_from?.data ? { id: response.embeds.branched_from.data.id, - title: response.embeds.branched_from.data.attributes.title, + title: replaceBadEncodedChars(response.embeds.branched_from.data.attributes.title), filesLink: response.embeds?.branched_from?.data.relationships?.files?.links?.related?.href, type: response.embeds.branched_from.data.type, } : { id: response.relationships.branched_from?.data?.id || '', - title: response.attributes.title, + title: replaceBadEncodedChars(response.attributes.title), filesLink: response.relationships.branched_from?.links?.related.href + 'files/', type: response.relationships.branched_from?.data?.type, }, @@ -61,8 +62,8 @@ export class RegistrationMapper { static fromDraftToRegistrationCard(registration: DraftRegistrationDataJsonApi): RegistrationCard { return { id: registration.id, - title: registration.attributes.title, - description: registration.attributes.description || '', + title: replaceBadEncodedChars(registration.attributes.title), + description: replaceBadEncodedChars(registration.attributes.description) || '', status: RegistryStatus.None, dateCreated: registration.attributes.datetime_initiated, dateModified: registration.attributes.datetime_updated, @@ -77,8 +78,8 @@ export class RegistrationMapper { static fromRegistrationToRegistrationCard(registration: RegistrationDataJsonApi): RegistrationCard { return { id: registration.id, - title: registration.attributes.title, - description: registration.attributes.description || '', + title: replaceBadEncodedChars(registration.attributes.title), + description: replaceBadEncodedChars(registration.attributes.description) || '', status: MapRegistryStatus(registration.attributes), dateCreated: registration.attributes.date_created, dateModified: registration.attributes.date_modified, diff --git a/src/app/shared/mappers/search/search.mapper.ts b/src/app/shared/mappers/search/search.mapper.ts index 1ee0cd9a7..e0aad24e9 100644 --- a/src/app/shared/mappers/search/search.mapper.ts +++ b/src/app/shared/mappers/search/search.mapper.ts @@ -1,4 +1,5 @@ import { ResourceType } from '@shared/enums/resource-type.enum'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; import { IndexCardDataJsonApi, IndexCardSearchResponseJsonApi, @@ -34,7 +35,7 @@ export function MapResources(indexCardSearchResponseJsonApi: IndexCardSearchResp absoluteUrl: resourceMetadata['@id'], resourceType: ResourceType[resourceMetadata.resourceType[0]['@id'] as keyof typeof ResourceType], name: resourceMetadata.name?.[0]?.['@value'], - title: resourceMetadata.title?.[0]?.['@value'], + title: replaceBadEncodedChars(resourceMetadata.title?.[0]?.['@value']), fileName: resourceMetadata.fileName?.[0]?.['@value'], description: resourceMetadata.description?.[0]?.['@value'], diff --git a/src/app/shared/mappers/wiki/wiki.mapper.ts b/src/app/shared/mappers/wiki/wiki.mapper.ts index e79ea6c30..8d16aaedb 100644 --- a/src/app/shared/mappers/wiki/wiki.mapper.ts +++ b/src/app/shared/mappers/wiki/wiki.mapper.ts @@ -11,6 +11,7 @@ import { WikiVersionJsonApi, } from '@osf/shared/models/wiki/wiki.model'; import { ComponentWiki } from '@osf/shared/stores/wiki'; +import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; export class WikiMapper { private static translate: TranslateService; @@ -47,7 +48,7 @@ export class WikiMapper { static fromGetComponentsWikiResponse(response: ComponentsWikiGetResponse): ComponentWiki { return { id: response.id, - title: response.attributes.title, + title: replaceBadEncodedChars(response.attributes.title), list: response.embeds?.wikis?.data.map((wiki) => WikiMapper.fromGetWikiResponse(wiki)) || [], }; } From 671bdd0b9cc86d660224de77caf06c6d83c6a4a3 Mon Sep 17 00:00:00 2001 From: Bohdan Odintsov Date: Wed, 19 Nov 2025 12:27:59 +0200 Subject: [PATCH 17/74] fix(format-bad-encoding): make method work with undefined values --- src/app/shared/helpers/format-bad-encoding.helper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/helpers/format-bad-encoding.helper.ts b/src/app/shared/helpers/format-bad-encoding.helper.ts index 8df1c1d44..ff4a3465b 100644 --- a/src/app/shared/helpers/format-bad-encoding.helper.ts +++ b/src/app/shared/helpers/format-bad-encoding.helper.ts @@ -1,3 +1,4 @@ export function replaceBadEncodedChars(text: string) { + if (!text) return ''; return text.replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>'); } From fa185af1e8923f5040d29e596a5c23ae1a3e64b8 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 24 Nov 2025 16:24:11 +0200 Subject: [PATCH 18/74] avoid check because registration will have always at least one contributor and contributors may not load syncronically after step change --- .../features/registries/components/drafts/drafts.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts index 353456834..a1d9fd448 100644 --- a/src/app/features/registries/components/drafts/drafts.component.ts +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -74,8 +74,7 @@ export class DraftsComponent implements OnDestroy { !this.draftRegistration()?.title || !this.draftRegistration()?.description || !this.registrationLicense() || - !this.selectedSubjects()?.length || - !this.initialContributors()?.length + !this.selectedSubjects()?.length ); }); From f8a25dac491658f007cc04203272acccb0a2497e Mon Sep 17 00:00:00 2001 From: Oleh Paduchak Date: Fri, 14 Nov 2025 13:47:11 +0200 Subject: [PATCH 19/74] feat(contributors): delete contributors from all components --- .../contributors/contributors.component.ts | 50 ++++++++++++------- .../shared/components/contributors/index.ts | 1 + .../remove-contributor-dialog.component.html | 32 ++++++++++++ .../remove-contributor-dialog.component.scss | 0 ...emove-contributor-dialog.component.spec.ts | 22 ++++++++ .../remove-contributor-dialog.component.ts | 38 ++++++++++++++ .../shared/services/contributors.service.ts | 12 ++++- .../contributors/contributors.actions.ts | 3 +- .../stores/contributors/contributors.state.ts | 2 +- src/assets/i18n/en.json | 2 + 10 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.scss create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts index 118d380f1..83667f3ad 100644 --- a/src/app/features/contributors/contributors.component.ts +++ b/src/app/features/contributors/contributors.component.ts @@ -28,6 +28,7 @@ import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, ContributorsTableComponent, + RemoveContributorDialogComponent, RequestAccessTableComponent, } from '@osf/shared/components/contributors'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; @@ -395,26 +396,37 @@ export class ContributorsComponent implements OnInit, OnDestroy { removeContributor(contributor: ContributorModel) { const isDeletingSelf = contributor.userId === this.currentUser()?.id; - this.customConfirmationService.confirmDelete({ - headerKey: 'project.contributors.removeDialog.title', - messageKey: 'project.contributors.removeDialog.message', - messageParams: { name: contributor.fullName }, - acceptLabelKey: 'common.buttons.remove', - onConfirm: () => { - this.actions - .deleteContributor(this.resourceId(), this.resourceType(), contributor.userId, isDeletingSelf) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { - name: contributor.fullName, - }); + this.customDialogService + .open(RemoveContributorDialogComponent, { + header: 'project.contributors.removeDialog.title', + width: '448px', + data: { + messageKey: 'project.contributors.removeDialog.message', + messageParams: { name: contributor.fullName }, + }, + }) + .onClose.pipe( + filter((res) => res !== undefined), + switchMap((removeFromChildren: boolean) => + this.actions.deleteContributor( + this.resourceId(), + this.resourceType(), + contributor.userId, + isDeletingSelf, + removeFromChildren + ) + ), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { + this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { + name: contributor.fullName, + }); - if (isDeletingSelf) { - this.router.navigate(['/']); - } - }); - }, - }); + if (isDeletingSelf) { + this.router.navigate(['/']); + } + }); } loadMoreContributors(): void { diff --git a/src/app/shared/components/contributors/index.ts b/src/app/shared/components/contributors/index.ts index bd33928cd..5ab666f29 100644 --- a/src/app/shared/components/contributors/index.ts +++ b/src/app/shared/components/contributors/index.ts @@ -1,4 +1,5 @@ export * from './add-contributor-dialog/add-contributor-dialog.component'; export * from './add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component'; export * from './contributors-table/contributors-table.component'; +export * from './remove-contributor-dialog/remove-contributor-dialog.component'; export * from './request-access-table/request-access-table.component'; diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html new file mode 100644 index 000000000..838bd02f1 --- /dev/null +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html @@ -0,0 +1,32 @@ +
+

+ +
+ + + +
+
+ + +
+ +
+ + +
+
diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.scss b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts new file mode 100644 index 000000000..65273fab7 --- /dev/null +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RemoveContributorDialogComponent } from './remove-contributor-dialog.component'; + +describe('RemoveContributorDialogComponent', () => { + let component: RemoveContributorDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RemoveContributorDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RemoveContributorDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts new file mode 100644 index 000000000..041d5c90c --- /dev/null +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts @@ -0,0 +1,38 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { RadioButton } from 'primeng/radiobutton'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'osf-remove-contributor-dialog', + imports: [RadioButton, FormsModule, Button, TranslatePipe], + templateUrl: './remove-contributor-dialog.component.html', + styleUrl: './remove-contributor-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RemoveContributorDialogComponent { + readonly dialogRef = inject(DynamicDialogRef); + readonly config = inject(DynamicDialogConfig); + selectedOption = false; + + get messageKey(): string | undefined { + return this.config?.data?.messageKey as string | undefined; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get messageParams(): any { + return this.config?.data?.messageParams; + } + + confirm(): void { + this.dialogRef.close(this.selectedOption); + } + + cancel(): void { + this.dialogRef.close(); + } +} diff --git a/src/app/shared/services/contributors.service.ts b/src/app/shared/services/contributors.service.ts index df2af3cf0..f527c445b 100644 --- a/src/app/shared/services/contributors.service.ts +++ b/src/app/shared/services/contributors.service.ts @@ -155,8 +155,16 @@ export class ContributorsService { return this.jsonApiService.patch(baseUrl, contributorData); } - deleteContributor(resourceType: ResourceType, resourceId: string, userId: string): Observable { - const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${userId}/`; + deleteContributor( + resourceType: ResourceType, + resourceId: string, + userId: string, + removeFromChildren = false + ): Observable { + let baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${userId}/`; + if (removeFromChildren) { + baseUrl = baseUrl.concat('?propagate_to_children=true'); + } return this.jsonApiService.delete(baseUrl); } diff --git a/src/app/shared/stores/contributors/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts index 470948c3a..561e6dcec 100644 --- a/src/app/shared/stores/contributors/contributors.actions.ts +++ b/src/app/shared/stores/contributors/contributors.actions.ts @@ -70,7 +70,8 @@ export class DeleteContributor { public resourceId: string | undefined | null, public resourceType: ResourceType | undefined, public contributorId: string, - public skipRefresh = false + public skipRefresh = false, + public removeFromChildren = false ) {} } diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts index 768983661..e3cf92b6e 100644 --- a/src/app/shared/stores/contributors/contributors.state.ts +++ b/src/app/shared/stores/contributors/contributors.state.ts @@ -228,7 +228,7 @@ export class ContributorsState { }); return this.contributorsService - .deleteContributor(action.resourceType, action.resourceId, action.contributorId) + .deleteContributor(action.resourceType, action.resourceId, action.contributorId, action.removeFromChildren) .pipe( tap(() => { if (!action.skipRefresh) { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 7c9090df4..fc4f4759e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -618,6 +618,8 @@ }, "removeDialog": { "title": "Remove contributor", + "thisProjectOnly": "This project only", + "thisProjectAndComponents": "This project and all it's components", "message": "Are you sure you want to remove {{name}} contributor?", "successMessage": "Contributor {{name}} successfully removed." }, From 3c05beeaa709476818a3dbb3ee4fd171fb496c54 Mon Sep 17 00:00:00 2001 From: Andriy Sheredko Date: Wed, 26 Nov 2025 09:34:33 +0200 Subject: [PATCH 20/74] feat(contributors): delete contributors from all components --- .../contributors/contributors.component.ts | 4 ++-- .../remove-contributor-dialog.component.html | 14 +++++++---- ...emove-contributor-dialog.component.spec.ts | 24 +++++++++++++++++++ .../remove-contributor-dialog.component.ts | 9 ++++--- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts index 83667f3ad..8db8f09f6 100644 --- a/src/app/features/contributors/contributors.component.ts +++ b/src/app/features/contributors/contributors.component.ts @@ -401,8 +401,8 @@ export class ContributorsComponent implements OnInit, OnDestroy { header: 'project.contributors.removeDialog.title', width: '448px', data: { - messageKey: 'project.contributors.removeDialog.message', - messageParams: { name: contributor.fullName }, + name: contributor.fullName, + hasChildren: !!this.resourceChildren()?.length, }, }) .onClose.pipe( diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html index 838bd02f1..fe28bcf57 100644 --- a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html @@ -1,5 +1,5 @@
-

+

{{ 'project.contributors.removeDialog.message' | translate: { name: name } }}

@@ -7,7 +7,13 @@
- + + @@ -17,14 +23,14 @@ diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts index 65273fab7..b699581b8 100644 --- a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts @@ -1,3 +1,5 @@ +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RemoveContributorDialogComponent } from './remove-contributor-dialog.component'; @@ -5,10 +7,17 @@ import { RemoveContributorDialogComponent } from './remove-contributor-dialog.co describe('RemoveContributorDialogComponent', () => { let component: RemoveContributorDialogComponent; let fixture: ComponentFixture; + let dialogRef: DynamicDialogRef; beforeEach(async () => { + dialogRef = { close: jasmine.createSpy('close') } as any; + await TestBed.configureTestingModule({ imports: [RemoveContributorDialogComponent], + providers: [ + { provide: DynamicDialogRef, useValue: dialogRef }, + { provide: DynamicDialogConfig, useValue: { data: { name: 'John Doe', hasChildren: true } } }, + ], }).compileComponents(); fixture = TestBed.createComponent(RemoveContributorDialogComponent); @@ -19,4 +28,19 @@ describe('RemoveContributorDialogComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should pass name from config', () => { + expect(component.name).toBe('John Doe'); + }); + + it('should close dialog with selected option on confirm', () => { + component.selectedOption = true; + component.confirm(); + expect(dialogRef.close).toHaveBeenCalledWith(true); + }); + + it('should close dialog without value on cancel', () => { + component.cancel(); + expect(dialogRef.close).toHaveBeenCalledWith(); + }); }); diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts index 041d5c90c..c5515cf46 100644 --- a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts @@ -19,13 +19,12 @@ export class RemoveContributorDialogComponent { readonly config = inject(DynamicDialogConfig); selectedOption = false; - get messageKey(): string | undefined { - return this.config?.data?.messageKey as string | undefined; + get name(): string | undefined { + return this.config?.data?.name; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get messageParams(): any { - return this.config?.data?.messageParams; + get hasChildren(): boolean { + return this.config?.data?.hasChildren ?? false; } confirm(): void { From b793c63f7e98048ae4a909463c12999c568b0d65 Mon Sep 17 00:00:00 2001 From: Andriy Sheredko Date: Wed, 26 Nov 2025 09:39:12 +0200 Subject: [PATCH 21/74] feat(contributors): fix linting --- .../remove-contributor-dialog.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html index fe28bcf57..6b579178e 100644 --- a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html @@ -12,7 +12,8 @@ [value]="true" [(ngModel)]="selectedOption" [disabled]="!hasChildren" - inputId="projectAll"> + inputId="projectAll" + >

- +