From 8f5c860e4e58e969a7f4743dfb86db8a6ebe7b15 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 26 Aug 2025 17:24:45 +0300 Subject: [PATCH 01/12] fix(meetings): fixed meetings small issues --- .../meetings-feature-card.component.spec.ts | 2 +- .../features/meetings/mappers/meetings.mapper.ts | 2 +- .../features/meetings/models/meetings.models.ts | 4 ++-- .../meeting-details.component.html | 10 +++++----- .../meeting-details/meeting-details.component.ts | 16 ++++++++-------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts index dbc686b94..50eeef7dc 100644 --- a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts +++ b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts @@ -13,7 +13,7 @@ describe('MeetingsFeatureCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MeetingsFeatureCardComponent, MockPipe(TranslatePipe, (value) => value)], + imports: [MeetingsFeatureCardComponent, MockPipe(TranslatePipe)], }).compileComponents(); fixture = TestBed.createComponent(MeetingsFeatureCardComponent); diff --git a/src/app/features/meetings/mappers/meetings.mapper.ts b/src/app/features/meetings/mappers/meetings.mapper.ts index c78f90829..42ed13328 100644 --- a/src/app/features/meetings/mappers/meetings.mapper.ts +++ b/src/app/features/meetings/mappers/meetings.mapper.ts @@ -31,7 +31,7 @@ export class MeetingsMapper { title: item.attributes.title, dateCreated: item.attributes.date_created, authorName: item.attributes.author_name, - downloadCount: item.attributes.download_count, + downloadCount: item.attributes.download_count || 0, meetingCategory: item.attributes.meeting_category, downloadLink: item.links.download, })), diff --git a/src/app/features/meetings/models/meetings.models.ts b/src/app/features/meetings/models/meetings.models.ts index a442e7406..a672a4551 100644 --- a/src/app/features/meetings/models/meetings.models.ts +++ b/src/app/features/meetings/models/meetings.models.ts @@ -1,4 +1,4 @@ -import { NumberOrNull, StringOrNull } from '@osf/shared/helpers'; +import { StringOrNull } from '@osf/shared/helpers'; export interface Meeting { id: string; @@ -19,7 +19,7 @@ export interface MeetingSubmission { title: string; dateCreated: Date; authorName: string; - downloadCount: NumberOrNull; + downloadCount: number; meetingCategory: string; downloadLink: StringOrNull; } diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html index 5646f45f1..c3f24e5da 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html @@ -57,24 +57,24 @@ @if (item?.id) { - + {{ item.title }} {{ item.authorName }} {{ item.meetingCategory }} {{ item.dateCreated | date: 'MMM d, y, h:mm a' }} - @if (item.downloadCount) { + @if (item.downloadLink) { - {{ item.downloadCount }} + {{ item.downloadCount }} } @else { - - + - } diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts b/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts index 8c12fa7f8..ba43de55d 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts @@ -25,14 +25,14 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { MEETING_SUBMISSIONS_TABLE_PARAMS } from '@osf/features/meetings/constants'; -import { MeetingSubmission } from '@osf/features/meetings/models'; -import { GetMeetingById, GetMeetingSubmissions, MeetingsSelectors } from '@osf/features/meetings/store'; -import { parseQueryFilterParams } from '@osf/shared/helpers/http.helper'; -import { SearchInputComponent, SubHeaderComponent } from '@shared/components'; -import { SortOrder } from '@shared/enums'; -import { QueryParams, TableParameters } from '@shared/models'; -import { SearchFilters } from '@shared/models/filters'; +import { SearchInputComponent, SubHeaderComponent } from '@osf/shared/components'; +import { SortOrder } from '@osf/shared/enums'; +import { parseQueryFilterParams } from '@osf/shared/helpers'; +import { QueryParams, SearchFilters, TableParameters } from '@osf/shared/models'; + +import { MEETING_SUBMISSIONS_TABLE_PARAMS } from '../../constants'; +import { MeetingSubmission } from '../../models'; +import { GetMeetingById, GetMeetingSubmissions, MeetingsSelectors } from '../../store'; @Component({ selector: 'osf-meeting-details', From 8de9aefe5cacb6a7237669e44de76edcf9f71c65 Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 27 Aug 2025 15:00:20 +0300 Subject: [PATCH 02/12] fix(tooltips): added tooltips --- .../project-contributors-step.component.html | 8 +- .../project-contributors-step.component.ts | 28 ++++--- .../file-browser-info.component.html | 19 +++++ .../file-browser-info.component.scss | 0 .../file-browser-info.component.spec.ts | 22 ++++++ .../file-browser-info.component.ts | 30 ++++++++ src/app/features/files/components/index.ts | 1 + .../constants/file-browser-info.constants.ts | 61 +++++++++++++++ src/app/features/files/constants/index.ts | 1 + src/app/features/files/models/index.ts | 1 + .../features/files/models/info-item.model.ts | 7 ++ .../files/pages/files/files.component.html | 10 ++- .../files/pages/files/files.component.ts | 77 ++++++++++++------- .../registry-settings.component.html | 2 - .../registry-settings.component.ts | 4 +- .../file-step/file-step.component.html | 16 ++-- .../custom-step/custom-step.component.html | 45 +++++++---- .../custom-step/custom-step.component.ts | 8 +- ...raft-registration-custom-step.component.ts | 25 ++---- .../registry-resources.component.html | 7 +- .../default-storage-location.component.ts | 4 +- .../mappers/regions.mapper.ts | 10 +-- .../models/osf-models/index.ts | 1 - .../models/osf-models/region.model.ts | 4 - .../services/account-settings.service.ts | 5 +- .../store/account-settings.model.ts | 6 +- .../store/account-settings.selectors.ts | 6 +- .../constants/notifications-constants.ts | 8 -- .../notifications.component.html | 11 ++- .../notifications/notifications.component.ts | 27 ++++--- .../tokens/services/tokens.service.spec.ts | 10 +-- .../info-icon/info-icon.component.scss | 4 + .../wiki-syntax-help-dialog.component.html | 7 +- .../subscriptions/subscription-event.enum.ts | 3 - .../registration/page-schema.mapper.ts | 6 +- .../models/registration/page-schema.model.ts | 4 +- src/assets/i18n/en.json | 42 ++++++---- 37 files changed, 369 insertions(+), 161 deletions(-) create mode 100644 src/app/features/files/components/file-browser-info/file-browser-info.component.html create mode 100644 src/app/features/files/components/file-browser-info/file-browser-info.component.scss create mode 100644 src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts create mode 100644 src/app/features/files/components/file-browser-info/file-browser-info.component.ts create mode 100644 src/app/features/files/constants/file-browser-info.constants.ts create mode 100644 src/app/features/files/models/info-item.model.ts delete mode 100644 src/app/features/settings/account-settings/models/osf-models/region.model.ts diff --git a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html index 39ed90188..f093255b4 100644 --- a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html +++ b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html @@ -7,7 +7,13 @@
-

{{ 'collections.addToCollection.projectContributors' | translate }}

+
+

{{ 'collections.addToCollection.projectContributors' | translate }}

+ +
@if (!isDisabled() && stepperActiveValue() !== targetStepValue()) { @if (projectContributors().length) {
diff --git a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts index 90a390ca2..9a94426e2 100644 --- a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts +++ b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.ts @@ -13,21 +13,27 @@ import { filter } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { findChangedItems } from '@osf/shared/helpers'; +import { InfoIconComponent } from '@osf/shared/components'; import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, ContributorsListComponent, -} from '@shared/components/contributors'; -import { AddContributorType, ResourceType } from '@shared/enums'; -import { ContributorDialogAddModel, ContributorModel } from '@shared/models'; -import { CustomConfirmationService, ToastService } from '@shared/services'; -import { AddContributor, ContributorsSelectors, DeleteContributor, UpdateContributor } from '@shared/stores'; -import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; +} from '@osf/shared/components/contributors'; +import { AddContributorType, ResourceType } from '@osf/shared/enums'; +import { findChangedItems } from '@osf/shared/helpers'; +import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; +import { + AddContributor, + ContributorsSelectors, + DeleteContributor, + ProjectsSelectors, + UpdateContributor, +} from '@osf/shared/stores'; @Component({ selector: 'osf-project-contributors-step', - imports: [Button, TranslatePipe, ContributorsListComponent, Step, StepItem, StepPanel, Tooltip], + imports: [Button, Step, StepItem, StepPanel, Tooltip, TranslatePipe, ContributorsListComponent, InfoIconComponent], templateUrl: './project-contributors-step.component.html', styleUrl: './project-contributors-step.component.scss', providers: [DialogService], @@ -40,9 +46,9 @@ export class ProjectContributorsStepComponent { private readonly toastService = inject(ToastService); private readonly customConfirmationService = inject(CustomConfirmationService); - protected readonly projectContributors = select(ContributorsSelectors.getContributors); - protected readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); - protected readonly selectedProject = select(ProjectsSelectors.getSelectedProject); + readonly projectContributors = select(ContributorsSelectors.getContributors); + readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); + readonly selectedProject = select(ProjectsSelectors.getSelectedProject); private initialContributors = signal([]); diff --git a/src/app/features/files/components/file-browser-info/file-browser-info.component.html b/src/app/features/files/components/file-browser-info/file-browser-info.component.html new file mode 100644 index 000000000..c9fd941c7 --- /dev/null +++ b/src/app/features/files/components/file-browser-info/file-browser-info.component.html @@ -0,0 +1,19 @@ +
+ @for (item of filteredInfoItems(); track item.titleKey) { +
+

{{ item.titleKey | translate }}

+

{{ item.descriptionKey | translate }}

+
+ } + +

+ {{ 'files.filesBrowserDialog.moreInfo' | translate }} + + {{ 'files.filesBrowserDialog.helpGuides' | translate }} + +

+
+ +
+ +
diff --git a/src/app/features/files/components/file-browser-info/file-browser-info.component.scss b/src/app/features/files/components/file-browser-info/file-browser-info.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts b/src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts new file mode 100644 index 000000000..827173f6b --- /dev/null +++ b/src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileBrowserInfoComponent } from './file-browser-info.component'; + +describe('FileBrowserInfoComponent', () => { + let component: FileBrowserInfoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileBrowserInfoComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FileBrowserInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/files/components/file-browser-info/file-browser-info.component.ts b/src/app/features/files/components/file-browser-info/file-browser-info.component.ts new file mode 100644 index 000000000..4cb855e03 --- /dev/null +++ b/src/app/features/files/components/file-browser-info/file-browser-info.component.ts @@ -0,0 +1,30 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; + +import { ResourceType } from '@osf/shared/enums'; + +import { FILE_BROWSER_INFO_ITEMS } from '../../constants'; + +@Component({ + selector: 'osf-file-browser-info', + imports: [Button, TranslatePipe], + templateUrl: './file-browser-info.component.html', + styleUrl: './file-browser-info.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FileBrowserInfoComponent { + readonly dialogRef = inject(DynamicDialogRef); + readonly config = inject(DynamicDialogConfig); + + readonly resourceType = computed(() => (this.config.data as ResourceType) || ResourceType.Project); + + readonly infoItems = FILE_BROWSER_INFO_ITEMS; + + readonly filteredInfoItems = computed(() => { + return this.infoItems.filter((item) => item.showForResourceTypes.includes(this.resourceType())); + }); +} diff --git a/src/app/features/files/components/index.ts b/src/app/features/files/components/index.ts index 80c02901d..8f96ee952 100644 --- a/src/app/features/files/components/index.ts +++ b/src/app/features/files/components/index.ts @@ -1,5 +1,6 @@ export { CreateFolderDialogComponent } from './create-folder-dialog/create-folder-dialog.component'; export { EditFileMetadataDialogComponent } from './edit-file-metadata-dialog/edit-file-metadata-dialog.component'; +export { FileBrowserInfoComponent } from './file-browser-info/file-browser-info.component'; export { FileKeywordsComponent } from './file-keywords/file-keywords.component'; export { FileMetadataComponent } from './file-metadata/file-metadata.component'; export { FileResourceMetadataComponent } from './file-resource-metadata/file-resource-metadata.component'; diff --git a/src/app/features/files/constants/file-browser-info.constants.ts b/src/app/features/files/constants/file-browser-info.constants.ts new file mode 100644 index 000000000..fb7e8b99a --- /dev/null +++ b/src/app/features/files/constants/file-browser-info.constants.ts @@ -0,0 +1,61 @@ +import { ResourceType } from '@osf/shared/enums'; + +import { FileInfoItem } from '../models'; + +export const FILE_BROWSER_INFO_ITEMS: FileInfoItem[] = [ + { + titleKey: 'files.filesBrowserDialog.seeAllFiles', + descriptionKey: 'files.filesBrowserDialog.seeAllFilesDescription', + showForResourceTypes: [ResourceType.Project, ResourceType.Registration], + }, + { + titleKey: 'files.filesBrowserDialog.selectFilesFolders', + descriptionKey: 'files.filesBrowserDialog.selectFilesFoldersDescription', + showForResourceTypes: [ResourceType.Project], + }, + { + titleKey: 'files.filesBrowserDialog.openViewFiles', + descriptionKey: 'files.filesBrowserDialog.openViewFilesDescription', + showForResourceTypes: [ResourceType.Project, ResourceType.Registration], + }, + { + titleKey: 'files.filesBrowserDialog.upload', + descriptionKey: 'files.filesBrowserDialog.uploadDescription', + showForResourceTypes: [ResourceType.Project], + }, + { + titleKey: 'files.filesBrowserDialog.createFolder', + descriptionKey: 'files.filesBrowserDialog.createFolderDescription', + showForResourceTypes: [ResourceType.Project], + }, + { + titleKey: 'files.filesBrowserDialog.renameFolderFile', + descriptionKey: 'files.filesBrowserDialog.renameFolderFileDescription', + showForResourceTypes: [ResourceType.Project], + }, + { + titleKey: 'files.filesBrowserDialog.move', + descriptionKey: 'files.filesBrowserDialog.moveDescription', + showForResourceTypes: [ResourceType.Project], + }, + { + titleKey: 'files.filesBrowserDialog.copy', + descriptionKey: 'files.filesBrowserDialog.copyDescription', + showForResourceTypes: [ResourceType.Project], + }, + { + titleKey: 'files.filesBrowserDialog.downloadAllFilesZip', + descriptionKey: 'files.filesBrowserDialog.downloadAllFilesZipDescription', + showForResourceTypes: [ResourceType.Project, ResourceType.Registration], + }, + { + titleKey: 'files.filesBrowserDialog.downloadFolderZip', + descriptionKey: 'files.filesBrowserDialog.downloadFolderZipDescription', + showForResourceTypes: [ResourceType.Project, ResourceType.Registration], + }, + { + titleKey: 'files.filesBrowserDialog.downloadFile', + descriptionKey: 'files.filesBrowserDialog.downloadFileDescription', + showForResourceTypes: [ResourceType.Project, ResourceType.Registration], + }, +]; diff --git a/src/app/features/files/constants/index.ts b/src/app/features/files/constants/index.ts index 72af33152..af9827638 100644 --- a/src/app/features/files/constants/index.ts +++ b/src/app/features/files/constants/index.ts @@ -1,3 +1,4 @@ export * from './embed-content.constants'; +export * from './file-browser-info.constants'; export * from './file-metadata-fields.constants'; export * from './file-provider.constants'; diff --git a/src/app/features/files/models/index.ts b/src/app/features/files/models/index.ts index f3e000f81..078892263 100644 --- a/src/app/features/files/models/index.ts +++ b/src/app/features/files/models/index.ts @@ -9,4 +9,5 @@ export * from './get-file-metadata-response.model'; export * from './get-file-revisions-response.model'; export * from './get-file-target-response.model'; export * from './get-short-info-response.model'; +export * from './info-item.model'; export * from './patch-file-metadata.model'; diff --git a/src/app/features/files/models/info-item.model.ts b/src/app/features/files/models/info-item.model.ts new file mode 100644 index 000000000..7224bb6a2 --- /dev/null +++ b/src/app/features/files/models/info-item.model.ts @@ -0,0 +1,7 @@ +import { ResourceType } from '@osf/shared/enums'; + +export interface FileInfoItem { + titleKey: string; + descriptionKey: string; + showForResourceTypes: ResourceType[]; +} diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 9e25d61ce..6980c972c 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -42,17 +42,19 @@
-
+
+ + @if (!isViewOnly()) { @@ -72,7 +74,7 @@ severity="success" [icon]="'fas fa-upload'" [label]="'files.actions.uploadFile' | translate" - (click)="fileInput.click()" + (onClick)="fileInput.click()" > } diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index 80e6b0d3e..176fc245b 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -24,7 +24,7 @@ import { model, signal, } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; @@ -44,6 +44,7 @@ import { } from '@osf/features/files/store'; import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; import { ResourceType } from '@osf/shared/enums'; +import { IS_MEDIUM } from '@osf/shared/helpers'; import { FilesTreeComponent, FormSelectComponent, @@ -54,7 +55,7 @@ import { import { ConfiguredStorageAddonModel, FilesTreeActions, OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; -import { CreateFolderDialogComponent } from '../../components'; +import { CreateFolderDialogComponent, FileBrowserInfoComponent } from '../../components'; import { FileProvider } from '../../constants'; import { FilesSelectors } from '../../store'; @@ -106,29 +107,40 @@ export class FilesComponent { resetState: ResetState, }); - protected readonly files = select(FilesSelectors.getFiles); - protected readonly isFilesLoading = select(FilesSelectors.isFilesLoading); - protected readonly currentFolder = select(FilesSelectors.getCurrentFolder); - protected readonly provider = select(FilesSelectors.getProvider); - - protected readonly resourceId = signal(''); - private readonly rootFolders = select(FilesSelectors.getRootFolders); - protected isRootFoldersLoading = select(FilesSelectors.isRootFoldersLoading); - private readonly configuredStorageAddons = select(FilesSelectors.getConfiguredStorageAddons); - protected isConfiguredStorageAddonsLoading = select(FilesSelectors.isConfiguredStorageAddonsLoading); - protected currentRootFolder = model<{ label: string; folder: OsfFile } | null>(null); - protected readonly progress = signal(0); - protected readonly fileName = signal(''); - protected readonly dataLoaded = signal(false); - protected readonly searchControl = new FormControl(''); - protected readonly sortControl = new FormControl(ALL_SORT_OPTIONS[0].value); + isMedium = toSignal(inject(IS_MEDIUM)); + + readonly files = select(FilesSelectors.getFiles); + readonly isFilesLoading = select(FilesSelectors.isFilesLoading); + readonly currentFolder = select(FilesSelectors.getCurrentFolder); + readonly provider = select(FilesSelectors.getProvider); + + readonly resourceId = signal(''); + readonly rootFolders = select(FilesSelectors.getRootFolders); + readonly isRootFoldersLoading = select(FilesSelectors.isRootFoldersLoading); + readonly configuredStorageAddons = select(FilesSelectors.getConfiguredStorageAddons); + readonly isConfiguredStorageAddonsLoading = select(FilesSelectors.isConfiguredStorageAddonsLoading); + + readonly progress = signal(0); + readonly fileName = signal(''); + readonly dataLoaded = signal(false); + readonly searchControl = new FormControl(''); + readonly sortControl = new FormControl(ALL_SORT_OPTIONS[0].value); + + currentRootFolder = model<{ label: string; folder: OsfFile } | null>(null); + + fileIsUploading = signal(false); + isFolderOpening = signal(false); + + sortOptions = ALL_SORT_OPTIONS; + + storageProvider = FileProvider.OsfStorage; private readonly urlMap = new Map([ [ResourceType.Project, 'nodes'], [ResourceType.Registration, 'registrations'], ]); - protected readonly rootFoldersOptions = computed(() => { + readonly rootFoldersOptions = computed(() => { const rootFolders = this.rootFolders(); const addons = this.configuredStorageAddons(); if (rootFolders && addons) { @@ -144,22 +156,15 @@ export class FilesComponent { this.activeRoute.parent?.parent?.snapshot.data['resourceType'] || ResourceType.Project ); - protected readonly isViewOnly = computed(() => { + readonly isViewOnly = computed(() => { return this.resourceType() === ResourceType.Registration; }); - protected readonly isViewOnlyDownloadable = computed(() => { + readonly isViewOnlyDownloadable = computed(() => { return this.resourceType() === ResourceType.Registration; }); - fileIsUploading = signal(false); - isFolderOpening = signal(false); - - sortOptions = ALL_SORT_OPTIONS; - - storageProvider = FileProvider.OsfStorage; - - protected readonly filesTreeActions: FilesTreeActions = { + readonly filesTreeActions: FilesTreeActions = { setCurrentFolder: (folder) => this.actions.setCurrentFolder(folder), setFilesIsLoading: (isLoading) => this.actions.setFilesIsLoading(isLoading), getFiles: (filesLink) => this.actions.getFiles(filesLink), @@ -324,6 +329,20 @@ export class FilesComponent { } } + showInfoDialog() { + const dialogWidth = this.isMedium() ? '850px' : '95vw'; + + this.dialogService.open(FileBrowserInfoComponent, { + width: dialogWidth, + focusOnShow: false, + header: this.translateService.instant('files.filesBrowserDialog.title'), + closeOnEscape: true, + modal: true, + closable: true, + data: this.resourceType(), + }); + } + updateFilesList(): Observable { const currentFolder = this.currentFolder(); if (currentFolder?.relationships.filesLink) { diff --git a/src/app/features/moderation/components/registry-settings/registry-settings.component.html b/src/app/features/moderation/components/registry-settings/registry-settings.component.html index bd2e358c7..0621ed927 100644 --- a/src/app/features/moderation/components/registry-settings/registry-settings.component.html +++ b/src/app/features/moderation/components/registry-settings/registry-settings.component.html @@ -4,5 +4,3 @@ {{ 'moderation.userSettings' | translate }}

- - diff --git a/src/app/features/moderation/components/registry-settings/registry-settings.component.ts b/src/app/features/moderation/components/registry-settings/registry-settings.component.ts index dce5e933a..976cdabe2 100644 --- a/src/app/features/moderation/components/registry-settings/registry-settings.component.ts +++ b/src/app/features/moderation/components/registry-settings/registry-settings.component.ts @@ -3,11 +3,9 @@ import { TranslatePipe } from '@ngx-translate/core'; import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { BulkUploadComponent } from '@osf/features/moderation/components/bulk-upload/bulk-upload.component'; - @Component({ selector: 'osf-registry-settings', - imports: [TranslatePipe, RouterLink, BulkUploadComponent], + imports: [TranslatePipe, RouterLink], templateUrl: './registry-settings.component.html', styleUrl: './registry-settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.html b/src/app/features/preprints/components/stepper/file-step/file-step.component.html index 1194b744e..5c1947e69 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.html +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.html @@ -20,9 +20,9 @@

{{ 'preprints.preprintStepper.file.title' | translate }}

[label]="'preprints.preprintStepper.file.uploadFromComputer' | translate | titlecase" severity="secondary" [disabled]="isFileSourceSelected()" - [pTooltip]="isFileSourceSelected() ? ('preprints.preprintStepper.file.tooltips.computerDisabled' | translate) : ''" + [pTooltip]="'preprints.preprintStepper.file.tooltips.computerDisabled' | translate" tooltipPosition="top" - (click)="selectFileSource(PreprintFileSource.Computer)" + (onClick)="selectFileSource(PreprintFileSource.Computer)" /> @@ -50,7 +50,7 @@

{{ 'preprints.preprintStepper.file.title' | translate }}

severity="success" [icon]="'fas fa-upload'" [label]="'preprints.preprintStepper.file.uploadFileButton' | translate | titlecase" - (click)="fileInput.click()" + (onClick)="fileInput.click()" /> @@ -109,7 +109,7 @@

{{ 'preprints.preprintStepper.file.title' | translate }}

{{ file.name }}

- + } } @@ -122,13 +122,13 @@

{{ 'preprints.preprintStepper.file.title' | translate }}

styleClass="w-full" [label]="'common.buttons.back' | translate" severity="info" - (click)="backButtonClicked()" + (onClick)="backButtonClicked()" /> diff --git a/src/app/features/registries/components/custom-step/custom-step.component.html b/src/app/features/registries/components/custom-step/custom-step.component.html index f2113e9eb..02ae4fc0f 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.html +++ b/src/app/features/registries/components/custom-step/custom-step.component.html @@ -1,13 +1,28 @@
@if (currentPage()) { -

{{ currentPage().title }}

+
+

{{ currentPage().title }}

+ + @let helpText = currentPage().helpText; + + @if (helpText) { + + } +
@let questions = currentPage().questions || []; @if (currentPage().sections?.length) { @for (section of currentPage().sections; track section.id) { -

{{ section.title }}

+
+

{{ section.title }}

+ + @if (section.helpText) { + + } +
+ @if (section.description) {

{{ section.description }}

} @@ -20,18 +35,21 @@

{{ section.title }}

@for (q of questions; track q.id) { -
+
@if (q.paragraphText) {

{{ q.paragraphText }}

} @@ -88,7 +106,7 @@

> @if (option.helpText) { - + } } @@ -113,7 +131,7 @@

> @if (option.helpText) { - + } } @@ -180,6 +198,7 @@

{{ 'files.actions.uploadFile' | translate }}

}
} +
{ - return this.draftRegistration()?.branchedFrom?.filesLink || ''; - }); + readonly stepsData = select(RegistriesSelectors.getStepsData); + readonly draftRegistration = select(RegistriesSelectors.getDraftRegistration); + readonly actions = createDispatchMap({ updateDraft: UpdateDraft }); - provider = computed(() => { - return this.draftRegistration()?.providerId || ''; - }); - - projectId = computed(() => { - return this.draftRegistration()?.branchedFrom?.id || ''; - }); + filesLink = computed(() => this.draftRegistration()?.branchedFrom?.filesLink || ''); + provider = computed(() => this.draftRegistration()?.providerId || ''); + projectId = computed(() => this.draftRegistration()?.branchedFrom?.id || ''); onUpdateAction(attributes: Partial): void { - const payload = { - registration_responses: { ...attributes }, - }; + const payload = { registration_responses: { ...attributes } }; this.actions.updateDraft(this.route.snapshot.params['id'], payload); } diff --git a/src/app/features/registry/pages/registry-resources/registry-resources.component.html b/src/app/features/registry/pages/registry-resources/registry-resources.component.html index 03f2ae84d..1ca013a57 100644 --- a/src/app/features/registry/pages/registry-resources/registry-resources.component.html +++ b/src/app/features/registry/pages/registry-resources/registry-resources.component.html @@ -10,7 +10,12 @@ } @else {
-

{{ 'resources.description' | translate }}

+

+ {{ 'resources.description' | translate }} + + {{ 'common.labels.learnMore' | translate }} + +

@for (resource of resources(); track resource.id) { diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts index 82f6dff21..07e748f89 100644 --- a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.ts @@ -12,9 +12,9 @@ import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@ang import { FormsModule } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; +import { IdName } from '@osf/shared/models'; import { LoaderService, ToastService } from '@osf/shared/services'; -import { Region } from '../../models'; import { AccountSettingsSelectors, UpdateRegion } from '../../store'; @Component({ @@ -31,7 +31,7 @@ export class DefaultStorageLocationComponent { protected readonly currentUser = select(UserSelectors.getCurrentUser); protected readonly regions = select(AccountSettingsSelectors.getRegions); - protected selectedRegion = signal(undefined); + protected selectedRegion = signal(undefined); constructor() { effect(() => { diff --git a/src/app/features/settings/account-settings/mappers/regions.mapper.ts b/src/app/features/settings/account-settings/mappers/regions.mapper.ts index bce848f6f..b817a06f6 100644 --- a/src/app/features/settings/account-settings/mappers/regions.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/regions.mapper.ts @@ -1,9 +1,7 @@ -import { ApiData } from '@osf/shared/models'; +import { ApiData, IdName } from '@osf/shared/models'; -import { Region } from '../models'; - -export function MapRegions(data: ApiData<{ name: string }, null, null, null>[]): Region[] { - const regions: Region[] = []; +export function MapRegions(data: ApiData<{ name: string }, null, null, null>[]): IdName[] { + const regions: IdName[] = []; for (const region of data) { regions.push(MapRegion(region)); @@ -12,7 +10,7 @@ export function MapRegions(data: ApiData<{ name: string }, null, null, null>[]): return regions; } -export function MapRegion(data: ApiData<{ name: string }, null, null, null>): Region { +export function MapRegion(data: ApiData<{ name: string }, null, null, null>): IdName { return { id: data.id, name: data.attributes.name, diff --git a/src/app/features/settings/account-settings/models/osf-models/index.ts b/src/app/features/settings/account-settings/models/osf-models/index.ts index dbf70a075..9d46a9787 100644 --- a/src/app/features/settings/account-settings/models/osf-models/index.ts +++ b/src/app/features/settings/account-settings/models/osf-models/index.ts @@ -1,4 +1,3 @@ export * from './account-email.model'; export * from './account-settings.model'; export * from './external-institution.model'; -export * from './region.model'; diff --git a/src/app/features/settings/account-settings/models/osf-models/region.model.ts b/src/app/features/settings/account-settings/models/osf-models/region.model.ts deleted file mode 100644 index 4068f4f1e..000000000 --- a/src/app/features/settings/account-settings/models/osf-models/region.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Region { - id: string; - name: string; -} diff --git a/src/app/features/settings/account-settings/services/account-settings.service.ts b/src/app/features/settings/account-settings/services/account-settings.service.ts index a140fe28e..ad78b2abb 100644 --- a/src/app/features/settings/account-settings/services/account-settings.service.ts +++ b/src/app/features/settings/account-settings/services/account-settings.service.ts @@ -6,7 +6,7 @@ import { inject, Injectable } from '@angular/core'; import { UserSelectors } from '@osf/core/store/user'; import { UserMapper } from '@osf/shared/mappers'; -import { ApiData, JsonApiResponse, User, UserGetResponse } from '@osf/shared/models'; +import { ApiData, IdName, JsonApiResponse, User, UserGetResponse } from '@osf/shared/models'; import { JsonApiService } from '@osf/shared/services'; import { MapAccountSettings, MapEmail, MapEmails, MapExternalIdentities, MapRegions } from '../mappers'; @@ -20,7 +20,6 @@ import { GetRegionsResponseJsonApi, ListEmailsResponseJsonApi, ListIdentitiesResponseJsonApi, - Region, } from '../models'; import { environment } from 'src/environments/environment'; @@ -141,7 +140,7 @@ export class AccountSettingsService { .pipe(map((response) => MapEmail(response))); } - getRegions(): Observable { + getRegions(): Observable { return this.jsonApiService .get(`${environment.apiUrl}/regions/`) .pipe(map((response) => MapRegions(response.data))); diff --git a/src/app/features/settings/account-settings/store/account-settings.model.ts b/src/app/features/settings/account-settings/store/account-settings.model.ts index 1e95cb543..6dc3b7a81 100644 --- a/src/app/features/settings/account-settings/store/account-settings.model.ts +++ b/src/app/features/settings/account-settings/store/account-settings.model.ts @@ -1,10 +1,10 @@ -import { AsyncStateModel, Institution } from '@shared/models'; +import { AsyncStateModel, IdName, Institution } from '@shared/models'; -import { AccountEmail, AccountSettings, ExternalIdentity, Region } from '../models'; +import { AccountEmail, AccountSettings, ExternalIdentity } from '../models'; export interface AccountSettingsStateModel { emails: AsyncStateModel; - regions: Region[]; + regions: IdName[]; externalIdentities: ExternalIdentity[]; accountSettings: AccountSettings; userInstitutions: Institution[]; diff --git a/src/app/features/settings/account-settings/store/account-settings.selectors.ts b/src/app/features/settings/account-settings/store/account-settings.selectors.ts index 3614770f5..aa58509a3 100644 --- a/src/app/features/settings/account-settings/store/account-settings.selectors.ts +++ b/src/app/features/settings/account-settings/store/account-settings.selectors.ts @@ -1,8 +1,8 @@ import { Selector } from '@ngxs/store'; -import { Institution } from '@shared/models'; +import { IdName, Institution } from '@shared/models'; -import { AccountEmail, AccountSettings, ExternalIdentity, Region } from '../models'; +import { AccountEmail, AccountSettings, ExternalIdentity } from '../models'; import { AccountSettingsStateModel } from './account-settings.model'; import { AccountSettingsState } from './account-settings.state'; @@ -24,7 +24,7 @@ export class AccountSettingsSelectors { } @Selector([AccountSettingsState]) - static getRegions(state: AccountSettingsStateModel): Region[] { + static getRegions(state: AccountSettingsStateModel): IdName[] { return state.regions; } diff --git a/src/app/features/settings/notifications/constants/notifications-constants.ts b/src/app/features/settings/notifications/constants/notifications-constants.ts index d3919011f..7b6bbf1de 100644 --- a/src/app/features/settings/notifications/constants/notifications-constants.ts +++ b/src/app/features/settings/notifications/constants/notifications-constants.ts @@ -3,14 +3,6 @@ import { SubscriptionEvent } from '@shared/enums'; import { SubscriptionEventModel } from '../models'; export const SUBSCRIPTION_EVENTS: SubscriptionEventModel[] = [ - { - event: SubscriptionEvent.GlobalCommentReplies, - labelKey: 'settings.notifications.notificationPreferences.items.replies', - }, - { - event: SubscriptionEvent.GlobalComments, - labelKey: 'settings.notifications.notificationPreferences.items.comments', - }, { event: SubscriptionEvent.GlobalFileUpdated, labelKey: 'settings.notifications.notificationPreferences.items.files', diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index bc9adbad6..0da3bff0e 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -1,4 +1,4 @@ - +
@if (!isEmailPreferencesLoading()) { @@ -72,7 +72,14 @@

{{ 'settings.notifications.emailPreferences.title' | translate }}

@if (!isNotificationSubscriptionsLoading()) {
-

{{ 'settings.notifications.notificationPreferences.title' | translate }}

+
+

{{ 'settings.notifications.notificationPreferences.title' | translate }}

+ + +

{{ 'settings.notifications.notificationPreferences.note' | translate }}

diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 548bf917c..aa0f58c16 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -10,11 +10,11 @@ import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnInit } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { GetCurrentUserSettings, UpdateUserSettings, UserSelectors } from '@osf/core/store/user'; -import { SubHeaderComponent } from '@osf/shared/components'; +import { GetCurrentUserSettings, UpdateUserSettings, UserSelectors } from '@core/store/user'; +import { InfoIconComponent, SubHeaderComponent } from '@osf/shared/components'; +import { SubscriptionEvent, SubscriptionFrequency } from '@osf/shared/enums'; import { UserSettings } from '@osf/shared/models'; import { LoaderService, ToastService } from '@osf/shared/services'; -import { SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; import { SUBSCRIPTION_EVENTS } from './constants'; import { EmailPreferencesForm, EmailPreferencesFormControls } from './models'; @@ -26,7 +26,16 @@ import { @Component({ selector: 'osf-notifications', - imports: [SubHeaderComponent, Checkbox, Button, TranslatePipe, ReactiveFormsModule, Skeleton, Select], + imports: [ + Checkbox, + Button, + TranslatePipe, + ReactiveFormsModule, + Skeleton, + Select, + InfoIconComponent, + SubHeaderComponent, + ], templateUrl: './notifications.component.html', styleUrl: './notifications.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -48,13 +57,13 @@ export class NotificationsComponent implements OnInit { private emailPreferences = select(UserSelectors.getCurrentUserSettings); private notificationSubscriptions = select(NotificationSubscriptionSelectors.getAllGlobalNotificationSubscriptions); - protected isEmailPreferencesLoading = select(UserSelectors.isUserSettingsLoading); - protected isSubmittingEmailPreferences = select(UserSelectors.isUserSettingsSubmitting); + isEmailPreferencesLoading = select(UserSelectors.isUserSettingsLoading); + isSubmittingEmailPreferences = select(UserSelectors.isUserSettingsSubmitting); - protected isNotificationSubscriptionsLoading = select(NotificationSubscriptionSelectors.isLoading); + isNotificationSubscriptionsLoading = select(NotificationSubscriptionSelectors.isLoading); - protected EmailPreferencesFormControls = EmailPreferencesFormControls; - protected emailPreferencesForm: EmailPreferencesForm = new FormGroup({ + EmailPreferencesFormControls = EmailPreferencesFormControls; + emailPreferencesForm: EmailPreferencesForm = new FormGroup({ [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.fb.control(false, { nonNullable: true }), [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.fb.control(false, { nonNullable: true }), }); diff --git a/src/app/features/settings/tokens/services/tokens.service.spec.ts b/src/app/features/settings/tokens/services/tokens.service.spec.ts index 01e930403..7d14565e0 100644 --- a/src/app/features/settings/tokens/services/tokens.service.spec.ts +++ b/src/app/features/settings/tokens/services/tokens.service.spec.ts @@ -6,7 +6,7 @@ import { JsonApiResponse } from '@osf/shared/models'; import { JsonApiService } from '@osf/shared/services'; import { ScopeMapper, TokenMapper } from '../mappers'; -import { ScopeJsonApi, ScopeModel, TokenCreateResponseJsonApi, TokenGetResponseJsonApi, TokenModel } from '../models'; +import { ScopeJsonApi, ScopeModel, TokenGetResponseJsonApi, TokenModel } from '../models'; import { TokensService } from './tokens.service'; @@ -86,11 +86,11 @@ describe('TokensService', () => { const scopes = ['read']; const requestBody = { name, scopes }; - const apiResponse = { data: { id: 'xyz' } } as JsonApiResponse; + const apiResponse = { data: { id: 'xyz' } } as JsonApiResponse; const mapped = { id: 'xyz' } as TokenModel; (TokenMapper.toRequest as jest.Mock).mockReturnValue(requestBody); - (TokenMapper.fromCreateResponse as jest.Mock).mockReturnValue(mapped); + (TokenMapper.fromGetResponse as jest.Mock).mockReturnValue(mapped); jsonApiServiceMock.post.mockReturnValue(of(apiResponse)); service.createToken(name, scopes).subscribe((token) => { @@ -106,11 +106,11 @@ describe('TokensService', () => { const scopes = ['write']; const requestBody = { name, scopes }; - const apiResponse = { id: tokenId } as TokenCreateResponseJsonApi; + const apiResponse = { id: tokenId } as TokenGetResponseJsonApi; const mapped = { id: tokenId } as TokenModel; (TokenMapper.toRequest as jest.Mock).mockReturnValue(requestBody); - (TokenMapper.fromCreateResponse as jest.Mock).mockReturnValue(mapped); + (TokenMapper.fromGetResponse as jest.Mock).mockReturnValue(mapped); jsonApiServiceMock.patch.mockReturnValue(of(apiResponse)); service.updateToken(tokenId, name, scopes).subscribe((token) => { diff --git a/src/app/shared/components/info-icon/info-icon.component.scss b/src/app/shared/components/info-icon/info-icon.component.scss index e69de29bb..4b374974e 100644 --- a/src/app/shared/components/info-icon/info-icon.component.scss +++ b/src/app/shared/components/info-icon/info-icon.component.scss @@ -0,0 +1,4 @@ +:host { + display: flex; + align-items: center; +} diff --git a/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.html b/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.html index 3c3417133..09b500b35 100644 --- a/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.html +++ b/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.html @@ -1,11 +1,14 @@

{{ 'project.wiki.syntaxHelp.startMsg' | translate }} - + {{ 'project.wiki.syntaxHelp.links.markdown' | translate }} {{ 'project.wiki.syntaxHelp.endMsg' | translate - }} {{ 'project.wiki.syntaxHelp.links.guide' | translate }}. + }} + {{ 'project.wiki.syntaxHelp.links.guide' | translate }}.

+
diff --git a/src/app/shared/enums/subscriptions/subscription-event.enum.ts b/src/app/shared/enums/subscriptions/subscription-event.enum.ts index 344013dda..f3913133f 100644 --- a/src/app/shared/enums/subscriptions/subscription-event.enum.ts +++ b/src/app/shared/enums/subscriptions/subscription-event.enum.ts @@ -1,9 +1,6 @@ export enum SubscriptionEvent { - GlobalCommentReplies = 'global_comment_replies', - GlobalComments = 'global_comments', GlobalFileUpdated = 'global_file_updated', GlobalMentions = 'global_mentions', GlobalReviews = 'global_reviews', - Comments = 'comments', FileUpdated = 'file_updated', } diff --git a/src/app/shared/mappers/registration/page-schema.mapper.ts b/src/app/shared/mappers/registration/page-schema.mapper.ts index 140ae9641..c82a2b454 100644 --- a/src/app/shared/mappers/registration/page-schema.mapper.ts +++ b/src/app/shared/mappers/registration/page-schema.mapper.ts @@ -1,5 +1,4 @@ -import { BlockType } from '@osf/shared/enums/block-type.enum'; -import { FieldType } from '@osf/shared/enums/field-type.enum'; +import { BlockType, FieldType } from '@osf/shared/enums'; import { PageSchema, Question, SchemaBlocksResponseJsonApi, Section } from '@osf/shared/models'; export class PageSchemaMapper { @@ -8,12 +7,14 @@ export class PageSchemaMapper { let currentPage!: PageSchema; let currentQuestion: Question | null = null; let currentSection: Section | null = null; + response.data.map((item) => { switch (item.attributes.block_type) { case BlockType.PageHeading: currentPage = { id: item.id, title: item.attributes.display_text, + helpText: item.attributes.help_text, questions: [], }; currentQuestion = null; @@ -25,6 +26,7 @@ export class PageSchemaMapper { currentSection = { id: item.id, title: item.attributes.display_text, + helpText: item.attributes.help_text, questions: [], }; currentPage.sections = currentPage.sections || []; diff --git a/src/app/shared/models/registration/page-schema.model.ts b/src/app/shared/models/registration/page-schema.model.ts index 95ee897f1..a235f5a92 100644 --- a/src/app/shared/models/registration/page-schema.model.ts +++ b/src/app/shared/models/registration/page-schema.model.ts @@ -1,8 +1,9 @@ -import { FieldType } from '@osf/shared/enums/field-type.enum'; +import { FieldType } from '@osf/shared/enums'; export interface PageSchema { id: string; title: string; + helpText?: string; description?: string; questions?: Question[]; sections?: Section[]; @@ -12,6 +13,7 @@ export interface Section { id: string; title: string; description?: string; + helpText?: string; questions?: Question[]; } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 4c408ceea..1dbe0333e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -399,9 +399,6 @@ "wikiText": "Enable the wiki In [Dashboard] New and Noteworthy.", "wikiConfigureTitle": "Configure", "wikiConfigureText": "Create a link to share this project so those who have the link can view—but not edit—the project.", - "commenting": "Commenting", - "contributorsCanPost": "Only contributors can post comments", - "osfUserCanPost": "When the project is public, any OSF user can post comments", "emailNotifications": "Email Notifications", "emailNotificationsText": "These notification settings only apply to you. They do NOT affect any other contributor on this project.", "redirectLink": "Redirect Link", @@ -427,11 +424,6 @@ "message": "Are you sure you want to delete this view only link?" }, "descriptions": { - "comments": { - "instant": "You'll be notified immediately when someone adds a comment.", - "daily": "You'll receive a daily summary of comments.", - "none": "You won't receive comment notifications." - }, "file_updated": { "instant": "You'll be notified immediately when files are updated.", "daily": "You'll receive a daily summary of file updates.", @@ -972,6 +964,33 @@ "message": "Are you sure you want to delete {{name}}?" } }, + "filesBrowserDialog": { + "title": "Using the OSF Files Browser", + "seeAllFiles": "See All Files in a Provider", + "seeAllFilesDescription": "All connected storage providers are displayed in the left navbar. Choose one provider to view contents.", + "selectFilesFolders": "Select Files/Folders", + "selectFilesFoldersDescription": "Click on a row (outside of the file or folder name) to show further actions in the top toolbar. Click on more rows to select multiple files. To select a range of files, click a file to begin selection and then shift+click another file to select the range.", + "openViewFiles": "Open/View Files", + "openViewFilesDescription": "Click a file name to go to view the file in the OSF. Opens file in a new tab.", + "upload": "Upload.", + "uploadDescription": "There are two ways to upload files. Open the storage provider or folder where you intend to upload files; you can then drag files from your desktop into files list area OR click “Upload Files”, and select your files. Note: File names with special characters may cause unexpected behavior with certain addons.", + "createFolder": "Create a folder", + "createFolderDescription": "To create a folder to help organize your files, click “Create a Folder”. Note: You can also organize your project content by creating Components.", + "renameFolderFile": "Rename a folder or file", + "renameFolderFileDescription": "In the files list, select the file that you want to rename and click the vertical ellipses to open the dropdown. Select “Rename”. Note: Some special characters may cause unexpected behavior with certain addons.", + "move": "Move", + "moveDescription": "You can move your files from one part of your project or component to another. Select the files that you want to move, and click the vertical ellipses to open the dropdown, then click “move”. Choose a component, provider, or folder and then click “Move”.", + "copy": "Copy", + "copyDescription": "You can copy your files from one part of your project or component to another. Select the files that you want to copy, and click the vertical ellipses to open the dropdown, click the “Copy” button. Choose a component, provider, or folder and then click “Copy”. Your files will now appear in both the original and chosen locations.", + "downloadAllFilesZip": "Download All Files As a Zip", + "downloadAllFilesZipDescription": "Select the storage provider from the left navigation, and then click “Download As Zip” in the top toolbar.", + "downloadFolderZip": "Download a Folder as Zip", + "downloadFolderZipDescription": "To download a specific folder as a zip, click the vertical ellipses on the folder’s row to open the dropdown. Click “Download”. Alternatively, open the folder and then click “Download As Zip” in the top toolbar.", + "downloadFile": "Download a File", + "downloadFileDescription": "With the file open: Click the vertical ellipses to open the dropdown. Click “Download”. From the file list: Click the vertical ellipses of the file to open the dropdown. Click “Download”.", + "moreInfo": "For more information please see our", + "helpGuides": "help guides on project files." + }, "dropText": "Drop a file to upload", "emptyState": "This folder is empty", "detail": { @@ -1129,6 +1148,7 @@ "projectContributors": "Project Contributors", "collectionMetadata": "Collection Metadata", "tooltipMessage": "Complete previous step to edit this section", + "contributorsTooltip": "Projects must have at least one registered administrator and one author showing in the citation at all times. A registered administrator is a user who has both confirmed their account and has administrator privileges.", "noDescription": "No description", "noLicense": "No license", "noTags": "No tags", @@ -1388,9 +1408,6 @@ } }, "notifications": { - "header": { - "title": "Notifications" - }, "emailPreferences": { "title": "Configure Email Preferences", "items": { @@ -1408,9 +1425,8 @@ "notificationPreferences": { "title": "Configure Notification Preferences", "note": "Note: Transactional and administrative service emails will still be sent.", + "tooltipText": "These are default settings for new projects you create or are added to. Modifying these settings will not modify settings on existing projects.", "items": { - "replies": "Replies to your comments", - "comments": "Comments added", "files": "Files updated", "mentions": "Mentions added", "preprints": "Preprint submissions updated" From 193a0b576841c4316a34ddad6400c4903e3b5870 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 11:33:40 +0300 Subject: [PATCH 03/12] fix(table): updated sorting --- src/app/core/constants/nav-items.constant.ts | 2 +- .../admin-table/admin-table.component.html | 78 +++++++----- .../admin-table/admin-table.component.ts | 7 +- .../admin-institutions/helpers/index.ts | 1 + .../institutions-preprints.component.html | 35 ++--- .../institutions-preprints.component.ts | 5 +- .../institutions-projects.component.html | 47 +++---- .../institutions-projects.component.ts | 5 +- .../institutions-registrations.component.html | 37 +++--- .../institutions-registrations.component.ts | 5 +- .../institutions-users.component.html | 89 ++++++------- .../institutions-users.component.ts | 6 +- .../confirm-email.component.html | 4 +- .../confirm-email/confirm-email.component.ts | 4 +- .../pages/dashboard/dashboard.component.html | 4 +- .../pages/dashboard/dashboard.component.ts | 77 +++++------ .../meeting-details.component.html | 2 +- .../meeting-details.component.ts | 2 +- .../meetings-landing.component.html | 2 +- .../meetings-landing.component.ts | 2 +- .../create-project-dialog.component.ts | 2 +- .../my-projects/my-projects.component.ts | 77 ++++++----- .../my-preprints/my-preprints.component.html | 2 +- .../my-preprints/my-preprints.component.ts | 24 ++-- .../link-resource-dialog.component.html | 110 +++++++--------- .../link-resource-dialog.component.ts | 68 +++++----- .../institution-filter.component.spec.ts | 5 +- .../my-projects-table.component.html | 120 ++++++++---------- .../my-projects-table.component.ts | 2 + src/app/shared/enums/sort-order.enum.ts | 4 +- .../shared/models/table-parameters.model.ts | 2 +- src/assets/i18n/en.json | 2 +- 32 files changed, 381 insertions(+), 451 deletions(-) diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 4a2cd9db7..c6b8ab56a 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -181,7 +181,7 @@ export const MENU_ITEMS: MenuItem[] = [ }, { id: 'my-resources', - label: 'navigation.myResources', + label: 'navigation.myOsf', icon: 'custom-icon-projects', routerLinkActiveOptions: { exact: true }, visible: false, diff --git a/src/app/features/admin-institutions/components/admin-table/admin-table.component.html b/src/app/features/admin-institutions/components/admin-table/admin-table.component.html index b3a97c3b8..30d4a2fc6 100644 --- a/src/app/features/admin-institutions/components/admin-table/admin-table.component.html +++ b/src/app/features/admin-institutions/components/admin-table/admin-table.component.html @@ -50,7 +50,7 @@ outlined class="grey-border-color download-button" severity="info" - (click)="downloadMenu.toggle($event)" + (onClick)="downloadMenu.toggle($event)" /> @if (reportsLink()) { @@ -69,7 +69,7 @@ - - @for (col of columns; track col.field) { - -
- @if (col.isLink && isLink(rowData[col.field])) { - + @if (isLoading()) { + + + + + + } @else { + + @for (col of columns; track col.field) { + +
+ @if (col.isLink && isLink(rowData[col.field])) { + + @if (col.dateFormat) { + {{ getCellValue(rowData[col.field]) | date: col.dateFormat }} + } @else { + {{ getCellValue(rowData[col.field]) }} + } + + } @else { @if (col.dateFormat) { {{ getCellValue(rowData[col.field]) | date: col.dateFormat }} } @else { {{ getCellValue(rowData[col.field]) }} } - - } @else { - @if (col.dateFormat) { - {{ getCellValue(rowData[col.field]) | date: col.dateFormat }} - } @else { - {{ getCellValue(rowData[col.field]) }} } - } - @if (col.showIcon) { - - } -
- - } - + @if (col.showIcon) { + + } +
+ + } + + }
@@ -154,7 +162,7 @@ severity="contrast" text [disabled]="!prevLink()" - (click)="switchPage(prevLink())" + (onClick)="switchPage(prevLink())" > @@ -163,7 +171,7 @@ severity="contrast" text [disabled]="!nextLink()" - (click)="switchPage(nextLink())" + (onClick)="switchPage(nextLink())" >
diff --git a/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts b/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts index 6dc0a535f..1830cf04c 100644 --- a/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts +++ b/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts @@ -5,6 +5,7 @@ import { Button, ButtonDirective } from 'primeng/button'; import { Menu } from 'primeng/menu'; import { MultiSelect } from 'primeng/multiselect'; import { PaginatorState } from 'primeng/paginator'; +import { Skeleton } from 'primeng/skeleton'; import { TableModule } from 'primeng/table'; import { Tooltip } from 'primeng/tooltip'; @@ -37,6 +38,7 @@ import { DownloadType } from '../../enums'; TranslatePipe, Button, Menu, + Skeleton, StopPropagationDirective, DatePipe, ], @@ -52,6 +54,7 @@ export class AdminTableComponent { tableColumns = input.required(); tableData = input.required(); + isLoading = input(false); enablePagination = input(false); totalCount = input(0); currentPage = input(1); @@ -60,6 +63,7 @@ export class AdminTableComponent { sortField = input(''); sortOrder = input(1); + reportsLink = input(''); isNextPreviousPagination = input(false); @@ -79,8 +83,7 @@ export class AdminTableComponent { linkPageChanged = output(); downloadClicked = output(); - reportsLink = input(''); - + skeletonData: TableCellData[] = Array.from({ length: 10 }, () => ({}) as TableCellData); selectedColumns = signal([]); downloadMenuItems = DOWNLOAD_OPTIONS; diff --git a/src/app/features/admin-institutions/helpers/index.ts b/src/app/features/admin-institutions/helpers/index.ts index 45eb44ccd..32049f042 100644 --- a/src/app/features/admin-institutions/helpers/index.ts +++ b/src/app/features/admin-institutions/helpers/index.ts @@ -1,2 +1,3 @@ +export * from './camel-to-snake.helper'; export * from './download-url.helper'; export * from './extract-path-after-domain.helper'; diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html index c914d18fc..af42fcf0c 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html @@ -1,22 +1,15 @@ -@if (isLoading()) { -
- + +
+

{{ totalCount() }} {{ 'adminInstitutions.preprints.totalPreprints' | translate | lowercase }}

-} @else { - -
-

- {{ totalCount() }} {{ 'adminInstitutions.preprints.totalPreprints' | translate | lowercase }} -

-
-
-} +
diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts index 8ae90eebd..1f5d3a5c8 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts @@ -6,7 +6,6 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { LoadingSpinnerComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@osf/shared/constants'; import { SortOrder } from '@osf/shared/enums'; import { Institution, QueryParams } from '@osf/shared/models'; @@ -22,7 +21,7 @@ import { FetchPreprints, InstitutionsAdminSelectors } from '../../store'; @Component({ selector: 'osf-institutions-preprints', - imports: [CommonModule, AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], + imports: [CommonModule, AdminTableComponent, TranslatePipe], templateUrl: './institutions-preprints.component.html', styleUrl: './institutions-preprints.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -77,7 +76,7 @@ export class InstitutionsPreprintsComponent implements OnInit { const sortField = this.sortField(); const sortOrder = this.sortOrder(); - const sortParam = sortOrder === -1 ? `-${sortField}` : sortField; + const sortParam = sortOrder === SortOrder.Desc ? `-${sortField}` : sortField; const institution = this.institution() as Institution; const institutionIris = institution.iris || []; diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html index d51a97e2d..0a197c067 100644 --- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html +++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html @@ -1,27 +1,22 @@ -@if (isLoading()) { -
- + +
+

{{ totalCount() }} {{ 'adminInstitutions.projects.totalProjects' | translate }}

-} @else { - -
-

{{ totalCount() }} {{ 'adminInstitutions.projects.totalProjects' | translate }}

-
-
-} +
diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts index f6ef5e56f..d5a1c7437 100644 --- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts @@ -11,7 +11,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; import { UserSelectors } from '@osf/core/store/user'; -import { LoadingSpinnerComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@osf/shared/constants'; import { SortOrder } from '@osf/shared/enums'; import { Institution, QueryParams } from '@osf/shared/models'; @@ -29,7 +28,7 @@ import { FetchProjects, InstitutionsAdminSelectors, RequestProjectAccess, SendUs @Component({ selector: 'osf-institutions-projects', - imports: [AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], + imports: [AdminTableComponent, TranslatePipe], templateUrl: './institutions-projects.component.html', styleUrl: './institutions-projects.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -95,7 +94,7 @@ export class InstitutionsProjectsComponent implements OnInit { const sortField = this.sortField(); const sortOrder = this.sortOrder(); - const sortParam = sortOrder === -1 ? `-${sortField}` : sortField; + const sortParam = sortOrder === SortOrder.Desc ? `-${sortField}` : sortField; const institution = this.institution() as Institution; const institutionIris = institution.iris || []; diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html index a4c009522..b62de5613 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html @@ -1,22 +1,17 @@ -@if (isLoading()) { -
- + +
+

+ {{ totalCount() }} {{ 'adminInstitutions.registrations.totalRegistrations' | translate | lowercase }} +

-} @else { - -
-

- {{ totalCount() }} {{ 'adminInstitutions.registrations.totalRegistrations' | translate | lowercase }} -

-
-
-} +
diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts index cfdcbbb3b..d8763889d 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts @@ -6,7 +6,6 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { LoadingSpinnerComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@osf/shared/constants'; import { SortOrder } from '@osf/shared/enums'; import { Institution, QueryParams } from '@osf/shared/models'; @@ -22,7 +21,7 @@ import { FetchRegistrations, InstitutionsAdminSelectors } from '../../store'; @Component({ selector: 'osf-institutions-registrations', - imports: [CommonModule, AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], + imports: [CommonModule, AdminTableComponent, TranslatePipe], templateUrl: './institutions-registrations.component.html', styleUrl: './institutions-registrations.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -75,7 +74,7 @@ export class InstitutionsRegistrationsComponent implements OnInit { const sortField = this.sortField(); const sortOrder = this.sortOrder(); - const sortParam = sortOrder === -1 ? `-${sortField}` : sortField; + const sortParam = sortOrder === SortOrder.Desc ? `-${sortField}` : sortField; const institution = this.institution() as Institution; const institutionIris = institution.iris || []; diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html index b38e96b27..59bd55443 100644 --- a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html +++ b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html @@ -1,51 +1,46 @@ -@if (isLoading()) { -
- + +
+

{{ amountText() }}

-} @else { - -
-

{{ amountText() }}

-
-
-
- - - -
+
+
+ + + +
-
- -
+
+
- -} +
+ diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts index 1a8180f06..829a8fdd9 100644 --- a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts @@ -23,7 +23,7 @@ import { FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { UserSelectors } from '@osf/core/store/user'; -import { LoadingSpinnerComponent, SelectComponent } from '@osf/shared/components'; +import { SelectComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@osf/shared/constants'; import { SortOrder } from '@osf/shared/enums'; import { Primitive } from '@osf/shared/helpers'; @@ -35,14 +35,14 @@ import { AdminTableComponent } from '../../components'; import { departmentOptions, userTableColumns } from '../../constants'; import { SendEmailDialogComponent } from '../../dialogs'; import { DownloadType } from '../../enums'; -import { camelToSnakeCase } from '../../helpers/camel-to-snake.helper'; +import { camelToSnakeCase } from '../../helpers'; import { mapUserToTableCellData } from '../../mappers'; import { InstitutionUser, SendEmailDialogData, TableCellData, TableCellLink, TableIconClickEvent } from '../../models'; import { FetchInstitutionUsers, InstitutionsAdminSelectors, SendUserMessage } from '../../store'; @Component({ selector: 'osf-institutions-users', - imports: [AdminTableComponent, FormsModule, SelectComponent, CheckboxModule, TranslatePipe, LoadingSpinnerComponent], + imports: [AdminTableComponent, FormsModule, SelectComponent, CheckboxModule, TranslatePipe], templateUrl: './institutions-users.component.html', styleUrl: './institutions-users.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/home/components/confirm-email/confirm-email.component.html b/src/app/features/home/components/confirm-email/confirm-email.component.html index 00bb2a80c..8c6acabfd 100644 --- a/src/app/features/home/components/confirm-email/confirm-email.component.html +++ b/src/app/features/home/components/confirm-email/confirm-email.component.html @@ -11,14 +11,14 @@

severity="info" class="w-full" styleClass="w-full" - (click)="closeDialog()" + (onClick)="closeDialog()" [label]="'common.buttons.cancel' | translate" >

diff --git a/src/app/features/home/components/confirm-email/confirm-email.component.ts b/src/app/features/home/components/confirm-email/confirm-email.component.ts index 7d781d2db..366a156b0 100644 --- a/src/app/features/home/components/confirm-email/confirm-email.component.ts +++ b/src/app/features/home/components/confirm-email/confirm-email.component.ts @@ -11,7 +11,7 @@ import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { AccountSettingsService } from '@osf/features/settings/account-settings/services'; -import { LoadingSpinnerComponent } from '@shared/components'; +import { LoadingSpinnerComponent } from '@osf/shared/components'; @Component({ selector: 'osf-confirm-email', @@ -31,7 +31,7 @@ export class ConfirmEmailComponent { verifyingEmail = signal(false); closeDialog() { - this.router.navigate(['/home']); + this.router.navigate(['/']); this.dialogRef.close(); } diff --git a/src/app/features/home/pages/dashboard/dashboard.component.html b/src/app/features/home/pages/dashboard/dashboard.component.html index 46a55cd5e..738974891 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.html +++ b/src/app/features/home/pages/dashboard/dashboard.component.html @@ -1,4 +1,4 @@ -
+
(''); + readonly activeProject = signal(null); + readonly sortColumn = signal(undefined); + readonly sortOrder = signal(SortOrder.Asc); + readonly tableParams = signal({ ...MY_PROJECTS_TABLE_PARAMS }); - protected readonly searchControl = new FormControl(''); - protected readonly activeProject = signal(null); - protected readonly sortColumn = signal(undefined); - protected readonly sortOrder = signal(SortOrder.Asc); - protected readonly tableParams = signal({ - ...MY_PROJECTS_TABLE_PARAMS, - }); + readonly projects = select(MyResourcesSelectors.getProjects); + readonly totalProjectsCount = select(MyResourcesSelectors.getTotalProjects); + readonly areProjectsLoading = select(MyResourcesSelectors.getProjectsLoading); - protected readonly projects = select(MyResourcesSelectors.getProjects); - protected readonly totalProjectsCount = select(MyResourcesSelectors.getTotalProjects); + readonly actions = createDispatchMap({ getMyProjects: GetMyProjects, clearMyResources: ClearMyResources }); - protected readonly filteredProjects = computed(() => { + readonly filteredProjects = computed(() => { const search = this.searchControl.value?.toLowerCase() ?? ''; return this.projects().filter((project) => project.title.toLowerCase().includes(search)); }); @@ -125,7 +122,7 @@ export class DashboardComponent implements OnInit { if (sortField) { this.sortColumn.set(sortField); - this.sortOrder.set(sortOrder || SortOrder.Asc); + this.sortOrder.set(+sortOrder || SortOrder.Asc); } if (search) { @@ -154,25 +151,14 @@ export class DashboardComponent implements OnInit { setupCleanup(): void { this.destroyRef.onDestroy(() => { - this.store.dispatch(new ClearMyResources()); + this.actions.clearMyResources(); }); } fetchProjects(): void { - this.isLoading.set(true); const filters = this.createFilters(); const page = Math.floor(this.tableParams().firstRowIndex / this.tableParams().rows) + 1; - this.store - .dispatch(new GetMyProjects(page, this.tableParams().rows, filters)) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe({ - complete: () => { - this.isLoading.set(false); - }, - error: () => { - this.isLoading.set(false); - }, - }); + this.actions.getMyProjects(page, this.tableParams().rows, filters); } createFilters(): MyResourcesSearchFilters { @@ -201,7 +187,7 @@ export class DashboardComponent implements OnInit { }); } - protected onPageChange(event: TablePageEvent): void { + onPageChange(event: TablePageEvent): void { this.tableParams.update((current) => ({ ...current, rows: event.rows, @@ -211,34 +197,29 @@ export class DashboardComponent implements OnInit { this.updateQueryParams(); } - protected onSort(event: SortEvent): void { + onSort(event: SortEvent): void { if (event.field) { this.sortColumn.set(event.field); - this.sortOrder.set(event.order === -1 ? SortOrder.Desc : SortOrder.Asc); + this.sortOrder.set(event.order as SortOrder); this.updateQueryParams(); } } - protected navigateToProject(project: MyResourcesItem): void { + navigateToProject(project: MyResourcesItem): void { this.activeProject.set(project); this.router.navigate([project.id]); } - protected createProject(): void { + createProject(): void { const dialogWidth = this.isMedium() ? '850px' : '95vw'; - this.isSubmitting.set(true); - this.dialogService - .open(CreateProjectDialogComponent, { - width: dialogWidth, - focusOnShow: false, - header: this.translateService.instant('myProjects.header.createProject'), - closeOnEscape: true, - modal: true, - closable: true, - }) - .onClose.subscribe(() => { - this.isSubmitting.set(false); - }); + this.dialogService.open(CreateProjectDialogComponent, { + width: dialogWidth, + focusOnShow: false, + header: this.translateService.instant('myProjects.header.createProject'), + closeOnEscape: true, + modal: true, + closable: true, + }); } } diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html index c3f24e5da..8a1994f8d 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html @@ -27,7 +27,7 @@ (onPage)="onPageChange($event)" (onSort)="onSort($event)" [sortField]="sortColumn()" - [sortOrder]="sortOrder() === 0 ? 1 : -1" + [sortOrder]="sortOrder()" [customSort]="true" [resetPageOnSort]="false" > diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts b/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts index ba43de55d..9229a7e4a 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts @@ -138,7 +138,7 @@ export class MeetingDetailsComponent { if (event.field) { this.updateQueryParams({ sortColumn: event.field, - sortOrder: event.order === -1 ? SortOrder.Desc : SortOrder.Asc, + sortOrder: event.order as SortOrder, }); } } diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html index 87b658163..8a8c42613 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html @@ -33,7 +33,7 @@ (onPage)="onPageChange($event)" (onSort)="onSort($event)" [sortField]="sortColumn()" - [sortOrder]="sortOrder() === 0 ? 1 : -1" + [sortOrder]="sortOrder()" [customSort]="true" [resetPageOnSort]="false" > diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.ts b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.ts index eb69a5af2..9e1c24ba7 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.ts +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.ts @@ -100,7 +100,7 @@ export class MeetingsLandingComponent { if (event.field) { this.updateQueryParams({ sortColumn: event.field, - sortOrder: event.order === -1 ? SortOrder.Desc : SortOrder.Asc, + sortOrder: event.order as SortOrder, }); } } diff --git a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.ts b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.ts index 2f5c8506d..0e967d8f4 100644 --- a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.ts +++ b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.ts @@ -23,7 +23,7 @@ import { CreateProject, GetMyProjects, MyResourcesSelectors } from '@osf/shared/ changeDetection: ChangeDetectionStrategy.OnPush, }) export class CreateProjectDialogComponent { - protected readonly dialogRef = inject(DynamicDialogRef); + readonly dialogRef = inject(DynamicDialogRef); private actions = createDispatchMap({ getMyProjects: GetMyProjects, diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index a5b517912..e3624a907 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -69,37 +69,34 @@ export class MyProjectsComponent implements OnInit { readonly route = inject(ActivatedRoute); readonly translateService = inject(TranslateService); - protected readonly isLoading = signal(false); - protected readonly isTablet = toSignal(inject(IS_MEDIUM)); - protected readonly tabOptions = MY_PROJECTS_TABS; - protected readonly tabOption = MyProjectsTab; - - protected readonly searchControl = new FormControl(''); - - protected readonly queryParams = toSignal(this.route.queryParams); - protected readonly currentPage = signal(1); - protected readonly currentPageSize = signal(MY_PROJECTS_TABLE_PARAMS.rows); - protected readonly selectedTab = signal(MyProjectsTab.Projects); - protected readonly activeProject = signal(null); - protected readonly sortColumn = signal(undefined); - protected readonly sortOrder = signal(SortOrder.Asc); - protected readonly tableParams = signal({ - ...MY_PROJECTS_TABLE_PARAMS, - firstRowIndex: 0, - }); - - protected readonly projects = select(MyResourcesSelectors.getProjects); - protected readonly registrations = select(MyResourcesSelectors.getRegistrations); - protected readonly preprints = select(MyResourcesSelectors.getPreprints); - protected readonly bookmarks = select(MyResourcesSelectors.getBookmarks); - protected readonly totalProjectsCount = select(MyResourcesSelectors.getTotalProjects); - protected readonly totalRegistrationsCount = select(MyResourcesSelectors.getTotalRegistrations); - protected readonly totalPreprintsCount = select(MyResourcesSelectors.getTotalPreprints); - protected readonly totalBookmarksCount = select(MyResourcesSelectors.getTotalBookmarks); - - protected readonly bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId); - - protected readonly actions = createDispatchMap({ + readonly isLoading = signal(false); + readonly isTablet = toSignal(inject(IS_MEDIUM)); + readonly tabOptions = MY_PROJECTS_TABS; + readonly tabOption = MyProjectsTab; + + readonly searchControl = new FormControl(''); + + readonly queryParams = toSignal(this.route.queryParams); + readonly currentPage = signal(1); + readonly currentPageSize = signal(MY_PROJECTS_TABLE_PARAMS.rows); + readonly selectedTab = signal(MyProjectsTab.Projects); + readonly activeProject = signal(null); + readonly sortColumn = signal(undefined); + readonly sortOrder = signal(SortOrder.Asc); + readonly tableParams = signal({ ...MY_PROJECTS_TABLE_PARAMS, firstRowIndex: 0 }); + + readonly projects = select(MyResourcesSelectors.getProjects); + readonly registrations = select(MyResourcesSelectors.getRegistrations); + readonly preprints = select(MyResourcesSelectors.getPreprints); + readonly bookmarks = select(MyResourcesSelectors.getBookmarks); + readonly totalProjectsCount = select(MyResourcesSelectors.getTotalProjects); + readonly totalRegistrationsCount = select(MyResourcesSelectors.getTotalRegistrations); + readonly totalPreprintsCount = select(MyResourcesSelectors.getTotalPreprints); + readonly totalBookmarksCount = select(MyResourcesSelectors.getTotalBookmarks); + + readonly bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId); + + readonly actions = createDispatchMap({ getBookmarksCollectionId: GetBookmarksCollectionId, clearMyProjects: ClearMyResources, getMyProjects: GetMyProjects, @@ -128,9 +125,7 @@ export class MyProjectsComponent implements OnInit { setupSearchSubscription(): void { this.searchControl.valueChanges .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) - .subscribe((searchValue) => { - this.handleSearch(searchValue ?? ''); - }); + .subscribe((searchValue) => this.handleSearch(searchValue ?? '')); } setupTotalRecordsEffect(): void { @@ -292,7 +287,7 @@ export class MyProjectsComponent implements OnInit { }); } - protected onPageChange(event: TablePageEvent): void { + onPageChange(event: TablePageEvent): void { const page = Math.floor(event.first / event.rows) + 1; const currentParams = this.queryParams() || {}; @@ -304,16 +299,16 @@ export class MyProjectsComponent implements OnInit { }); } - protected onSort(event: SortEvent): void { + onSort(event: SortEvent): void { if (event.field) { this.updateQueryParams({ sortColumn: event.field, - sortOrder: event.order === -1 ? SortOrder.Desc : SortOrder.Asc, + sortOrder: event.order as SortOrder, }); } } - protected onTabChange(tabIndex: number): void { + onTabChange(tabIndex: number): void { this.actions.clearMyProjects(); this.selectedTab.set(tabIndex); const currentParams = this.queryParams() || {}; @@ -327,7 +322,7 @@ export class MyProjectsComponent implements OnInit { }); } - protected createProject(): void { + createProject(): void { const dialogWidth = this.isTablet() ? '850px' : '95vw'; this.dialogService.open(CreateProjectDialogComponent, { @@ -340,12 +335,12 @@ export class MyProjectsComponent implements OnInit { }); } - protected navigateToProject(project: MyResourcesItem): void { + navigateToProject(project: MyResourcesItem): void { this.activeProject.set(project); this.router.navigate([project.id]); } - protected navigateToRegistry(registry: MyResourcesItem): void { + navigateToRegistry(registry: MyResourcesItem): void { this.activeProject.set(registry); this.router.navigate([registry.id]); } diff --git a/src/app/features/preprints/pages/my-preprints/my-preprints.component.html b/src/app/features/preprints/pages/my-preprints/my-preprints.component.html index e914d66ba..cef30ce96 100644 --- a/src/app/features/preprints/pages/my-preprints/my-preprints.component.html +++ b/src/app/features/preprints/pages/my-preprints/my-preprints.component.html @@ -32,7 +32,7 @@ (onPage)="onPageChange($event)" (onSort)="onSort($event)" [sortField]="sortColumn()" - [sortOrder]="sortOrder() === 0 ? 1 : -1" + [sortOrder]="sortOrder()" [customSort]="true" [resetPageOnSort]="false" > diff --git a/src/app/features/preprints/pages/my-preprints/my-preprints.component.ts b/src/app/features/preprints/pages/my-preprints/my-preprints.component.ts index c7671d651..6e12c9e42 100644 --- a/src/app/features/preprints/pages/my-preprints/my-preprints.component.ts +++ b/src/app/features/preprints/pages/my-preprints/my-preprints.component.ts @@ -23,13 +23,14 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { PreprintShortInfo } from '@osf/features/preprints/models'; -import { FetchMyPreprints, PreprintSelectors } from '@osf/features/preprints/store/preprint'; +import { ListInfoShortenerComponent, SearchInputComponent, SubHeaderComponent } from '@osf/shared/components'; +import { TABLE_PARAMS } from '@osf/shared/constants'; +import { SortOrder } from '@osf/shared/enums'; import { parseQueryFilterParams } from '@osf/shared/helpers'; -import { ListInfoShortenerComponent, SearchInputComponent, SubHeaderComponent } from '@shared/components'; -import { TABLE_PARAMS } from '@shared/constants'; -import { SortOrder } from '@shared/enums'; -import { QueryParams, SearchFilters, TableParameters } from '@shared/models'; +import { QueryParams, SearchFilters, TableParameters } from '@osf/shared/models'; + +import { PreprintShortInfo } from '../../models'; +import { FetchMyPreprints, PreprintSelectors } from '../../store/preprint'; @Component({ selector: 'osf-my-preprints', @@ -52,9 +53,7 @@ export class MyPreprintsComponent { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); - private actions = createDispatchMap({ - fetchMyPreprints: FetchMyPreprints, - }); + private readonly actions = createDispatchMap({ fetchMyPreprints: FetchMyPreprints }); searchControl = new FormControl(''); @@ -63,10 +62,7 @@ export class MyPreprintsComponent { sortOrder = signal(SortOrder.Asc); currentPage = signal(1); currentPageSize = signal(TABLE_PARAMS.rows); - tableParams = signal({ - ...TABLE_PARAMS, - firstRowIndex: 0, - }); + tableParams = signal({ ...TABLE_PARAMS, firstRowIndex: 0 }); preprints = select(PreprintSelectors.getMyPreprints); preprintsTotalCount = select(PreprintSelectors.getMyPreprintsTotalCount); @@ -96,7 +92,7 @@ export class MyPreprintsComponent { if (event.field) { this.updateQueryParams({ sortColumn: event.field, - sortOrder: event.order === -1 ? SortOrder.Desc : SortOrder.Asc, + sortOrder: event.order as SortOrder.Asc, }); } } diff --git a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.html b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.html index e61ebec0e..557a7f29f 100644 --- a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.html +++ b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.html @@ -36,62 +36,38 @@ > - @if (isCurrentTableLoading()) { - - - - - {{ 'project.overview.dialog.linkProject.table.title' | translate }} - - {{ 'project.overview.dialog.linkProject.table.created' | translate }} - - {{ 'project.overview.dialog.linkProject.table.modified' | translate }} - - - {{ 'project.overview.dialog.linkProject.table.contributors' | translate }} - - - - - - - - - - - - } @else { - - - - - {{ 'project.overview.dialog.linkProject.table.title' | translate }} - - {{ 'project.overview.dialog.linkProject.table.created' | translate }} - - {{ 'project.overview.dialog.linkProject.table.modified' | translate }} - - - {{ 'project.overview.dialog.linkProject.table.contributors' | translate }} - - - - + + + + + {{ 'project.overview.dialog.linkProject.table.title' | translate }} + + {{ 'project.overview.dialog.linkProject.table.created' | translate }} + + {{ 'project.overview.dialog.linkProject.table.modified' | translate }} + + + {{ 'project.overview.dialog.linkProject.table.contributors' | translate }} + + + + + @if (item.id) {

@@ -113,15 +89,21 @@ } - - - - - {{ 'common.search.noResultsFound' | translate }} + } @else { + + + + - - - } + } + + + + + {{ 'common.search.noResultsFound' | translate }} + + + (ResourceSearchMode.User); - protected resourceType = signal(ResourceType.Project); - protected searchControl = new FormControl(''); - - protected currentProject = select(ProjectOverviewSelectors.getProject); - protected myProjects = select(MyResourcesSelectors.getProjects); - protected isMyProjectsLoading = select(MyResourcesSelectors.getProjectsLoading); - protected myRegistrations = select(MyResourcesSelectors.getRegistrations); - protected isMyRegistrationsLoading = select(MyResourcesSelectors.getRegistrationsLoading); - protected totalProjectsCount = select(MyResourcesSelectors.getTotalProjects); - protected totalRegistrationsCount = select(MyResourcesSelectors.getTotalRegistrations); - protected isNodeLinksSubmitting = select(NodeLinksSelectors.getNodeLinksSubmitting); - protected linkedResources = select(NodeLinksSelectors.getLinkedResources); - - protected currentTableItems = computed(() => { - return this.resourceType() === ResourceType.Project ? this.myProjects() : this.myRegistrations(); - }); - - protected isCurrentTableLoading = computed(() => { - return this.resourceType() === ResourceType.Project ? this.isMyProjectsLoading() : this.isMyRegistrationsLoading(); - }); - - protected currentTotalCount = computed(() => { - return this.resourceType() === ResourceType.Project ? this.totalProjectsCount() : this.totalRegistrationsCount(); - }); - - protected isItemLinked = computed(() => { + readonly dialogRef = inject(DynamicDialogRef); + readonly ResourceSearchMode = ResourceSearchMode; + readonly ResourceType = ResourceType; + + currentPage = signal(1); + searchMode = signal(ResourceSearchMode.User); + resourceType = signal(ResourceType.Project); + searchControl = new FormControl(''); + + skeletonData: MyResourcesItem[] = Array.from({ length: 10 }, () => ({}) as MyResourcesItem); + + currentProject = select(ProjectOverviewSelectors.getProject); + myProjects = select(MyResourcesSelectors.getProjects); + isMyProjectsLoading = select(MyResourcesSelectors.getProjectsLoading); + myRegistrations = select(MyResourcesSelectors.getRegistrations); + isMyRegistrationsLoading = select(MyResourcesSelectors.getRegistrationsLoading); + totalProjectsCount = select(MyResourcesSelectors.getTotalProjects); + totalRegistrationsCount = select(MyResourcesSelectors.getTotalRegistrations); + isNodeLinksSubmitting = select(NodeLinksSelectors.getNodeLinksSubmitting); + linkedResources = select(NodeLinksSelectors.getLinkedResources); + + currentTableItems = computed(() => + this.resourceType() === ResourceType.Project ? this.myProjects() : this.myRegistrations() + ); + + isCurrentTableLoading = computed(() => + this.resourceType() === ResourceType.Project ? this.isMyProjectsLoading() : this.isMyRegistrationsLoading() + ); + + currentTotalCount = computed(() => + this.resourceType() === ResourceType.Project ? this.totalProjectsCount() : this.totalRegistrationsCount() + ); + + isItemLinked = computed(() => { const linkedResources = this.linkedResources(); const linkedTargetIds = new Set(linkedResources.map((resource) => resource.id)); return (itemId: string) => linkedTargetIds.has(itemId); }); - protected actions = createDispatchMap({ + actions = createDispatchMap({ getProjects: GetMyProjects, getRegistrations: GetMyRegistrations, createNodeLink: CreateNodeLink, diff --git a/src/app/features/search/components/filters/institution-filter/institution-filter.component.spec.ts b/src/app/features/search/components/filters/institution-filter/institution-filter.component.spec.ts index 30f9aabda..96581d199 100644 --- a/src/app/features/search/components/filters/institution-filter/institution-filter.component.spec.ts +++ b/src/app/features/search/components/filters/institution-filter/institution-filter.component.spec.ts @@ -1,6 +1,7 @@ import { Store } from '@ngxs/store'; -import { MockProvider } from 'ng-mocks'; +import { TranslatePipe } from '@ngx-translate/core'; +import { MockPipe, MockProvider } from 'ng-mocks'; import { SelectChangeEvent } from 'primeng/select'; @@ -41,7 +42,7 @@ describe('InstitutionFilterComponent', () => { }); await TestBed.configureTestingModule({ - imports: [InstitutionFilterComponent], + imports: [InstitutionFilterComponent, MockPipe(TranslatePipe)], providers: [MockProvider(Store, store)], }).compileComponents(); diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.html b/src/app/shared/components/my-projects-table/my-projects-table.component.html index 141f1fda4..9e0685657 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.html +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.html @@ -1,65 +1,43 @@

- @if (isLoading()) { - - - - - {{ 'myProjects.table.columns.title' | translate }} - - - {{ 'myProjects.table.columns.contributors' | translate }} - - {{ 'myProjects.table.columns.modified' | translate }} - - - - - - - - - - - - - } @else { - - - - - {{ 'myProjects.table.columns.title' | translate }} - - - {{ 'myProjects.table.columns.contributors' | translate }} - - {{ 'myProjects.table.columns.modified' | translate }} - - - - - + + + + + + {{ 'myProjects.table.columns.title' | translate }} + + + {{ 'myProjects.table.columns.contributors' | translate }} + + {{ 'myProjects.table.columns.modified' | translate }} + + + + + + @if (item?.id) {

@@ -74,13 +52,19 @@ {{ item.dateModified | date: 'MMM d, y, h:mm a' }} - - - - - {{ 'common.search.noResultsFound' | translate }} + } @else { + + + + - - - } + } + + + + + {{ 'common.search.noResultsFound' | translate }} + + +

diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.ts b/src/app/shared/components/my-projects-table/my-projects-table.component.ts index a748d2238..f553444df 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.ts +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.ts @@ -34,6 +34,8 @@ export class MyProjectsTableComponent { sort = output(); itemClick = output(); + skeletonData: MyResourcesItem[] = Array.from({ length: 10 }, () => ({}) as MyResourcesItem); + protected onPageChange(event: TablePageEvent): void { this.pageChange.emit(event); } diff --git a/src/app/shared/enums/sort-order.enum.ts b/src/app/shared/enums/sort-order.enum.ts index 7b5414cff..bc8b3edf3 100644 --- a/src/app/shared/enums/sort-order.enum.ts +++ b/src/app/shared/enums/sort-order.enum.ts @@ -1,4 +1,4 @@ export enum SortOrder { - Asc = 0, - Desc, + Asc = 1, + Desc = -1, } diff --git a/src/app/shared/models/table-parameters.model.ts b/src/app/shared/models/table-parameters.model.ts index 5b8ea2259..97617946d 100644 --- a/src/app/shared/models/table-parameters.model.ts +++ b/src/app/shared/models/table-parameters.model.ts @@ -1,4 +1,4 @@ -import { SortOrder } from '@osf/shared/enums/sort-order.enum'; +import { SortOrder } from '../enums'; export interface TableParameters { rows: number; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e1aef0f18..6633a8bee 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -145,7 +145,7 @@ "meetings": "Meetings", "myProjects": "My Projects", "projects": "Projects", - "myResources": "My Resources", + "myOsf": "My OSF", "registries": "Registries", "overview": "Overview", "discover": "Discover", From 35212d571a90b8b0446816ac8bffed48c010a19c Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 12:57:22 +0300 Subject: [PATCH 04/12] fix(settings): fixed update project --- .../features/project/settings/mappers/settings.mapper.ts | 8 +++++--- .../project/settings/services/settings.service.ts | 5 +++-- src/app/features/project/settings/store/settings.state.ts | 7 +++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/app/features/project/settings/mappers/settings.mapper.ts b/src/app/features/project/settings/mappers/settings.mapper.ts index 7ffca3f16..9ebc60308 100644 --- a/src/app/features/project/settings/mappers/settings.mapper.ts +++ b/src/app/features/project/settings/mappers/settings.mapper.ts @@ -36,10 +36,12 @@ export class SettingsMapper { description: data.attributes.description, isPublic: data.attributes.public, region: { - id: data.embeds.region.data.id, - name: data.embeds.region.data.attributes.name, + id: data.embeds?.region.data.id, + name: data.embeds?.region.data.attributes.name, }, - affiliatedInstitutions: InstitutionsMapper.fromInstitutionsResponse(data.embeds.affiliated_institutions), + affiliatedInstitutions: data.embeds + ? InstitutionsMapper.fromInstitutionsResponse(data.embeds.affiliated_institutions) + : [], lastFetched: Date.now(), }; } diff --git a/src/app/features/project/settings/services/settings.service.ts b/src/app/features/project/settings/services/settings.service.ts index 5a5cb8981..85f121369 100644 --- a/src/app/features/project/settings/services/settings.service.ts +++ b/src/app/features/project/settings/services/settings.service.ts @@ -14,6 +14,7 @@ import { JsonApiService } from '@shared/services'; import { SettingsMapper } from '../mappers'; import { + NodeDataJsonApi, NodeDetailsModel, NodeResponseJsonApi, ProjectSettingsData, @@ -74,8 +75,8 @@ export class SettingsService { updateProjectById(model: UpdateNodeRequestModel): Observable { return this.jsonApiService - .patch(`${this.baseUrl}/nodes/${model?.data?.id}/`, model) - .pipe(map((response) => SettingsMapper.fromNodeResponse(response.data))); + .patch(`${this.baseUrl}/nodes/${model?.data?.id}/`, model) + .pipe(map((response) => SettingsMapper.fromNodeResponse(response))); } deleteProject(projectId: string): Observable { diff --git a/src/app/features/project/settings/store/settings.state.ts b/src/app/features/project/settings/store/settings.state.ts index 4e3698001..1debaaad3 100644 --- a/src/app/features/project/settings/store/settings.state.ts +++ b/src/app/features/project/settings/store/settings.state.ts @@ -99,8 +99,11 @@ export class SettingsState { tap((updatedProject) => { ctx.patchState({ projectDetails: { - ...ctx.getState().projectDetails, - data: updatedProject, + data: { + ...ctx.getState().projectDetails.data, + title: updatedProject.title, + description: updatedProject.description, + }, isLoading: false, error: null, }, From 1cf324efe14921c509386fa3a0034943bc3112b9 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 13:44:20 +0300 Subject: [PATCH 05/12] fix(bookmarks): updated bookmarks --- .../my-projects/my-projects.component.ts | 3 +- .../overview-toolbar.component.ts | 25 ++--- src/app/shared/services/bookmarks.service.ts | 65 ++++--------- .../stores/bookmarks/bookmarks.model.ts | 9 ++ .../stores/bookmarks/bookmarks.selectors.ts | 5 - .../stores/bookmarks/bookmarks.state.ts | 96 ++++++------------- src/assets/i18n/en.json | 4 +- 7 files changed, 71 insertions(+), 136 deletions(-) diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index e3624a907..8c09683b5 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -237,7 +237,8 @@ export class MyProjectsComponent implements OnInit { createFilters(params: QueryParams): MyResourcesSearchFilters { return { searchValue: params.search || '', - searchFields: ['title', 'tags', 'description'], + searchFields: + this.selectedTab() === MyProjectsTab.Preprints ? ['title', 'tags'] : ['title', 'tags', 'description'], sortColumn: params.sortColumn, sortOrder: params.sortOrder, }; diff --git a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts index cbb76ab8f..11a667ff2 100644 --- a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts +++ b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts @@ -58,11 +58,12 @@ export class OverviewToolbarComponent { private translateService = inject(TranslateService); private toastService = inject(ToastService); private socialShareService = inject(SocialShareService); - protected destroyRef = inject(DestroyRef); private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); - protected isPublic = signal(false); - protected isBookmarked = signal(false); + + destroyRef = inject(DestroyRef); + isPublic = signal(false); + isBookmarked = signal(false); isCollectionsRoute = input(false); isAdmin = input.required(); @@ -71,16 +72,16 @@ export class OverviewToolbarComponent { projectDescription = input(''); showViewOnlyLinks = input(true); - protected isBookmarksLoading = select(MyResourcesSelectors.getBookmarksLoading); - protected isBookmarksSubmitting = select(BookmarksSelectors.getBookmarksCollectionIdSubmitting); - protected bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId); - protected bookmarkedProjects = select(MyResourcesSelectors.getBookmarks); - protected socialsActionItems = computed(() => { + isBookmarksLoading = select(MyResourcesSelectors.getBookmarksLoading); + isBookmarksSubmitting = select(BookmarksSelectors.getBookmarksCollectionIdSubmitting); + bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId); + bookmarkedProjects = select(MyResourcesSelectors.getBookmarks); + socialsActionItems = computed(() => { const shareableContent = this.createShareableContent(); return shareableContent ? this.buildSocialActionItems(shareableContent) : []; }); - protected readonly forkActionItems = [ + readonly forkActionItems = [ { label: 'project.overview.actions.forkProject', command: () => this.handleForkResource(), @@ -96,7 +97,7 @@ export class OverviewToolbarComponent { }, }, ]; - protected readonly ResourceType = ResourceType; + readonly ResourceType = ResourceType; get isRegistration(): boolean { return this.currentResource()?.resourceType === ResourceType.Registration; @@ -134,7 +135,7 @@ export class OverviewToolbarComponent { }); } - protected handleToggleProjectPublicity(): void { + handleToggleProjectPublicity(): void { const resource = this.currentResource(); if (!resource) return; @@ -162,7 +163,7 @@ export class OverviewToolbarComponent { }); } - protected toggleBookmark(): void { + toggleBookmark(): void { const resource = this.currentResource(); const bookmarksId = this.bookmarksCollectionId(); diff --git a/src/app/shared/services/bookmarks.service.ts b/src/app/shared/services/bookmarks.service.ts index 6216fa276..e4f03586e 100644 --- a/src/app/shared/services/bookmarks.service.ts +++ b/src/app/shared/services/bookmarks.service.ts @@ -5,6 +5,8 @@ import { inject, Injectable } from '@angular/core'; import { SparseCollectionsResponseJsonApi } from '@shared/models'; import { JsonApiService } from '@shared/services'; +import { ResourceType } from '../enums'; + import { environment } from 'src/environments/environment'; @Injectable({ @@ -12,6 +14,15 @@ import { environment } from 'src/environments/environment'; }) export class BookmarksService { private jsonApiService = inject(JsonApiService); + private readonly urlMap = new Map([ + [ResourceType.Project, 'linked_nodes'], + [ResourceType.Registration, 'linked_registrations'], + ]); + + private readonly resourceMap = new Map([ + [ResourceType.Project, 'nodes'], + [ResourceType.Registration, 'registrations'], + ]); getBookmarksCollectionId(): Observable { const params: Record = { @@ -28,58 +39,16 @@ export class BookmarksService { ); } - addProjectToBookmarks(bookmarksId: string, projectId: string): Observable { - const url = `${environment.apiUrl}/collections/${bookmarksId}/relationships/linked_nodes/`; - const payload = { - data: [ - { - type: 'nodes', - id: projectId, - }, - ], - }; - - return this.jsonApiService.post(url, payload); - } - - removeProjectFromBookmarks(bookmarksId: string, projectId: string): Observable { - const url = `${environment.apiUrl}/collections/${bookmarksId}/relationships/linked_nodes/`; - const payload = { - data: [ - { - type: 'nodes', - id: projectId, - }, - ], - }; - - return this.jsonApiService.delete(url, payload); - } - - addRegistrationToBookmarks(bookmarksId: string, registryId: string): Observable { - const url = `${environment.apiUrl}/collections/${bookmarksId}/relationships/linked_registrations/`; - const payload = { - data: [ - { - type: 'registrations', - id: registryId, - }, - ], - }; + addResourceToBookmarks(bookmarksId: string, resourceId: string, resourceType: ResourceType): Observable { + const url = `${environment.apiUrl}/collections/${bookmarksId}/relationships/${this.urlMap.get(resourceType)}/`; + const payload = { data: [{ type: this.resourceMap.get(resourceType), id: resourceId }] }; return this.jsonApiService.post(url, payload); } - removeRegistrationFromBookmarks(bookmarksId: string, registryId: string): Observable { - const url = `${environment.apiUrl}/collections/${bookmarksId}/relationships/linked_registrations/`; - const payload = { - data: [ - { - type: 'registrations', - id: registryId, - }, - ], - }; + removeResourceFromBookmarks(bookmarksId: string, resourceId: string, resourceType: ResourceType): Observable { + const url = `${environment.apiUrl}/collections/${bookmarksId}/relationships/${this.urlMap.get(resourceType)}/`; + const payload = { data: [{ type: this.resourceMap.get(resourceType), id: resourceId }] }; return this.jsonApiService.delete(url, payload); } diff --git a/src/app/shared/stores/bookmarks/bookmarks.model.ts b/src/app/shared/stores/bookmarks/bookmarks.model.ts index fb832bb89..dca9eb9b6 100644 --- a/src/app/shared/stores/bookmarks/bookmarks.model.ts +++ b/src/app/shared/stores/bookmarks/bookmarks.model.ts @@ -3,3 +3,12 @@ import { AsyncStateModel } from '@shared/models/store'; export interface BookmarksStateModel { bookmarksId: AsyncStateModel; } + +export const BOOKMARKS_DEFAULTS: BookmarksStateModel = { + bookmarksId: { + data: '', + isLoading: false, + isSubmitting: false, + error: null, + }, +}; diff --git a/src/app/shared/stores/bookmarks/bookmarks.selectors.ts b/src/app/shared/stores/bookmarks/bookmarks.selectors.ts index d040c90f1..bb3781f10 100644 --- a/src/app/shared/stores/bookmarks/bookmarks.selectors.ts +++ b/src/app/shared/stores/bookmarks/bookmarks.selectors.ts @@ -18,9 +18,4 @@ export class BookmarksSelectors { static getBookmarksCollectionIdSubmitting(state: BookmarksStateModel) { return state.bookmarksId.isSubmitting; } - - @Selector([BookmarksState]) - static getBookmarksError(state: BookmarksStateModel) { - return state.bookmarksId.error; - } } diff --git a/src/app/shared/stores/bookmarks/bookmarks.state.ts b/src/app/shared/stores/bookmarks/bookmarks.state.ts index 25020a87d..90cb96a30 100644 --- a/src/app/shared/stores/bookmarks/bookmarks.state.ts +++ b/src/app/shared/stores/bookmarks/bookmarks.state.ts @@ -1,24 +1,14 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { catchError, EMPTY, tap } from 'rxjs'; +import { catchError, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { ResourceType } from '@shared/enums'; import { handleSectionError } from '@shared/helpers'; import { BookmarksService } from '@shared/services'; import { AddResourceToBookmarks, GetBookmarksCollectionId, RemoveResourceFromBookmarks } from './bookmarks.actions'; -import { BookmarksStateModel } from './bookmarks.model'; - -const BOOKMARKS_DEFAULTS: BookmarksStateModel = { - bookmarksId: { - data: '', - isLoading: false, - isSubmitting: false, - error: null, - }, -}; +import { BOOKMARKS_DEFAULTS, BookmarksStateModel } from './bookmarks.model'; @State({ name: 'bookmarks', @@ -63,34 +53,19 @@ export class BookmarksState { }, }); - switch (action.resourceType) { - case ResourceType.Project: - return this.bookmarksService.addProjectToBookmarks(action.bookmarksId, action.resourceId).pipe( - tap(() => { - ctx.patchState({ - bookmarksId: { - ...state.bookmarksId, - isSubmitting: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'bookmarksId', error)) - ); - case ResourceType.Registration: - return this.bookmarksService.addRegistrationToBookmarks(action.bookmarksId, action.resourceId).pipe( - tap(() => { - ctx.patchState({ - bookmarksId: { - ...state.bookmarksId, - isSubmitting: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'bookmarksId', error)) - ); - default: - return EMPTY; - } + return this.bookmarksService + .addResourceToBookmarks(action.bookmarksId, action.resourceId, action.resourceType) + .pipe( + tap(() => { + ctx.patchState({ + bookmarksId: { + ...state.bookmarksId, + isSubmitting: false, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'bookmarksId', error)) + ); } @Action(RemoveResourceFromBookmarks) @@ -103,33 +78,18 @@ export class BookmarksState { }, }); - switch (action.resourceType) { - case ResourceType.Project: - return this.bookmarksService.removeProjectFromBookmarks(action.bookmarksId, action.resourceId).pipe( - tap(() => { - ctx.patchState({ - bookmarksId: { - ...state.bookmarksId, - isSubmitting: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'bookmarksId', error)) - ); - case ResourceType.Registration: - return this.bookmarksService.removeRegistrationFromBookmarks(action.bookmarksId, action.resourceId).pipe( - tap(() => { - ctx.patchState({ - bookmarksId: { - ...state.bookmarksId, - isSubmitting: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'bookmarksId', error)) - ); - default: - return EMPTY; - } + return this.bookmarksService + .removeResourceFromBookmarks(action.bookmarksId, action.resourceId, action.resourceType) + .pipe( + tap(() => { + ctx.patchState({ + bookmarksId: { + ...state.bookmarksId, + isSubmitting: false, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'bookmarksId', error)) + ); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 6633a8bee..5e5b2cf11 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -650,8 +650,8 @@ "success": "Project has been duplicated successfully" }, "bookmark": { - "add": "Project has been added to bookmarks", - "remove": "Project has been removed from bookmarks" + "add": "Successfully added to bookmarks", + "remove": "Successfully removed from bookmarks" } }, "linkProject": { From 6d76abc7b8d39f64ecdebfaa44a8ea57c6a7e202 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 13:44:45 +0300 Subject: [PATCH 06/12] fix(my-registrations): fixed my registrations --- .../my-registrations.component.html | 9 ++-- .../my-registrations.component.ts | 43 ++++++++++--------- .../registration/registration.mapper.ts | 2 +- .../registration-json-api.model.ts | 2 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/app/features/registries/pages/my-registrations/my-registrations.component.html b/src/app/features/registries/pages/my-registrations/my-registrations.component.html index 999717b41..fc1b1733f 100644 --- a/src/app/features/registries/pages/my-registrations/my-registrations.component.html +++ b/src/app/features/registries/pages/my-registrations/my-registrations.component.html @@ -1,4 +1,4 @@ -
+
@if (isDraftRegistrationsLoading()) { @for (item of skeletons; track $index) { - + } } @else { @if (draftRegistrationsTotalCount() === 0) { @@ -58,7 +58,7 @@ @if (isSubmittedRegistrationsLoading()) { @for (item of skeletons; track $index) { - + } } @else { @if (submittedRegistrationsTotalCount() === 0) { @@ -91,8 +91,7 @@
diff --git a/src/app/features/registries/pages/my-registrations/my-registrations.component.ts b/src/app/features/registries/pages/my-registrations/my-registrations.component.ts index df5120703..8b0dda23d 100644 --- a/src/app/features/registries/pages/my-registrations/my-registrations.component.ts +++ b/src/app/features/registries/pages/my-registrations/my-registrations.component.ts @@ -15,13 +15,17 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { UserSelectors } from '@osf/core/store/user'; -import { CustomPaginatorComponent, SelectComponent, SubHeaderComponent } from '@osf/shared/components'; -import { RegistrationCardComponent } from '@osf/shared/components/registration-card/registration-card.component'; +import { UserSelectors } from '@core/store/user'; +import { + CustomPaginatorComponent, + RegistrationCardComponent, + SelectComponent, + SubHeaderComponent, +} from '@osf/shared/components'; import { IS_XSMALL } from '@osf/shared/helpers'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; -import { REGISTRATIONS_TABS } from '../../constants/registrations-tabs'; +import { REGISTRATIONS_TABS } from '../../constants'; import { RegistrationTab } from '../../enums'; import { CreateSchemaResponse, @@ -60,19 +64,19 @@ export class MyRegistrationsComponent { private readonly customConfirmationService = inject(CustomConfirmationService); private readonly toastService = inject(ToastService); - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - protected readonly tabOptions = REGISTRATIONS_TABS; + readonly isMobile = toSignal(inject(IS_XSMALL)); + readonly tabOptions = REGISTRATIONS_TABS; private currentUser = select(UserSelectors.getCurrentUser); - protected draftRegistrations = select(RegistriesSelectors.getDraftRegistrations); - protected draftRegistrationsTotalCount = select(RegistriesSelectors.getDraftRegistrationsTotalCount); - protected isDraftRegistrationsLoading = select(RegistriesSelectors.isDraftRegistrationsLoading); - protected submittedRegistrations = select(RegistriesSelectors.getSubmittedRegistrations); - protected submittedRegistrationsTotalCount = select(RegistriesSelectors.getSubmittedRegistrationsTotalCount); - protected isSubmittedRegistrationsLoading = select(RegistriesSelectors.isSubmittedRegistrationsLoading); - protected schemaResponse = select(RegistriesSelectors.getSchemaResponse); - - protected actions = createDispatchMap({ + draftRegistrations = select(RegistriesSelectors.getDraftRegistrations); + draftRegistrationsTotalCount = select(RegistriesSelectors.getDraftRegistrationsTotalCount); + isDraftRegistrationsLoading = select(RegistriesSelectors.isDraftRegistrationsLoading); + submittedRegistrations = select(RegistriesSelectors.getSubmittedRegistrations); + submittedRegistrationsTotalCount = select(RegistriesSelectors.getSubmittedRegistrationsTotalCount); + isSubmittedRegistrationsLoading = select(RegistriesSelectors.isSubmittedRegistrationsLoading); + schemaResponse = select(RegistriesSelectors.getSchemaResponse); + + actions = createDispatchMap({ getDraftRegistrations: FetchDraftRegistrations, getSubmittedRegistrations: FetchSubmittedRegistrations, deleteDraft: DeleteDraft, @@ -80,7 +84,7 @@ export class MyRegistrationsComponent { createSchemaResponse: CreateSchemaResponse, }); - protected readonly RegistrationTab = RegistrationTab; + readonly RegistrationTab = RegistrationTab; readonly provider = environment.defaultProvider; @@ -108,6 +112,7 @@ export class MyRegistrationsComponent { this.submittedFirst = 0; this.actions.getSubmittedRegistrations(this.currentUser()?.id); } + this.router.navigate([], { relativeTo: this.route, queryParams: { tab: tab === RegistrationTab.Drafts ? 'drafts' : 'submitted' }, @@ -148,11 +153,7 @@ export class MyRegistrationsComponent { onUpdateRegistration(id: string): void { this.actions .createSchemaResponse(id) - .pipe( - tap(() => { - this.navigateToJustificationPage(); - }) - ) + .pipe(tap(() => this.navigateToJustificationPage())) .subscribe(); } diff --git a/src/app/shared/mappers/registration/registration.mapper.ts b/src/app/shared/mappers/registration/registration.mapper.ts index d9ca3570f..6ef5830ce 100644 --- a/src/app/shared/mappers/registration/registration.mapper.ts +++ b/src/app/shared/mappers/registration/registration.mapper.ts @@ -80,7 +80,7 @@ export class RegistrationMapper { title: registration.attributes.title, description: registration.attributes.description || '', status: MapRegistryStatus(registration.attributes), - dateCreated: registration.attributes.datetime_initiated, + dateCreated: registration.attributes.date_created, dateModified: registration.attributes.date_modified, registrationTemplate: registration.embeds?.registration_schema?.data?.attributes?.name || '', registry: registration.embeds?.provider?.data?.attributes?.name || '', diff --git a/src/app/shared/models/registration/registration-json-api.model.ts b/src/app/shared/models/registration/registration-json-api.model.ts index 873388f81..370c1e0aa 100644 --- a/src/app/shared/models/registration/registration-json-api.model.ts +++ b/src/app/shared/models/registration/registration-json-api.model.ts @@ -44,7 +44,7 @@ export interface DraftRegistrationAttributesJsonApi { export interface RegistrationAttributesJsonApi { access_requests_enabled: boolean; - datetime_initiated: string; + date_created: string; date_modified: string; description: string; embargoed: boolean; From fc5c30352a3fc5f415658f5d6dc415405e7d0efb Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 16:01:28 +0300 Subject: [PATCH 07/12] fix(developer-apps): fixed developer apps --- ...developer-app-add-edit-form.component.html | 2 +- .../developer-app-add-edit-form.component.ts | 28 ++-- .../developer-app-details.component.html | 144 +++++++++--------- .../developer-app-details.component.scss | 4 + .../developer-app-details.component.ts | 30 ++-- .../developer-apps-list.component.ts | 10 +- .../services/developer-apps.service.ts | 12 +- .../store/developer-apps.state-model.ts | 6 + .../store/developer-apps.state.ts | 10 +- src/assets/i18n/en.json | 6 +- 10 files changed, 137 insertions(+), 115 deletions(-) diff --git a/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.html b/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.html index eb77e35d6..b4652f129 100644 --- a/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.html +++ b/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.html @@ -48,7 +48,7 @@ class="w-12rem btn-full-width" [label]="'settings.developerApps.form.buttons.cancel' | translate" severity="info" - (click)="dialogRef.close()" + (onClick)="dialogRef.close()" /> { - return this.isLoading() ? this.appForm.disable() : this.appForm.enable(); - }); + effect(() => (this.isLoading() ? this.appForm.disable() : this.appForm.enable())); } ngOnInit(): void { @@ -91,21 +89,21 @@ export class DeveloperAppAddEditFormComponent implements OnInit { return; } - if (!this.isEditMode()) { - this.actions.createDeveloperApp({ ...this.appForm.value } as DeveloperAppCreateUpdate).subscribe({ - next: () => this.toastService.showSuccess('settings.developerApps.form.createSuccess'), - complete: () => this.dialogRef.close(), - }); - } else { + if (this.isEditMode()) { this.actions .updateDeveloperApp(this.initialValues()!.clientId, { ...this.appForm.value, id: this.initialValues()!.id, } as DeveloperAppCreateUpdate) .subscribe({ - next: () => this.toastService.showSuccess('settings.developerApps.form.createSuccess'), + next: () => this.toastService.showSuccess('settings.developerApps.form.updateSuccess'), complete: () => this.router.navigate(['settings/developer-apps']), }); + } else { + this.actions.createDeveloperApp({ ...this.appForm.value } as DeveloperAppCreateUpdate).subscribe({ + next: () => this.toastService.showSuccess('settings.developerApps.form.createSuccess'), + complete: () => this.dialogRef.close(), + }); } } } diff --git a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html index c74d99e72..a65cd6d94 100644 --- a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html +++ b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html @@ -1,4 +1,4 @@ -
+
- @if (developerApp()) { -
-

{{ developerApp()?.name }}

+ @if (isLoading()) { + + } @else { + @if (developerApp()) { +
+

{{ developerApp()?.name }}

- -
+ +
-
- -
-

- {{ 'settings.developerApps.details.clientId.title' | translate }} -

+
+ +
+

+ {{ 'settings.developerApps.details.clientId.title' | translate }} +

-

- {{ 'settings.developerApps.details.clientId.description' | translate }} -

+

+ {{ 'settings.developerApps.details.clientId.description' | translate }} +

-
- - - - - - -
-
-
- - -
-

- {{ 'settings.developerApps.details.clientSecret.title' | translate }} -

- -

- {{ 'settings.developerApps.details.clientSecret.description' | translate }} -

- -
-
+
- + - +
- -
+ + + +
+

+ {{ 'settings.developerApps.details.clientSecret.title' | translate }} +

-
- +

+ {{ 'settings.developerApps.details.clientSecret.description' | translate }} +

+ +
+
+ + + + + + +
+ + +
+ +
+ +
-
-
+ - -
-

{{ 'settings.developerApps.form.editTitle' | translate }}

- -
-
-
+ +
+

{{ 'settings.developerApps.form.editTitle' | translate }}

+ +
+
+
+ } }
diff --git a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.scss b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.scss index 507eb8f27..4c8ed8c3a 100644 --- a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.scss +++ b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.scss @@ -1,3 +1,7 @@ +:host { + flex: 1; +} + .p-iconfield { --p-icon-size: 1.5rem; } diff --git a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.ts b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.ts index ef106ce8a..5b39b234a 100644 --- a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.ts +++ b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.ts @@ -1,4 +1,4 @@ -import { createDispatchMap, Store } from '@ngxs/store'; +import { createDispatchMap, select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -15,9 +15,8 @@ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@a import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { CopyButtonComponent, IconComponent } from '@osf/shared/components'; -import { IS_XSMALL } from '@osf/shared/helpers'; -import { CustomConfirmationService } from '@osf/shared/services'; +import { CopyButtonComponent, IconComponent, LoadingSpinnerComponent } from '@osf/shared/components'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { DeveloperAppAddEditFormComponent } from '../../components'; import { DeleteDeveloperApp, DeveloperAppsSelectors, GetDeveloperAppDetails, ResetClientSecret } from '../../store'; @@ -35,6 +34,7 @@ import { DeleteDeveloperApp, DeveloperAppsSelectors, GetDeveloperAppDetails, Res TranslatePipe, CopyButtonComponent, IconComponent, + LoadingSpinnerComponent, ], templateUrl: './developer-app-details.component.html', styleUrl: './developer-app-details.component.scss', @@ -46,17 +46,17 @@ export class DeveloperAppDetailsComponent { private readonly activatedRoute = inject(ActivatedRoute); private readonly router = inject(Router); private readonly store = inject(Store); + private readonly toastService = inject(ToastService); private readonly actions = createDispatchMap({ getDeveloperAppDetails: GetDeveloperAppDetails, deleteDeveloperApp: DeleteDeveloperApp, resetClientSecret: ResetClientSecret, }); - protected readonly isXSmall = toSignal(inject(IS_XSMALL)); - - protected readonly isClientSecretVisible = signal(false); - protected readonly clientSecret = computed(() => this.developerApp()?.clientSecret ?? ''); - protected readonly hiddenClientSecret = computed(() => '*'.repeat(this.clientSecret().length)); + readonly isClientSecretVisible = signal(false); + readonly clientSecret = computed(() => this.developerApp()?.clientSecret ?? ''); + readonly hiddenClientSecret = computed(() => '*'.repeat(this.clientSecret().length)); + readonly isLoading = select(DeveloperAppsSelectors.isLoading); readonly clientId = toSignal( this.activatedRoute.params.pipe( @@ -86,10 +86,9 @@ export class DeveloperAppDetailsComponent { headerParams: { name: this.developerApp()?.name }, messageKey: 'settings.developerApps.confirmation.delete.message', onConfirm: () => { - this.actions.deleteDeveloperApp(this.clientId()).subscribe({ - complete: () => { - this.router.navigate(['settings/developer-apps']); - }, + this.actions.deleteDeveloperApp(this.clientId()).subscribe(() => { + this.router.navigate(['settings/developer-apps']); + this.toastService.showSuccess('settings.developerApps.confirmation.delete.success'); }); }, }); @@ -101,7 +100,10 @@ export class DeveloperAppDetailsComponent { headerParams: { name: this.developerApp()?.name }, messageKey: 'settings.developerApps.confirmation.resetSecret.message', acceptLabelKey: 'settings.developerApps.details.clientSecret.reset', - onConfirm: () => this.actions.resetClientSecret(this.clientId()), + onConfirm: () => + this.actions + .resetClientSecret(this.clientId()) + .subscribe(() => this.toastService.showSuccess('settings.developerApps.confirmation.resetSecret.success')), }); } } diff --git a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.ts b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.ts index 7389509eb..11fa6a47c 100644 --- a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.ts +++ b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.ts @@ -9,7 +9,7 @@ import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { CustomConfirmationService } from '@osf/shared/services'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { DeveloperApp } from '../../models'; import { DeleteDeveloperApp, DeveloperAppsSelectors, GetDeveloperApps } from '../../store'; @@ -27,8 +27,9 @@ export class DeveloperAppsListComponent implements OnInit { deleteDeveloperApp: DeleteDeveloperApp, }); private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly toastService = inject(ToastService); - protected readonly isLoading = select(DeveloperAppsSelectors.isLoading); + readonly isLoading = select(DeveloperAppsSelectors.isLoading); readonly developerApplications = select(DeveloperAppsSelectors.getDeveloperApps); ngOnInit(): void { @@ -42,7 +43,10 @@ export class DeveloperAppsListComponent implements OnInit { headerKey: 'settings.developerApps.confirmation.delete.title', headerParams: { name: developerApp.name }, messageKey: 'settings.developerApps.confirmation.delete.message', - onConfirm: () => this.actions.deleteDeveloperApp(developerApp.clientId), + onConfirm: () => + this.actions + .deleteDeveloperApp(developerApp.clientId) + .subscribe(() => this.toastService.showSuccess('settings.developerApps.confirmation.delete.success')), }); } } diff --git a/src/app/features/settings/developer-apps/services/developer-apps.service.ts b/src/app/features/settings/developer-apps/services/developer-apps.service.ts index 23a02ec70..200e3a8fa 100644 --- a/src/app/features/settings/developer-apps/services/developer-apps.service.ts +++ b/src/app/features/settings/developer-apps/services/developer-apps.service.ts @@ -14,8 +14,8 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class DeveloperApplicationsService { - jsonApiService = inject(JsonApiService); - baseUrl = `${environment.apiUrl}/applications/`; + private readonly jsonApiService = inject(JsonApiService); + private readonly baseUrl = `${environment.apiUrl}/applications/`; getApplications(): Observable { return this.jsonApiService @@ -25,7 +25,7 @@ export class DeveloperApplicationsService { getApplicationDetails(clientId: string): Observable { return this.jsonApiService - .get>(`${this.baseUrl}/${clientId}/`) + .get>(`${this.baseUrl}${clientId}/`) .pipe(map((response) => DeveloperAppMapper.fromGetResponse(response.data))); } @@ -41,7 +41,7 @@ export class DeveloperApplicationsService { const request = DeveloperAppMapper.toUpdateRequest(developerAppUpdate); return this.jsonApiService - .patch(`${this.baseUrl}/${clientId}/`, request) + .patch(`${this.baseUrl}${clientId}/`, request) .pipe(map((response) => DeveloperAppMapper.fromGetResponse(response))); } @@ -49,11 +49,11 @@ export class DeveloperApplicationsService { const request = DeveloperAppMapper.toResetSecretRequest(clientId); return this.jsonApiService - .patch(`${this.baseUrl}/${clientId}/`, request) + .patch(`${this.baseUrl}${clientId}/`, request) .pipe(map((response) => DeveloperAppMapper.fromGetResponse(response))); } deleteApplication(clientId: string): Observable { - return this.jsonApiService.delete(`${this.baseUrl}/${clientId}/`); + return this.jsonApiService.delete(`${this.baseUrl}${clientId}/`); } } diff --git a/src/app/features/settings/developer-apps/store/developer-apps.state-model.ts b/src/app/features/settings/developer-apps/store/developer-apps.state-model.ts index 1e8fa57b7..f4c70b6d7 100644 --- a/src/app/features/settings/developer-apps/store/developer-apps.state-model.ts +++ b/src/app/features/settings/developer-apps/store/developer-apps.state-model.ts @@ -3,3 +3,9 @@ import { AsyncStateModel } from '@osf/shared/models'; import { DeveloperApp } from '../models'; export type DeveloperAppsStateModel = AsyncStateModel; + +export const DEVELOPER_APPS_STATE_DEFAULTS: DeveloperAppsStateModel = { + data: [], + isLoading: false, + error: null, +}; diff --git a/src/app/features/settings/developer-apps/store/developer-apps.state.ts b/src/app/features/settings/developer-apps/store/developer-apps.state.ts index 52193733e..c83afbe13 100644 --- a/src/app/features/settings/developer-apps/store/developer-apps.state.ts +++ b/src/app/features/settings/developer-apps/store/developer-apps.state.ts @@ -16,15 +16,11 @@ import { ResetClientSecret, UpdateDeveloperApp, } from './developer-apps.actions'; -import { DeveloperAppsStateModel } from './developer-apps.state-model'; +import { DEVELOPER_APPS_STATE_DEFAULTS, DeveloperAppsStateModel } from './developer-apps.state-model'; @State({ name: 'developerApps', - defaults: { - data: [], - isLoading: false, - error: null, - }, + defaults: DEVELOPER_APPS_STATE_DEFAULTS, }) @Injectable() export class DeveloperAppsState { @@ -52,6 +48,8 @@ export class DeveloperAppsState { return of(developerAppFromState); } + ctx.patchState({ ...state.data, isLoading: true, error: null }); + return this.developerAppsService.getApplicationDetails(action.clientId).pipe( tap((fetchedApp) => { ctx.setState( diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 5e5b2cf11..ed3ac79de 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1349,11 +1349,13 @@ "confirmation": { "delete": { "title": "Delete App {{name}}?", - "message": "Are you sure you want to delete this developer app? All users' access tokens will be revoked. This cannot be reversed." + "message": "Are you sure you want to delete this developer app? All users' access tokens will be revoked. This cannot be reversed.", + "success": "Delete App successfully deleted." }, "resetSecret": { "title": "Reset Client Secret?", - "message": "Resetting the client secret will render your application unusable until it is updated with the new client secret, and all users must reauthorize access. Previously issued access tokens will no longer work.

Are you sure you want to reset the client secret? This cannot be reversed." + "message": "Resetting the client secret will render your application unusable until it is updated with the new client secret, and all users must reauthorize access. Previously issued access tokens will no longer work.

Are you sure you want to reset the client secret? This cannot be reversed.", + "success": "Client secret successfully reset." } } }, From 2bf55dd9ebfaee6928d9e4fc963da42d7a62d11a Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 16:02:17 +0300 Subject: [PATCH 08/12] fix(settings): updated tokens and notifications --- .../notifications.component.html | 2 +- .../notifications/notifications.component.ts | 17 ++++++------- .../token-add-edit-form.component.html | 2 +- .../token-details.component.html | 2 +- .../settings/tokens/store/tokens.selectors.ts | 5 ---- .../settings/tokens/tokens.component.spec.ts | 24 +++++++++++++++++++ .../shared/services/my-resources.service.ts | 1 - 7 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 src/app/features/settings/tokens/tokens.component.spec.ts diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index 0da3bff0e..cc51848d8 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -103,7 +103,7 @@

{{ 'settings.notifications.notificationPreferences.title' | translate }}

- @for (_ of [1, 2, 3, 4, 5]; track $index) { + @for (_ of [1, 2, 3]; track $index) { } diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index aa0f58c16..d69669bcd 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -68,12 +68,13 @@ export class NotificationsComponent implements OnInit { [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.fb.control(false, { nonNullable: true }), }); - protected readonly SUBSCRIPTION_EVENTS = SUBSCRIPTION_EVENTS; - protected subscriptionFrequencyOptions = Object.entries(SubscriptionFrequency).map(([key, value]) => ({ + readonly SUBSCRIPTION_EVENTS = SUBSCRIPTION_EVENTS; + subscriptionFrequencyOptions = Object.entries(SubscriptionFrequency).map(([key, value]) => ({ label: key, value, })); - protected notificationSubscriptionsForm = this.fb.group( + + notificationSubscriptionsForm = this.fb.group( SUBSCRIPTION_EVENTS.reduce( (control, { event }) => { control[event] = this.fb.control(SubscriptionFrequency.Never, { nonNullable: true }); @@ -127,11 +128,9 @@ export class NotificationsComponent implements OnInit { const id = `${user.id}_${event}`; this.loaderService.show(); - this.actions.updateNotificationSubscription({ id, frequency }).subscribe({ - complete: () => { - this.loaderService.hide(); - this.toastService.showSuccess('settings.notifications.notificationPreferences.successUpdate'); - }, + this.actions.updateNotificationSubscription({ id, frequency }).subscribe(() => { + this.loaderService.hide(); + this.toastService.showSuccess('settings.notifications.notificationPreferences.successUpdate'); }); } @@ -144,9 +143,11 @@ export class NotificationsComponent implements OnInit { private updateNotificationSubscriptionsForm() { const patch: Record = {}; + for (const sub of this.notificationSubscriptions()) { patch[sub.event] = sub.frequency; } + this.notificationSubscriptionsForm.patchValue(patch); } } diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html index 58e74fb85..df184c214 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.html @@ -49,7 +49,7 @@ class="w-12rem btn-full-width" [label]="'settings.tokens.form.buttons.cancel' | translate" severity="info" - (click)="dialogRef.close()" + (onClick)="dialogRef.close()" /> {{ token()?.name }} class="btn-full-width" [label]="'settings.tokens.details.deleteButton' | translate" severity="danger" - (click)="deleteToken()" + (onClick)="deleteToken()" />
diff --git a/src/app/features/settings/tokens/store/tokens.selectors.ts b/src/app/features/settings/tokens/store/tokens.selectors.ts index 5faf7c033..899161344 100644 --- a/src/app/features/settings/tokens/store/tokens.selectors.ts +++ b/src/app/features/settings/tokens/store/tokens.selectors.ts @@ -11,11 +11,6 @@ export class TokensSelectors { return state.scopes.data; } - @Selector([TokensState]) - static isScopesLoading(state: TokensStateModel) { - return state.scopes.isLoading; - } - @Selector([TokensState]) static getTokens(state: TokensStateModel): TokenModel[] { return state.tokens.data; diff --git a/src/app/features/settings/tokens/tokens.component.spec.ts b/src/app/features/settings/tokens/tokens.component.spec.ts new file mode 100644 index 000000000..de8bf16cc --- /dev/null +++ b/src/app/features/settings/tokens/tokens.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TokensComponent } from './tokens.component'; + +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe.skip('TokensComponent', () => { + let component: TokensComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TokensComponent, OSFTestingModule], + }).compileComponents(); + + fixture = TestBed.createComponent(TokensComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/services/my-resources.service.ts b/src/app/shared/services/my-resources.service.ts index d7cb7b931..a8bd35b3f 100644 --- a/src/app/shared/services/my-resources.service.ts +++ b/src/app/shared/services/my-resources.service.ts @@ -23,7 +23,6 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class MyResourcesService { - private apiUrl = environment.apiUrl; private sortFieldMap: Record = { title: 'title', dateModified: 'date_modified', From 78a26e2c04ea2be6d5cedd57efea457c82481fc6 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 17:15:37 +0300 Subject: [PATCH 09/12] fix(translation): removed dot --- src/assets/i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ed3ac79de..175d86bcd 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -972,7 +972,7 @@ "selectFilesFoldersDescription": "Click on a row (outside of the file or folder name) to show further actions in the top toolbar. Click on more rows to select multiple files. To select a range of files, click a file to begin selection and then shift+click another file to select the range.", "openViewFiles": "Open/View Files", "openViewFilesDescription": "Click a file name to go to view the file in the OSF. Opens file in a new tab.", - "upload": "Upload.", + "upload": "Upload", "uploadDescription": "There are two ways to upload files. Open the storage provider or folder where you intend to upload files; you can then drag files from your desktop into files list area OR click “Upload Files”, and select your files. Note: File names with special characters may cause unexpected behavior with certain addons.", "createFolder": "Create a folder", "createFolderDescription": "To create a folder to help organize your files, click “Create a Folder”. Note: You can also organize your project content by creating Components.", From 4112bea6350f65e863443de1d19d83d93dba4dc4 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 17:27:58 +0300 Subject: [PATCH 10/12] fix(info-icon): updated info icon translate --- .../project-contributors-step.component.html | 2 +- .../components/file-revisions/file-revisions.component.html | 4 ++-- .../settings/notifications/notifications.component.html | 2 +- src/app/shared/components/info-icon/info-icon.component.html | 2 +- .../shared/components/info-icon/info-icon.component.spec.ts | 5 ++++- src/app/shared/components/info-icon/info-icon.component.ts | 4 +++- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html index f093255b4..3bb5e8140 100644 --- a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html +++ b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.html @@ -10,7 +10,7 @@

{{ 'collections.addToCollection.projectContributors' | translate }}

diff --git a/src/app/features/files/components/file-revisions/file-revisions.component.html b/src/app/features/files/components/file-revisions/file-revisions.component.html index 2c7e02e54..b67282be0 100644 --- a/src/app/features/files/components/file-revisions/file-revisions.component.html +++ b/src/app/features/files/components/file-revisions/file-revisions.component.html @@ -23,7 +23,7 @@

{{ 'files.detail.revisions.title' | translate }}

> @@ -36,7 +36,7 @@

{{ 'files.detail.revisions.title' | translate }}

> diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index cc51848d8..c24010149 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -76,7 +76,7 @@

{{ 'settings.notifications.emailPreferences.title' | translate }}

{{ 'settings.notifications.notificationPreferences.title' | translate }}

diff --git a/src/app/shared/components/info-icon/info-icon.component.html b/src/app/shared/components/info-icon/info-icon.component.html index 698078345..991deeb72 100644 --- a/src/app/shared/components/info-icon/info-icon.component.html +++ b/src/app/shared/components/info-icon/info-icon.component.html @@ -1,5 +1,5 @@ diff --git a/src/app/shared/components/info-icon/info-icon.component.spec.ts b/src/app/shared/components/info-icon/info-icon.component.spec.ts index 120a5b814..e82e56d95 100644 --- a/src/app/shared/components/info-icon/info-icon.component.spec.ts +++ b/src/app/shared/components/info-icon/info-icon.component.spec.ts @@ -1,3 +1,6 @@ +import { TranslatePipe } from '@ngx-translate/core'; +import { MockPipe } from 'ng-mocks'; + import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -13,7 +16,7 @@ describe('InfoIconComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [InfoIconComponent], + imports: [InfoIconComponent, MockPipe(TranslatePipe)], }).compileComponents(); fixture = TestBed.createComponent(InfoIconComponent); diff --git a/src/app/shared/components/info-icon/info-icon.component.ts b/src/app/shared/components/info-icon/info-icon.component.ts index c663489cb..2db436fd7 100644 --- a/src/app/shared/components/info-icon/info-icon.component.ts +++ b/src/app/shared/components/info-icon/info-icon.component.ts @@ -1,3 +1,5 @@ +import { TranslatePipe } from '@ngx-translate/core'; + import { Tooltip } from 'primeng/tooltip'; import { ChangeDetectionStrategy, Component, input } from '@angular/core'; @@ -6,7 +8,7 @@ import { TooltipPosition } from '@osf/shared/models'; @Component({ selector: 'osf-info-icon', - imports: [Tooltip], + imports: [Tooltip, TranslatePipe], templateUrl: './info-icon.component.html', styleUrl: './info-icon.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, From 72d5703d970d3084b9a5b0d50e7801dcf2ab4c63 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 18:58:07 +0300 Subject: [PATCH 11/12] fix(profile-settings): fixed profile settings --- src/app/core/store/user/user.actions.ts | 8 ++-- src/app/core/store/user/user.state.ts | 8 ++-- .../education-form.component.html | 16 ++++++- .../education-form.component.ts | 34 ++++++++++---- .../education/education.component.spec.ts | 46 +++++++++---------- .../education/education.component.ts | 15 +++--- .../employment-form.component.html | 28 ++++++++--- .../employment-form.component.ts | 41 +++++++++++++---- .../employment/employment.component.spec.ts | 28 ++++++----- .../employment/employment.component.ts | 15 +++--- .../components/name/name.component.spec.ts | 6 +-- .../components/name/name.component.ts | 12 ++--- .../components/social/social.component.ts | 2 +- .../helpers/education-comparison.helper.ts | 2 +- .../helpers/employment-comparison.helper.ts | 2 +- .../profile-settings.component.ts | 8 ++-- .../helpers/custom-form-validators.helper.ts | 7 ++- 17 files changed, 171 insertions(+), 107 deletions(-) diff --git a/src/app/core/store/user/user.actions.ts b/src/app/core/store/user/user.actions.ts index 591b586bb..716f9c09f 100644 --- a/src/app/core/store/user/user.actions.ts +++ b/src/app/core/store/user/user.actions.ts @@ -24,25 +24,25 @@ export class UpdateUserSettings { export class UpdateProfileSettingsEmployment { static readonly type = '[Profile Settings] Update Employment'; - constructor(public payload: { employment: Employment[] }) {} + constructor(public payload: Employment[]) {} } export class UpdateProfileSettingsEducation { static readonly type = '[Profile Settings] Update Education'; - constructor(public payload: { education: Education[] }) {} + constructor(public payload: Education[]) {} } export class UpdateProfileSettingsSocialLinks { static readonly type = '[Profile Settings] Update Social Links'; - constructor(public payload: { socialLinks: Partial[] }) {} + constructor(public payload: Partial[]) {} } export class UpdateProfileSettingsUser { static readonly type = '[Profile Settings] Update User'; - constructor(public payload: { user: Partial }) {} + constructor(public payload: Partial) {} } export class SetUserAsModerator { diff --git a/src/app/core/store/user/user.state.ts b/src/app/core/store/user/user.state.ts index 8b23a6aea..70d30969d 100644 --- a/src/app/core/store/user/user.state.ts +++ b/src/app/core/store/user/user.state.ts @@ -135,7 +135,7 @@ export class UserState { return; } - const withoutNulls = payload.employment.map((item) => removeNullable(item)); + const withoutNulls = payload.map((item) => removeNullable(item)); return this.userService.updateUserProfile(userId, ProfileSettingsKey.Employment, withoutNulls).pipe( tap((user) => { @@ -160,7 +160,7 @@ export class UserState { return; } - const withoutNulls = payload.education.map((item) => removeNullable(item)); + const withoutNulls = payload.map((item) => removeNullable(item)); return this.userService.updateUserProfile(userId, ProfileSettingsKey.Education, withoutNulls).pipe( tap((user) => { @@ -185,7 +185,7 @@ export class UserState { return; } - const withoutNulls = UserMapper.toNamesRequest(removeNullable(payload.user)); + const withoutNulls = UserMapper.toNamesRequest(removeNullable(payload)); return this.userService.updateUserProfile(userId, ProfileSettingsKey.User, withoutNulls).pipe( tap((user) => { @@ -212,7 +212,7 @@ export class UserState { let social = {} as Partial; - payload.socialLinks.forEach((item) => { + payload.forEach((item) => { social = { ...social, ...item, diff --git a/src/app/features/settings/profile-settings/components/education-form/education-form.component.html b/src/app/features/settings/profile-settings/components/education-form/education-form.component.html index 5fe9ca914..d839c628f 100644 --- a/src/app/features/settings/profile-settings/components/education-form/education-form.component.html +++ b/src/app/features/settings/profile-settings/components/education-form/education-form.component.html @@ -52,9 +52,16 @@

[dateFormat]="dateFormat" [iconDisplay]="'input'" [showIcon]="true" + view="month" [maxDate]="maxDate" [minDate]="minDate" > + + @if (startDateRequiredError) { + + {{ 'validation.required' | translate }} + + }

@if (!group().controls['ongoing'].value) { @@ -68,10 +75,17 @@

formControlName="endDate" [dateFormat]="dateFormat" [iconDisplay]="'input'" + view="month" [showIcon]="true" [maxDate]="maxDate" [minDate]="minDate" > + + @if (endDateRequiredError) { + + {{ 'validation.required' | translate }} + + }

}
@@ -91,7 +105,7 @@

@if (isDateError) { - + {{ 'settings.profileSettings.endDateError' | translate }} } diff --git a/src/app/features/settings/profile-settings/components/education-form/education-form.component.ts b/src/app/features/settings/profile-settings/components/education-form/education-form.component.ts index 0bfeab450..86a4e0762 100644 --- a/src/app/features/settings/profile-settings/components/education-form/education-form.component.ts +++ b/src/app/features/settings/profile-settings/components/education-form/education-form.component.ts @@ -6,11 +6,9 @@ import { DatePicker } from 'primeng/datepicker'; import { InputText } from 'primeng/inputtext'; import { Message } from 'primeng/message'; -import { filter } from 'rxjs'; - import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, OnInit, output } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { TextInputComponent } from '@osf/shared/components'; import { InputLimits } from '@osf/shared/constants'; @@ -28,7 +26,7 @@ export class EducationFormComponent implements OnInit { readonly maxDate = MAX_DATE; readonly minDate = MIN_DATE; readonly institutionMaxLength = InputLimits.fullName.maxLength; - readonly dateFormat = 'mm/dd/yy'; + readonly dateFormat = 'mm/yy'; private readonly destroyRef = inject(DestroyRef); @@ -46,12 +44,30 @@ export class EducationFormComponent implements OnInit { return form.errors && form.errors['dateRangeInvalid']; } + get startDateRequiredError() { + const control = this.group().controls['startDate']; + return control.invalid && control.errors?.['required'] && (control.touched || control.dirty); + } + + get endDateRequiredError() { + const control = this.group().controls['endDate']; + return control.invalid && control.errors?.['required'] && (control.touched || control.dirty); + } + ngOnInit() { this.group() - .controls['ongoing'].valueChanges.pipe( - filter((res) => !!res), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe(() => this.group().controls['endDate'].setValue(null)); + .controls['ongoing'].valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((ongoing) => { + const endDateControl = this.group().controls['endDate']; + + if (ongoing) { + endDateControl.setValue(null); + endDateControl.clearValidators(); + } else { + endDateControl.setValidators([Validators.required]); + } + + endDateControl.updateValueAndValidity(); + }); } } diff --git a/src/app/features/settings/profile-settings/components/education/education.component.spec.ts b/src/app/features/settings/profile-settings/components/education/education.component.spec.ts index 862d4318c..2a2c21ac3 100644 --- a/src/app/features/settings/profile-settings/components/education/education.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/education/education.component.spec.ts @@ -125,30 +125,28 @@ describe('EducationComponent', () => { component.saveEducation(); expect(mockStore.dispatch).toHaveBeenCalledWith( - new UpdateProfileSettingsEducation({ - education: [ - { - institution: 'Test University', - department: 'Engineering', - degree: 'Bachelor', - startYear: 2020, - startMonth: 0, - endYear: 2024, - endMonth: 6, - ongoing: false, - }, - { - institution: 'Advanced University', - department: 'Software Engineering', - degree: 'Master of Science', - startYear: 2020, - startMonth: 8, - endYear: null, - endMonth: null, - ongoing: false, - }, - ], - }) + new UpdateProfileSettingsEducation([ + { + institution: 'Test University', + department: 'Engineering', + degree: 'Bachelor', + startYear: 2020, + startMonth: 0, + endYear: 2024, + endMonth: 6, + ongoing: false, + }, + { + institution: 'Advanced University', + department: 'Software Engineering', + degree: 'Master of Science', + startYear: 2020, + startMonth: 8, + endYear: null, + endMonth: null, + ongoing: false, + }, + ]) ); }); }); diff --git a/src/app/features/settings/profile-settings/components/education/education.component.ts b/src/app/features/settings/profile-settings/components/education/education.component.ts index d8864c649..4ffe3bae2 100644 --- a/src/app/features/settings/profile-settings/components/education/education.component.ts +++ b/src/app/features/settings/profile-settings/components/education/education.component.ts @@ -14,7 +14,7 @@ import { inject, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormArray, FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { UpdateProfileSettingsEducation, UserSelectors } from '@osf/core/store/user'; import { CustomValidators } from '@osf/shared/helpers'; @@ -96,7 +96,7 @@ export class EducationComponent { this.loaderService.show(); this.actions - .updateProfileSettingsEducation({ education: formattedEducation }) + .updateProfileSettingsEducation(formattedEducation) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { @@ -132,16 +132,19 @@ export class EducationComponent { } private createEducationFormGroup(education?: Partial): FormGroup { + const isOngoing = education?.ongoing ?? false; + const endDateValidators = isOngoing ? [] : [Validators.required]; + return this.fb.group( { institution: [education?.institution ?? '', CustomValidators.requiredTrimmed()], department: [education?.department ?? ''], degree: [education?.degree ?? ''], - startDate: [education?.startDate ?? null], - endDate: [education?.endDate ?? null], - ongoing: [education?.ongoing ?? false], + startDate: [education?.startDate ?? null, Validators.required], + endDate: [education?.endDate ?? null, endDateValidators], + ongoing: [isOngoing], }, - { validators: CustomValidators.dateRangeValidator } + { validators: CustomValidators.monthYearRangeValidator } ); } diff --git a/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.html b/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.html index 59d623cc7..b6c8b7394 100644 --- a/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.html +++ b/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.html @@ -9,7 +9,7 @@

[label]="'common.buttons.remove' | translate" severity="danger" variant="text" - (click)="remove.emit()" + (onClick)="remove.emit()" /> } @@ -33,11 +33,11 @@

- - - +
@@ -53,10 +53,17 @@

formControlName="startDate" [dateFormat]="dateFormat" [iconDisplay]="'input'" + view="month" [showIcon]="true" [maxDate]="maxDate" [minDate]="minDate" > + + @if (startDateRequiredError) { + + {{ 'validation.required' | translate }} + + } @if (!group().controls['ongoing'].value) { @@ -70,10 +77,17 @@

formControlName="endDate" [dateFormat]="dateFormat" [iconDisplay]="'input'" + view="month" [showIcon]="true" [maxDate]="maxDate" [minDate]="minDate" > + + @if (endDateRequiredError) { + + {{ 'validation.required' | translate }} + + } } @@ -93,7 +107,7 @@

@if (isDateError) { - + {{ 'settings.profileSettings.endDateError' | translate }} } diff --git a/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.ts b/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.ts index da5d0bbe9..d441bcba9 100644 --- a/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.ts +++ b/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.ts @@ -6,16 +6,15 @@ import { DatePicker } from 'primeng/datepicker'; import { InputText } from 'primeng/inputtext'; import { Message } from 'primeng/message'; -import { filter } from 'rxjs'; - import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, OnInit, output } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { MAX_DATE, MIN_DATE } from '@osf/features/settings/profile-settings/constants'; import { TextInputComponent } from '@osf/shared/components'; import { InputLimits } from '@osf/shared/constants'; +import { MAX_DATE, MIN_DATE } from '../../constants'; + @Component({ selector: 'osf-employment-form', imports: [ReactiveFormsModule, Button, InputText, DatePicker, Checkbox, Message, TranslatePipe, TextInputComponent], @@ -27,7 +26,7 @@ export class EmploymentFormComponent implements OnInit { readonly maxDate = MAX_DATE; readonly minDate = MIN_DATE; readonly institutionMaxLength = InputLimits.fullName.maxLength; - readonly dateFormat = 'mm/dd/yy'; + readonly dateFormat = 'mm/yy'; private readonly destroyRef = inject(DestroyRef); @@ -40,17 +39,39 @@ export class EmploymentFormComponent implements OnInit { return this.group().controls['title'] as FormControl; } + get institutionControl() { + return this.group().controls['institution'] as FormControl; + } + get isDateError() { const form = this.group(); return form.errors && form.errors['dateRangeInvalid']; } + get startDateRequiredError() { + const control = this.group().controls['startDate']; + return control.invalid && control.errors?.['required'] && (control.touched || control.dirty); + } + + get endDateRequiredError() { + const control = this.group().controls['endDate']; + return control.invalid && control.errors?.['required'] && (control.touched || control.dirty); + } + ngOnInit() { this.group() - .controls['ongoing'].valueChanges.pipe( - filter((res) => !!res), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe(() => this.group().controls['endDate'].setValue(null)); + .controls['ongoing'].valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((ongoing) => { + const endDateControl = this.group().controls['endDate']; + + if (ongoing) { + endDateControl.setValue(null); + endDateControl.clearValidators(); + } else { + endDateControl.setValidators([Validators.required]); + } + + endDateControl.updateValueAndValidity(); + }); } } diff --git a/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts b/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts index 0ea72671f..b319d0dc6 100644 --- a/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts @@ -124,21 +124,19 @@ describe('EmploymentComponent', () => { component.saveEmployment(); expect(mockStore.dispatch).toHaveBeenCalledWith( - new UpdateProfileSettingsEmployment({ - employment: [ - { - title: 'Software Engineer Intern', - institution: 'Test University', - department: 'Engineering', - startYear: 2020, - startMonth: 1, - endYear: 2024, - endMonth: 6, - ongoing: false, - }, - expect.any(Object), - ], - }) + new UpdateProfileSettingsEmployment([ + { + title: 'Software Engineer Intern', + institution: 'Test University', + department: 'Engineering', + startYear: 2020, + startMonth: 1, + endYear: 2024, + endMonth: 6, + ongoing: false, + }, + expect.any(Object), + ]) ); }); }); diff --git a/src/app/features/settings/profile-settings/components/employment/employment.component.ts b/src/app/features/settings/profile-settings/components/employment/employment.component.ts index bfb0c7f3d..cd5b7b08e 100644 --- a/src/app/features/settings/profile-settings/components/employment/employment.component.ts +++ b/src/app/features/settings/profile-settings/components/employment/employment.component.ts @@ -14,7 +14,7 @@ import { inject, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormArray, FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { FormArray, FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { UpdateProfileSettingsEmployment, UserSelectors } from '@osf/core/store/user'; import { CustomValidators } from '@osf/shared/helpers'; @@ -97,7 +97,7 @@ export class EmploymentComponent { this.loaderService.show(); this.actions - .updateProfileSettingsEmployment({ employment: formattedEmployment }) + .updateProfileSettingsEmployment(formattedEmployment) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { @@ -137,16 +137,19 @@ export class EmploymentComponent { } private createEmploymentFormGroup(employment?: Partial): FormGroup { + const isOngoing = employment?.ongoing ?? false; + const endDateValidators = isOngoing ? [] : [Validators.required]; + return this.fb.group( { title: [employment?.title ?? '', CustomValidators.requiredTrimmed()], - institution: [employment?.institution ?? ''], + institution: [employment?.institution ?? '', CustomValidators.requiredTrimmed()], department: [employment?.department ?? ''], - startDate: [employment?.startDate ?? null], - endDate: [employment?.endDate ?? null], + startDate: [employment?.startDate ?? null, Validators.required], + endDate: [employment?.endDate ?? null, endDateValidators], ongoing: [employment?.ongoing ?? false], }, - { validators: CustomValidators.dateRangeValidator } + { validators: CustomValidators.monthYearRangeValidator } ); } diff --git a/src/app/features/settings/profile-settings/components/name/name.component.spec.ts b/src/app/features/settings/profile-settings/components/name/name.component.spec.ts index 1bc1eb5b6..778c0eedd 100644 --- a/src/app/features/settings/profile-settings/components/name/name.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/name/name.component.spec.ts @@ -82,11 +82,7 @@ describe('NameComponent', () => { component.saveChanges(); - expect(mockStore.dispatch).toHaveBeenCalledWith( - new UpdateProfileSettingsUser({ - user: formData, - }) - ); + expect(mockStore.dispatch).toHaveBeenCalledWith(new UpdateProfileSettingsUser(formData)); }); it('should reset form to current user data', () => { diff --git a/src/app/features/settings/profile-settings/components/name/name.component.ts b/src/app/features/settings/profile-settings/components/name/name.component.ts index 2206d7b71..5d75a5d01 100644 --- a/src/app/features/settings/profile-settings/components/name/name.component.ts +++ b/src/app/features/settings/profile-settings/components/name/name.component.ts @@ -72,13 +72,11 @@ export class NameComponent { this.loaderService.show(); this.actions .updateProfileSettingsUser({ - user: { - fullName, - givenName, - middleNames, - familyName, - suffix, - }, + fullName, + givenName, + middleNames, + familyName, + suffix, }) .subscribe(() => { this.loaderService.hide(); diff --git a/src/app/features/settings/profile-settings/components/social/social.component.ts b/src/app/features/settings/profile-settings/components/social/social.component.ts index cd023b17b..b98815a4e 100644 --- a/src/app/features/settings/profile-settings/components/social/social.component.ts +++ b/src/app/features/settings/profile-settings/components/social/social.component.ts @@ -84,7 +84,7 @@ export class SocialComponent { this.loaderService.show(); this.actions - .updateProfileSettingsSocialLinks({ socialLinks: mappedLinks }) + .updateProfileSettingsSocialLinks(mappedLinks) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { diff --git a/src/app/features/settings/profile-settings/helpers/education-comparison.helper.ts b/src/app/features/settings/profile-settings/helpers/education-comparison.helper.ts index c87afdfec..cd14eced9 100644 --- a/src/app/features/settings/profile-settings/helpers/education-comparison.helper.ts +++ b/src/app/features/settings/profile-settings/helpers/education-comparison.helper.ts @@ -9,7 +9,7 @@ export function mapFormToEducation(education: EducationForm): Education { department: education.department, degree: education.degree, startYear: education.startDate?.getFullYear() ?? null, - startMonth: education.startDate?.getMonth() ?? null, + startMonth: education.startDate?.getMonth() + 1, endYear: education.ongoing ? null : (education.endDate?.getFullYear() ?? null), endMonth: education.ongoing ? null : education.endDate ? education.endDate.getMonth() + 1 : null, ongoing: education.ongoing, diff --git a/src/app/features/settings/profile-settings/helpers/employment-comparison.helper.ts b/src/app/features/settings/profile-settings/helpers/employment-comparison.helper.ts index 4f0c1cc68..26df69c78 100644 --- a/src/app/features/settings/profile-settings/helpers/employment-comparison.helper.ts +++ b/src/app/features/settings/profile-settings/helpers/employment-comparison.helper.ts @@ -9,7 +9,7 @@ export function mapFormToEmployment(employment: EmploymentForm): Employment { department: employment.department, institution: employment.institution, startYear: employment.startDate?.getFullYear() ?? new Date().getFullYear(), - startMonth: (employment.startDate?.getMonth() ?? 0) + 1, + startMonth: employment.startDate?.getMonth() + 1, endYear: employment.ongoing ? null : (employment.endDate?.getFullYear() ?? null), endMonth: employment.ongoing ? null : employment.endDate ? employment.endDate.getMonth() + 1 : null, ongoing: employment.ongoing, diff --git a/src/app/features/settings/profile-settings/profile-settings.component.ts b/src/app/features/settings/profile-settings/profile-settings.component.ts index 8e8cd3c8e..c61b6c068 100644 --- a/src/app/features/settings/profile-settings/profile-settings.component.ts +++ b/src/app/features/settings/profile-settings/profile-settings.component.ts @@ -36,11 +36,11 @@ import { ProfileSettingsTabOption } from './enums'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProfileSettingsComponent { - protected readonly isMedium = toSignal(inject(IS_MEDIUM)); - protected readonly tabOptions = PROFILE_SETTINGS_TAB_OPTIONS; - protected readonly tabOption = ProfileSettingsTabOption; + readonly isMedium = toSignal(inject(IS_MEDIUM)); + readonly tabOptions = PROFILE_SETTINGS_TAB_OPTIONS; + readonly tabOption = ProfileSettingsTabOption; - protected selectedTab = this.tabOption.Name; + selectedTab = this.tabOption.Name; onTabChange(index: number): void { this.selectedTab = index; diff --git a/src/app/shared/helpers/custom-form-validators.helper.ts b/src/app/shared/helpers/custom-form-validators.helper.ts index 4fd1fc432..1b66206e3 100644 --- a/src/app/shared/helpers/custom-form-validators.helper.ts +++ b/src/app/shared/helpers/custom-form-validators.helper.ts @@ -51,7 +51,7 @@ export class CustomValidators { }; } - static dateRangeValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { + static monthYearRangeValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { const start = control.get('startDate')?.value; const end = control.get('endDate')?.value; @@ -60,7 +60,10 @@ export class CustomValidators { const startDate = new Date(start); const endDate = new Date(end); - return endDate > startDate ? null : { dateRangeInvalid: true }; + const startYearMonth = startDate.getFullYear() * 12 + startDate.getMonth(); + const endYearMonth = endDate.getFullYear() * 12 + endDate.getMonth(); + + return endYearMonth > startYearMonth ? null : { dateRangeInvalid: true }; }; static doiValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { From 72ac75d9531e8398c8a854843b405f5d00a71675 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 28 Aug 2025 19:43:25 +0300 Subject: [PATCH 12/12] fix(test): updated tests --- .../developer-app-details.component.spec.ts | 5 ++--- .../developer-apps-list.component.spec.ts | 3 ++- .../notifications/notifications.component.spec.ts | 8 ++++---- .../components/education/education.component.spec.ts | 8 ++++---- src/app/shared/mocks/education.mock.ts | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts index 51995ad5c..e59555ae9 100644 --- a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts +++ b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts @@ -12,8 +12,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; -import { IS_XSMALL } from '@osf/shared/helpers'; -import { CustomConfirmationService } from '@osf/shared/services'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { DeveloperAppsState } from '../../store'; @@ -42,7 +41,7 @@ describe('DeveloperAppDetailsComponent', () => { MockProvider(ActivatedRoute, { params: of({ id: 'test-client-id' }) }), MockProvider(Router, mockRouter), MockProvider(CustomConfirmationService), - MockProvider(IS_XSMALL, of(false)), + MockProvider(ToastService), ], }).compileComponents(); diff --git a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts index e54c506e5..1babc448f 100644 --- a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts +++ b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts @@ -9,7 +9,7 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CustomConfirmationService } from '@osf/shared/services'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { MOCK_DEVELOPER_APP } from '@shared/mocks/developer-app.mock'; import { DeveloperAppsState } from '../../store'; @@ -31,6 +31,7 @@ describe('DeveloperApplicationsListComponent', () => { MockProvider(ConfirmationService), MockProvider(TranslateService), MockProvider(CustomConfirmationService), + MockProvider(ToastService), ], }).compileComponents(); diff --git a/src/app/features/settings/notifications/notifications.component.spec.ts b/src/app/features/settings/notifications/notifications.component.spec.ts index 3d8c60051..192cb7d70 100644 --- a/src/app/features/settings/notifications/notifications.component.spec.ts +++ b/src/app/features/settings/notifications/notifications.component.spec.ts @@ -14,7 +14,7 @@ import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; import { LoaderService, ToastService } from '@osf/shared/services'; import { SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; -import { MOCK_STORE, MOCK_USER } from '@shared/mocks'; +import { MOCK_STORE, MOCK_USER, TranslateServiceMock } from '@shared/mocks'; import { UserSettings } from '@shared/models'; import { NotificationsComponent } from './notifications.component'; @@ -31,7 +31,7 @@ describe('NotificationsComponent', () => { }; const mockNotificationSubscriptions = [ - { id: 'id1', event: SubscriptionEvent.GlobalComments, frequency: SubscriptionFrequency.Daily }, + { id: 'id1', event: SubscriptionEvent.GlobalMentions, frequency: SubscriptionFrequency.Daily }, { id: 'id2', event: SubscriptionEvent.GlobalMentions, @@ -79,7 +79,7 @@ describe('NotificationsComponent', () => { providers: [ provideHttpClient(), provideHttpClientTesting(), - MockProvider(TranslatePipe), + TranslateServiceMock, MockProvider(Store, MOCK_STORE), MockProvider(LoaderService, mockLoaderService), MockProvider(ToastService, mockToastService), @@ -127,7 +127,7 @@ describe('NotificationsComponent', () => { it('should call dispatch only once per subscription change', () => { const mockDispatch = jest.fn().mockReturnValue(of({})); MOCK_STORE.dispatch.mockImplementation(mockDispatch); - const event = SubscriptionEvent.GlobalComments; + const event = SubscriptionEvent.GlobalMentions; const frequency = SubscriptionFrequency.Daily; component.onSubscriptionChange(event, frequency); diff --git a/src/app/features/settings/profile-settings/components/education/education.component.spec.ts b/src/app/features/settings/profile-settings/components/education/education.component.spec.ts index 2a2c21ac3..cf459628d 100644 --- a/src/app/features/settings/profile-settings/components/education/education.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/education/education.component.spec.ts @@ -131,7 +131,7 @@ describe('EducationComponent', () => { department: 'Engineering', degree: 'Bachelor', startYear: 2020, - startMonth: 0, + startMonth: 1, endYear: 2024, endMonth: 6, ongoing: false, @@ -141,9 +141,9 @@ describe('EducationComponent', () => { department: 'Software Engineering', degree: 'Master of Science', startYear: 2020, - startMonth: 8, - endYear: null, - endMonth: null, + startMonth: 9, + endYear: 2025, + endMonth: 8, ongoing: false, }, ]) diff --git a/src/app/shared/mocks/education.mock.ts b/src/app/shared/mocks/education.mock.ts index 7eca317ca..23400c19c 100644 --- a/src/app/shared/mocks/education.mock.ts +++ b/src/app/shared/mocks/education.mock.ts @@ -17,8 +17,8 @@ export const MOCK_EDUCATION: Education[] = [ degree: 'Master of Science', startMonth: 9, startYear: 2020, - endMonth: null, - endYear: null, + endMonth: 8, + endYear: 2025, ongoing: false, }, ];