From 79eca722007db3e87f4c0a59d1208aa75a8aa382 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 10 Jul 2025 13:04:12 +0300 Subject: [PATCH 1/7] fix(settings): updated settings --- .../account-settings.component.ts | 27 +++--- .../add-email/add-email.component.ts | 11 ++- .../affiliated-institutions.component.scss | 82 ++++++++--------- .../affiliated-institutions.component.ts | 8 +- .../cancel-deactivation.component.ts | 4 +- .../change-password.component.ts | 13 ++- .../connected-identities.component.ts | 10 +-- .../default-storage-location.component.html | 1 - .../default-storage-location.component.ts | 15 ++-- .../share-indexing.component.ts | 12 +-- .../configure-two-factor.component.ts | 9 +- .../verify-two-factor.component.html | 1 + .../verify-two-factor.component.ts | 9 +- .../two-factor-auth.component.ts | 35 ++++---- .../mappers/account-settings.mapper.ts | 4 +- .../account-settings/mappers/emails.mapper.ts | 6 +- .../mappers/external-identities.mapper.ts | 6 +- .../mappers/regions.mapper.ts | 1 + .../get-account-settings-response.model.ts | 4 +- .../responses/get-email-response.model.ts | 4 +- .../responses/get-regions-response.model.ts | 4 +- .../models/responses/list-emails.model.ts | 4 +- .../list-identities-response.model.ts | 7 +- .../services/account-settings.service.ts | 87 +++++++++---------- .../store/account-settings.state.ts | 51 +++++------ .../settings/addons/addons.component.html | 7 +- .../settings/addons/addons.component.scss | 7 +- .../settings/addons/addons.component.ts | 3 + .../connect-addon.component.scss | 21 +++-- .../connect-addon/connect-addon.component.ts | 4 +- .../constants/notifications-constants.ts | 7 +- .../notification-subscription.mapper.ts | 8 +- .../settings/notifications/models/index.ts | 4 +- ...tification-subscription-json-api.models.ts | 20 +++++ .../notification-subscription.models.ts | 21 ----- ....models.ts => notifications-form.model.ts} | 0 .../models/subscription-event.model.ts | 6 ++ .../notifications/notifications.component.ts | 17 ++-- .../notification-subscription.service.ts | 14 ++- .../store/notification-subscription.state.ts | 10 +-- .../education/education.component.html | 3 +- .../education/education.component.ts | 20 ++--- .../employment/employment.component.ts | 23 ++--- .../components/name/name.component.html | 1 + .../components/name/name.component.scss | 34 ++++---- .../components/social/social.component.ts | 25 +++--- .../profile-settings/{ => constants}/data.ts | 2 +- .../profile-settings/constants/index.ts | 1 + .../profile-settings-tab-options.const.ts | 10 +++ .../settings/profile-settings/enums/index.ts | 1 + .../enums/profile-settings-tab-option.enum.ts | 6 ++ .../profile-settings.component.html | 35 ++++---- .../profile-settings.component.spec.ts | 36 +------- .../profile-settings.component.ts | 24 +++-- .../services/profile-settings.api.service.ts | 5 +- .../store/profile-settings.state.ts | 28 +++--- 56 files changed, 412 insertions(+), 406 deletions(-) create mode 100644 src/app/features/settings/notifications/models/notification-subscription-json-api.models.ts rename src/app/features/settings/notifications/models/{notifications-form.models.ts => notifications-form.model.ts} (100%) create mode 100644 src/app/features/settings/notifications/models/subscription-event.model.ts rename src/app/features/settings/profile-settings/{ => constants}/data.ts (97%) create mode 100644 src/app/features/settings/profile-settings/constants/index.ts create mode 100644 src/app/features/settings/profile-settings/constants/profile-settings-tab-options.const.ts create mode 100644 src/app/features/settings/profile-settings/enums/index.ts create mode 100644 src/app/features/settings/profile-settings/enums/profile-settings-tab-option.enum.ts diff --git a/src/app/features/settings/account-settings/account-settings.component.ts b/src/app/features/settings/account-settings/account-settings.component.ts index 3e1bb48e9..dde2087b6 100644 --- a/src/app/features/settings/account-settings/account-settings.component.ts +++ b/src/app/features/settings/account-settings/account-settings.component.ts @@ -1,16 +1,14 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { DialogService } from 'primeng/dynamicdialog'; -import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { ChangeDetectionStrategy, Component, effect } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; import { SubHeaderComponent } from '@osf/shared/components'; -import { IS_XSMALL } from '@osf/shared/utils'; import { AffiliatedInstitutionsComponent, @@ -45,18 +43,23 @@ import { GetAccountSettings, GetEmails, GetExternalIdentities, GetRegions, GetUs changeDetection: ChangeDetectionStrategy.OnPush, }) export class AccountSettingsComponent { - readonly #store = inject(Store); - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + readonly actions = createDispatchMap({ + getAccountSettings: GetAccountSettings, + getEmails: GetEmails, + getExternalIdentities: GetExternalIdentities, + getRegions: GetRegions, + getUserInstitutions: GetUserInstitutions, + }); + protected readonly currentUser = select(UserSelectors.getCurrentUser); constructor() { effect(() => { if (this.currentUser()) { - this.#store.dispatch(GetAccountSettings); - this.#store.dispatch(GetEmails); - this.#store.dispatch(GetExternalIdentities); - this.#store.dispatch(GetRegions); - this.#store.dispatch(GetUserInstitutions); + this.actions.getAccountSettings(); + this.actions.getEmails(); + this.actions.getExternalIdentities(); + this.actions.getRegions(); + this.actions.getUserInstitutions(); } }); } diff --git a/src/app/features/settings/account-settings/components/add-email/add-email.component.ts b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts index cf1737b04..a3cfd1360 100644 --- a/src/app/features/settings/account-settings/components/add-email/add-email.component.ts +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -9,6 +9,8 @@ import { InputText } from 'primeng/inputtext'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CustomValidators } from '@osf/shared/utils'; + import { AddEmail } from '../../store'; @Component({ @@ -19,15 +21,16 @@ import { AddEmail } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AddEmailComponent { - readonly #store = inject(Store); + readonly action = createDispatchMap({ addEmail: AddEmail }); readonly dialogRef = inject(DynamicDialogRef); - protected readonly emailControl = new FormControl('', [Validators.email, Validators.required]); + protected readonly emailControl = new FormControl('', [Validators.email, CustomValidators.requiredTrimmed()]); addEmail() { if (this.emailControl.value) { - this.#store.dispatch(new AddEmail(this.emailControl.value)); + this.action.addEmail(this.emailControl.value); } + this.dialogRef.close(); } } diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss index 21819b894..0b43d96c2 100644 --- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss @@ -3,55 +3,55 @@ :host { @extend .account-setting; +} + +h3, +p { + font-weight: 400; + text-transform: none; +} - h3, - p { - font-weight: 400; - text-transform: none; +.account-setting { + &-emails { + display: flex; + flex-direction: column; + gap: 1.7rem; } - .account-setting { - &-emails { - display: flex; - flex-direction: column; - gap: 1.7rem; - } + &-email { + display: flex; + gap: 2rem; + align-items: start; - &-email { + &--readonly { display: flex; - gap: 2rem; - align-items: start; - - &--readonly { - display: flex; - align-items: center; - border: 1px solid var.$grey-2; - padding: 0.285rem 0.85rem; - border-radius: 0.285rem; - min-height: 2.8rem; - - &--address { - max-width: 14rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - i { - color: var.$dark-blue-1; - font-size: 0.7rem; - margin-left: 0.7rem; - cursor: pointer; - font-weight: 400; - } + align-items: center; + border: 1px solid var.$grey-2; + padding: 0.285rem 0.85rem; + border-radius: 0.285rem; + min-height: 2.8rem; + + &--address { + max-width: 14rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - &__value { - display: flex; - align-items: center; - gap: 0.428rem; - min-height: 2.8rem; + i { + color: var.$dark-blue-1; + font-size: 0.7rem; + margin-left: 0.7rem; + cursor: pointer; + font-weight: 400; } } + + &__value { + display: flex; + align-items: center; + gap: 0.428rem; + min-height: 2.8rem; + } } } diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts index bba4653d7..639767bc3 100644 --- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts @@ -1,8 +1,8 @@ -import { select, Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { UserSelectors } from '@osf/core/store/user'; @@ -16,13 +16,13 @@ import { AccountSettingsSelectors, DeleteUserInstitution } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AffiliatedInstitutionsComponent { - private readonly store = inject(Store); + private readonly actions = createDispatchMap({ deleteUserInstitution: DeleteUserInstitution }); protected institutions = select(AccountSettingsSelectors.getUserInstitutions); protected currentUser = select(UserSelectors.getCurrentUser); deleteInstitution(id: string) { if (this.currentUser()?.id) { - this.store.dispatch(new DeleteUserInstitution(id, this.currentUser()!.id)); + this.actions.deleteUserInstitution(id, this.currentUser()!.id); } } } diff --git a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts index bbe68c7ee..45ecc8fbc 100644 --- a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts +++ b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts @@ -17,8 +17,8 @@ import { CancelDeactivationRequest } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CancelDeactivationComponent { - action = createDispatchMap({ cancelDeactivationRequest: CancelDeactivationRequest }); - dialogRef = inject(DynamicDialogRef); + private readonly action = createDispatchMap({ cancelDeactivationRequest: CancelDeactivationRequest }); + readonly dialogRef = inject(DynamicDialogRef); cancelDeactivation(): void { this.action.cancelDeactivationRequest(); diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.ts b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts index b8c1a1c0d..b8186c6cf 100644 --- a/src/app/features/settings/account-settings/components/change-password/change-password.component.ts +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts @@ -26,8 +26,9 @@ import { AccountSettingsService } from '../../services'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChangePasswordComponent implements OnInit { - readonly #accountSettingsService = inject(AccountSettingsService); - readonly #translateService = inject(TranslateService); + private readonly accountSettingsService = inject(AccountSettingsService); + private readonly translateService = inject(TranslateService); + readonly passwordForm: AccountSettingsPasswordForm = new FormGroup({ [AccountSettingsPasswordFormControls.OldPassword]: new FormControl('', { nonNullable: true, @@ -51,7 +52,6 @@ export class ChangePasswordComponent implements OnInit { protected errorMessage = signal(''); ngOnInit(): void { - // Add form-level validator for password matching and old password check this.passwordForm.addValidators((control: AbstractControl): ValidationErrors | null => { const oldPassword = control.get(AccountSettingsPasswordFormControls.OldPassword)?.value; const newPassword = control.get(AccountSettingsPasswordFormControls.NewPassword)?.value; @@ -59,12 +59,10 @@ export class ChangePasswordComponent implements OnInit { const errors: ValidationErrors = {}; - // Check if new password matches old password if (oldPassword && newPassword && oldPassword === newPassword) { errors['sameAsOldPassword'] = true; } - // Check if confirm password matches new password if (newPassword && confirmPassword && newPassword !== confirmPassword) { errors['passwordMismatch'] = true; } @@ -72,7 +70,6 @@ export class ChangePasswordComponent implements OnInit { return Object.keys(errors).length > 0 ? errors : null; }); - // Update validation when any password field changes this.passwordForm.get(AccountSettingsPasswordFormControls.OldPassword)?.valueChanges.subscribe(() => { this.passwordForm.updateValueAndValidity(); }); @@ -96,7 +93,7 @@ export class ChangePasswordComponent implements OnInit { const oldPassword = this.passwordForm.get(AccountSettingsPasswordFormControls.OldPassword)?.value ?? ''; const newPassword = this.passwordForm.get(AccountSettingsPasswordFormControls.NewPassword)?.value ?? ''; - this.#accountSettingsService.updatePassword(oldPassword, newPassword).subscribe({ + this.accountSettingsService.updatePassword(oldPassword, newPassword).subscribe({ next: () => { this.passwordForm.reset(); Object.values(this.passwordForm.controls).forEach((control) => { @@ -108,7 +105,7 @@ export class ChangePasswordComponent implements OnInit { this.errorMessage.set(error.error.errors[0].detail); } else { this.errorMessage.set( - this.#translateService.instant('settings.accountSettings.changePassword.messages.error') + this.translateService.instant('settings.accountSettings.changePassword.messages.error') ); } }, diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts index 2f6f4c7a1..ebb7f98d1 100644 --- a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts @@ -1,8 +1,8 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { AccountSettingsSelectors, DeleteExternalIdentity } from '../../store'; @@ -14,10 +14,10 @@ import { AccountSettingsSelectors, DeleteExternalIdentity } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConnectedIdentitiesComponent { - readonly #store = inject(Store); - readonly externalIdentities = this.#store.selectSignal(AccountSettingsSelectors.getExternalIdentities); + readonly actions = createDispatchMap({ deleteExternalIdentity: DeleteExternalIdentity }); + readonly externalIdentities = select(AccountSettingsSelectors.getExternalIdentities); deleteExternalIdentity(id: string): void { - this.#store.dispatch(new DeleteExternalIdentity(id)); + this.actions.deleteExternalIdentity(id); } } diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html index edf459f77..d734e802d 100644 --- a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html @@ -10,7 +10,6 @@

{{ 'settings.accountSettings.defaultStorageL styleClass="account-setting-select" optionLabel="name" [options]="regions()" - [class.mobile]="isMobile()" [(ngModel)]="selectedRegion" > 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 a0ce8686c..9777e5e15 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 @@ -1,16 +1,14 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Select } from 'primeng/select'; -import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { ChangeDetectionStrategy, Component, effect, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; -import { IS_XSMALL } from '@osf/shared/utils'; import { Region } from '../../models'; import { AccountSettingsSelectors, UpdateRegion } from '../../store'; @@ -23,11 +21,10 @@ import { AccountSettingsSelectors, UpdateRegion } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class DefaultStorageLocationComponent { - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - readonly #store = inject(Store); + readonly actions = createDispatchMap({ updateRegion: UpdateRegion }); - protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); - protected readonly regions = this.#store.selectSignal(AccountSettingsSelectors.getRegions); + protected readonly currentUser = select(UserSelectors.getCurrentUser); + protected readonly regions = select(AccountSettingsSelectors.getRegions); protected selectedRegion = signal(undefined); constructor() { @@ -43,7 +40,7 @@ export class DefaultStorageLocationComponent { updateLocation(): void { if (this.selectedRegion()?.id) { - this.#store.dispatch(new UpdateRegion(this.selectedRegion()!.id)); + this.actions.updateRegion(this.selectedRegion()!.id); } } } diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts index 8995a1f1d..a8fa5f85b 100644 --- a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts @@ -1,11 +1,11 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { RadioButton } from 'primeng/radiobutton'; -import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, signal } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; @@ -21,16 +21,16 @@ import { UpdateIndexing } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ShareIndexingComponent { - readonly #store = inject(Store); + readonly actions = createDispatchMap({ updateIndexing: UpdateIndexing }); protected indexing = signal(ShareIndexingEnum.None); - protected readonly currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + protected readonly currentUser = select(UserSelectors.getCurrentUser); updateIndexing = () => { if (this.currentUser()?.id) { if (this.indexing() === ShareIndexingEnum.OptIn) { - this.#store.dispatch(new UpdateIndexing(true)); + this.actions.updateIndexing(true); } else if (this.indexing() === ShareIndexingEnum.OutOf) { - this.#store.dispatch(new UpdateIndexing(false)); + this.actions.updateIndexing(false); } } }; diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts index b5837d04d..1bd83ba06 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -19,14 +19,15 @@ import { EnableTwoFactorAuth } from '@osf/features/settings/account-settings/sto changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConfigureTwoFactorComponent { - #store = inject(Store); - dialogRef = inject(DynamicDialogRef); + private readonly actions = createDispatchMap({ enableTwoFactorAuth: EnableTwoFactorAuth }); readonly config = inject(DynamicDialogConfig); + dialogRef = inject(DynamicDialogRef); + enableTwoFactor(): void { const settings = this.config.data as AccountSettings; settings.twoFactorEnabled = true; - this.#store.dispatch(EnableTwoFactorAuth); + this.actions.enableTwoFactorAuth(); this.dialogRef.close(); } } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html index 83697af35..fd6fe754f 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html +++ b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html @@ -2,6 +2,7 @@ +
{ return `otpauth://totp/OSF:${this.currentUser()?.email}?secret=${this.accountSettings()?.secret}`; @@ -48,10 +53,10 @@ export class TwoFactorAuthComponent { errorMessage = signal(''); configureTwoFactorAuth(): void { - this.dialogRef = this.#dialogService.open(ConfigureTwoFactorComponent, { + this.dialogRef = this.dialogService.open(ConfigureTwoFactorComponent, { width: '520px', focusOnShow: false, - header: this.#translateService.instant('settings.accountSettings.twoFactorAuth.dialog.configure.title'), + header: this.translateService.instant('settings.accountSettings.twoFactorAuth.dialog.configure.title'), closeOnEscape: true, modal: true, closable: true, @@ -60,10 +65,10 @@ export class TwoFactorAuthComponent { } openDisableDialog() { - this.dialogRef = this.#dialogService.open(VerifyTwoFactorComponent, { + this.dialogRef = this.dialogService.open(VerifyTwoFactorComponent, { width: '520px', focusOnShow: false, - header: this.#translateService.instant('settings.accountSettings.twoFactorAuth.dialog.disable.title'), + header: this.translateService.instant('settings.accountSettings.twoFactorAuth.dialog.disable.title'), closeOnEscape: true, modal: true, closable: true, @@ -71,16 +76,16 @@ export class TwoFactorAuthComponent { } enableTwoFactor(): void { - this.#accountSettingsService.updateSettings({ two_factor_verification: this.verificationCode.value }).subscribe({ + this.accountSettingsService.updateSettings({ two_factor_verification: this.verificationCode.value }).subscribe({ next: (response: AccountSettings) => { - this.#store.dispatch(new SetAccountSettings(response)); + this.actions.setAccountSettings(response); }, error: (error: HttpErrorResponse) => { if (error.error?.errors?.[0]?.detail) { this.errorMessage.set(error.error.errors[0].detail); } else { this.errorMessage.set( - this.#translateService.instant('settings.accountSettings.twoFactorAuth.verification.error') + this.translateService.instant('settings.accountSettings.twoFactorAuth.verification.error') ); } }, @@ -88,6 +93,6 @@ export class TwoFactorAuthComponent { } disableTwoFactor(): void { - this.#store.dispatch(DisableTwoFactorAuth); + this.actions.disableTwoFactorAuth(); } } diff --git a/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts b/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts index d507d9b9d..bf57f6333 100644 --- a/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/account-settings.mapper.ts @@ -1,8 +1,8 @@ import { ApiData } from '@osf/core/models'; -import { AccountSettings, AccountSettingsResponse } from '../models'; +import { AccountSettings, AccountSettingsResponseJsonApi } from '../models'; -export function MapAccountSettings(data: ApiData): AccountSettings { +export function MapAccountSettings(data: ApiData): AccountSettings { return { twoFactorEnabled: data.attributes.two_factor_enabled, twoFactorConfirmed: data.attributes.two_factor_confirmed, diff --git a/src/app/features/settings/account-settings/mappers/emails.mapper.ts b/src/app/features/settings/account-settings/mappers/emails.mapper.ts index e0a9849cb..c7777e7f2 100644 --- a/src/app/features/settings/account-settings/mappers/emails.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/emails.mapper.ts @@ -1,8 +1,8 @@ import { ApiData } from '@osf/core/models'; -import { AccountEmail, AccountEmailResponse } from '../models'; +import { AccountEmail, AccountEmailResponseJsonApi } from '../models'; -export function MapEmails(emails: ApiData[]): AccountEmail[] { +export function MapEmails(emails: ApiData[]): AccountEmail[] { const accountEmails: AccountEmail[] = []; emails.forEach((email) => { accountEmails.push(MapEmail(email)); @@ -10,7 +10,7 @@ export function MapEmails(emails: ApiData): AccountEmail { +export function MapEmail(email: ApiData): AccountEmail { return { id: email.id, emailAddress: email.attributes.email_address, diff --git a/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts b/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts index 6f6b75ac1..601b1b175 100644 --- a/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/external-identities.mapper.ts @@ -1,8 +1,10 @@ import { ApiData } from '@osf/core/models'; -import { ExternalIdentity, ExternalIdentityResponse } from '../models'; +import { ExternalIdentity, ExternalIdentityResponseJsonApi } from '../models'; -export function MapExternalIdentities(data: ApiData[]): ExternalIdentity[] { +export function MapExternalIdentities( + data: ApiData[] +): ExternalIdentity[] { const identities: ExternalIdentity[] = []; for (const item of data) { identities.push({ 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 5351eff7d..78cb7b63a 100644 --- a/src/app/features/settings/account-settings/mappers/regions.mapper.ts +++ b/src/app/features/settings/account-settings/mappers/regions.mapper.ts @@ -4,6 +4,7 @@ import { Region } from '../models'; export function MapRegions(data: ApiData<{ name: string }, null, null, null>[]): Region[] { const regions: Region[] = []; + for (const region of data) { regions.push(MapRegion(region)); } diff --git a/src/app/features/settings/account-settings/models/responses/get-account-settings-response.model.ts b/src/app/features/settings/account-settings/models/responses/get-account-settings-response.model.ts index 9fccab52a..5dd2b29ff 100644 --- a/src/app/features/settings/account-settings/models/responses/get-account-settings-response.model.ts +++ b/src/app/features/settings/account-settings/models/responses/get-account-settings-response.model.ts @@ -1,8 +1,8 @@ import { ApiData } from '@osf/core/models'; -export type GetAccountSettingsResponse = ApiData; +export type GetAccountSettingsResponseJsonApi = ApiData; -export interface AccountSettingsResponse { +export interface AccountSettingsResponseJsonApi { two_factor_enabled: boolean; two_factor_confirmed: boolean; subscribe_osf_general_email: boolean; diff --git a/src/app/features/settings/account-settings/models/responses/get-email-response.model.ts b/src/app/features/settings/account-settings/models/responses/get-email-response.model.ts index b850020cc..a8eb44899 100644 --- a/src/app/features/settings/account-settings/models/responses/get-email-response.model.ts +++ b/src/app/features/settings/account-settings/models/responses/get-email-response.model.ts @@ -1,5 +1,5 @@ import { ApiData, JsonApiResponse } from '@osf/core/models'; -import { AccountEmailResponse } from './list-emails.model'; +import { AccountEmailResponseJsonApi } from './list-emails.model'; -export type GetEmailResponse = JsonApiResponse, null>; +export type GetEmailResponseJsonApi = JsonApiResponse, null>; diff --git a/src/app/features/settings/account-settings/models/responses/get-regions-response.model.ts b/src/app/features/settings/account-settings/models/responses/get-regions-response.model.ts index 6247c3a23..6ffdde930 100644 --- a/src/app/features/settings/account-settings/models/responses/get-regions-response.model.ts +++ b/src/app/features/settings/account-settings/models/responses/get-regions-response.model.ts @@ -1,4 +1,4 @@ import { ApiData, JsonApiResponse } from '@osf/core/models'; -export type GetRegionsResponse = JsonApiResponse[], null>; -export type GetRegionResponse = JsonApiResponse, null>; +export type GetRegionsResponseJsonApi = JsonApiResponse[], null>; +export type GetRegionResponseJsonApi = JsonApiResponse, null>; diff --git a/src/app/features/settings/account-settings/models/responses/list-emails.model.ts b/src/app/features/settings/account-settings/models/responses/list-emails.model.ts index 867ed5c22..d236ef696 100644 --- a/src/app/features/settings/account-settings/models/responses/list-emails.model.ts +++ b/src/app/features/settings/account-settings/models/responses/list-emails.model.ts @@ -1,8 +1,8 @@ import { ApiData, JsonApiResponse } from '@osf/core/models'; -export type ListEmailsResponse = JsonApiResponse[], null>; +export type ListEmailsResponseJsonApi = JsonApiResponse[], null>; -export interface AccountEmailResponse { +export interface AccountEmailResponseJsonApi { email_address: string; confirmed: boolean; verified: boolean; diff --git a/src/app/features/settings/account-settings/models/responses/list-identities-response.model.ts b/src/app/features/settings/account-settings/models/responses/list-identities-response.model.ts index 9084f5169..563fce5be 100644 --- a/src/app/features/settings/account-settings/models/responses/list-identities-response.model.ts +++ b/src/app/features/settings/account-settings/models/responses/list-identities-response.model.ts @@ -1,8 +1,11 @@ import { ApiData, JsonApiResponse } from '@osf/core/models'; -export type ListIdentitiesResponse = JsonApiResponse[], null>; +export type ListIdentitiesResponseJsonApi = JsonApiResponse< + ApiData[], + null +>; -export interface ExternalIdentityResponse { +export interface ExternalIdentityResponseJsonApi { external_id: string; status: 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 11cd65570..09dec97f0 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 @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select } from '@ngxs/store'; import { map, Observable } from 'rxjs'; @@ -11,14 +11,14 @@ import { UserSelectors } from '@osf/core/store/user'; import { MapAccountSettings, MapEmail, MapEmails, MapExternalIdentities, MapRegions } from '../mappers'; import { AccountEmail, - AccountEmailResponse, + AccountEmailResponseJsonApi, AccountSettings, ExternalIdentity, - GetAccountSettingsResponse, - GetEmailResponse, - GetRegionsResponse, - ListEmailsResponse, - ListIdentitiesResponse, + GetAccountSettingsResponseJsonApi, + GetEmailResponseJsonApi, + GetRegionsResponseJsonApi, + ListEmailsResponseJsonApi, + ListIdentitiesResponseJsonApi, Region, } from '../models'; @@ -28,9 +28,8 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class AccountSettingsService { - #store = inject(Store); - #jsonApiService = inject(JsonApiService); - #currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); + private readonly jsonApiService = inject(JsonApiService); + private readonly currentUser = select(UserSelectors.getCurrentUser); getEmails(): Observable { const params: Record = { @@ -38,8 +37,8 @@ export class AccountSettingsService { 'page[size]': '10', }; - return this.#jsonApiService - .get(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails`, params) + return this.jsonApiService + .get(`${environment.apiUrl}/users/${this.currentUser()?.id}/settings/emails`, params) .pipe(map((response) => MapEmails(response.data))); } @@ -48,8 +47,8 @@ export class AccountSettingsService { userId: string, params: Record | undefined = undefined ): Observable { - return this.#jsonApiService - .get(`${environment.apiUrl}/users/${userId}/settings/emails/${emailId}`, params) + return this.jsonApiService + .get(`${environment.apiUrl}/users/${userId}/settings/emails/${emailId}`, params) .pipe(map((response) => MapEmail(response.data))); } @@ -70,7 +69,7 @@ export class AccountSettingsService { relationships: { user: { data: { - id: this.#currentUser()?.id, + id: this.currentUser()?.id, type: 'users', }, }, @@ -79,16 +78,16 @@ export class AccountSettingsService { }, }; - return this.#jsonApiService + return this.jsonApiService .post< - ApiData - >(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails/`, body) + ApiData + >(`${environment.apiUrl}/users/${this.currentUser()?.id}/settings/emails/`, body) .pipe(map((response) => MapEmail(response))); } deleteEmail(emailId: string): Observable { - return this.#jsonApiService.delete( - `${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails/${emailId}` + return this.jsonApiService.delete( + `${environment.apiUrl}/users/${this.currentUser()?.id}/settings/emails/${emailId}` ); } @@ -102,7 +101,7 @@ export class AccountSettingsService { }, }, }; - return this.#jsonApiService.post(`${environment.apiUrl}/users/${userId}/confirm/`, body); + return this.jsonApiService.post(`${environment.apiUrl}/users/${userId}/confirm/`, body); } verifyEmail(userId: string, emailId: string): Observable { @@ -116,9 +115,9 @@ export class AccountSettingsService { }, }; - return this.#jsonApiService + return this.jsonApiService .patch< - ApiData + ApiData >(`${environment.apiUrl}/users/${userId}/settings/emails/${emailId}/`, body) .pipe(map((response) => MapEmail(response))); } @@ -134,23 +133,23 @@ export class AccountSettingsService { }, }; - return this.#jsonApiService + return this.jsonApiService .patch< - ApiData - >(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/emails/${emailId}/`, body) + ApiData + >(`${environment.apiUrl}/users/${this.currentUser()?.id}/settings/emails/${emailId}/`, body) .pipe(map((response) => MapEmail(response))); } getRegions(): Observable { - return this.#jsonApiService - .get(`${environment.apiUrl}/regions/`) + return this.jsonApiService + .get(`${environment.apiUrl}/regions/`) .pipe(map((response) => MapRegions(response.data))); } updateLocation(locationId: string): Observable { const body = { data: { - id: this.#currentUser()?.id, + id: this.currentUser()?.id, attributes: {}, relationships: { default_region: { @@ -164,15 +163,15 @@ export class AccountSettingsService { }, }; - return this.#jsonApiService - .patch(`${environment.apiUrl}/users/${this.#currentUser()?.id}`, body) + return this.jsonApiService + .patch(`${environment.apiUrl}/users/${this.currentUser()?.id}`, body) .pipe(map((user) => UserMapper.fromUserGetResponse(user))); } updateIndexing(allowIndexing: boolean): Observable { const body = { data: { - id: this.#currentUser()?.id, + id: this.currentUser()?.id, attributes: { allow_indexing: allowIndexing, }, @@ -181,8 +180,8 @@ export class AccountSettingsService { }, }; - return this.#jsonApiService - .patch(`${environment.apiUrl}/users/${this.#currentUser()?.id}`, body) + return this.jsonApiService + .patch(`${environment.apiUrl}/users/${this.currentUser()?.id}`, body) .pipe(map((user) => UserMapper.fromUserGetResponse(user))); } @@ -197,7 +196,7 @@ export class AccountSettingsService { }, }; - return this.#jsonApiService.post(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings/password`, body); + return this.jsonApiService.post(`${environment.apiUrl}/users/${this.currentUser()?.id}/settings/password`, body); } getExternalIdentities(): Observable { @@ -206,35 +205,35 @@ export class AccountSettingsService { 'page[size]': '10', }; - return this.#jsonApiService - .get(`${environment.apiUrl}/users/me/settings/identities/`, params) + return this.jsonApiService + .get(`${environment.apiUrl}/users/me/settings/identities/`, params) .pipe(map((response) => MapExternalIdentities(response.data))); } deleteExternalIdentity(id: string): Observable { - return this.#jsonApiService.delete(`${environment.apiUrl}/users/me/settings/identities/${id}`); + return this.jsonApiService.delete(`${environment.apiUrl}/users/me/settings/identities/${id}`); } getSettings(): Observable { - return this.#jsonApiService + return this.jsonApiService .get< - JsonApiResponse - >(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings`) + JsonApiResponse + >(`${environment.apiUrl}/users/${this.currentUser()?.id}/settings`) .pipe(map((response) => MapAccountSettings(response.data))); } updateSettings(settings: Record): Observable { const body = { data: { - id: this.#currentUser()?.id, + id: this.currentUser()?.id, attributes: settings, relationships: {}, type: 'user_settings', }, }; - return this.#jsonApiService - .patch(`${environment.apiUrl}/users/${this.#currentUser()?.id}/settings`, body) + return this.jsonApiService + .patch(`${environment.apiUrl}/users/${this.currentUser()?.id}/settings`, body) .pipe(map((response) => MapAccountSettings(response))); } } diff --git a/src/app/features/settings/account-settings/store/account-settings.state.ts b/src/app/features/settings/account-settings/store/account-settings.state.ts index 852d4e815..b6fe870fd 100644 --- a/src/app/features/settings/account-settings/store/account-settings.state.ts +++ b/src/app/features/settings/account-settings/store/account-settings.state.ts @@ -53,15 +53,14 @@ import { AccountSettingsStateModel } from './account-settings.model'; }, }) export class AccountSettingsState { - #accountSettingsService = inject(AccountSettingsService); - #institutionsService = inject(InstitutionsService); + private readonly accountSettingsService = inject(AccountSettingsService); + private readonly institutionsService = inject(InstitutionsService); @Action(GetEmails) getEmails(ctx: StateContext) { - ctx.patchState({ - emailsLoading: true, - }); - return this.#accountSettingsService.getEmails().pipe( + ctx.patchState({ emailsLoading: true }); + + return this.accountSettingsService.getEmails().pipe( tap({ next: (emails) => { ctx.patchState({ @@ -75,7 +74,7 @@ export class AccountSettingsState { @Action(AddEmail) addEmail(ctx: StateContext, action: AddEmail) { - return this.#accountSettingsService.addEmail(action.email).pipe( + return this.accountSettingsService.addEmail(action.email).pipe( tap((email) => { if (email.emailAddress && !email.confirmed) { ctx.dispatch(GetEmails); @@ -86,18 +85,16 @@ export class AccountSettingsState { @Action(DeleteEmail) deleteEmail(ctx: StateContext, action: DeleteEmail) { - return this.#accountSettingsService.deleteEmail(action.email).pipe( - tap({ - next: () => { - ctx.dispatch(GetEmails); - }, + return this.accountSettingsService.deleteEmail(action.email).pipe( + tap(() => { + ctx.dispatch(GetEmails); }) ); } @Action(VerifyEmail) verifyEmail(ctx: StateContext, action: VerifyEmail) { - return this.#accountSettingsService.verifyEmail(action.userId, action.emailId).pipe( + return this.accountSettingsService.verifyEmail(action.userId, action.emailId).pipe( tap((email) => { if (email.verified) { ctx.dispatch(GetEmails); @@ -108,7 +105,7 @@ export class AccountSettingsState { @Action(MakePrimary) makePrimary(ctx: StateContext, action: MakePrimary) { - return this.#accountSettingsService.makePrimary(action.emailId).pipe( + return this.accountSettingsService.makePrimary(action.emailId).pipe( tap((email) => { if (email.verified) { ctx.dispatch(GetEmails); @@ -119,7 +116,7 @@ export class AccountSettingsState { @Action(GetRegions) getRegions(ctx: StateContext) { - return this.#accountSettingsService.getRegions().pipe( + return this.accountSettingsService.getRegions().pipe( tap({ next: (regions) => ctx.patchState({ regions: regions }), }) @@ -128,7 +125,7 @@ export class AccountSettingsState { @Action(UpdateRegion) updateRegion(ctx: StateContext, action: UpdateRegion) { - return this.#accountSettingsService.updateLocation(action.regionId).pipe( + return this.accountSettingsService.updateLocation(action.regionId).pipe( tap({ next: (user) => { ctx.dispatch(new SetCurrentUser(user)); @@ -139,7 +136,7 @@ export class AccountSettingsState { @Action(UpdateIndexing) updateIndexing(ctx: StateContext, action: UpdateIndexing) { - return this.#accountSettingsService.updateIndexing(action.allowIndexing).pipe( + return this.accountSettingsService.updateIndexing(action.allowIndexing).pipe( tap({ next: (user) => { ctx.dispatch(new SetCurrentUser(user)); @@ -150,7 +147,7 @@ export class AccountSettingsState { @Action(GetExternalIdentities) getExternalIdentities(ctx: StateContext) { - return this.#accountSettingsService.getExternalIdentities().pipe( + return this.accountSettingsService.getExternalIdentities().pipe( tap({ next: (identities) => ctx.patchState({ externalIdentities: identities }), }) @@ -159,7 +156,7 @@ export class AccountSettingsState { @Action(DeleteExternalIdentity) deleteExternalIdentity(ctx: StateContext, action: DeleteExternalIdentity) { - return this.#accountSettingsService.deleteExternalIdentity(action.externalId).pipe( + return this.accountSettingsService.deleteExternalIdentity(action.externalId).pipe( tap(() => { ctx.dispatch(GetExternalIdentities); }) @@ -168,14 +165,14 @@ export class AccountSettingsState { @Action(GetUserInstitutions) getUserInstitutions(ctx: StateContext) { - return this.#institutionsService + return this.institutionsService .getUserInstitutions() .pipe(tap((userInstitutions) => ctx.patchState({ userInstitutions }))); } @Action(DeleteUserInstitution) deleteUserInstitution(ctx: StateContext, action: DeleteUserInstitution) { - return this.#institutionsService.deleteUserInstitution(action.id, action.userId).pipe( + return this.institutionsService.deleteUserInstitution(action.id, action.userId).pipe( tap(() => { ctx.dispatch(GetUserInstitutions); }) @@ -184,7 +181,7 @@ export class AccountSettingsState { @Action(GetAccountSettings) getAccountSettings(ctx: StateContext) { - return this.#accountSettingsService.getSettings().pipe( + return this.accountSettingsService.getSettings().pipe( tap({ next: (settings) => { ctx.patchState({ @@ -197,7 +194,7 @@ export class AccountSettingsState { @Action(UpdateAccountSettings) updateAccountSettings(ctx: StateContext, action: UpdateAccountSettings) { - return this.#accountSettingsService.updateSettings(action.accountSettings).pipe( + return this.accountSettingsService.updateSettings(action.accountSettings).pipe( tap({ next: (settings) => { ctx.patchState({ @@ -210,7 +207,7 @@ export class AccountSettingsState { @Action(DisableTwoFactorAuth) disableTwoFactorAuth(ctx: StateContext) { - return this.#accountSettingsService.updateSettings({ two_factor_enabled: 'false' }).pipe( + return this.accountSettingsService.updateSettings({ two_factor_enabled: 'false' }).pipe( tap({ next: (settings) => { ctx.patchState({ @@ -223,7 +220,7 @@ export class AccountSettingsState { @Action(EnableTwoFactorAuth) enableTwoFactorAuth(ctx: StateContext) { - return this.#accountSettingsService.updateSettings({ two_factor_enabled: 'true' }).pipe( + return this.accountSettingsService.updateSettings({ two_factor_enabled: 'true' }).pipe( tap({ next: (settings) => { ctx.patchState({ @@ -241,7 +238,7 @@ export class AccountSettingsState { @Action(DeactivateAccount) deactivateAccount(ctx: StateContext) { - return this.#accountSettingsService.updateSettings({ deactivation_requested: 'true' }).pipe( + return this.accountSettingsService.updateSettings({ deactivation_requested: 'true' }).pipe( tap({ next: (settings) => { ctx.patchState({ @@ -254,7 +251,7 @@ export class AccountSettingsState { @Action(CancelDeactivationRequest) cancelDeactivationRequest(ctx: StateContext) { - return this.#accountSettingsService.updateSettings({ deactivation_requested: 'false' }).pipe( + return this.accountSettingsService.updateSettings({ deactivation_requested: 'false' }).pipe( tap({ next: (settings) => { ctx.patchState({ diff --git a/src/app/features/settings/addons/addons.component.html b/src/app/features/settings/addons/addons.component.html index c2cb58c8f..ffc5bbb6e 100644 --- a/src/app/features/settings/addons/addons.component.html +++ b/src/app/features/settings/addons/addons.component.html @@ -2,10 +2,11 @@
@@ -21,6 +22,7 @@

{{ 'settings.addons.description' | translate }}

+
} + (''); + protected searchValue = signal(''); protected selectedCategory = signal(AddonCategory.EXTERNAL_STORAGE_SERVICES); protected selectedTab = signal(this.defaultTabValue); diff --git a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.scss b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.scss index fea06004b..b7c557a76 100644 --- a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.scss +++ b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.scss @@ -1,21 +1,20 @@ @use "assets/styles/mixins" as mix; -@use "assets/styles/variables" as var; :host { flex: 1; @include mix.flex-column; +} - .stepper-container { - background-color: var.$white; - flex: 1; +.stepper-container { + background-color: var(--white); + flex: 1; - button { - width: 100%; - } + button { + width: 100%; } +} - .folders-list { - border: 1px solid var.$grey-2; - border-radius: 0.57rem; - } +.folders-list { + border: 1px solid var(--grey-2); + border-radius: 0.5rem; } diff --git a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts index aef52ffa8..99ac1c8dc 100644 --- a/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts +++ b/src/app/features/settings/addons/components/connect-addon/connect-addon.component.ts @@ -37,9 +37,11 @@ import { getAddonTypeString, isAuthorizedAddon } from '@shared/utils'; styleUrl: './connect-addon.component.scss', }) export class ConnectAddonComponent { - private router = inject(Router); + private readonly router = inject(Router); + protected readonly stepper = viewChild(Stepper); protected readonly ProjectAddonsStepperValue = ProjectAddonsStepperValue; + protected terms = signal([]); protected addon = signal(null); protected addonAuthUrl = signal('/settings/addons'); diff --git a/src/app/features/settings/notifications/constants/notifications-constants.ts b/src/app/features/settings/notifications/constants/notifications-constants.ts index 42aae5f3d..d3919011f 100644 --- a/src/app/features/settings/notifications/constants/notifications-constants.ts +++ b/src/app/features/settings/notifications/constants/notifications-constants.ts @@ -1,9 +1,8 @@ import { SubscriptionEvent } from '@shared/enums'; -export const SUBSCRIPTION_EVENTS: { - event: SubscriptionEvent; - labelKey: string; -}[] = [ +import { SubscriptionEventModel } from '../models'; + +export const SUBSCRIPTION_EVENTS: SubscriptionEventModel[] = [ { event: SubscriptionEvent.GlobalCommentReplies, labelKey: 'settings.notifications.notificationPreferences.items.replies', diff --git a/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts b/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts index 1f86378c9..387603e99 100644 --- a/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts +++ b/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts @@ -2,12 +2,12 @@ import { SubscriptionEvent, SubscriptionFrequency, SubscriptionType } from '@sha import { NotificationSubscription, - NotificationSubscriptionGetResponse, - NotificationSubscriptionUpdateRequest, + NotificationSubscriptionGetResponseJsonApi, + NotificationSubscriptionUpdateRequestJsonApi, } from '../models'; export class NotificationSubscriptionMapper { - static fromGetResponse(response: NotificationSubscriptionGetResponse): NotificationSubscription { + static fromGetResponse(response: NotificationSubscriptionGetResponseJsonApi): NotificationSubscription { return { id: response.id, event: response.attributes.event_name as SubscriptionEvent, @@ -19,7 +19,7 @@ export class NotificationSubscriptionMapper { id: string, frequency: SubscriptionFrequency, isNodeSubscription?: boolean - ): NotificationSubscriptionUpdateRequest { + ): NotificationSubscriptionUpdateRequestJsonApi { const baseAttributes = { frequency: frequency, }; diff --git a/src/app/features/settings/notifications/models/index.ts b/src/app/features/settings/notifications/models/index.ts index 0f9926c54..a62c932ec 100644 --- a/src/app/features/settings/notifications/models/index.ts +++ b/src/app/features/settings/notifications/models/index.ts @@ -1,2 +1,4 @@ export * from './notification-subscription.models'; -export * from './notifications-form.models'; +export * from './notification-subscription-json-api.models'; +export * from './notifications-form.model'; +export * from './subscription-event.model'; diff --git a/src/app/features/settings/notifications/models/notification-subscription-json-api.models.ts b/src/app/features/settings/notifications/models/notification-subscription-json-api.models.ts new file mode 100644 index 000000000..5d7714c85 --- /dev/null +++ b/src/app/features/settings/notifications/models/notification-subscription-json-api.models.ts @@ -0,0 +1,20 @@ +import { SubscriptionFrequency } from '@shared/enums'; + +export interface NotificationSubscriptionGetResponseJsonApi { + id: string; + type: 'subscription' | 'user-provider-subscription'; + attributes: { + event_name: string; + frequency: string; + }; +} + +export interface NotificationSubscriptionUpdateRequestJsonApi { + data: { + id?: string; + type: 'subscription' | 'user-provider-subscription'; + attributes: { + frequency: SubscriptionFrequency; + }; + }; +} diff --git a/src/app/features/settings/notifications/models/notification-subscription.models.ts b/src/app/features/settings/notifications/models/notification-subscription.models.ts index 7a0db607c..9ad8d3701 100644 --- a/src/app/features/settings/notifications/models/notification-subscription.models.ts +++ b/src/app/features/settings/notifications/models/notification-subscription.models.ts @@ -1,28 +1,7 @@ import { SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; -//domain models export interface NotificationSubscription { id: string; event: SubscriptionEvent; frequency: SubscriptionFrequency; } - -//api models -export interface NotificationSubscriptionGetResponse { - id: string; - type: 'subscription' | 'user-provider-subscription'; - attributes: { - event_name: string; - frequency: string; - }; -} - -export interface NotificationSubscriptionUpdateRequest { - data: { - id?: string; - type: 'subscription' | 'user-provider-subscription'; - attributes: { - frequency: SubscriptionFrequency; - }; - }; -} diff --git a/src/app/features/settings/notifications/models/notifications-form.models.ts b/src/app/features/settings/notifications/models/notifications-form.model.ts similarity index 100% rename from src/app/features/settings/notifications/models/notifications-form.models.ts rename to src/app/features/settings/notifications/models/notifications-form.model.ts diff --git a/src/app/features/settings/notifications/models/subscription-event.model.ts b/src/app/features/settings/notifications/models/subscription-event.model.ts new file mode 100644 index 000000000..73d5428ac --- /dev/null +++ b/src/app/features/settings/notifications/models/subscription-event.model.ts @@ -0,0 +1,6 @@ +import { SubscriptionEvent } from '@osf/shared/enums'; + +export interface SubscriptionEventModel { + event: SubscriptionEvent; + labelKey: string; +} diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 5e671c4ca..2a6bfdd9d 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -1,4 +1,4 @@ -import { select, Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -33,7 +33,12 @@ import { export class NotificationsComponent implements OnInit { @HostBinding('class') classes = 'flex flex-1 flex-column'; - private readonly store = inject(Store); + private readonly actions = createDispatchMap({ + getCurrentUserSettings: GetCurrentUserSettings, + getAllGlobalNotificationSubscriptions: GetAllGlobalNotificationSubscriptions, + updateUserSettings: UpdateUserSettings, + updateNotificationSubscription: UpdateNotificationSubscription, + }); private readonly fb = inject(FormBuilder); private currentUser = select(UserSelectors.getCurrentUser); @@ -83,11 +88,11 @@ export class NotificationsComponent implements OnInit { ngOnInit(): void { if (!this.notificationSubscriptions().length) { - this.store.dispatch(new GetAllGlobalNotificationSubscriptions()); + this.actions.getAllGlobalNotificationSubscriptions(); } if (!this.emailPreferences()) { - this.store.dispatch(new GetCurrentUserSettings()); + this.actions.getCurrentUserSettings(); } } @@ -97,7 +102,7 @@ export class NotificationsComponent implements OnInit { } const formValue = this.emailPreferencesForm.value as UserSettings; - this.store.dispatch(new UpdateUserSettings(this.currentUser()!.id, formValue)); + this.actions.updateUserSettings(this.currentUser()!.id, formValue); } onSubscriptionChange(event: SubscriptionEvent, frequency: SubscriptionFrequency) { @@ -106,7 +111,7 @@ export class NotificationsComponent implements OnInit { const id = `${user.id}_${event}`; this.loadingEvents.update((list) => [...list, event]); - this.store.dispatch(new UpdateNotificationSubscription({ id, frequency })).subscribe({ + this.actions.updateNotificationSubscription({ id, frequency }).subscribe({ complete: () => { this.loadingEvents.update((list) => list.filter((item) => item !== event)); }, diff --git a/src/app/features/settings/notifications/services/notification-subscription.service.ts b/src/app/features/settings/notifications/services/notification-subscription.service.ts index f7bd5a60c..5384cd134 100644 --- a/src/app/features/settings/notifications/services/notification-subscription.service.ts +++ b/src/app/features/settings/notifications/services/notification-subscription.service.ts @@ -7,7 +7,7 @@ import { JsonApiService } from '@osf/core/services'; import { SubscriptionFrequency } from '@shared/enums'; import { NotificationSubscriptionMapper } from '../mappers'; -import { NotificationSubscription, NotificationSubscriptionGetResponse } from '../models'; +import { NotificationSubscription, NotificationSubscriptionGetResponseJsonApi } from '../models'; import { environment } from 'src/environments/environment'; @@ -15,8 +15,8 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class NotificationSubscriptionService { - jsonApiService = inject(JsonApiService); - baseUrl = `${environment.apiUrl}/subscriptions/`; + private readonly jsonApiService = inject(JsonApiService); + private readonly baseUrl = `${environment.apiUrl}/subscriptions/`; getAllGlobalNotificationSubscriptions(nodeId?: string): Observable { let params: Record; @@ -32,11 +32,9 @@ export class NotificationSubscriptionService { } return this.jsonApiService - .get>(this.baseUrl, params) + .get>(this.baseUrl, params) .pipe( - map((responses) => { - return responses.data.map((response) => NotificationSubscriptionMapper.fromGetResponse(response)); - }) + map((responses) => responses.data.map((response) => NotificationSubscriptionMapper.fromGetResponse(response))) ); } @@ -48,7 +46,7 @@ export class NotificationSubscriptionService { const request = NotificationSubscriptionMapper.toUpdateRequest(id, frequency, isNodeSubscription); return this.jsonApiService - .patch(this.baseUrl + id + '/', request) + .patch(`${this.baseUrl}/${id}/`, request) .pipe(map((response) => NotificationSubscriptionMapper.fromGetResponse(response))); } } diff --git a/src/app/features/settings/notifications/store/notification-subscription.state.ts b/src/app/features/settings/notifications/store/notification-subscription.state.ts index 33976cb99..02c4fcf33 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.state.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.state.ts @@ -33,13 +33,13 @@ import { NotificationSubscriptionModel } from './notification-subscription.model }) @Injectable() export class NotificationSubscriptionState { - #notificationSubscriptionService = inject(NotificationSubscriptionService); + private readonly notificationSubscriptionService = inject(NotificationSubscriptionService); @Action(GetAllGlobalNotificationSubscriptions) getAllGlobalNotificationSubscriptions(ctx: StateContext) { ctx.setState(patch({ notificationSubscriptions: patch({ isLoading: true }) })); - return this.#notificationSubscriptionService.getAllGlobalNotificationSubscriptions().pipe( + return this.notificationSubscriptionService.getAllGlobalNotificationSubscriptions().pipe( tap((notificationSubscriptions) => { ctx.setState( patch({ @@ -58,7 +58,7 @@ export class NotificationSubscriptionState { ctx: StateContext, action: GetNotificationSubscriptionsByNodeId ) { - return this.#notificationSubscriptionService.getAllGlobalNotificationSubscriptions(action.nodeId).pipe( + return this.notificationSubscriptionService.getAllGlobalNotificationSubscriptions(action.nodeId).pipe( tap((notificationSubscriptions) => { ctx.setState( patch({ @@ -77,7 +77,7 @@ export class NotificationSubscriptionState { ctx: StateContext, action: UpdateNotificationSubscription ) { - return this.#notificationSubscriptionService.updateSubscription(action.payload.id, action.payload.frequency).pipe( + return this.notificationSubscriptionService.updateSubscription(action.payload.id, action.payload.frequency).pipe( tap((updatedSubscription) => { ctx.setState( patch({ @@ -97,7 +97,7 @@ export class NotificationSubscriptionState { ctx: StateContext, action: UpdateNotificationSubscription ) { - return this.#notificationSubscriptionService + return this.notificationSubscriptionService .updateSubscription(action.payload.id, action.payload.frequency, true) .pipe( tap((updatedSubscription) => { diff --git a/src/app/features/settings/profile-settings/components/education/education.component.html b/src/app/features/settings/profile-settings/components/education/education.component.html index 8162ca613..fa1e4da90 100644 --- a/src/app/features/settings/profile-settings/components/education/education.component.html +++ b/src/app/features/settings/profile-settings/components/education/education.component.html @@ -51,6 +51,7 @@

+ severity="info" disabled="true" /> -
+
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 5a2a16d1f..1fe57a832 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 @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -24,12 +24,12 @@ import { ProfileSettingsSelectors, UpdateProfileSettingsEducation } from '../../ }) export class EducationComponent { @HostBinding('class') classes = 'flex flex-column gap-5'; - readonly #fb = inject(FormBuilder); - protected readonly educationForm = this.#fb.group({ - educations: this.#fb.array([]), - }); - readonly #store = inject(Store); - readonly educationItems = this.#store.selectSignal(ProfileSettingsSelectors.educations); + + readonly fb = inject(FormBuilder); + protected readonly educationForm = this.fb.group({ educations: this.fb.array([]) }); + + readonly actions = createDispatchMap({ updateProfileSettingsEducation: UpdateProfileSettingsEducation }); + readonly educationItems = select(ProfileSettingsSelectors.educations); constructor() { effect(() => { @@ -37,7 +37,7 @@ export class EducationComponent { if (educations && educations.length > 0) { this.educations.clear(); educations.forEach((education) => { - const newEducation = this.#fb.group({ + const newEducation = this.fb.group({ institution: [education.institution], department: [education.department], degree: [education.degree], @@ -64,7 +64,7 @@ export class EducationComponent { } addEducation(): void { - const newEducation = this.#fb.group({ + const newEducation = this.fb.group({ institution: [''], department: [''], degree: [''], @@ -89,7 +89,7 @@ export class EducationComponent { ongoing: education.ongoing, })) satisfies Education[]; - this.#store.dispatch(new UpdateProfileSettingsEducation({ education: formattedEducation })); + this.actions.updateProfileSettingsEducation({ education: formattedEducation }); } private setupDates( 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 46d2e4d20..4a66c6db3 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 @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -11,6 +11,7 @@ import { ChangeDetectionStrategy, Component, effect, HostBinding, inject } from import { FormArray, FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { Employment } from '@osf/shared/models'; +import { CustomValidators } from '@osf/shared/utils'; import { EmploymentForm } from '../../models'; import { ProfileSettingsSelectors, UpdateProfileSettingsEmployment } from '../../store'; @@ -24,12 +25,12 @@ import { ProfileSettingsSelectors, UpdateProfileSettingsEmployment } from '../.. }) export class EmploymentComponent { @HostBinding('class') classes = 'flex flex-column gap-5'; - readonly #store = inject(Store); - readonly employment = this.#store.selectSignal(ProfileSettingsSelectors.employment); - readonly #fb = inject(FormBuilder); - readonly employmentForm = this.#fb.group({ - positions: this.#fb.array([]), - }); + + readonly actions = createDispatchMap({ updateProfileSettingsEmployment: UpdateProfileSettingsEmployment }); + readonly employment = select(ProfileSettingsSelectors.employment); + + readonly fb = inject(FormBuilder); + readonly employmentForm = this.fb.group({ positions: this.fb.array([]) }); constructor() { effect(() => { @@ -39,7 +40,7 @@ export class EmploymentComponent { this.positions.clear(); employment.forEach((position) => { - const positionGroup = this.#fb.group({ + const positionGroup = this.fb.group({ title: [position.title, Validators.required], department: [position.department], institution: [position.institution, Validators.required], @@ -63,8 +64,8 @@ export class EmploymentComponent { } addPosition(): void { - const positionGroup = this.#fb.group({ - title: ['', Validators.required], + const positionGroup = this.fb.group({ + title: ['', CustomValidators.requiredTrimmed()], department: [''], institution: ['', Validators.required], startDate: [null, Validators.required], @@ -93,7 +94,7 @@ export class EmploymentComponent { ongoing: !employment.ongoing, })) satisfies Employment[]; - this.#store.dispatch(new UpdateProfileSettingsEmployment({ employment: formattedEmployments })); + this.actions.updateProfileSettingsEmployment({ employment: formattedEmployments }); } private setupDates( diff --git a/src/app/features/settings/profile-settings/components/name/name.component.html b/src/app/features/settings/profile-settings/components/name/name.component.html index 4f468d717..b539822cb 100644 --- a/src/app/features/settings/profile-settings/components/name/name.component.html +++ b/src/app/features/settings/profile-settings/components/name/name.component.html @@ -57,6 +57,7 @@

{{ 'settings.profileSettings.name.citationPreview.title' | translate }}

+
diff --git a/src/app/features/settings/profile-settings/components/name/name.component.scss b/src/app/features/settings/profile-settings/components/name/name.component.scss index 62e06c7f4..d2453d507 100644 --- a/src/app/features/settings/profile-settings/components/name/name.component.scss +++ b/src/app/features/settings/profile-settings/components/name/name.component.scss @@ -1,28 +1,26 @@ @use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; -:host { - .name-container { - border: 1px solid var.$grey-2; - border-radius: 8px; +.name-container { + border: 1px solid var(--grey-2); + border-radius: 0.5rem; - label { - font-weight: 300; - color: var.$dark-blue-1; - } + label { + font-weight: 300; + color: var(--dark-blue-1); + } - .name-input { - width: 100%; - border: 1px solid var.$grey-2; - border-radius: 8px; - } + .name-input { + width: 100%; + border: 1px solid (--grey-2); + border-radius: 0.5rem; + } - .styles-container { - column-gap: 8.5rem; + .styles-container { + column-gap: 8.5rem; - .style-wrapper { - row-gap: 0.85rem; - } + .style-wrapper { + row-gap: 0.85rem; } } } 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 e53deade3..0adb86ebc 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 @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -13,7 +13,7 @@ import { FormArray, FormBuilder, ReactiveFormsModule, Validators } from '@angula import { Social } from '@osf/shared/models'; -import { socials } from '../../data'; +import { socials } from '../../constants/data'; import { SOCIAL_KEYS, SocialLinksForm, SocialLinksKeys, UserSocialLink } from '../../models'; import { ProfileSettingsSelectors, UpdateProfileSettingsSocialLinks } from '../../store'; @@ -26,14 +26,15 @@ import { ProfileSettingsSelectors, UpdateProfileSettingsSocialLinks } from '../. }) export class SocialComponent { @HostBinding('class') class = 'flex flex-column gap-5'; - readonly userSocialLinks: UserSocialLink[] = []; + protected readonly socials = socials; - readonly #store = inject(Store); - readonly socialLinks = this.#store.selectSignal(ProfileSettingsSelectors.socialLinks); - readonly #fb = inject(FormBuilder); - readonly socialLinksForm = this.#fb.group({ - links: this.#fb.array([]), - }); + readonly userSocialLinks: UserSocialLink[] = []; + + readonly actions = createDispatchMap({ updateProfileSettingsSocialLinks: UpdateProfileSettingsSocialLinks }); + readonly socialLinks = select(ProfileSettingsSelectors.socialLinks); + + readonly fb = inject(FormBuilder); + readonly socialLinksForm = this.fb.group({ links: this.fb.array([]) }); constructor() { effect(() => { @@ -42,7 +43,7 @@ export class SocialComponent { for (const socialLinksKey in socialLinks) { const socialLink = socialLinks[socialLinksKey as SocialLinksKeys]; - const socialLinkGroup = this.#fb.group({ + const socialLinkGroup = this.fb.group({ socialOutput: [this.socials.find((social) => social.key === socialLinksKey), Validators.required], webAddress: [socialLink, Validators.required], }); @@ -57,7 +58,7 @@ export class SocialComponent { } addLink(): void { - const linkGroup = this.#fb.group({ + const linkGroup = this.fb.group({ socialOutput: [this.socials[0], Validators.required], webAddress: ['', Validators.required], }); @@ -90,6 +91,6 @@ export class SocialComponent { }; }) satisfies Partial[]; - this.#store.dispatch(new UpdateProfileSettingsSocialLinks({ socialLinks: mappedLinks })); + this.actions.updateProfileSettingsSocialLinks({ socialLinks: mappedLinks }); } } diff --git a/src/app/features/settings/profile-settings/data.ts b/src/app/features/settings/profile-settings/constants/data.ts similarity index 97% rename from src/app/features/settings/profile-settings/data.ts rename to src/app/features/settings/profile-settings/constants/data.ts index 3bd7e4a5d..1834b37a9 100644 --- a/src/app/features/settings/profile-settings/data.ts +++ b/src/app/features/settings/profile-settings/constants/data.ts @@ -1,4 +1,4 @@ -import { SocialLinksModel } from './models'; +import { SocialLinksModel } from '../models'; export const socials: SocialLinksModel[] = [ { diff --git a/src/app/features/settings/profile-settings/constants/index.ts b/src/app/features/settings/profile-settings/constants/index.ts new file mode 100644 index 000000000..db5b8f111 --- /dev/null +++ b/src/app/features/settings/profile-settings/constants/index.ts @@ -0,0 +1 @@ +export * from './profile-settings-tab-options.const'; diff --git a/src/app/features/settings/profile-settings/constants/profile-settings-tab-options.const.ts b/src/app/features/settings/profile-settings/constants/profile-settings-tab-options.const.ts new file mode 100644 index 000000000..9a0ea2b87 --- /dev/null +++ b/src/app/features/settings/profile-settings/constants/profile-settings-tab-options.const.ts @@ -0,0 +1,10 @@ +import { TabOption } from '@osf/shared/models'; + +import { ProfileSettingsTabOption } from '../enums'; + +export const PROFILE_SETTINGS_TAB_OPTIONS: TabOption[] = [ + { label: 'settings.profileSettings.tabs.name', value: ProfileSettingsTabOption.Name }, + { label: 'settings.profileSettings.tabs.social', value: ProfileSettingsTabOption.Social }, + { label: 'settings.profileSettings.tabs.employment', value: ProfileSettingsTabOption.Employment }, + { label: 'settings.profileSettings.tabs.education', value: ProfileSettingsTabOption.Education }, +]; diff --git a/src/app/features/settings/profile-settings/enums/index.ts b/src/app/features/settings/profile-settings/enums/index.ts new file mode 100644 index 000000000..570ee4de5 --- /dev/null +++ b/src/app/features/settings/profile-settings/enums/index.ts @@ -0,0 +1 @@ +export * from './profile-settings-tab-option.enum'; diff --git a/src/app/features/settings/profile-settings/enums/profile-settings-tab-option.enum.ts b/src/app/features/settings/profile-settings/enums/profile-settings-tab-option.enum.ts new file mode 100644 index 000000000..5527c389e --- /dev/null +++ b/src/app/features/settings/profile-settings/enums/profile-settings-tab-option.enum.ts @@ -0,0 +1,6 @@ +export enum ProfileSettingsTabOption { + Name = 1, + Social, + Employment, + Education, +} diff --git a/src/app/features/settings/profile-settings/profile-settings.component.html b/src/app/features/settings/profile-settings/profile-settings.component.html index 2354e8fc3..d6ed64385 100644 --- a/src/app/features/settings/profile-settings/profile-settings.component.html +++ b/src/app/features/settings/profile-settings/profile-settings.component.html @@ -2,42 +2,37 @@
- @if (!isMobile()) { + @if (isMedium()) { - {{ 'settings.profileSettings.tabs.name' | translate }} - {{ 'settings.profileSettings.tabs.social' | translate }} - {{ 'settings.profileSettings.tabs.employment' | translate }} - {{ 'settings.profileSettings.tabs.education' | translate }} + @for (item of tabOptions; track $index) { + {{ item.label | translate }} + } } - @if (isMobile()) { - + [fullWidth]="true" + [(selectedValue)]="selectedTab" + > } - - + + - - + - - + - - + diff --git a/src/app/features/settings/profile-settings/profile-settings.component.spec.ts b/src/app/features/settings/profile-settings/profile-settings.component.spec.ts index aa7fd6c9c..4d5fdb6f2 100644 --- a/src/app/features/settings/profile-settings/profile-settings.component.spec.ts +++ b/src/app/features/settings/profile-settings/profile-settings.component.spec.ts @@ -7,7 +7,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { SubHeaderComponent } from '@osf/shared/components'; -import { IS_XSMALL } from '@osf/shared/utils'; +import { IS_MEDIUM } from '@osf/shared/utils'; import { EducationComponent, EmploymentComponent, NameComponent, SocialComponent } from './components'; import { ProfileSettingsComponent } from './profile-settings.component'; @@ -15,10 +15,10 @@ import { ProfileSettingsComponent } from './profile-settings.component'; describe('ProfileSettingsComponent', () => { let component: ProfileSettingsComponent; let fixture: ComponentFixture; - let isXSmall: BehaviorSubject; + let isMedium: BehaviorSubject; beforeEach(async () => { - isXSmall = new BehaviorSubject(false); + isMedium = new BehaviorSubject(false); await TestBed.configureTestingModule({ imports: [ @@ -26,7 +26,7 @@ describe('ProfileSettingsComponent', () => { MockPipe(TranslatePipe), ...MockComponents(SubHeaderComponent, NameComponent, SocialComponent, EmploymentComponent, EducationComponent), ], - providers: [MockProvider(IS_XSMALL, isXSmall), MockProvider(TranslateService)], + providers: [MockProvider(IS_MEDIUM, isMedium), MockProvider(TranslateService)], }).compileComponents(); fixture = TestBed.createComponent(ProfileSettingsComponent); @@ -38,10 +38,6 @@ describe('ProfileSettingsComponent', () => { expect(component).toBeTruthy(); }); - it('should initialize with default tab value', () => { - expect(fixture.componentInstance['selectedTab']).toBe(fixture.componentInstance['defaultTabValue']); - }); - it('should update selected tab when onTabChange is called', () => { const newTabIndex = 2; component.onTabChange(newTabIndex); @@ -53,30 +49,6 @@ describe('ProfileSettingsComponent', () => { expect(tabElements.length).toBe(fixture.componentInstance['tabOptions'].length); }); - it('should show select dropdown in mobile view', () => { - isXSmall.next(true); - fixture.detectChanges(); - - const selectElement = fixture.debugElement.query(By.css('p-select')); - expect(selectElement).toBeTruthy(); - }); - - it('should hide tab list in mobile view', () => { - isXSmall.next(true); - fixture.detectChanges(); - - const tabListElement = fixture.debugElement.query(By.css('p-tablist')); - expect(tabListElement).toBeFalsy(); - }); - - it('should show tab list in desktop view', () => { - isXSmall.next(false); - fixture.detectChanges(); - - const tabListElement = fixture.debugElement.query(By.css('p-tablist')); - expect(tabListElement).toBeTruthy(); - }); - it('should render all tab panels', () => { const tabPanels = fixture.debugElement.queryAll(By.css('p-tabpanel')); expect(tabPanels.length).toBe(4); 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 cbe07e2a7..17951d100 100644 --- a/src/app/features/settings/profile-settings/profile-settings.component.ts +++ b/src/app/features/settings/profile-settings/profile-settings.component.ts @@ -1,17 +1,17 @@ import { TranslatePipe } from '@ngx-translate/core'; -import { Select } from 'primeng/select'; import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { SubHeaderComponent } from '@osf/shared/components'; -import { TabOption } from '@osf/shared/models'; -import { IS_XSMALL } from '@osf/shared/utils'; +import { SelectComponent, SubHeaderComponent } from '@osf/shared/components'; +import { IS_MEDIUM } from '@osf/shared/utils'; import { EducationComponent, EmploymentComponent, NameComponent, SocialComponent } from './components'; +import { PROFILE_SETTINGS_TAB_OPTIONS } from './constants'; +import { ProfileSettingsTabOption } from './enums'; @Component({ selector: 'osf-profile-settings', @@ -23,28 +23,24 @@ import { EducationComponent, EmploymentComponent, NameComponent, SocialComponent TabPanel, TabPanels, ReactiveFormsModule, - Select, FormsModule, EducationComponent, EmploymentComponent, NameComponent, SocialComponent, TranslatePipe, + SelectComponent, ], templateUrl: './profile-settings.component.html', styleUrl: './profile-settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProfileSettingsComponent { - protected defaultTabValue = 0; - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - protected readonly tabOptions: TabOption[] = [ - { label: 'Name', value: 0 }, - { label: 'Social', value: 1 }, - { label: 'Employment', value: 2 }, - { label: 'Education', value: 3 }, - ]; - protected selectedTab = this.defaultTabValue; + protected readonly isMedium = toSignal(inject(IS_MEDIUM)); + protected readonly tabOptions = PROFILE_SETTINGS_TAB_OPTIONS; + protected readonly tabOption = ProfileSettingsTabOption; + + protected selectedTab = this.tabOption.Name; onTabChange(index: number): void { this.selectedTab = index; diff --git a/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts b/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts index 4838927cf..27bd1a6ea 100644 --- a/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts +++ b/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts @@ -11,11 +11,12 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class ProfileSettingsApiService { - readonly #jsonApiService = inject(JsonApiService); + private readonly jsonApiService = inject(JsonApiService); patchUserSettings(userId: string, key: keyof ProfileSettingsStateModel, data: ProfileSettingsUpdate) { const patchedData = { [key]: data }; - return this.#jsonApiService.patch>(`${environment.apiUrl}users/${userId}/`, { + + return this.jsonApiService.patch>(`${environment.apiUrl}users/${userId}/`, { data: { type: 'users', id: userId, attributes: patchedData }, }); } diff --git a/src/app/features/settings/profile-settings/store/profile-settings.state.ts b/src/app/features/settings/profile-settings/store/profile-settings.state.ts index 3a8728810..35191904a 100644 --- a/src/app/features/settings/profile-settings/store/profile-settings.state.ts +++ b/src/app/features/settings/profile-settings/store/profile-settings.state.ts @@ -1,9 +1,10 @@ import { Action, State, StateContext, Store } from '@ngxs/store'; -import { tap } from 'rxjs'; +import { catchError, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { handleSectionError } from '@osf/core/handlers'; import { UserSelectors } from '@osf/core/store/user'; import { removeNullable } from '@osf/shared/constants'; import { Social } from '@osf/shared/models'; @@ -30,13 +31,13 @@ import { }) @Injectable() export class ProfileSettingsState { - readonly #store = inject(Store); - readonly #profileSettingsService = inject(ProfileSettingsApiService); + private readonly store = inject(Store); + private readonly profileSettingsService = inject(ProfileSettingsApiService); @Action(SetupProfileSettings) setupProfileSettings(ctx: StateContext): void { const state = ctx.getState(); - const profileSettings = this.#store.selectSnapshot(UserSelectors.getProfileSettings); + const profileSettings = this.store.selectSnapshot(UserSelectors.getProfileSettings); ctx.patchState({ ...state, @@ -56,11 +57,9 @@ export class ProfileSettingsState { return; } - const withoutNulls = payload.employment.map((item) => { - return removeNullable(item); - }); + const withoutNulls = payload.employment.map((item) => removeNullable(item)); - return this.#profileSettingsService.patchUserSettings(userId, 'employment', withoutNulls).pipe( + return this.profileSettingsService.patchUserSettings(userId, 'employment', withoutNulls).pipe( tap((response) => { ctx.patchState({ ...state, @@ -82,17 +81,16 @@ export class ProfileSettingsState { return; } - const withoutNulls = payload.education.map((item) => { - return removeNullable(item); - }); + const withoutNulls = payload.education.map((item) => removeNullable(item)); - return this.#profileSettingsService.patchUserSettings(userId, 'education', withoutNulls).pipe( + return this.profileSettingsService.patchUserSettings(userId, 'education', withoutNulls).pipe( tap((response) => { ctx.patchState({ ...state, education: response.data.attributes.education, }); - }) + }), + catchError((error) => handleSectionError(ctx, 'education', error)) ); } @@ -107,7 +105,7 @@ export class ProfileSettingsState { const withoutNulls = mapNameToDto(removeNullable(payload.user)); - return this.#profileSettingsService.patchUserSettings(userId, 'user', withoutNulls).pipe( + return this.profileSettingsService.patchUserSettings(userId, 'user', withoutNulls).pipe( tap((response) => { ctx.patchState({ ...state, @@ -138,7 +136,7 @@ export class ProfileSettingsState { }; }); - return this.#profileSettingsService.patchUserSettings(userId, 'social', social).pipe( + return this.profileSettingsService.patchUserSettings(userId, 'social', social).pipe( tap((response) => { ctx.patchState({ ...state, From ff7adf4bafbab8bb91ce7626c20076e45e4f731e Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 10 Jul 2025 14:43:03 +0300 Subject: [PATCH 2/7] fix(search): updated search and my profile search --- .../confirm-email/confirm-email.component.ts | 15 +-- src/app/features/home/home.component.scss | 13 ++- .../my-profile-filter-chips.component.ts | 27 ++--- ...my-profile-resource-filters.component.scss | 20 ++-- .../my-profile-resource-filters.component.ts | 52 ++++----- .../my-profile-resource-filters.state.ts | 7 +- .../my-profile-resources.component.html | 25 ++--- .../my-profile-resources.component.scss | 102 +++++++++--------- .../my-profile-resources.component.ts | 54 +++++----- .../my-profile-search.component.html | 8 +- .../my-profile-search.component.scss | 7 +- .../my-profile-search.component.ts | 52 ++++----- .../my-profile/my-profile.component.scss | 15 ++- .../my-profile-resource-filters.service.ts | 36 +++---- .../filter-chips/filter-chips.component.ts | 29 +++-- .../resource-filters.component.scss | 18 ++-- .../resource-filters.component.ts | 43 +++----- .../resources-wrapper.component.ts | 95 ++++++++-------- .../resources/resources.component.html | 27 ++--- .../resources/resources.component.scss | 98 +++++++++-------- .../resources/resources.component.ts | 52 ++++----- src/app/features/search/search.component.scss | 4 +- src/app/features/search/search.component.ts | 42 ++++---- .../services/resource-filters.service.ts | 34 +++--- 24 files changed, 404 insertions(+), 471 deletions(-) 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 cf141caf6..7d781d2db 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 @@ -23,28 +23,29 @@ import { LoadingSpinnerComponent } from '@shared/components'; export class ConfirmEmailComponent { readonly dialogRef = inject(DynamicDialogRef); readonly config = inject(DynamicDialogConfig); - readonly #router = inject(Router); - readonly #accountSettingsService = inject(AccountSettingsService); - readonly #destroyRef = inject(DestroyRef); + + private readonly router = inject(Router); + private readonly accountSettingsService = inject(AccountSettingsService); + private readonly destroyRef = inject(DestroyRef); verifyingEmail = signal(false); closeDialog() { - this.#router.navigate(['/home']); + this.router.navigate(['/home']); this.dialogRef.close(); } verifyEmail() { this.verifyingEmail.set(true); - this.#accountSettingsService + this.accountSettingsService .confirmEmail(this.config.data.userId, this.config.data.token) .pipe( - takeUntilDestroyed(this.#destroyRef), + takeUntilDestroyed(this.destroyRef), finalize(() => this.verifyingEmail.set(false)) ) .subscribe({ next: () => { - this.#router.navigate(['/settings/account-settings']); + this.router.navigate(['/settings/account-settings']); this.dialogRef.close(); }, error: () => this.closeDialog(), diff --git a/src/app/features/home/home.component.scss b/src/app/features/home/home.component.scss index 3b68f25f5..e146c9417 100644 --- a/src/app/features/home/home.component.scss +++ b/src/app/features/home/home.component.scss @@ -1,4 +1,3 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; :host { @@ -12,29 +11,29 @@ } .quick-search-container { - background-color: var.$white; + background-color: var(--white); .text-center { - color: var.$dark-blue-1; + color: var(--dark-blue-1); } } .public-projects-container { - background-color: var.$gradient-1; + background-color: var(--gradient-1); .osf-icon-search { - color: var.$dark-blue-1; + color: var(--dark-blue-1); font-size: mix.rem(32px); margin-right: mix.rem(12px); } } .latest-research-container { - background-color: var.$bg-blue-3; + background-color: var(--bg-blue-3); row-gap: mix.rem(24px); } .hosting-container { - background-color: var.$bg-blue-2; + background-color: var(--bg-blue-2); row-gap: mix.rem(24px); } diff --git a/src/app/features/my-profile/components/my-profile-filter-chips/my-profile-filter-chips.component.ts b/src/app/features/my-profile/components/my-profile-filter-chips/my-profile-filter-chips.component.ts index 64f3e0588..9162924b5 100644 --- a/src/app/features/my-profile/components/my-profile-filter-chips/my-profile-filter-chips.component.ts +++ b/src/app/features/my-profile/components/my-profile-filter-chips/my-profile-filter-chips.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { Chip } from 'primeng/chip'; @@ -28,40 +28,41 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyProfileFilterChipsComponent { - readonly #store = inject(Store); + readonly store = inject(Store); - protected filters = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getAllFilters); + protected filters = select(MyProfileResourceFiltersSelectors.getAllFilters); - readonly isMyProfilePage = this.#store.selectSignal(MyProfileSelectors.getIsMyProfile); + readonly isMyProfilePage = select(MyProfileSelectors.getIsMyProfile); clearFilter(filter: FilterType) { switch (filter) { case FilterType.DateCreated: - this.#store.dispatch(new SetDateCreated('')); + this.store.dispatch(new SetDateCreated('')); break; case FilterType.Funder: - this.#store.dispatch(new SetFunder('', '')); + this.store.dispatch(new SetFunder('', '')); break; case FilterType.Subject: - this.#store.dispatch(new SetSubject('', '')); + this.store.dispatch(new SetSubject('', '')); break; case FilterType.License: - this.#store.dispatch(new SetLicense('', '')); + this.store.dispatch(new SetLicense('', '')); break; case FilterType.ResourceType: - this.#store.dispatch(new SetResourceType('', '')); + this.store.dispatch(new SetResourceType('', '')); break; case FilterType.Institution: - this.#store.dispatch(new SetInstitution('', '')); + this.store.dispatch(new SetInstitution('', '')); break; case FilterType.Provider: - this.#store.dispatch(new SetProvider('', '')); + this.store.dispatch(new SetProvider('', '')); break; case FilterType.PartOfCollection: - this.#store.dispatch(new SetPartOfCollection('', '')); + this.store.dispatch(new SetPartOfCollection('', '')); break; } - this.#store.dispatch(GetAllOptions); + + this.store.dispatch(GetAllOptions); } protected readonly FilterType = FilterType; diff --git a/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.scss b/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.scss index 74ec4adc2..600c1aab8 100644 --- a/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.scss +++ b/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.scss @@ -1,15 +1,13 @@ -@use "assets/styles/variables" as var; - :host { width: 30%; +} - .filters { - border: 1px solid var.$grey-2; - border-radius: 12px; - padding: 0 1.7rem 0 1.7rem; - display: flex; - flex-direction: column; - row-gap: 0.8rem; - height: fit-content; - } +.filters { + border: 1px solid var(--grey-2); + border-radius: 12px; + padding: 0 1.7rem 0 1.7rem; + display: flex; + flex-direction: column; + row-gap: 0.8rem; + height: fit-content; } diff --git a/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts b/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts index c20df2aa3..cc99dd232 100644 --- a/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts +++ b/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts @@ -1,8 +1,8 @@ -import { Store } from '@ngxs/store'; +import { select } from '@ngxs/store'; import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; -import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; import { MyProfileSelectors } from '../../store'; import { @@ -38,57 +38,51 @@ import { MyProfileResourceFiltersOptionsSelectors } from '../filters/store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyProfileResourceFiltersComponent { - readonly #store = inject(Store); - readonly datesOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getDatesCreated)() - .reduce((accumulator, date) => accumulator + date.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getDatesCreated)().reduce( + (accumulator, date) => accumulator + date.count, + 0 + ); }); readonly funderOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getFunders)() - .reduce((acc, item) => acc + item.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getFunders)().reduce((acc, item) => acc + item.count, 0); }); readonly subjectOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getSubjects)() - .reduce((acc, item) => acc + item.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getSubjects)().reduce((acc, item) => acc + item.count, 0); }); readonly licenseOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getLicenses)() - .reduce((acc, item) => acc + item.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getLicenses)().reduce((acc, item) => acc + item.count, 0); }); readonly resourceTypeOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getResourceTypes)() - .reduce((acc, item) => acc + item.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getResourceTypes)().reduce( + (acc, item) => acc + item.count, + 0 + ); }); readonly institutionOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getInstitutions)() - .reduce((acc, item) => acc + item.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getInstitutions)().reduce( + (acc, item) => acc + item.count, + 0 + ); }); readonly providerOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getProviders)() - .reduce((acc, item) => acc + item.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getProviders)().reduce((acc, item) => acc + item.count, 0); }); readonly partOfCollectionOptionsCount = computed(() => { - return this.#store - .selectSignal(MyProfileResourceFiltersOptionsSelectors.getPartOfCollection)() - .reduce((acc, item) => acc + item.count, 0); + return select(MyProfileResourceFiltersOptionsSelectors.getPartOfCollection)().reduce( + (acc, item) => acc + item.count, + 0 + ); }); - readonly isMyProfilePage = this.#store.selectSignal(MyProfileSelectors.getIsMyProfile); + readonly isMyProfilePage = select(MyProfileSelectors.getIsMyProfile); readonly anyOptionsCount = computed(() => { return ( diff --git a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts index 6bed76c34..c92c0c3f4 100644 --- a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts +++ b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts @@ -19,18 +19,17 @@ import { } from './my-profile-resource-filters.actions'; import { MyProfileResourceFiltersStateModel } from './my-profile-resource-filters.model'; -// Store for user selected filters values @State({ name: 'myProfileResourceFilters', defaults: resourceFiltersDefaults, }) @Injectable() export class MyProfileResourceFiltersState implements NgxsOnInit { - #store = inject(Store); - #currentUser = this.#store.select(UserSelectors.getCurrentUser); + store = inject(Store); + currentUser = this.store.select(UserSelectors.getCurrentUser); ngxsOnInit(ctx: StateContext) { - this.#currentUser.subscribe((user) => { + this.currentUser.subscribe((user) => { if (user) { ctx.patchState({ creator: { diff --git a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html index 37fe757c6..6880e09c5 100644 --- a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html +++ b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html @@ -1,34 +1,27 @@
@if (isMobile()) { - + } + @if (searchCount() > 10000) { -

10 000+ results

+

{{ 'collections.searchResults.10000results' | translate }}

} @else if (searchCount() > 0) { -

{{ searchCount() }} results

+

{{ searchCount() }} {{ 'collections.searchResults.results' | translate }}

} @else { -

0 results

+

{{ 'collections.searchResults.noResults' | translate }}

}
@if (isWeb()) { -

Sort by:

- {{ 'collections.filters.sortBy' | translate }}:

+ + [(selectedValue)]="selectedSort" + > } @else { @if (isAnyFilterOptions()) { diff --git a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.scss b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.scss index 67a66fa96..aeda3cb11 100644 --- a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.scss +++ b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.scss @@ -1,71 +1,67 @@ -@use "assets/styles/variables" as var; - -:host { - h3 { - color: var.$pr-blue-1; - } +h3 { + color: var(--pr-blue-1); +} - .sorting-container { - display: flex; - align-items: center; +.sorting-container { + display: flex; + align-items: center; - h3 { - color: var.$dark-blue-1; - font-weight: 400; - text-wrap: nowrap; - margin-right: 0.5rem; - } + h3 { + color: var(--dark-blue-1); + font-weight: 400; + text-wrap: nowrap; + margin-right: 0.5rem; } +} - .filter-full-size { - flex: 1; - } +.filter-full-size { + flex: 1; +} - .sort-card { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 44px; - border: 1px solid var.$grey-2; - border-radius: 12px; - padding: 0 1.7rem 0 1.7rem; - cursor: pointer; - } +.sort-card { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 44px; + border: 1px solid var(--grey-2); + border-radius: 12px; + padding: 0 1.7rem 0 1.7rem; + cursor: pointer; +} - .card-selected { - background: var.$bg-blue-2; - } +.card-selected { + background: var(--bg-blue-2); +} - .filters-resources-web { - .resources-container { - flex: 1; +.filters-resources-web { + .resources-container { + flex: 1; - .resources-list { - width: 100%; - display: flex; - flex-direction: column; - row-gap: 0.85rem; - } + .resources-list { + width: 100%; + display: flex; + flex-direction: column; + row-gap: 0.85rem; + } - .switch-icon { - &:hover { - cursor: pointer; - } + .switch-icon { + &:hover { + cursor: pointer; } + } - .icon-disabled { - opacity: 0.5; - cursor: none; - } + .icon-disabled { + opacity: 0.5; + cursor: none; + } - .icon-active { - fill: var.$grey-1; - } + .icon-active { + fill: var(--grey-1); } } } .switch-icon { - color: var.$grey-1; + color: var(--grey-1); } diff --git a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts index 1a4bbe66f..77deb9dae 100644 --- a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts +++ b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts @@ -1,18 +1,20 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DataView } from 'primeng/dataview'; -import { Select } from 'primeng/select'; import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { MyProfileFilterChipsComponent, MyProfileResourceFiltersComponent } from '@osf/features/my-profile/components'; +import { SelectComponent } from '@osf/shared/components'; import { ResourceTab } from '@osf/shared/enums'; import { IS_WEB, IS_XSMALL } from '@osf/shared/utils'; import { ResourceCardComponent } from '@shared/components/resource-card/resource-card.component'; -import { searchSortingOptions } from '@shared/constants'; +import { SEARCH_TAB_OPTIONS, searchSortingOptions } from '@shared/constants'; import { GetResourcesByLink, MyProfileSelectors, SetResourceTab, SetSortBy } from '../../store'; import { MyProfileResourceFiltersOptionsSelectors } from '../filters/store'; @@ -24,34 +26,40 @@ import { MyProfileResourceFiltersSelectors } from '../my-profile-resource-filter DataView, MyProfileFilterChipsComponent, MyProfileResourceFiltersComponent, - Select, FormsModule, ResourceCardComponent, Button, + SelectComponent, + TranslatePipe, ], templateUrl: './my-profile-resources.component.html', styleUrl: './my-profile-resources.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyProfileResourcesComponent { - readonly #store = inject(Store); + private readonly actions = createDispatchMap({ + getResourcesByLink: GetResourcesByLink, + setResourceTab: SetResourceTab, + setSortBy: SetSortBy, + }); + protected readonly searchSortingOptions = searchSortingOptions; - selectedTabStore = this.#store.selectSignal(MyProfileSelectors.getResourceTab); - searchCount = this.#store.selectSignal(MyProfileSelectors.getResourcesCount); - resources = this.#store.selectSignal(MyProfileSelectors.getResources); - sortBy = this.#store.selectSignal(MyProfileSelectors.getSortBy); - first = this.#store.selectSignal(MyProfileSelectors.getFirst); - next = this.#store.selectSignal(MyProfileSelectors.getNext); - prev = this.#store.selectSignal(MyProfileSelectors.getPrevious); + selectedTabStore = select(MyProfileSelectors.getResourceTab); + searchCount = select(MyProfileSelectors.getResourcesCount); + resources = select(MyProfileSelectors.getResources); + sortBy = select(MyProfileSelectors.getSortBy); + first = select(MyProfileSelectors.getFirst); + next = select(MyProfileSelectors.getNext); + prev = select(MyProfileSelectors.getPrevious); isWeb = toSignal(inject(IS_WEB)); isFiltersOpen = signal(false); isSortingOpen = signal(false); - protected filters = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getAllFilters); - protected filtersOptions = this.#store.selectSignal(MyProfileResourceFiltersOptionsSelectors.getAllOptions); + protected filters = select(MyProfileResourceFiltersSelectors.getAllFilters); + protected filtersOptions = select(MyProfileResourceFiltersOptionsSelectors.getAllOptions); protected isAnyFilterSelected = computed(() => { return ( this.filters().dateCreated.value || @@ -81,18 +89,10 @@ export class MyProfileResourcesComponent { protected selectedSort = signal(''); + protected readonly tabsOptions = SEARCH_TAB_OPTIONS.filter((x) => x.value !== ResourceTab.Users); protected selectedTab = signal(ResourceTab.All); - protected readonly tabsOptions = [ - { label: 'All', value: ResourceTab.All }, - { label: 'Projects', value: ResourceTab.Projects }, - { label: 'Registrations', value: ResourceTab.Registrations }, - { label: 'Preprints', value: ResourceTab.Preprints }, - { label: 'Files', value: ResourceTab.Files }, - { label: 'Users', value: ResourceTab.Users }, - ]; constructor() { - // if new value for sorting in store, update value in dropdown effect(() => { const storeValue = this.sortBy(); const currentInput = untracked(() => this.selectedSort()); @@ -102,13 +102,12 @@ export class MyProfileResourcesComponent { } }); - // if the sorting was changed, set new value to store effect(() => { const chosenValue = this.selectedSort(); const storeValue = untracked(() => this.sortBy()); if (chosenValue !== storeValue) { - this.#store.dispatch(new SetSortBy(chosenValue)); + this.actions.setSortBy(chosenValue); } }); @@ -126,14 +125,13 @@ export class MyProfileResourcesComponent { const storeValue = untracked(() => this.selectedTabStore()); if (chosenValue !== storeValue) { - this.#store.dispatch(new SetResourceTab(chosenValue)); + this.actions.setResourceTab(chosenValue); } }); } - // pagination switchPage(link: string) { - this.#store.dispatch(new GetResourcesByLink(link)); + this.actions.getResourcesByLink(link); } openFilters() { diff --git a/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.html b/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.html index 4066a1f5e..5d932472a 100644 --- a/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.html +++ b/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.html @@ -11,11 +11,9 @@ @if (!isMobile()) { - All - Projects - Registrations - Preprints - Files + @for (item of resourceTabOptions; track $index) { + {{ item.label | translate }} + } } diff --git a/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.scss b/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.scss index 1d515fd2d..8f2a65cb5 100644 --- a/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.scss +++ b/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.scss @@ -1,4 +1,3 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; .search-container { @@ -14,15 +13,15 @@ .resources { position: relative; - background: var.$white; + background: var(--white); } .stepper { position: absolute; display: flex; flex-direction: column; - background: var.$white; - border: 1px solid var.$grey-2; + background: var(--white); + border: 1px solid var(--grey-2); border-radius: 12px; row-gap: mix.rem(24px); padding: mix.rem(24px); diff --git a/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.ts b/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.ts index 7c6a7b3e2..a5c415689 100644 --- a/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.ts +++ b/src/app/features/my-profile/components/my-profile-search/my-profile-search.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -12,6 +12,7 @@ import { FormControl } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; import { SearchHelpTutorialComponent, SearchInputComponent } from '@osf/shared/components'; +import { SEARCH_TAB_OPTIONS } from '@osf/shared/constants'; import { ResourceTab } from '@osf/shared/enums'; import { IS_XSMALL } from '@osf/shared/utils'; @@ -36,39 +37,38 @@ import { MyProfileResourcesComponent } from '../my-profile-resources/my-profile- changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyProfileSearchComponent { - readonly #store = inject(Store); + readonly store = inject(Store); protected searchControl = new FormControl(''); protected readonly isMobile = toSignal(inject(IS_XSMALL)); private readonly destroyRef = inject(DestroyRef); - protected readonly dateCreatedFilter = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getDateCreated); - protected readonly funderFilter = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getFunder); - protected readonly subjectFilter = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getSubject); - protected readonly licenseFilter = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getLicense); - protected readonly resourceTypeFilter = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getResourceType); - protected readonly institutionFilter = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getInstitution); - protected readonly providerFilter = this.#store.selectSignal(MyProfileResourceFiltersSelectors.getProvider); - protected readonly partOfCollectionFilter = this.#store.selectSignal( - MyProfileResourceFiltersSelectors.getPartOfCollection - ); - protected searchStoreValue = this.#store.selectSignal(MyProfileSelectors.getSearchText); - protected resourcesTabStoreValue = this.#store.selectSignal(MyProfileSelectors.getResourceTab); - protected sortByStoreValue = this.#store.selectSignal(MyProfileSelectors.getSortBy); - readonly isMyProfilePage = this.#store.selectSignal(MyProfileSelectors.getIsMyProfile); - readonly currentUser = this.#store.select(UserSelectors.getCurrentUser); - + protected readonly dateCreatedFilter = select(MyProfileResourceFiltersSelectors.getDateCreated); + protected readonly funderFilter = select(MyProfileResourceFiltersSelectors.getFunder); + protected readonly subjectFilter = select(MyProfileResourceFiltersSelectors.getSubject); + protected readonly licenseFilter = select(MyProfileResourceFiltersSelectors.getLicense); + protected readonly resourceTypeFilter = select(MyProfileResourceFiltersSelectors.getResourceType); + protected readonly institutionFilter = select(MyProfileResourceFiltersSelectors.getInstitution); + protected readonly providerFilter = select(MyProfileResourceFiltersSelectors.getProvider); + protected readonly partOfCollectionFilter = select(MyProfileResourceFiltersSelectors.getPartOfCollection); + protected searchStoreValue = select(MyProfileSelectors.getSearchText); + protected resourcesTabStoreValue = select(MyProfileSelectors.getResourceTab); + protected sortByStoreValue = select(MyProfileSelectors.getSortBy); + readonly isMyProfilePage = select(MyProfileSelectors.getIsMyProfile); + readonly currentUser = this.store.select(UserSelectors.getCurrentUser); + + protected readonly resourceTabOptions = SEARCH_TAB_OPTIONS.filter((x) => x.value !== ResourceTab.Users); protected selectedTab: ResourceTab = ResourceTab.All; - protected readonly ResourceTab = ResourceTab; + protected currentStep = signal(0); private skipInitializationEffects = 0; constructor() { this.currentUser.subscribe((user) => { if (user?.id) { - this.#store.dispatch(GetAllOptions); - this.#store.dispatch(GetResources); + this.store.dispatch(GetAllOptions); + this.store.dispatch(GetResources); } }); @@ -85,7 +85,7 @@ export class MyProfileSearchComponent { this.resourcesTabStoreValue(); this.sortByStoreValue(); if (this.skipInitializationEffects > 0) { - this.#store.dispatch(GetResources); + this.store.dispatch(GetResources); } this.skipInitializationEffects += 1; }); @@ -93,8 +93,8 @@ export class MyProfileSearchComponent { this.searchControl.valueChanges .pipe(skip(1), debounceTime(500), takeUntilDestroyed(this.destroyRef)) .subscribe((searchText) => { - this.#store.dispatch(new SetSearchText(searchText ?? '')); - this.#store.dispatch(GetAllOptions); + this.store.dispatch(new SetSearchText(searchText ?? '')); + this.store.dispatch(GetAllOptions); }); effect(() => { @@ -114,9 +114,9 @@ export class MyProfileSearchComponent { } onTabChange(index: ResourceTab): void { - this.#store.dispatch(new SetResourceTab(index)); + this.store.dispatch(new SetResourceTab(index)); this.selectedTab = index; - this.#store.dispatch(GetAllOptions); + this.store.dispatch(GetAllOptions); } showTutorial() { diff --git a/src/app/features/my-profile/my-profile.component.scss b/src/app/features/my-profile/my-profile.component.scss index f7fa85b5d..7fa294908 100644 --- a/src/app/features/my-profile/my-profile.component.scss +++ b/src/app/features/my-profile/my-profile.component.scss @@ -1,15 +1,12 @@ -@use "assets/styles/variables" as var; -@use "assets/styles/mixins" as mix; +:host { + flex: 1; +} .cards { - background-color: var.$white; + background-color: var(--white); } .card { - border: 1px solid var.$grey-2; - border-radius: mix.rem(12px); -} - -:host { - flex: 1; + border: 1px solid var(--grey-2); + border-radius: 0.75rem; } diff --git a/src/app/features/my-profile/services/my-profile-resource-filters.service.ts b/src/app/features/my-profile/services/my-profile-resource-filters.service.ts index d7b587822..b553e9ee9 100644 --- a/src/app/features/my-profile/services/my-profile-resource-filters.service.ts +++ b/src/app/features/my-profile/services/my-profile-resource-filters.service.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; @@ -24,20 +24,20 @@ import { MyProfileSelectors } from '../store'; providedIn: 'root', }) export class MyProfileFiltersOptionsService { - #store = inject(Store); - #filtersOptions = inject(FiltersOptionsService); + private readonly store = inject(Store); + private readonly filtersOptions = inject(FiltersOptionsService); - #getFilterParams(): Record { - return addFiltersParams(this.#store.selectSignal(MyProfileResourceFiltersSelectors.getAllFilters)()); + getFilterParams(): Record { + return addFiltersParams(select(MyProfileResourceFiltersSelectors.getAllFilters)()); } - #getParams(): Record { + getParams(): Record { const params: Record = {}; - const resourceTab = this.#store.selectSnapshot(MyProfileSelectors.getResourceTab); + const resourceTab = this.store.selectSnapshot(MyProfileSelectors.getResourceTab); const resourceTypes = getResourceTypes(resourceTab); - const searchText = this.#store.selectSnapshot(MyProfileSelectors.getSearchText); - const sort = this.#store.selectSnapshot(MyProfileSelectors.getSortBy); - const user = this.#store.selectSnapshot(UserSelectors.getCurrentUser); + const searchText = this.store.selectSnapshot(MyProfileSelectors.getSearchText); + const sort = this.store.selectSnapshot(MyProfileSelectors.getSortBy); + const user = this.store.selectSnapshot(UserSelectors.getCurrentUser); params['cardSearchFilter[resourceType]'] = resourceTypes; params['cardSearchFilter[accessService]'] = 'https://staging4.osf.io/'; @@ -49,34 +49,34 @@ export class MyProfileFiltersOptionsService { } getDates(): Observable { - return this.#filtersOptions.getDates(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getDates(this.getParams(), this.getFilterParams()); } getFunders(): Observable { - return this.#filtersOptions.getFunders(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getFunders(this.getParams(), this.getFilterParams()); } getSubjects(): Observable { - return this.#filtersOptions.getSubjects(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getSubjects(this.getParams(), this.getFilterParams()); } getLicenses(): Observable { - return this.#filtersOptions.getLicenses(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getLicenses(this.getParams(), this.getFilterParams()); } getResourceTypes(): Observable { - return this.#filtersOptions.getResourceTypes(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getResourceTypes(this.getParams(), this.getFilterParams()); } getInstitutions(): Observable { - return this.#filtersOptions.getInstitutions(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getInstitutions(this.getParams(), this.getFilterParams()); } getProviders(): Observable { - return this.#filtersOptions.getProviders(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getProviders(this.getParams(), this.getFilterParams()); } getPartOtCollections(): Observable { - return this.#filtersOptions.getPartOtCollections(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getPartOtCollections(this.getParams(), this.getFilterParams()); } } diff --git a/src/app/features/search/components/filter-chips/filter-chips.component.ts b/src/app/features/search/components/filter-chips/filter-chips.component.ts index 1f73dedfd..afabc3332 100644 --- a/src/app/features/search/components/filter-chips/filter-chips.component.ts +++ b/src/app/features/search/components/filter-chips/filter-chips.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { Chip } from 'primeng/chip'; @@ -29,43 +29,42 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class FilterChipsComponent { - readonly #store = inject(Store); + readonly store = inject(Store); - protected filters = this.#store.selectSignal(ResourceFiltersSelectors.getAllFilters); - - readonly isMyProfilePage = this.#store.selectSignal(SearchSelectors.getIsMyProfile); + protected filters = select(ResourceFiltersSelectors.getAllFilters); + readonly isMyProfilePage = select(SearchSelectors.getIsMyProfile); clearFilter(filter: FilterType) { switch (filter) { case FilterType.Creator: - this.#store.dispatch(new SetCreator('', '')); + this.store.dispatch(new SetCreator('', '')); break; case FilterType.DateCreated: - this.#store.dispatch(new SetDateCreated('')); + this.store.dispatch(new SetDateCreated('')); break; case FilterType.Funder: - this.#store.dispatch(new SetFunder('', '')); + this.store.dispatch(new SetFunder('', '')); break; case FilterType.Subject: - this.#store.dispatch(new SetSubject('', '')); + this.store.dispatch(new SetSubject('', '')); break; case FilterType.License: - this.#store.dispatch(new SetLicense('', '')); + this.store.dispatch(new SetLicense('', '')); break; case FilterType.ResourceType: - this.#store.dispatch(new SetResourceType('', '')); + this.store.dispatch(new SetResourceType('', '')); break; case FilterType.Institution: - this.#store.dispatch(new SetInstitution('', '')); + this.store.dispatch(new SetInstitution('', '')); break; case FilterType.Provider: - this.#store.dispatch(new SetProvider('', '')); + this.store.dispatch(new SetProvider('', '')); break; case FilterType.PartOfCollection: - this.#store.dispatch(new SetPartOfCollection('', '')); + this.store.dispatch(new SetPartOfCollection('', '')); break; } - this.#store.dispatch(GetAllOptions); + this.store.dispatch(GetAllOptions); } protected readonly FilterType = FilterType; diff --git a/src/app/features/search/components/resource-filters/resource-filters.component.scss b/src/app/features/search/components/resource-filters/resource-filters.component.scss index 74ec4adc2..8a9b88d9e 100644 --- a/src/app/features/search/components/resource-filters/resource-filters.component.scss +++ b/src/app/features/search/components/resource-filters/resource-filters.component.scss @@ -2,14 +2,14 @@ :host { width: 30%; +} - .filters { - border: 1px solid var.$grey-2; - border-radius: 12px; - padding: 0 1.7rem 0 1.7rem; - display: flex; - flex-direction: column; - row-gap: 0.8rem; - height: fit-content; - } +.filters { + border: 1px solid var.$grey-2; + border-radius: 12px; + padding: 0 1.7rem 0 1.7rem; + display: flex; + flex-direction: column; + row-gap: 0.8rem; + height: fit-content; } diff --git a/src/app/features/search/components/resource-filters/resource-filters.component.ts b/src/app/features/search/components/resource-filters/resource-filters.component.ts index c63ec60b3..9b9f62fe4 100644 --- a/src/app/features/search/components/resource-filters/resource-filters.component.ts +++ b/src/app/features/search/components/resource-filters/resource-filters.component.ts @@ -1,8 +1,8 @@ -import { Store } from '@ngxs/store'; +import { select } from '@ngxs/store'; import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; -import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { SearchSelectors } from '../../store'; @@ -42,57 +42,42 @@ import { ResourceFiltersOptionsSelectors } from '../filters/store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ResourceFiltersComponent { - readonly #store = inject(Store); - readonly datesOptionsCount = computed(() => { - return this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getDatesCreated)() - .reduce((accumulator, date) => accumulator + date.count, 0); + return select(ResourceFiltersOptionsSelectors.getDatesCreated)().reduce( + (accumulator, date) => accumulator + date.count, + 0 + ); }); readonly funderOptionsCount = computed(() => - this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getFunders)() - .reduce((acc, item) => acc + item.count, 0) + select(ResourceFiltersOptionsSelectors.getFunders)().reduce((acc, item) => acc + item.count, 0) ); readonly subjectOptionsCount = computed(() => - this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getSubjects)() - .reduce((acc, item) => acc + item.count, 0) + select(ResourceFiltersOptionsSelectors.getSubjects)().reduce((acc, item) => acc + item.count, 0) ); readonly licenseOptionsCount = computed(() => - this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getLicenses)() - .reduce((acc, item) => acc + item.count, 0) + select(ResourceFiltersOptionsSelectors.getLicenses)().reduce((acc, item) => acc + item.count, 0) ); readonly resourceTypeOptionsCount = computed(() => - this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getResourceTypes)() - .reduce((acc, item) => acc + item.count, 0) + select(ResourceFiltersOptionsSelectors.getResourceTypes)().reduce((acc, item) => acc + item.count, 0) ); readonly institutionOptionsCount = computed(() => - this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getInstitutions)() - .reduce((acc, item) => acc + item.count, 0) + select(ResourceFiltersOptionsSelectors.getInstitutions)().reduce((acc, item) => acc + item.count, 0) ); readonly providerOptionsCount = computed(() => - this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getProviders)() - .reduce((acc, item) => acc + item.count, 0) + select(ResourceFiltersOptionsSelectors.getProviders)().reduce((acc, item) => acc + item.count, 0) ); readonly partOfCollectionOptionsCount = computed(() => - this.#store - .selectSignal(ResourceFiltersOptionsSelectors.getPartOfCollection)() - .reduce((acc, item) => acc + item.count, 0) + select(ResourceFiltersOptionsSelectors.getPartOfCollection)().reduce((acc, item) => acc + item.count, 0) ); - readonly isMyProfilePage = this.#store.selectSignal(SearchSelectors.getIsMyProfile); + readonly isMyProfilePage = select(SearchSelectors.getIsMyProfile); readonly anyOptionsCount = computed(() => { return ( diff --git a/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts b/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts index 4939bf518..25876672a 100644 --- a/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts +++ b/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { take } from 'rxjs'; @@ -32,26 +32,25 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ResourcesWrapperComponent implements OnInit { - readonly #store = inject(Store); - readonly #activeRoute = inject(ActivatedRoute); - readonly #router = inject(Router); - - creatorSelected = this.#store.selectSignal(ResourceFiltersSelectors.getCreator); - dateCreatedSelected = this.#store.selectSignal(ResourceFiltersSelectors.getDateCreated); - funderSelected = this.#store.selectSignal(ResourceFiltersSelectors.getFunder); - subjectSelected = this.#store.selectSignal(ResourceFiltersSelectors.getSubject); - licenseSelected = this.#store.selectSignal(ResourceFiltersSelectors.getLicense); - resourceTypeSelected = this.#store.selectSignal(ResourceFiltersSelectors.getResourceType); - institutionSelected = this.#store.selectSignal(ResourceFiltersSelectors.getInstitution); - providerSelected = this.#store.selectSignal(ResourceFiltersSelectors.getProvider); - partOfCollectionSelected = this.#store.selectSignal(ResourceFiltersSelectors.getPartOfCollection); - sortSelected = this.#store.selectSignal(SearchSelectors.getSortBy); - searchInput = this.#store.selectSignal(SearchSelectors.getSearchText); - resourceTabSelected = this.#store.selectSignal(SearchSelectors.getResourceTab); - isMyProfilePage = this.#store.selectSignal(SearchSelectors.getIsMyProfile); + readonly store = inject(Store); + readonly activeRoute = inject(ActivatedRoute); + readonly router = inject(Router); + + creatorSelected = select(ResourceFiltersSelectors.getCreator); + dateCreatedSelected = select(ResourceFiltersSelectors.getDateCreated); + funderSelected = select(ResourceFiltersSelectors.getFunder); + subjectSelected = select(ResourceFiltersSelectors.getSubject); + licenseSelected = select(ResourceFiltersSelectors.getLicense); + resourceTypeSelected = select(ResourceFiltersSelectors.getResourceType); + institutionSelected = select(ResourceFiltersSelectors.getInstitution); + providerSelected = select(ResourceFiltersSelectors.getProvider); + partOfCollectionSelected = select(ResourceFiltersSelectors.getPartOfCollection); + sortSelected = select(SearchSelectors.getSortBy); + searchInput = select(SearchSelectors.getSearchText); + resourceTabSelected = select(SearchSelectors.getResourceTab); + isMyProfilePage = select(SearchSelectors.getIsMyProfile); constructor() { - // if new value for some filter was put in store, add it to route effect(() => this.syncFilterToQuery('Creator', this.creatorSelected())); effect(() => this.syncFilterToQuery('DateCreated', this.dateCreatedSelected())); effect(() => this.syncFilterToQuery('Funder', this.funderSelected())); @@ -67,8 +66,7 @@ export class ResourcesWrapperComponent implements OnInit { } ngOnInit() { - // set all query parameters from route to store when page is loaded - this.#activeRoute.queryParamMap.pipe(take(1)).subscribe((params) => { + this.activeRoute.queryParamMap.pipe(take(1)).subscribe((params) => { const activeFilters = params.get('activeFilters'); const filters = activeFilters ? JSON.parse(activeFilters) : []; const sortBy = params.get('sortBy'); @@ -86,44 +84,44 @@ export class ResourcesWrapperComponent implements OnInit { const partOfCollection = filters.find((p: ResourceFilterLabel) => p.filterName === 'PartOfCollection'); if (creator) { - this.#store.dispatch(new SetCreator(creator.label, creator.value)); + this.store.dispatch(new SetCreator(creator.label, creator.value)); } if (dateCreated) { - this.#store.dispatch(new SetDateCreated(dateCreated.value)); + this.store.dispatch(new SetDateCreated(dateCreated.value)); } if (funder) { - this.#store.dispatch(new SetFunder(funder.label, funder.value)); + this.store.dispatch(new SetFunder(funder.label, funder.value)); } if (subject) { - this.#store.dispatch(new SetSubject(subject.label, subject.value)); + this.store.dispatch(new SetSubject(subject.label, subject.value)); } if (license) { - this.#store.dispatch(new SetLicense(license.label, license.value)); + this.store.dispatch(new SetLicense(license.label, license.value)); } if (resourceType) { - this.#store.dispatch(new SetResourceType(resourceType.label, resourceType.value)); + this.store.dispatch(new SetResourceType(resourceType.label, resourceType.value)); } if (institution) { - this.#store.dispatch(new SetInstitution(institution.label, institution.value)); + this.store.dispatch(new SetInstitution(institution.label, institution.value)); } if (provider) { - this.#store.dispatch(new SetProvider(provider.label, provider.value)); + this.store.dispatch(new SetProvider(provider.label, provider.value)); } if (partOfCollection) { - this.#store.dispatch(new SetPartOfCollection(partOfCollection.label, partOfCollection.value)); + this.store.dispatch(new SetPartOfCollection(partOfCollection.label, partOfCollection.value)); } if (sortBy) { - this.#store.dispatch(new SetSortBy(sortBy)); + this.store.dispatch(new SetSortBy(sortBy)); } if (search) { - this.#store.dispatch(new SetSearchText(search)); + this.store.dispatch(new SetSearchText(search)); } if (resourceTab) { - this.#store.dispatch(new SetResourceTab(+resourceTab)); + this.store.dispatch(new SetResourceTab(+resourceTab)); } - this.#store.dispatch(GetAllOptions); + this.store.dispatch(GetAllOptions); }); } @@ -131,10 +129,9 @@ export class ResourcesWrapperComponent implements OnInit { if (this.isMyProfilePage()) { return; } - const paramMap = this.#activeRoute.snapshot.queryParamMap; - const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + const paramMap = this.activeRoute.snapshot.queryParamMap; + const currentParams = { ...this.activeRoute.snapshot.queryParams }; - // Read existing parameters const currentFiltersRaw = paramMap.get('activeFilters'); let filters: ResourceFilterLabel[] = []; @@ -149,7 +146,6 @@ export class ResourcesWrapperComponent implements OnInit { const hasValue = !!filterValue?.value; - // Update activeFilters array if (!hasValue && index !== -1) { filters.splice(index, 1); } else if (hasValue && filterValue?.label && filterValue.value) { @@ -172,9 +168,8 @@ export class ResourcesWrapperComponent implements OnInit { delete currentParams['activeFilters']; } - // Navigation - this.#router.navigate([], { - relativeTo: this.#activeRoute, + this.router.navigate([], { + relativeTo: this.activeRoute, queryParams: currentParams, replaceUrl: true, }); @@ -184,7 +179,7 @@ export class ResourcesWrapperComponent implements OnInit { if (this.isMyProfilePage()) { return; } - const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + const currentParams = { ...this.activeRoute.snapshot.queryParams }; if (sortBy && sortBy !== '-relevance') { currentParams['sortBy'] = sortBy; @@ -192,8 +187,8 @@ export class ResourcesWrapperComponent implements OnInit { delete currentParams['sortBy']; } - this.#router.navigate([], { - relativeTo: this.#activeRoute, + this.router.navigate([], { + relativeTo: this.activeRoute, queryParams: currentParams, replaceUrl: true, }); @@ -203,7 +198,7 @@ export class ResourcesWrapperComponent implements OnInit { if (this.isMyProfilePage()) { return; } - const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + const currentParams = { ...this.activeRoute.snapshot.queryParams }; if (search) { currentParams['search'] = search; @@ -211,8 +206,8 @@ export class ResourcesWrapperComponent implements OnInit { delete currentParams['search']; } - this.#router.navigate([], { - relativeTo: this.#activeRoute, + this.router.navigate([], { + relativeTo: this.activeRoute, queryParams: currentParams, replaceUrl: true, }); @@ -222,7 +217,7 @@ export class ResourcesWrapperComponent implements OnInit { if (this.isMyProfilePage()) { return; } - const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + const currentParams = { ...this.activeRoute.snapshot.queryParams }; if (resourceTab) { currentParams['resourceTab'] = resourceTab; @@ -230,8 +225,8 @@ export class ResourcesWrapperComponent implements OnInit { delete currentParams['resourceTab']; } - this.#router.navigate([], { - relativeTo: this.#activeRoute, + this.router.navigate([], { + relativeTo: this.activeRoute, queryParams: currentParams, replaceUrl: true, }); diff --git a/src/app/features/search/components/resources/resources.component.html b/src/app/features/search/components/resources/resources.component.html index 1820065d2..d81d06e5b 100644 --- a/src/app/features/search/components/resources/resources.component.html +++ b/src/app/features/search/components/resources/resources.component.html @@ -1,35 +1,28 @@
@if (isMobile()) { - + } + @if (searchCount() > 10000) { -

10 000+ results

+

{{ 'collections.searchResults.10000results' | translate }}

} @else if (searchCount() > 0) { -

{{ searchCount() }} results

+

{{ searchCount() }} {{ 'collections.searchResults.results' | translate }}

} @else { -

0 results

+

{{ 'collections.searchResults.noResults' | translate }}

}
@if (isWeb()) { -

Sort by:

+

{{ 'collections.filters.sortBy' | translate }}:

- + [(selectedValue)]="selectedSort" + > } @else { @if (isAnyFilterOptions()) { diff --git a/src/app/features/search/components/resources/resources.component.scss b/src/app/features/search/components/resources/resources.component.scss index c637cba9b..728af69d1 100644 --- a/src/app/features/search/components/resources/resources.component.scss +++ b/src/app/features/search/components/resources/resources.component.scss @@ -1,67 +1,65 @@ @use "assets/styles/variables" as var; -:host { - h3 { - color: var.$pr-blue-1; - } +h3 { + color: var.$pr-blue-1; +} - .sorting-container { - display: flex; - align-items: center; +.sorting-container { + display: flex; + align-items: center; - h3 { - color: var.$dark-blue-1; - font-weight: 400; - text-wrap: nowrap; - margin-right: 0.5rem; - } + h3 { + color: var.$dark-blue-1; + font-weight: 400; + text-wrap: nowrap; + margin-right: 0.5rem; } +} - .filter-full-size { - flex: 1; - } +.filter-full-size { + flex: 1; +} - .sort-card { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 44px; - border: 1px solid var.$grey-2; - border-radius: 12px; - padding: 0 1.7rem 0 1.7rem; - cursor: pointer; - } +.sort-card { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 44px; + border: 1px solid var.$grey-2; + border-radius: 12px; + padding: 0 1.7rem 0 1.7rem; + cursor: pointer; +} - .card-selected { - background: var.$bg-blue-2; - } +.card-selected { + background: var.$bg-blue-2; +} - .filters-resources-web { - .resources-container { - flex: 1; +.filters-resources-web { + .resources-container { + flex: 1; - .resources-list { - width: 100%; - display: flex; - flex-direction: column; - row-gap: 0.85rem; - } + .resources-list { + width: 100%; + display: flex; + flex-direction: column; + row-gap: 0.85rem; + } - .switch-icon { - &:hover { - cursor: pointer; - } + .switch-icon { + &:hover { + cursor: pointer; } + } - .icon-disabled { - opacity: 0.5; - cursor: none; - } + .icon-disabled { + opacity: 0.5; + cursor: none; + } - .icon-active { - fill: var.$grey-1; - } + .icon-active { + fill: var.$grey-1; } } } diff --git a/src/app/features/search/components/resources/resources.component.ts b/src/app/features/search/components/resources/resources.component.ts index 8b6a8494a..25a91a79c 100644 --- a/src/app/features/search/components/resources/resources.component.ts +++ b/src/app/features/search/components/resources/resources.component.ts @@ -1,9 +1,10 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; import { AccordionModule } from 'primeng/accordion'; import { Button } from 'primeng/button'; import { DataViewModule } from 'primeng/dataview'; -import { Select } from 'primeng/select'; import { TableModule } from 'primeng/table'; import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core'; @@ -13,8 +14,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FilterChipsComponent, ResourceFiltersComponent } from '@osf/features/search/components'; import { ResourceTab } from '@osf/shared/enums'; import { IS_WEB, IS_XSMALL } from '@osf/shared/utils'; -import { ResourceCardComponent } from '@shared/components'; -import { searchSortingOptions } from '@shared/constants'; +import { ResourceCardComponent, SelectComponent } from '@shared/components'; +import { SEARCH_TAB_OPTIONS, searchSortingOptions } from '@shared/constants'; import { GetResourcesByLink, SearchSelectors, SetResourceTab, SetSortBy } from '../../store'; import { ResourceFiltersOptionsSelectors } from '../filters/store'; @@ -30,34 +31,35 @@ import { ResourceFiltersSelectors } from '../resource-filters/store'; TableModule, DataViewModule, FilterChipsComponent, - Select, ResourceCardComponent, Button, + TranslatePipe, + SelectComponent, ], templateUrl: './resources.component.html', styleUrl: './resources.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ResourcesComponent { - readonly #store = inject(Store); + readonly store = inject(Store); protected readonly searchSortingOptions = searchSortingOptions; - selectedTabStore = this.#store.selectSignal(SearchSelectors.getResourceTab); - searchCount = this.#store.selectSignal(SearchSelectors.getResourcesCount); - resources = this.#store.selectSignal(SearchSelectors.getResources); - sortBy = this.#store.selectSignal(SearchSelectors.getSortBy); - first = this.#store.selectSignal(SearchSelectors.getFirst); - next = this.#store.selectSignal(SearchSelectors.getNext); - prev = this.#store.selectSignal(SearchSelectors.getPrevious); - isMyProfilePage = this.#store.selectSignal(SearchSelectors.getIsMyProfile); + selectedTabStore = select(SearchSelectors.getResourceTab); + searchCount = select(SearchSelectors.getResourcesCount); + resources = select(SearchSelectors.getResources); + sortBy = select(SearchSelectors.getSortBy); + first = select(SearchSelectors.getFirst); + next = select(SearchSelectors.getNext); + prev = select(SearchSelectors.getPrevious); + isMyProfilePage = select(SearchSelectors.getIsMyProfile); isWeb = toSignal(inject(IS_WEB)); isFiltersOpen = signal(false); isSortingOpen = signal(false); - protected filters = this.#store.selectSignal(ResourceFiltersSelectors.getAllFilters); - protected filtersOptions = this.#store.selectSignal(ResourceFiltersOptionsSelectors.getAllOptions); + protected filters = select(ResourceFiltersSelectors.getAllFilters); + protected filtersOptions = select(ResourceFiltersOptionsSelectors.getAllOptions); protected isAnyFilterSelected = computed(() => { return ( this.filters().creator.value || @@ -91,17 +93,9 @@ export class ResourcesComponent { protected selectedSort = signal(''); protected selectedTab = signal(ResourceTab.All); - protected readonly tabsOptions = [ - { label: 'All', value: ResourceTab.All }, - { label: 'Projects', value: ResourceTab.Projects }, - { label: 'Registrations', value: ResourceTab.Registrations }, - { label: 'Preprints', value: ResourceTab.Preprints }, - { label: 'Files', value: ResourceTab.Files }, - { label: 'Users', value: ResourceTab.Users }, - ]; + protected readonly tabsOptions = SEARCH_TAB_OPTIONS; constructor() { - // if new value for sorting in store, update value in dropdown effect(() => { const storeValue = this.sortBy(); const currentInput = untracked(() => this.selectedSort()); @@ -111,13 +105,12 @@ export class ResourcesComponent { } }); - // if the sorting was changed, set new value to store effect(() => { const chosenValue = this.selectedSort(); const storeValue = untracked(() => this.sortBy()); if (chosenValue !== storeValue) { - this.#store.dispatch(new SetSortBy(chosenValue)); + this.store.dispatch(new SetSortBy(chosenValue)); } }); @@ -135,14 +128,13 @@ export class ResourcesComponent { const storeValue = untracked(() => this.selectedTabStore()); if (chosenValue !== storeValue) { - this.#store.dispatch(new SetResourceTab(chosenValue)); + this.store.dispatch(new SetResourceTab(chosenValue)); } }); } - // pagination switchPage(link: string) { - this.#store.dispatch(new GetResourcesByLink(link)); + this.store.dispatch(new GetResourcesByLink(link)); } openFilters() { diff --git a/src/app/features/search/search.component.scss b/src/app/features/search/search.component.scss index 1ffdbbbe2..7fb5db331 100644 --- a/src/app/features/search/search.component.scss +++ b/src/app/features/search/search.component.scss @@ -1,5 +1,3 @@ -@use "assets/styles/variables" as var; - :host { display: flex; flex-direction: column; @@ -9,5 +7,5 @@ .resources { position: relative; - background: var.$white; + background: var(--white); } diff --git a/src/app/features/search/search.component.ts b/src/app/features/search/search.component.ts index 7741190a6..b7a4ee803 100644 --- a/src/app/features/search/search.component.ts +++ b/src/app/features/search/search.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -53,25 +53,25 @@ import { GetResources, ResetSearchState, SearchSelectors, SetResourceTab, SetSea changeDetection: ChangeDetectionStrategy.OnPush, }) export class SearchComponent implements OnDestroy { - readonly #store = inject(Store); + readonly store = inject(Store); protected searchControl = new FormControl(''); protected readonly isSmall = toSignal(inject(IS_SMALL)); private readonly destroyRef = inject(DestroyRef); - protected readonly creatorsFilter = this.#store.selectSignal(ResourceFiltersSelectors.getCreator); - protected readonly dateCreatedFilter = this.#store.selectSignal(ResourceFiltersSelectors.getDateCreated); - protected readonly funderFilter = this.#store.selectSignal(ResourceFiltersSelectors.getFunder); - protected readonly subjectFilter = this.#store.selectSignal(ResourceFiltersSelectors.getSubject); - protected readonly licenseFilter = this.#store.selectSignal(ResourceFiltersSelectors.getLicense); - protected readonly resourceTypeFilter = this.#store.selectSignal(ResourceFiltersSelectors.getResourceType); - protected readonly institutionFilter = this.#store.selectSignal(ResourceFiltersSelectors.getInstitution); - protected readonly providerFilter = this.#store.selectSignal(ResourceFiltersSelectors.getProvider); - protected readonly partOfCollectionFilter = this.#store.selectSignal(ResourceFiltersSelectors.getPartOfCollection); - protected searchStoreValue = this.#store.selectSignal(SearchSelectors.getSearchText); - protected resourcesTabStoreValue = this.#store.selectSignal(SearchSelectors.getResourceTab); - protected sortByStoreValue = this.#store.selectSignal(SearchSelectors.getSortBy); + protected readonly creatorsFilter = select(ResourceFiltersSelectors.getCreator); + protected readonly dateCreatedFilter = select(ResourceFiltersSelectors.getDateCreated); + protected readonly funderFilter = select(ResourceFiltersSelectors.getFunder); + protected readonly subjectFilter = select(ResourceFiltersSelectors.getSubject); + protected readonly licenseFilter = select(ResourceFiltersSelectors.getLicense); + protected readonly resourceTypeFilter = select(ResourceFiltersSelectors.getResourceType); + protected readonly institutionFilter = select(ResourceFiltersSelectors.getInstitution); + protected readonly providerFilter = select(ResourceFiltersSelectors.getProvider); + protected readonly partOfCollectionFilter = select(ResourceFiltersSelectors.getPartOfCollection); + protected searchStoreValue = select(SearchSelectors.getSearchText); + protected resourcesTabStoreValue = select(SearchSelectors.getResourceTab); + protected sortByStoreValue = select(SearchSelectors.getSortBy); protected readonly resourceTabOptions = SEARCH_TAB_OPTIONS; protected selectedTab: ResourceTab = ResourceTab.All; @@ -92,7 +92,7 @@ export class SearchComponent implements OnDestroy { this.searchStoreValue(); this.resourcesTabStoreValue(); this.sortByStoreValue(); - this.#store.dispatch(GetResources); + this.store.dispatch(GetResources); }); effect(() => { @@ -114,14 +114,14 @@ export class SearchComponent implements OnDestroy { } ngOnDestroy(): void { - this.#store.dispatch(ResetFiltersState); - this.#store.dispatch(ResetSearchState); + this.store.dispatch(ResetFiltersState); + this.store.dispatch(ResetSearchState); } onTabChange(index: ResourceTab): void { - this.#store.dispatch(new SetResourceTab(index)); + this.store.dispatch(new SetResourceTab(index)); this.selectedTab = index; - this.#store.dispatch(GetAllOptions); + this.store.dispatch(GetAllOptions); } showTutorial() { @@ -132,8 +132,8 @@ export class SearchComponent implements OnDestroy { this.searchControl.valueChanges .pipe(skip(1), debounceTime(500), takeUntilDestroyed(this.destroyRef)) .subscribe((searchText) => { - this.#store.dispatch(new SetSearchText(searchText ?? '')); - this.#store.dispatch(GetAllOptions); + this.store.dispatch(new SetSearchText(searchText ?? '')); + this.store.dispatch(GetAllOptions); }); } } diff --git a/src/app/features/search/services/resource-filters.service.ts b/src/app/features/search/services/resource-filters.service.ts index 5f3de92ad..90bde577b 100644 --- a/src/app/features/search/services/resource-filters.service.ts +++ b/src/app/features/search/services/resource-filters.service.ts @@ -24,19 +24,19 @@ import { SearchSelectors } from '../store'; providedIn: 'root', }) export class ResourceFiltersService { - #store = inject(Store); - #filtersOptions = inject(FiltersOptionsService); + store = inject(Store); + filtersOptions = inject(FiltersOptionsService); - #getFilterParams(): Record { - return addFiltersParams(this.#store.selectSignal(ResourceFiltersSelectors.getAllFilters)()); + getFilterParams(): Record { + return addFiltersParams(this.store.selectSignal(ResourceFiltersSelectors.getAllFilters)()); } - #getParams(): Record { + getParams(): Record { const params: Record = {}; - const resourceTab = this.#store.selectSnapshot(SearchSelectors.getResourceTab); + const resourceTab = this.store.selectSnapshot(SearchSelectors.getResourceTab); const resourceTypes = getResourceTypes(resourceTab); - const searchText = this.#store.selectSnapshot(SearchSelectors.getSearchText); - const sort = this.#store.selectSnapshot(SearchSelectors.getSortBy); + const searchText = this.store.selectSnapshot(SearchSelectors.getSearchText); + const sort = this.store.selectSnapshot(SearchSelectors.getSortBy); params['cardSearchFilter[resourceType]'] = resourceTypes; params['cardSearchFilter[accessService]'] = 'https://staging4.osf.io/'; @@ -47,38 +47,38 @@ export class ResourceFiltersService { } getCreators(valueSearchText: string): Observable { - return this.#filtersOptions.getCreators(valueSearchText, this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getCreators(valueSearchText, this.getParams(), this.getFilterParams()); } getDates(): Observable { - return this.#filtersOptions.getDates(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getDates(this.getParams(), this.getFilterParams()); } getFunders(): Observable { - return this.#filtersOptions.getFunders(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getFunders(this.getParams(), this.getFilterParams()); } getSubjects(): Observable { - return this.#filtersOptions.getSubjects(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getSubjects(this.getParams(), this.getFilterParams()); } getLicenses(): Observable { - return this.#filtersOptions.getLicenses(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getLicenses(this.getParams(), this.getFilterParams()); } getResourceTypes(): Observable { - return this.#filtersOptions.getResourceTypes(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getResourceTypes(this.getParams(), this.getFilterParams()); } getInstitutions(): Observable { - return this.#filtersOptions.getInstitutions(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getInstitutions(this.getParams(), this.getFilterParams()); } getProviders(): Observable { - return this.#filtersOptions.getProviders(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getProviders(this.getParams(), this.getFilterParams()); } getPartOtCollections(): Observable { - return this.#filtersOptions.getPartOtCollections(this.#getParams(), this.#getFilterParams()); + return this.filtersOptions.getPartOtCollections(this.getParams(), this.getFilterParams()); } } From 7b90ede1abeec05c0539fddc413c726b846a696a Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 10 Jul 2025 14:43:28 +0300 Subject: [PATCH 3/7] fix(my projects): updated my projects --- .../constants/storage-locations.constant.ts | 4 +- src/app/features/my-profile/utils/data.ts | 13 -- src/app/features/my-profile/utils/index.ts | 1 - .../create-project-dialog.component.html | 1 + .../features/my-projects/constants/index.ts | 1 + .../constants/my-projects-tabs.const.ts | 22 +++ src/app/features/my-projects/enums/index.ts | 1 + .../my-projects/enums/my-projects-tab.enum.ts | 6 + src/app/features/my-projects/models/index.ts | 1 - .../models/storage-location.model.ts | 4 - .../my-projects/my-projects.component.html | 39 ++-- .../my-projects/my-projects.component.scss | 1 - .../my-projects/my-projects.component.spec.ts | 8 +- .../my-projects/my-projects.component.ts | 172 ++++++++---------- .../my-projects/store/my-projects.model.ts | 27 +++ .../my-projects/store/my-projects.state.ts | 88 ++------- 16 files changed, 165 insertions(+), 224 deletions(-) delete mode 100644 src/app/features/my-profile/utils/data.ts delete mode 100644 src/app/features/my-profile/utils/index.ts create mode 100644 src/app/features/my-projects/constants/index.ts create mode 100644 src/app/features/my-projects/constants/my-projects-tabs.const.ts create mode 100644 src/app/features/my-projects/enums/index.ts create mode 100644 src/app/features/my-projects/enums/my-projects-tab.enum.ts delete mode 100644 src/app/features/my-projects/models/storage-location.model.ts diff --git a/src/app/core/constants/storage-locations.constant.ts b/src/app/core/constants/storage-locations.constant.ts index 215200959..0a38c5df1 100644 --- a/src/app/core/constants/storage-locations.constant.ts +++ b/src/app/core/constants/storage-locations.constant.ts @@ -1,6 +1,6 @@ -import { StorageLocation } from '@osf/features/my-projects/models/storage-location.model'; +import { CustomOption } from '@osf/shared/models'; -export const STORAGE_LOCATIONS: StorageLocation[] = [ +export const STORAGE_LOCATIONS: CustomOption[] = [ { label: 'United States', value: 'us' }, { label: 'Canada - Montréal', value: 'ca-1' }, { label: 'Germany - Frankfurt', value: 'de-1' }, diff --git a/src/app/features/my-profile/utils/data.ts b/src/app/features/my-profile/utils/data.ts deleted file mode 100644 index 90b77c161..000000000 --- a/src/app/features/my-profile/utils/data.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ResourceTab } from '@osf/shared/enums'; - -export const myProfileStateDefaults = { - resources: [], - resourcesCount: 0, - searchText: '', - sortBy: '-relevance', - resourceTab: ResourceTab.All, - first: '', - next: '', - previous: '', - isMyProfile: false, -}; diff --git a/src/app/features/my-profile/utils/index.ts b/src/app/features/my-profile/utils/index.ts deleted file mode 100644 index 370767922..000000000 --- a/src/app/features/my-profile/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './data'; diff --git a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.html b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.html index ed5d97356..2901c5dd2 100644 --- a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.html +++ b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.html @@ -1,4 +1,5 @@ +
- @if (!isMobile()) { + @if (isTablet()) { - {{ 'myProjects.tabs.myProjects' | translate }} - {{ 'myProjects.tabs.myRegistrations' | translate }} - {{ 'myProjects.tabs.myPreprints' | translate }} - {{ 'myProjects.tabs.bookmarks' | translate }} + @for (tab of tabOptions; track $index) { + {{ tab.label | translate }} + } } - @if (isMobile()) { - - - {{ selectedOption.label | translate }} - - - {{ item.label | translate }} - - + [(selectedValue)]="selectedTab" + [fullWidth]="true" + > } - + - + - + - + @if (!bookmarks().length && !isLoading()) {

{{ 'myProjects.bookmarks.emptyState' | translate }}

} @else { diff --git a/src/app/features/my-projects/my-projects.component.scss b/src/app/features/my-projects/my-projects.component.scss index f1c816620..e9dab5325 100644 --- a/src/app/features/my-projects/my-projects.component.scss +++ b/src/app/features/my-projects/my-projects.component.scss @@ -1,4 +1,3 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; :host { diff --git a/src/app/features/my-projects/my-projects.component.spec.ts b/src/app/features/my-projects/my-projects.component.spec.ts index 0b17f1651..267c30699 100644 --- a/src/app/features/my-projects/my-projects.component.spec.ts +++ b/src/app/features/my-projects/my-projects.component.spec.ts @@ -12,7 +12,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { IS_MEDIUM, IS_WEB, IS_XSMALL } from '@shared/utils'; +import { IS_MEDIUM } from '@shared/utils'; import { InstitutionsState } from '../../shared/stores/institutions'; @@ -22,14 +22,10 @@ import { MyProjectsComponent } from './my-projects.component'; describe('MyProjectsComponent', () => { let component: MyProjectsComponent; let fixture: ComponentFixture; - let isXSmallSubject: BehaviorSubject; let isMediumSubject: BehaviorSubject; - let isWebSubject: BehaviorSubject; beforeEach(async () => { - isXSmallSubject = new BehaviorSubject(false); isMediumSubject = new BehaviorSubject(false); - isWebSubject = new BehaviorSubject(true); await TestBed.configureTestingModule({ imports: [MyProjectsComponent, TranslateModule.forRoot()], @@ -39,9 +35,7 @@ describe('MyProjectsComponent', () => { provideHttpClientTesting(), MockProvider(DialogService), MockProvider(ActivatedRoute, { queryParams: of({}) }), - MockProvider(IS_XSMALL, isXSmallSubject), MockProvider(IS_MEDIUM, isMediumSubject), - MockProvider(IS_WEB, isWebSubject), ], }).compileComponents(); diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index 6830e85ac..d2c7ad67a 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -1,10 +1,9 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import type { SortEvent } from 'primeng/api'; import { DialogService } from 'primeng/dynamicdialog'; -import { Select } from 'primeng/select'; import { TablePageEvent } from 'primeng/table'; import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; @@ -27,13 +26,15 @@ import { ActivatedRoute, Router } from '@angular/router'; import { MY_PROJECTS_TABLE_PARAMS } from '@osf/core/constants'; import { parseQueryFilterParams } from '@osf/core/helpers'; import { CreateProjectDialogComponent } from '@osf/features/my-projects/components'; -import { MyProjectsTableComponent, SubHeaderComponent } from '@osf/shared/components'; +import { MyProjectsTableComponent, SelectComponent, SubHeaderComponent } from '@osf/shared/components'; import { ResourceType, SortOrder } from '@osf/shared/enums'; -import { QueryParams, TableParameters, TabOption } from '@osf/shared/models'; -import { IS_XSMALL } from '@osf/shared/utils'; +import { QueryParams, TableParameters } from '@osf/shared/models'; +import { IS_MEDIUM } from '@osf/shared/utils'; import { CollectionsSelectors, GetBookmarksCollectionId } from '../collections/store'; +import { MY_PROJECTS_TABS } from './constants'; +import { MyProjectsTab } from './enums'; import { MyProjectsItem, MyProjectsSearchFilters } from './models'; import { ClearMyProjects, @@ -49,7 +50,6 @@ import { imports: [ SubHeaderComponent, FormsModule, - Select, Tab, TabList, TabPanel, @@ -57,6 +57,7 @@ import { Tabs, MyProjectsTableComponent, TranslatePipe, + SelectComponent, ], templateUrl: './my-projects.component.html', styleUrl: './my-projects.component.scss', @@ -64,41 +65,24 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyProjectsComponent implements OnInit { - readonly #destroyRef = inject(DestroyRef); - readonly #dialogService = inject(DialogService); - readonly #store = inject(Store); - readonly #router = inject(Router); - readonly #route = inject(ActivatedRoute); - readonly #translateService = inject(TranslateService); - - protected readonly defaultTabValue = 0; + readonly destroyRef = inject(DestroyRef); + readonly dialogService = inject(DialogService); + readonly store = inject(Store); + readonly router = inject(Router); + readonly route = inject(ActivatedRoute); + readonly translateService = inject(TranslateService); + protected readonly isLoading = signal(false); - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - protected readonly tabOptions: TabOption[] = [ - { - label: 'myProjects.tabs.myProjects', - value: 0, - }, - { - label: 'myProjects.tabs.myRegistrations', - value: 1, - }, - { - label: 'myProjects.tabs.myPreprints', - value: 2, - }, - { - label: 'myProjects.tabs.bookmarks', - value: 3, - }, - ]; + 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 queryParams = toSignal(this.route.queryParams); protected readonly currentPage = signal(1); protected readonly currentPageSize = signal(MY_PROJECTS_TABLE_PARAMS.rows); - protected readonly selectedTab = signal(this.defaultTabValue); + protected readonly selectedTab = signal(MyProjectsTab.Projects); protected readonly activeProject = signal(null); protected readonly sortColumn = signal(undefined); protected readonly sortOrder = signal(SortOrder.Asc); @@ -107,75 +91,75 @@ export class MyProjectsComponent implements OnInit { firstRowIndex: 0, }); - protected readonly projects = this.#store.selectSignal(MyProjectsSelectors.getProjects); - protected readonly registrations = this.#store.selectSignal(MyProjectsSelectors.getRegistrations); - protected readonly preprints = this.#store.selectSignal(MyProjectsSelectors.getPreprints); - protected readonly bookmarks = this.#store.selectSignal(MyProjectsSelectors.getBookmarks); - protected readonly totalProjectsCount = this.#store.selectSignal(MyProjectsSelectors.getTotalProjects); - protected readonly totalRegistrationsCount = this.#store.selectSignal(MyProjectsSelectors.getTotalRegistrations); - protected readonly totalPreprintsCount = this.#store.selectSignal(MyProjectsSelectors.getTotalPreprints); - protected readonly totalBookmarksCount = this.#store.selectSignal(MyProjectsSelectors.getTotalBookmarks); + protected readonly projects = select(MyProjectsSelectors.getProjects); + protected readonly registrations = select(MyProjectsSelectors.getRegistrations); + protected readonly preprints = select(MyProjectsSelectors.getPreprints); + protected readonly bookmarks = select(MyProjectsSelectors.getBookmarks); + protected readonly totalProjectsCount = select(MyProjectsSelectors.getTotalProjects); + protected readonly totalRegistrationsCount = select(MyProjectsSelectors.getTotalRegistrations); + protected readonly totalPreprintsCount = select(MyProjectsSelectors.getTotalPreprints); + protected readonly totalBookmarksCount = select(MyProjectsSelectors.getTotalBookmarks); - protected readonly bookmarksCollectionId = this.#store.selectSignal(CollectionsSelectors.getBookmarksCollectionId); + protected readonly bookmarksCollectionId = select(CollectionsSelectors.getBookmarksCollectionId); constructor() { - this.#setupQueryParamsEffect(); - this.#setupSearchSubscription(); - this.#setupTotalRecordsEffect(); - this.#setupCleanup(); + this.setupQueryParamsEffect(); + this.setupSearchSubscription(); + this.setupTotalRecordsEffect(); + this.setupCleanup(); } ngOnInit(): void { - this.#store.dispatch(new GetBookmarksCollectionId()); + this.store.dispatch(new GetBookmarksCollectionId()); } - #setupCleanup(): void { - this.#destroyRef.onDestroy(() => { - this.#store.dispatch(new ClearMyProjects()); + setupCleanup(): void { + this.destroyRef.onDestroy(() => { + this.store.dispatch(new ClearMyProjects()); }); } - #setupSearchSubscription(): void { + setupSearchSubscription(): void { this.searchControl.valueChanges - .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.#destroyRef)) + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) .subscribe((searchValue) => { - this.#handleSearch(searchValue ?? ''); + this.handleSearch(searchValue ?? ''); }); } - #setupTotalRecordsEffect(): void { + setupTotalRecordsEffect(): void { effect(() => { - const totalRecords = this.#getTotalRecordsForCurrentTab(); + const totalRecords = this.getTotalRecordsForCurrentTab(); untracked(() => { - this.#updateTableParams({ totalRecords }); + this.updateTableParams({ totalRecords }); }); }); } - #getTotalRecordsForCurrentTab(): number { + getTotalRecordsForCurrentTab(): number { switch (this.selectedTab()) { - case 0: + case MyProjectsTab.Projects: return this.totalProjectsCount(); - case 1: + case MyProjectsTab.Registrations: return this.totalRegistrationsCount(); - case 2: + case MyProjectsTab.Preprints: return this.totalPreprintsCount(); - case 3: + case MyProjectsTab.Bookmarks: return this.totalBookmarksCount(); default: return 0; } } - #setupQueryParamsEffect(): void { + setupQueryParamsEffect(): void { effect(() => { const params = this.queryParams(); if (!params) return; const { page, size, search, sortColumn, sortOrder } = parseQueryFilterParams(params); - this.#updateComponentState({ page, size, search, sortColumn, sortOrder }); - this.#fetchDataForCurrentTab({ + this.updateComponentState({ page, size, search, sortColumn, sortOrder }); + this.fetchDataForCurrentTab({ page, size, search, @@ -185,7 +169,7 @@ export class MyProjectsComponent implements OnInit { }); } - #updateComponentState(params: QueryParams): void { + updateComponentState(params: QueryParams): void { untracked(() => { const size = params.size || MY_PROJECTS_TABLE_PARAMS.rows; @@ -195,47 +179,47 @@ export class MyProjectsComponent implements OnInit { this.sortColumn.set(params.sortColumn); this.sortOrder.set(params.sortOrder ?? SortOrder.Asc); - this.#updateTableParams({ + this.updateTableParams({ rows: size, firstRowIndex: ((params.page ?? 1) - 1) * size, }); }); } - #updateTableParams(updates: Partial): void { + updateTableParams(updates: Partial): void { this.tableParams.update((current) => ({ ...current, ...updates, })); } - #fetchDataForCurrentTab(params: QueryParams): void { + fetchDataForCurrentTab(params: QueryParams): void { this.isLoading.set(true); - const filters = this.#createFilters(params); + const filters = this.createFilters(params); const pageNumber = params.page ?? 1; const pageSize = params.size ?? MY_PROJECTS_TABLE_PARAMS.rows; let action$; switch (this.selectedTab()) { - case 0: - action$ = this.#store.dispatch(new GetMyProjects(pageNumber, pageSize, filters)); + case MyProjectsTab.Projects: + action$ = this.store.dispatch(new GetMyProjects(pageNumber, pageSize, filters)); break; - case 1: - action$ = this.#store.dispatch(new GetMyRegistrations(pageNumber, pageSize, filters)); + case MyProjectsTab.Registrations: + action$ = this.store.dispatch(new GetMyRegistrations(pageNumber, pageSize, filters)); break; - case 2: - action$ = this.#store.dispatch(new GetMyPreprints(pageNumber, pageSize, filters)); + case MyProjectsTab.Preprints: + action$ = this.store.dispatch(new GetMyPreprints(pageNumber, pageSize, filters)); break; - case 3: + case MyProjectsTab.Bookmarks: if (this.bookmarksCollectionId()) { - action$ = this.#store.dispatch( + action$ = this.store.dispatch( new GetMyBookmarks(this.bookmarksCollectionId(), pageNumber, pageSize, filters, ResourceType.Null) ); } break; } - action$?.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe({ + action$?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ complete: () => { this.isLoading.set(false); }, @@ -245,7 +229,7 @@ export class MyProjectsComponent implements OnInit { }); } - #createFilters(params: QueryParams): MyProjectsSearchFilters { + createFilters(params: QueryParams): MyProjectsSearchFilters { return { searchValue: params.search || '', searchFields: ['title', 'tags', 'description'], @@ -254,9 +238,9 @@ export class MyProjectsComponent implements OnInit { }; } - #handleSearch(searchValue: string): void { + handleSearch(searchValue: string): void { const currentParams = this.queryParams() || {}; - this.#updateQueryParams({ + this.updateQueryParams({ search: searchValue, page: 1, sortColumn: currentParams['sortColumn'], @@ -264,7 +248,7 @@ export class MyProjectsComponent implements OnInit { }); } - #updateQueryParams(updates: Partial): void { + updateQueryParams(updates: Partial): void { const currentParams = this.queryParams() || {}; const queryParams: Record = {}; @@ -292,8 +276,8 @@ export class MyProjectsComponent implements OnInit { queryParams['sortOrder'] = currentParams['sortOrder']; } - this.#router.navigate([], { - relativeTo: this.#route, + this.router.navigate([], { + relativeTo: this.route, queryParams, }); } @@ -302,7 +286,7 @@ export class MyProjectsComponent implements OnInit { const page = Math.floor(event.first / event.rows) + 1; const currentParams = this.queryParams() || {}; - this.#updateQueryParams({ + this.updateQueryParams({ page, size: event.rows, sortColumn: currentParams['sortColumn'], @@ -312,7 +296,7 @@ export class MyProjectsComponent implements OnInit { protected onSort(event: SortEvent): void { if (event.field) { - this.#updateQueryParams({ + this.updateQueryParams({ sortColumn: event.field, sortOrder: event.order === -1 ? SortOrder.Desc : SortOrder.Asc, }); @@ -320,11 +304,11 @@ export class MyProjectsComponent implements OnInit { } protected onTabChange(tabIndex: number): void { - this.#store.dispatch(new ClearMyProjects()); + this.store.dispatch(new ClearMyProjects()); this.selectedTab.set(tabIndex); const currentParams = this.queryParams() || {}; - this.#updateQueryParams({ + this.updateQueryParams({ page: 1, size: currentParams['size'], search: '', @@ -334,12 +318,12 @@ export class MyProjectsComponent implements OnInit { } protected createProject(): void { - const dialogWidth = this.isMobile() ? '95vw' : '850px'; + const dialogWidth = this.isTablet() ? '850px' : '95vw'; - this.#dialogService.open(CreateProjectDialogComponent, { + this.dialogService.open(CreateProjectDialogComponent, { width: dialogWidth, focusOnShow: false, - header: this.#translateService.instant('myProjects.header.createProject'), + header: this.translateService.instant('myProjects.header.createProject'), closeOnEscape: true, modal: true, closable: true, @@ -348,6 +332,6 @@ export class MyProjectsComponent implements OnInit { protected navigateToProject(project: MyProjectsItem): void { this.activeProject.set(project); - this.#router.navigate(['/my-projects', project.id]); + this.router.navigate(['/my-projects', project.id]); } } diff --git a/src/app/features/my-projects/store/my-projects.model.ts b/src/app/features/my-projects/store/my-projects.model.ts index 5b62ae3c7..c3e4873e5 100644 --- a/src/app/features/my-projects/store/my-projects.model.ts +++ b/src/app/features/my-projects/store/my-projects.model.ts @@ -12,3 +12,30 @@ export interface MyProjectsStateModel { totalPreprints: number; totalBookmarks: number; } + +export const MY_PROJECT_STATE_DEFAULTS: MyProjectsStateModel = { + projects: { + data: [], + isLoading: false, + error: null, + }, + registrations: { + data: [], + isLoading: false, + error: null, + }, + preprints: { + data: [], + isLoading: false, + error: null, + }, + bookmarks: { + data: [], + isLoading: false, + error: null, + }, + totalProjects: 0, + totalRegistrations: 0, + totalPreprints: 0, + totalBookmarks: 0, +}; diff --git a/src/app/features/my-projects/store/my-projects.state.ts b/src/app/features/my-projects/store/my-projects.state.ts index a0e1cc27a..1e80ac762 100644 --- a/src/app/features/my-projects/store/my-projects.state.ts +++ b/src/app/features/my-projects/store/my-projects.state.ts @@ -1,9 +1,10 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { catchError, forkJoin, tap, throwError } from 'rxjs'; +import { catchError, forkJoin, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { handleSectionError } from '@osf/core/handlers'; import { ResourceType } from '@shared/enums'; import { MyProjectsService } from '../services'; @@ -16,40 +17,15 @@ import { GetMyProjects, GetMyRegistrations, } from './my-projects.actions'; -import { MyProjectsStateModel } from './my-projects.model'; +import { MY_PROJECT_STATE_DEFAULTS, MyProjectsStateModel } from './my-projects.model'; @State({ name: 'myProjects', - defaults: { - projects: { - data: [], - isLoading: false, - error: null, - }, - registrations: { - data: [], - isLoading: false, - error: null, - }, - preprints: { - data: [], - isLoading: false, - error: null, - }, - bookmarks: { - data: [], - isLoading: false, - error: null, - }, - totalProjects: 0, - totalRegistrations: 0, - totalPreprints: 0, - totalBookmarks: 0, - }, + defaults: MY_PROJECT_STATE_DEFAULTS, }) @Injectable() export class MyProjectsState { - myProjectsService = inject(MyProjectsService); + private readonly myProjectsService = inject(MyProjectsService); @Action(GetMyProjects) getProjects(ctx: StateContext, action: GetMyProjects) { @@ -72,7 +48,7 @@ export class MyProjectsState { totalProjects: res.links.meta.total, }); }), - catchError((error) => this.handleError(ctx, 'projects', error)) + catchError((error) => handleSectionError(ctx, 'projects', error)) ); } @@ -97,7 +73,7 @@ export class MyProjectsState { totalRegistrations: res.links.meta.total, }); }), - catchError((error) => this.handleError(ctx, 'registrations', error)) + catchError((error) => handleSectionError(ctx, 'registrations', error)) ); } @@ -122,7 +98,7 @@ export class MyProjectsState { totalPreprints: res.links.meta.total, }); }), - catchError((error) => this.handleError(ctx, 'preprints', error)) + catchError((error) => handleSectionError(ctx, 'preprints', error)) ); } @@ -151,7 +127,7 @@ export class MyProjectsState { totalBookmarks: res.links.meta.total, }); }), - catchError((error) => this.handleError(ctx, 'bookmarks', error)) + catchError((error) => handleSectionError(ctx, 'bookmarks', error)) ); } else { return forkJoin({ @@ -193,39 +169,14 @@ export class MyProjectsState { totalBookmarks: totalCount, }); }), - catchError((error) => this.handleError(ctx, 'bookmarks', error)) + catchError((error) => handleSectionError(ctx, 'bookmarks', error)) ); } } @Action(ClearMyProjects) clearMyProjects(ctx: StateContext) { - ctx.patchState({ - projects: { - data: [], - isLoading: false, - error: null, - }, - registrations: { - data: [], - isLoading: false, - error: null, - }, - preprints: { - data: [], - isLoading: false, - error: null, - }, - bookmarks: { - data: [], - isLoading: false, - error: null, - }, - totalProjects: 0, - totalRegistrations: 0, - totalPreprints: 0, - totalBookmarks: 0, - }); + ctx.patchState(MY_PROJECT_STATE_DEFAULTS); } @Action(CreateProject) @@ -252,22 +203,7 @@ export class MyProjectsState { totalProjects: state.totalProjects + 1, }); }), - catchError((error) => this.handleError(ctx, 'projects', error)) + catchError((error) => handleSectionError(ctx, 'projects', error)) ); } - - private handleError(ctx: StateContext, section: keyof MyProjectsStateModel, error: Error) { - const state = ctx.getState(); - if (section === 'projects' || section === 'registrations' || section === 'preprints' || section === 'bookmarks') { - ctx.patchState({ - [section]: { - ...state[section], - isLoading: false, - error: error.message, - }, - }); - } - - return throwError(() => error); - } } From 2c81253bba8433f0c8d5116983ebb41d75eca0eb Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 10 Jul 2025 15:11:34 +0300 Subject: [PATCH 4/7] fix(private variables): removed # --- .../connect-configured-addon.component.scss | 9 ++++---- .../components/name/name.component.scss | 1 - .../components/name/name.component.ts | 22 +++++++++---------- .../services/profile-settings.api.service.ts | 2 +- .../bar-chart/bar-chart.component.ts | 8 +++---- .../line-chart/line-chart.component.ts | 8 +++---- .../pie-chart/pie-chart.component.ts | 8 +++---- .../truncated-text.component.scss | 2 -- .../truncated-text.component.ts | 7 ++---- src/app/shared/tokens/index.ts | 1 - src/app/shared/tokens/subjects.token.ts | 5 ----- 11 files changed, 30 insertions(+), 43 deletions(-) delete mode 100644 src/app/shared/tokens/index.ts delete mode 100644 src/app/shared/tokens/subjects.token.ts diff --git a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.scss b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.scss index bb28b1fd4..445e2f9a2 100644 --- a/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.scss +++ b/src/app/features/project/addons/components/connect-configured-addon/connect-configured-addon.component.scss @@ -1,12 +1,11 @@ @use "assets/styles/mixins" as mix; -@use "assets/styles/variables" as var; :host { flex: 1; @include mix.flex-column; +} - .stepper-container { - background-color: var.$white; - flex: 1; - } +.stepper-container { + background-color: var(--white); + flex: 1; } diff --git a/src/app/features/settings/profile-settings/components/name/name.component.scss b/src/app/features/settings/profile-settings/components/name/name.component.scss index d2453d507..4c3bab073 100644 --- a/src/app/features/settings/profile-settings/components/name/name.component.scss +++ b/src/app/features/settings/profile-settings/components/name/name.component.scss @@ -1,4 +1,3 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; .name-container { 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 09873d229..eb0c734c0 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 @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -21,16 +21,16 @@ import { ProfileSettingsSelectors, UpdateProfileSettingsUser } from '../../store export class NameComponent { @HostBinding('class') classes = 'flex flex-column gap-4 flex-1'; - readonly #fb = inject(FormBuilder); - readonly form = this.#fb.group({ - fullName: this.#fb.control('', { nonNullable: true }), - givenName: this.#fb.control('', { nonNullable: true }), - middleNames: this.#fb.control('', { nonNullable: true }), - familyName: this.#fb.control('', { nonNullable: true }), - suffix: this.#fb.control('', { nonNullable: true }), + readonly fb = inject(FormBuilder); + readonly form = this.fb.group({ + fullName: this.fb.control('', { nonNullable: true }), + givenName: this.fb.control('', { nonNullable: true }), + middleNames: this.fb.control('', { nonNullable: true }), + familyName: this.fb.control('', { nonNullable: true }), + suffix: this.fb.control('', { nonNullable: true }), }); - readonly #store = inject(Store); - readonly nameState = this.#store.selectSignal(ProfileSettingsSelectors.user); + readonly store = inject(Store); + readonly nameState = select(ProfileSettingsSelectors.user); constructor() { effect(() => { @@ -47,7 +47,7 @@ export class NameComponent { saveChanges() { const { fullName, givenName, middleNames, familyName, suffix } = this.form.getRawValue(); - this.#store.dispatch( + this.store.dispatch( new UpdateProfileSettingsUser({ user: { fullName, diff --git a/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts b/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts index 27bd1a6ea..4f13a9df1 100644 --- a/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts +++ b/src/app/features/settings/profile-settings/services/profile-settings.api.service.ts @@ -16,7 +16,7 @@ export class ProfileSettingsApiService { patchUserSettings(userId: string, key: keyof ProfileSettingsStateModel, data: ProfileSettingsUpdate) { const patchedData = { [key]: data }; - return this.jsonApiService.patch>(`${environment.apiUrl}users/${userId}/`, { + return this.jsonApiService.patch>(`${environment.apiUrl}/users/${userId}/`, { data: { type: 'users', id: userId, attributes: patchedData }, }); } diff --git a/src/app/shared/components/bar-chart/bar-chart.component.ts b/src/app/shared/components/bar-chart/bar-chart.component.ts index 59287845f..34242e5e2 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.ts +++ b/src/app/shared/components/bar-chart/bar-chart.component.ts @@ -52,15 +52,15 @@ export class BarChartComponent implements OnInit { protected options = signal({}); protected data = signal({} as ChartData); - #platformId = inject(PLATFORM_ID); - #cd = inject(ChangeDetectorRef); + platformId = inject(PLATFORM_ID); + cd = inject(ChangeDetectorRef); ngOnInit() { this.initChart(); } initChart() { - if (isPlatformBrowser(this.#platformId)) { + if (isPlatformBrowser(this.platformId)) { const documentStyle = getComputedStyle(document.documentElement); const textColorSecondary = documentStyle.getPropertyValue('--dark-blue-1'); const surfaceBorder = documentStyle.getPropertyValue('--grey-2'); @@ -70,7 +70,7 @@ export class BarChartComponent implements OnInit { this.setChartData(defaultBackgroundColor, defaultBorderColor); this.setChartOptions(textColorSecondary, surfaceBorder); - this.#cd.markForCheck(); + this.cd.markForCheck(); } } diff --git a/src/app/shared/components/line-chart/line-chart.component.ts b/src/app/shared/components/line-chart/line-chart.component.ts index f5ba441e4..0336624ee 100644 --- a/src/app/shared/components/line-chart/line-chart.component.ts +++ b/src/app/shared/components/line-chart/line-chart.component.ts @@ -38,15 +38,15 @@ export class LineChartComponent implements OnInit { protected options = signal({}); protected data = signal({} as ChartData); - #platformId = inject(PLATFORM_ID); - #cd = inject(ChangeDetectorRef); + platformId = inject(PLATFORM_ID); + cd = inject(ChangeDetectorRef); ngOnInit() { this.initChart(); } initChart() { - if (isPlatformBrowser(this.#platformId)) { + if (isPlatformBrowser(this.platformId)) { const documentStyle = getComputedStyle(document.documentElement); const textColorSecondary = documentStyle.getPropertyValue('--dark-blue-1'); const surfaceBorder = documentStyle.getPropertyValue('--grey-2'); @@ -56,7 +56,7 @@ export class LineChartComponent implements OnInit { this.setChartData(defaultBackgroundColor, defaultBorderColor); this.setChartOptions(textColorSecondary, surfaceBorder); - this.#cd.markForCheck(); + this.cd.markForCheck(); } } diff --git a/src/app/shared/components/pie-chart/pie-chart.component.ts b/src/app/shared/components/pie-chart/pie-chart.component.ts index 7959a6d39..cc786fcff 100644 --- a/src/app/shared/components/pie-chart/pie-chart.component.ts +++ b/src/app/shared/components/pie-chart/pie-chart.component.ts @@ -38,19 +38,19 @@ export class PieChartComponent implements OnInit { protected options = signal({}); protected data = signal({} as ChartData); - #platformId = inject(PLATFORM_ID); - #cd = inject(ChangeDetectorRef); + platformId = inject(PLATFORM_ID); + cd = inject(ChangeDetectorRef); ngOnInit() { this.initChart(); } initChart() { - if (isPlatformBrowser(this.#platformId)) { + if (isPlatformBrowser(this.platformId)) { this.setChartData(); this.setChartOptions(); - this.#cd.markForCheck(); + this.cd.markForCheck(); } } diff --git a/src/app/shared/components/truncated-text/truncated-text.component.scss b/src/app/shared/components/truncated-text/truncated-text.component.scss index a590d92fa..5fcf0c1d0 100644 --- a/src/app/shared/components/truncated-text/truncated-text.component.scss +++ b/src/app/shared/components/truncated-text/truncated-text.component.scss @@ -1,5 +1,3 @@ -@use "assets/styles/variables" as var; - .text-content { max-height: 300px; overflow: hidden; diff --git a/src/app/shared/components/truncated-text/truncated-text.component.ts b/src/app/shared/components/truncated-text/truncated-text.component.ts index d82798b03..c25dd2cfd 100644 --- a/src/app/shared/components/truncated-text/truncated-text.component.ts +++ b/src/app/shared/components/truncated-text/truncated-text.component.ts @@ -3,9 +3,6 @@ import { TranslatePipe } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { AfterViewInit, Component, ElementRef, input, signal, viewChild } from '@angular/core'; -// This component displays text with a "show more/less" functionality when content exceeds the specified number of lines. -// Use line-clamp CSS property for initial truncation and dynamically show/hide content. - @Component({ selector: 'osf-truncated-text', templateUrl: './truncated-text.component.html', @@ -21,10 +18,10 @@ export class TruncatedTextComponent implements AfterViewInit { protected hasOverflowingText = signal(false); ngAfterViewInit() { - this.#checkTextOverflow(); + this.checkTextOverflow(); } - #checkTextOverflow(): void { + checkTextOverflow(): void { const element = this.contentElement()?.nativeElement; if (!element) return; diff --git a/src/app/shared/tokens/index.ts b/src/app/shared/tokens/index.ts deleted file mode 100644 index 82d99a590..000000000 --- a/src/app/shared/tokens/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './subjects.token'; diff --git a/src/app/shared/tokens/subjects.token.ts b/src/app/shared/tokens/subjects.token.ts deleted file mode 100644 index b189b9cd2..000000000 --- a/src/app/shared/tokens/subjects.token.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -import { ISubjectsService } from '../models'; - -export const SUBJECTS_SERVICE = new InjectionToken('SUBJECTS_SERVICE'); From 7322a0844968fe45229d948a705230bc5ecdcf6e Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 11 Jul 2025 18:29:22 +0300 Subject: [PATCH 5/7] fix(store): fixed store issues --- .../my-profile-resource-filters.component.ts | 52 +++++++++++-------- .../my-profile-resources.component.html | 3 +- .../preprints-resources.component.html | 2 +- .../preprints-resources.component.ts | 3 ++ .../resource-filters.component.ts | 43 ++++++++++----- .../store/resource-filters.state.ts | 1 - .../resources/resources.component.html | 3 +- .../store/profile-settings.state.ts | 6 +-- 8 files changed, 68 insertions(+), 45 deletions(-) diff --git a/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts b/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts index cc99dd232..2b6031a16 100644 --- a/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts +++ b/src/app/features/my-profile/components/my-profile-resource-filters/my-profile-resource-filters.component.ts @@ -1,8 +1,8 @@ -import { select } from '@ngxs/store'; +import { Store } from '@ngxs/store'; import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; -import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import { MyProfileSelectors } from '../../store'; import { @@ -38,51 +38,57 @@ import { MyProfileResourceFiltersOptionsSelectors } from '../filters/store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyProfileResourceFiltersComponent { + readonly store = inject(Store); + readonly datesOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getDatesCreated)().reduce( - (accumulator, date) => accumulator + date.count, - 0 - ); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getDatesCreated)() + .reduce((accumulator, date) => accumulator + date.count, 0); }); readonly funderOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getFunders)().reduce((acc, item) => acc + item.count, 0); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getFunders)() + .reduce((acc, item) => acc + item.count, 0); }); readonly subjectOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getSubjects)().reduce((acc, item) => acc + item.count, 0); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getSubjects)() + .reduce((acc, item) => acc + item.count, 0); }); readonly licenseOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getLicenses)().reduce((acc, item) => acc + item.count, 0); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getLicenses)() + .reduce((acc, item) => acc + item.count, 0); }); readonly resourceTypeOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getResourceTypes)().reduce( - (acc, item) => acc + item.count, - 0 - ); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getResourceTypes)() + .reduce((acc, item) => acc + item.count, 0); }); readonly institutionOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getInstitutions)().reduce( - (acc, item) => acc + item.count, - 0 - ); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getInstitutions)() + .reduce((acc, item) => acc + item.count, 0); }); readonly providerOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getProviders)().reduce((acc, item) => acc + item.count, 0); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getProviders)() + .reduce((acc, item) => acc + item.count, 0); }); readonly partOfCollectionOptionsCount = computed(() => { - return select(MyProfileResourceFiltersOptionsSelectors.getPartOfCollection)().reduce( - (acc, item) => acc + item.count, - 0 - ); + return this.store + .selectSignal(MyProfileResourceFiltersOptionsSelectors.getPartOfCollection)() + .reduce((acc, item) => acc + item.count, 0); }); - readonly isMyProfilePage = select(MyProfileSelectors.getIsMyProfile); + readonly isMyProfilePage = this.store.selectSignal(MyProfileSelectors.getIsMyProfile); readonly anyOptionsCount = computed(() => { return ( diff --git a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html index 6880e09c5..01a2fc071 100644 --- a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html +++ b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html @@ -17,8 +17,9 @@

{{ 'collections.searchResults.noResults' | translate }}{{ 'collections.filters.sortBy' | translate }}:

diff --git a/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.html b/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.html index 51426d5c0..4e643a47f 100644 --- a/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.html +++ b/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.html @@ -10,7 +10,7 @@

0 results

@if (isWeb()) { -

Sort by:

+

{{ 'collections.filters.sortBy' | translate }}

{ - return select(ResourceFiltersOptionsSelectors.getDatesCreated)().reduce( - (accumulator, date) => accumulator + date.count, - 0 - ); + return this.store + .selectSignal(ResourceFiltersOptionsSelectors.getDatesCreated)() + .reduce((accumulator, date) => accumulator + date.count, 0); }); readonly funderOptionsCount = computed(() => - select(ResourceFiltersOptionsSelectors.getFunders)().reduce((acc, item) => acc + item.count, 0) + this.store + .selectSignal(ResourceFiltersOptionsSelectors.getFunders)() + .reduce((acc, item) => acc + item.count, 0) ); readonly subjectOptionsCount = computed(() => - select(ResourceFiltersOptionsSelectors.getSubjects)().reduce((acc, item) => acc + item.count, 0) + this.store + .selectSignal(ResourceFiltersOptionsSelectors.getSubjects)() + .reduce((acc, item) => acc + item.count, 0) ); readonly licenseOptionsCount = computed(() => - select(ResourceFiltersOptionsSelectors.getLicenses)().reduce((acc, item) => acc + item.count, 0) + this.store + .selectSignal(ResourceFiltersOptionsSelectors.getLicenses)() + .reduce((acc, item) => acc + item.count, 0) ); readonly resourceTypeOptionsCount = computed(() => - select(ResourceFiltersOptionsSelectors.getResourceTypes)().reduce((acc, item) => acc + item.count, 0) + this.store + .selectSignal(ResourceFiltersOptionsSelectors.getResourceTypes)() + .reduce((acc, item) => acc + item.count, 0) ); readonly institutionOptionsCount = computed(() => - select(ResourceFiltersOptionsSelectors.getInstitutions)().reduce((acc, item) => acc + item.count, 0) + this.store + .selectSignal(ResourceFiltersOptionsSelectors.getInstitutions)() + .reduce((acc, item) => acc + item.count, 0) ); readonly providerOptionsCount = computed(() => - select(ResourceFiltersOptionsSelectors.getProviders)().reduce((acc, item) => acc + item.count, 0) + this.store + .selectSignal(ResourceFiltersOptionsSelectors.getProviders)() + .reduce((acc, item) => acc + item.count, 0) ); readonly partOfCollectionOptionsCount = computed(() => - select(ResourceFiltersOptionsSelectors.getPartOfCollection)().reduce((acc, item) => acc + item.count, 0) + this.store + .selectSignal(ResourceFiltersOptionsSelectors.getPartOfCollection)() + .reduce((acc, item) => acc + item.count, 0) ); - readonly isMyProfilePage = select(SearchSelectors.getIsMyProfile); + readonly isMyProfilePage = this.store.selectSignal(SearchSelectors.getIsMyProfile); readonly anyOptionsCount = computed(() => { return ( diff --git a/src/app/features/search/components/resource-filters/store/resource-filters.state.ts b/src/app/features/search/components/resource-filters/store/resource-filters.state.ts index 8d541b3bf..fecc78655 100644 --- a/src/app/features/search/components/resource-filters/store/resource-filters.state.ts +++ b/src/app/features/search/components/resource-filters/store/resource-filters.state.ts @@ -19,7 +19,6 @@ import { } from './resource-filters.actions'; import { ResourceFiltersStateModel } from './resource-filters.model'; -// Store for user selected filters values @State({ name: 'resourceFilters', defaults: resourceFiltersDefaults, diff --git a/src/app/features/search/components/resources/resources.component.html b/src/app/features/search/components/resources/resources.component.html index d81d06e5b..0b804389c 100644 --- a/src/app/features/search/components/resources/resources.component.html +++ b/src/app/features/search/components/resources/resources.component.html @@ -18,8 +18,9 @@

{{ 'collections.searchResults.noResults' | translate }}{{ 'collections.filters.sortBy' | translate }}:

diff --git a/src/app/features/settings/profile-settings/store/profile-settings.state.ts b/src/app/features/settings/profile-settings/store/profile-settings.state.ts index 35191904a..e2c5e7f64 100644 --- a/src/app/features/settings/profile-settings/store/profile-settings.state.ts +++ b/src/app/features/settings/profile-settings/store/profile-settings.state.ts @@ -1,10 +1,9 @@ import { Action, State, StateContext, Store } from '@ngxs/store'; -import { catchError, tap } from 'rxjs'; +import { tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { handleSectionError } from '@osf/core/handlers'; import { UserSelectors } from '@osf/core/store/user'; import { removeNullable } from '@osf/shared/constants'; import { Social } from '@osf/shared/models'; @@ -89,8 +88,7 @@ export class ProfileSettingsState { ...state, education: response.data.attributes.education, }); - }), - catchError((error) => handleSectionError(ctx, 'education', error)) + }) ); } From 607c34850d40b4e3211ba64085a7f5bf00220d8a Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 16 Jul 2025 18:58:53 +0300 Subject: [PATCH 6/7] fix(fixes): updated account settings --- .../core/components/root/root.component.html | 2 +- src/app/core/store/user/user.selectors.ts | 5 + .../account-settings.component.html | 2 +- .../account-settings.component.scss | 113 ----------- .../add-email/add-email.component.html | 17 +- .../add-email/add-email.component.ts | 40 +++- .../affiliated-institutions.component.html | 28 +-- .../affiliated-institutions.component.scss | 57 ------ .../affiliated-institutions.component.ts | 35 +++- .../cancel-deactivation.component.html | 45 ++-- .../cancel-deactivation.component.scss | 5 - .../cancel-deactivation.component.ts | 8 +- .../change-password.component.html | 18 +- .../change-password.component.scss | 6 - .../change-password.component.ts | 3 +- .../confirmation-sent-dialog.component.html | 12 ++ .../confirmation-sent-dialog.component.scss | 0 ...onfirmation-sent-dialog.component.spec.ts} | 16 +- .../confirmation-sent-dialog.component.ts | 19 ++ .../connected-emails.component.html | 129 +++++------- .../connected-emails.component.scss | 111 ---------- .../connected-emails.component.ts | 124 ++++++----- .../connected-identities.component.html | 28 ++- .../connected-identities.component.scss | 5 - .../connected-identities.component.ts | 31 ++- .../deactivate-account.component.html | 20 +- .../deactivate-account.component.scss | 5 - .../deactivate-account.component.ts | 71 +++++-- .../deactivation-warning.component.html | 48 +++-- .../deactivation-warning.component.scss | 5 - .../deactivation-warning.component.ts | 2 +- .../default-storage-location.component.html | 22 +- .../default-storage-location.component.scss | 5 - .../default-storage-location.component.ts | 22 +- .../account-settings/components/index.ts | 1 + .../share-indexing.component.html | 39 ++-- .../share-indexing.component.scss | 5 - .../share-indexing.component.ts | 51 ++--- .../configure-two-factor.component.html | 28 --- .../configure-two-factor.component.scss | 5 - .../configure-two-factor.component.ts | 33 --- .../two-factor-auth/components/index.ts | 2 - .../verify-two-factor.component.html | 24 --- .../verify-two-factor.component.scss | 5 - .../verify-two-factor.component.spec.ts | 40 ---- .../verify-two-factor.component.ts | 29 --- .../two-factor-auth.component.html | 91 ++++----- .../two-factor-auth.component.scss | 12 +- .../two-factor-auth.component.ts | 93 +++++---- .../services/account-settings.service.ts | 4 +- .../store/account-settings.actions.ts | 17 +- .../store/account-settings.model.ts | 26 ++- .../store/account-settings.selectors.ts | 9 +- .../store/account-settings.state.ts | 192 +++++++++--------- src/app/shared/components/index.ts | 1 + .../readonly-input.component.html | 15 ++ .../readonly-input.component.scss | 7 + .../readonly-input.component.spec.ts | 113 +++++++++++ .../readonly-input.component.ts | 21 ++ .../shared/constants/input-limits.const.ts | 3 + src/assets/i18n/en.json | 76 ++++--- src/assets/styles/_common.scss | 5 + 62 files changed, 919 insertions(+), 1087 deletions(-) create mode 100644 src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.html create mode 100644 src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.scss rename src/app/features/settings/account-settings/components/{two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts => confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts} (61%) create mode 100644 src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.ts delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts delete mode 100644 src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts create mode 100644 src/app/shared/components/readonly-input/readonly-input.component.html create mode 100644 src/app/shared/components/readonly-input/readonly-input.component.scss create mode 100644 src/app/shared/components/readonly-input/readonly-input.component.spec.ts create mode 100644 src/app/shared/components/readonly-input/readonly-input.component.ts diff --git a/src/app/core/components/root/root.component.html b/src/app/core/components/root/root.component.html index f2bfc1c0e..735043347 100644 --- a/src/app/core/components/root/root.component.html +++ b/src/app/core/components/root/root.component.html @@ -26,4 +26,4 @@

} - + diff --git a/src/app/core/store/user/user.selectors.ts b/src/app/core/store/user/user.selectors.ts index bd0782e77..00150c09f 100644 --- a/src/app/core/store/user/user.selectors.ts +++ b/src/app/core/store/user/user.selectors.ts @@ -48,4 +48,9 @@ export class UserSelectors { static isUserSettingsSubmitting(state: UserStateModel): boolean { return state.currentUserSettings.isSubmitting!; } + + @Selector([UserState]) + static getShareIndexing(state: UserStateModel): boolean | undefined { + return state.currentUser.data?.allowIndexing; + } } diff --git a/src/app/features/settings/account-settings/account-settings.component.html b/src/app/features/settings/account-settings/account-settings.component.html index f1fba9c86..b7832fd0d 100644 --- a/src/app/features/settings/account-settings/account-settings.component.html +++ b/src/app/features/settings/account-settings/account-settings.component.html @@ -1,7 +1,7 @@ @if (currentUser()?.id) { -
+
diff --git a/src/app/features/settings/account-settings/account-settings.component.scss b/src/app/features/settings/account-settings/account-settings.component.scss index 9eb88126f..da0c027b5 100644 --- a/src/app/features/settings/account-settings/account-settings.component.scss +++ b/src/app/features/settings/account-settings/account-settings.component.scss @@ -1,118 +1,5 @@ -@use "assets/styles/mixins" as mix; -@use "assets/styles/variables" as var; - :host { display: flex; flex-direction: column; flex: 1; - color: var(--dark-blue-1); -} - -.header { - display: flex; - flex: 1; - padding: 7.14rem 1.71rem 3.43rem 1.71rem; - background: var(--gradient-1); - - h1 { - margin-left: 0.85rem; - } - - p-button { - margin-left: auto; - } - - i { - color: var(--dark-blue-1); - font-size: 2.6rem; - } -} - -.content { - display: flex; - flex-direction: column; - gap: 1.7rem; - padding: 1.7rem; - background: var.$white; -} - -.account-setting { - border: 1px solid var.$grey-2; - padding: 1.7rem; - display: flex; - flex-direction: column; - gap: 1.7rem; - border-radius: 0.5rem; - font-weight: 400; - text-transform: none; - - &-link { - font-weight: 600; - } - - &-radio-group { - display: flex; - flex: 1; - gap: 4rem; - } - - &-radio-item { - display: flex; - align-items: center; - gap: 0.5rem; - } - - &-content { - display: flex; - } - - &-description { - line-height: 2rem; - text-transform: none; - font-weight: 400; - } - - &-action { - display: flex; - justify-content: flex-end; - } - - &-emails { - display: flex; - flex-direction: column; - gap: 1.7rem; - } - - &-select { - width: 50%; - } - - &-email { - display: flex; - gap: 2rem; - align-items: start; - - &__title { - min-width: 10rem; - } - - &--readonly { - display: flex; - align-items: center; - border: 1px solid var.$grey-2; - padding: 0.285rem 0.85rem; - border-radius: 0.285rem; - - i { - color: var.$dark-blue-1; - font-size: 0.7rem; - margin-left: 0.7rem; - } - } - - &__value { - display: flex; - gap: 0.428rem; - } - } } diff --git a/src/app/features/settings/account-settings/components/add-email/add-email.component.html b/src/app/features/settings/account-settings/components/add-email/add-email.component.html index 519bd27e4..c41989c1e 100644 --- a/src/app/features/settings/account-settings/components/add-email/add-email.component.html +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.html @@ -1,14 +1,10 @@
-
- - -
+
diff --git a/src/app/features/settings/account-settings/components/add-email/add-email.component.ts b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts index a3cfd1360..42af6805b 100644 --- a/src/app/features/settings/account-settings/components/add-email/add-email.component.ts +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.ts @@ -1,36 +1,54 @@ -import { createDispatchMap } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; -import { InputText } from 'primeng/inputtext'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { TextInputComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; +import { ToastService } from '@osf/shared/services'; import { CustomValidators } from '@osf/shared/utils'; -import { AddEmail } from '../../store'; +import { AccountSettingsSelectors, AddEmail } from '../../store'; @Component({ - selector: 'osf-add-email', - imports: [InputText, ReactiveFormsModule, Button, TranslatePipe], + selector: 'osf-confirmation-sent-dialog', + imports: [TextInputComponent, ReactiveFormsModule, Button, TranslatePipe], templateUrl: './add-email.component.html', styleUrl: './add-email.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AddEmailComponent { - readonly action = createDispatchMap({ addEmail: AddEmail }); readonly dialogRef = inject(DynamicDialogRef); - protected readonly emailControl = new FormControl('', [Validators.email, CustomValidators.requiredTrimmed()]); + private readonly action = createDispatchMap({ addEmail: AddEmail }); + private readonly toastService = inject(ToastService); + + isSubmitting = select(AccountSettingsSelectors.isEmailsSubmitting); + + protected readonly emailControl = new FormControl('', { + nonNullable: true, + validators: [Validators.email, CustomValidators.requiredTrimmed()], + }); + + emailMaxLength = InputLimits.email.maxLength; + + constructor() { + effect(() => (this.isSubmitting() ? this.emailControl.disable() : this.emailControl.enable())); + } addEmail() { - if (this.emailControl.value) { - this.action.addEmail(this.emailControl.value); + if (this.emailControl.invalid) { + return; } - this.dialogRef.close(); + this.action.addEmail(this.emailControl.value).subscribe(() => { + this.dialogRef.close(this.emailControl.value); + this.toastService.showSuccess('settings.accountSettings.connectedEmails.successAdd'); + }); } } diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html index 69d6d3224..9ec09b40b 100644 --- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.html @@ -1,27 +1,19 @@ - + diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss index 0b43d96c2..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.scss @@ -1,57 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; -@use "assets/styles/variables" as var; - -:host { - @extend .account-setting; -} - -h3, -p { - font-weight: 400; - text-transform: none; -} - -.account-setting { - &-emails { - display: flex; - flex-direction: column; - gap: 1.7rem; - } - - &-email { - display: flex; - gap: 2rem; - align-items: start; - - &--readonly { - display: flex; - align-items: center; - border: 1px solid var.$grey-2; - padding: 0.285rem 0.85rem; - border-radius: 0.285rem; - min-height: 2.8rem; - - &--address { - max-width: 14rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - i { - color: var.$dark-blue-1; - font-size: 0.7rem; - margin-left: 0.7rem; - cursor: pointer; - font-weight: 400; - } - } - - &__value { - display: flex; - align-items: center; - gap: 0.428rem; - min-height: 2.8rem; - } - } -} diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts index 639767bc3..fbd92366a 100644 --- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts @@ -2,27 +2,50 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Card } from 'primeng/card'; + +import { finalize } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { UserSelectors } from '@osf/core/store/user'; +import { ReadonlyInputComponent } from '@osf/shared/components'; +import { Institution } from '@osf/shared/models'; +import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services'; import { AccountSettingsSelectors, DeleteUserInstitution } from '../../store'; @Component({ selector: 'osf-affiliated-institutions', - imports: [TranslatePipe], + imports: [Card, TranslatePipe, ReadonlyInputComponent], templateUrl: './affiliated-institutions.component.html', styleUrl: './affiliated-institutions.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AffiliatedInstitutionsComponent { + private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly toastService = inject(ToastService); + private readonly loaderService = inject(LoaderService); + private readonly actions = createDispatchMap({ deleteUserInstitution: DeleteUserInstitution }); protected institutions = select(AccountSettingsSelectors.getUserInstitutions); protected currentUser = select(UserSelectors.getCurrentUser); - deleteInstitution(id: string) { - if (this.currentUser()?.id) { - this.actions.deleteUserInstitution(id, this.currentUser()!.id); - } + deleteInstitution(institution: Institution) { + this.customConfirmationService.confirmDelete({ + headerKey: 'settings.accountSettings.affiliatedInstitutions.deleteDialog.header', + messageParams: { name: institution.name }, + messageKey: 'settings.accountSettings.affiliatedInstitutions.deleteDialog.message', + onConfirm: () => { + if (this.currentUser()?.id) { + this.actions + .deleteUserInstitution(institution.id, this.currentUser()!.id) + .pipe(finalize(() => this.loaderService.hide())) + .subscribe(() => + this.toastService.showSuccess('settings.accountSettings.affiliatedInstitutions.successDelete') + ); + } + }, + }); } } diff --git a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.html b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.html index 0a9837686..d93192a0e 100644 --- a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.html +++ b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.html @@ -1,29 +1,22 @@ -
-
- - - -
+
+

+ {{ 'settings.accountSettings.deactivateAccount.dialog.undo.message' | translate }} +

+
-
- +
+ - -
+
diff --git a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.scss b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.scss index 3f1e57889..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.scss +++ b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.scss @@ -1,5 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} diff --git a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts index 45ecc8fbc..5791aa60e 100644 --- a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts +++ b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.ts @@ -1,5 +1,3 @@ -import { createDispatchMap } from '@ngxs/store'; - import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; @@ -7,8 +5,6 @@ import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { CancelDeactivationRequest } from '../../store'; - @Component({ selector: 'osf-cancel-deactivation', imports: [Button, TranslatePipe], @@ -17,11 +13,9 @@ import { CancelDeactivationRequest } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CancelDeactivationComponent { - private readonly action = createDispatchMap({ cancelDeactivationRequest: CancelDeactivationRequest }); readonly dialogRef = inject(DynamicDialogRef); cancelDeactivation(): void { - this.action.cancelDeactivationRequest(); - this.dialogRef.close(); + this.dialogRef.close(true); } } diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.html b/src/app/features/settings/account-settings/components/change-password/change-password.component.html index 8991fe606..13e1747d8 100644 --- a/src/app/features/settings/account-settings/components/change-password/change-password.component.html +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.html @@ -1,11 +1,12 @@ - + diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.scss b/src/app/features/settings/account-settings/components/change-password/change-password.component.scss index e4f17c758..3a4cf6910 100644 --- a/src/app/features/settings/account-settings/components/change-password/change-password.component.scss +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.scss @@ -1,9 +1,3 @@ -@use "../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} - .password-help { color: var(--pr-blue-1); font-size: 0.75rem; diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.ts b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts index b8186c6cf..1a25fb057 100644 --- a/src/app/features/settings/account-settings/components/change-password/change-password.component.ts +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.ts @@ -1,6 +1,7 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; import { Password } from 'primeng/password'; import { CommonModule } from '@angular/common'; @@ -20,7 +21,7 @@ import { AccountSettingsService } from '../../services'; @Component({ selector: 'osf-change-password', - imports: [ReactiveFormsModule, Password, CommonModule, Button, TranslatePipe], + imports: [Card, ReactiveFormsModule, Password, CommonModule, Button, TranslatePipe], templateUrl: './change-password.component.html', styleUrl: './change-password.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.html b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.html new file mode 100644 index 000000000..f74c360e9 --- /dev/null +++ b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.html @@ -0,0 +1,12 @@ +
+

+ +
+ + +
+
diff --git a/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.scss b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts similarity index 61% rename from src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts rename to src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts index 81529a2df..ce1029717 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.spec.ts +++ b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts @@ -3,7 +3,7 @@ import { provideStore } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { MockPipe, MockProviders } from 'ng-mocks'; -import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -11,24 +11,24 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store'; -import { ConfigureTwoFactorComponent } from './configure-two-factor.component'; +import { ConfirmationSentDialogComponent } from './confirmation-sent-dialog.component'; -describe('ConfigureTwoFactorComponent', () => { - let component: ConfigureTwoFactorComponent; - let fixture: ComponentFixture; +describe('ConfirmationSentDialogComponent', () => { + let component: ConfirmationSentDialogComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConfigureTwoFactorComponent, MockPipe(TranslatePipe)], + imports: [ConfirmationSentDialogComponent, MockPipe(TranslatePipe)], providers: [ provideStore([AccountSettingsState]), provideHttpClient(), provideHttpClientTesting(), - MockProviders(DynamicDialogRef, DynamicDialogConfig), + MockProviders(DynamicDialogRef), ], }).compileComponents(); - fixture = TestBed.createComponent(ConfigureTwoFactorComponent); + fixture = TestBed.createComponent(ConfirmationSentDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.ts b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.ts new file mode 100644 index 000000000..52a417fd2 --- /dev/null +++ b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.ts @@ -0,0 +1,19 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; + +@Component({ + selector: 'osf-confirmation-sent-dialog', + imports: [ReactiveFormsModule, Button, TranslatePipe], + templateUrl: './confirmation-sent-dialog.component.html', + styleUrl: './confirmation-sent-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConfirmationSentDialogComponent { + readonly dialogRef = inject(DynamicDialogRef); + readonly config = inject(DynamicDialogConfig); +} diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html index b15989f1c..3b87b5728 100644 --- a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.html @@ -1,109 +1,78 @@ - + diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss index 51c1f57f6..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.scss @@ -1,111 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; -@use "assets/styles/variables" as var; - -:host { - @extend .account-setting; -} - -.icon { - &--sm { - width: 0.7rem; - height: 0.7rem; - margin-left: 0.7rem; - } - - &--md { - width: 1rem; - height: 1rem; - } -} - -.account-setting { - &-emails { - display: flex; - flex-direction: column; - gap: 1.7rem; - } - - &-email { - display: flex; - gap: 2rem; - align-items: start; - - &-center { - display: flex; - gap: 2rem; - align-items: center; - } - - &__title { - min-width: 10rem; - min-height: 2.8rem; - display: flex; - align-items: center; - } - - &__container { - display: flex; - flex-wrap: wrap; - gap: 2rem; - } - - &--readonly { - display: flex; - align-items: center; - border: 1px solid var.$grey-2; - padding: 0.285rem 0.85rem; - border-radius: 0.285rem; - min-height: 2.8rem; - - &--address { - max-width: 14rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - i { - color: var.$dark-blue-1; - font-size: 0.7rem; - margin-left: 0.7rem; - cursor: pointer; - font-weight: 400; - } - } - - &__value { - display: flex; - align-items: center; - gap: 0.428rem; - min-height: 2.8rem; - } - } -} - -@media (max-width: var.$breakpoint-sm) { - .account-setting-email { - flex-wrap: wrap; - gap: 0.5rem; - - &__title { - min-height: 2rem; - } - - &__container { - gap: 0.7rem; - } - - &__value { - min-height: 2rem; - } - } -} - -@media (max-width: 410px) { - .account-setting-email { - &__value { - min-height: 2rem; - flex-wrap: wrap; - } - } -} diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts index c8f12a07f..ee169a11b 100644 --- a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.ts @@ -3,27 +3,28 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; import { DialogService } from 'primeng/dynamicdialog'; -import { ProgressSpinner } from 'primeng/progressspinner'; import { Skeleton } from 'primeng/skeleton'; -import { finalize } from 'rxjs'; +import { filter, finalize } from 'rxjs'; -import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { UserSelectors } from '@osf/core/store/user'; -import { CustomConfirmationService } from '@osf/shared/services'; +import { ReadonlyInputComponent } from '@osf/shared/components'; +import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services'; import { IS_SMALL } from '@osf/shared/utils'; import { AccountEmail } from '../../models'; -import { AccountSettingsService } from '../../services'; -import { AccountSettingsSelectors, DeleteEmail, MakePrimary } from '../../store'; -import { AddEmailComponent } from '../add-email/add-email.component'; +import { AccountSettingsSelectors, DeleteEmail, MakePrimary, ResendConfirmation } from '../../store'; +import { ConfirmationSentDialogComponent } from '../confirmation-sent-dialog/confirmation-sent-dialog.component'; +import { AddEmailComponent } from '../'; @Component({ selector: 'osf-connected-emails', - imports: [Button, ProgressSpinner, Skeleton, TranslatePipe], + imports: [Button, Skeleton, Card, TranslatePipe, ReadonlyInputComponent], templateUrl: './connected-emails.component.html', styleUrl: './connected-emails.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -31,25 +32,23 @@ import { AddEmailComponent } from '../add-email/add-email.component'; export class ConnectedEmailsComponent { readonly isSmall = toSignal(inject(IS_SMALL)); - private readonly accountSettingsService = inject(AccountSettingsService); private readonly dialogService = inject(DialogService); private readonly translateService = inject(TranslateService); private readonly destroyRef = inject(DestroyRef); private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly loaderService = inject(LoaderService); + private readonly toastService = inject(ToastService); protected readonly currentUser = select(UserSelectors.getCurrentUser); protected readonly emails = select(AccountSettingsSelectors.getEmails); protected readonly isEmailsLoading = select(AccountSettingsSelectors.isEmailsLoading); private readonly actions = createDispatchMap({ + resendConfirmation: ResendConfirmation, deleteEmail: DeleteEmail, makePrimary: MakePrimary, }); - protected readonly deletingEmailIds = signal>(new Set()); - protected readonly resendingEmailIds = signal>(new Set()); - protected readonly makingPrimaryIds = signal>(new Set()); - protected readonly unconfirmedEmails = computed(() => { return this.emails().filter((email) => !email.confirmed && !email.primary); }); @@ -61,47 +60,75 @@ export class ConnectedEmailsComponent { }); addEmail() { - this.dialogService.open(AddEmailComponent, { + this.dialogService + .open(AddEmailComponent, { + width: '448px', + focusOnShow: false, + header: this.translateService.instant('settings.accountSettings.connectedEmails.dialog.title'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe(filter((email: string) => !!email)) + .subscribe((email) => this.showConfirmationSentDialog(email)); + } + + showConfirmationSentDialog(email: string) { + this.dialogService.open(ConfirmationSentDialogComponent, { width: '448px', focusOnShow: false, - header: this.translateService.instant('settings.accountSettings.connectedEmails.dialog.title'), + header: this.translateService.instant('settings.accountSettings.connectedEmails.confirmationSentDialog.header'), closeOnEscape: true, modal: true, closable: true, + data: email, }); } - resendConfirmation(emailId: string) { - if (this.currentUser()?.id) { - this.resendingEmailIds.set(new Set([...this.resendingEmailIds(), emailId])); - - this.accountSettingsService - .resendConfirmation(emailId, this.currentUser()!.id) - .pipe( - finalize(() => { - const currentIds = this.resendingEmailIds(); - const updatedIds = new Set([...currentIds].filter((id) => id !== emailId)); - this.resendingEmailIds.set(updatedIds); - }), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe(); - } + resendConfirmation(email: AccountEmail) { + this.customConfirmationService.confirmAccept({ + headerKey: 'settings.accountSettings.connectedEmails.resendDialog.header', + messageParams: { email: email.emailAddress }, + messageKey: 'settings.accountSettings.connectedEmails.resendDialog.message', + acceptLabelKey: 'settings.accountSettings.connectedEmails.buttons.resend', + onConfirm: () => { + if (this.currentUser()?.id) { + this.loaderService.show(); + + this.actions + .resendConfirmation(email.id, this.currentUser()!.id) + .pipe( + finalize(() => this.loaderService.hide()), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => this.toastService.showSuccess('settings.accountSettings.connectedEmails.successResend')); + } + }, + }); } - makePrimary(emailId: string) { - if (this.currentUser()?.id) { - this.makingPrimaryIds.set(new Set([...this.makingPrimaryIds(), emailId])); - - this.actions - .makePrimary(emailId) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - const currentIds = this.makingPrimaryIds(); - const updatedIds = new Set([...currentIds].filter((id) => id !== emailId)); - this.makingPrimaryIds.set(updatedIds); - }); - } + makePrimary(email: AccountEmail) { + this.customConfirmationService.confirmAccept({ + headerKey: 'settings.accountSettings.connectedEmails.makePrimaryDialog.header', + messageParams: { email: email.emailAddress }, + messageKey: 'settings.accountSettings.connectedEmails.makePrimaryDialog.message', + acceptLabelKey: 'settings.accountSettings.connectedEmails.buttons.makePrimary', + onConfirm: () => { + if (this.currentUser()?.id) { + this.loaderService.show(); + + this.actions + .makePrimary(email.id) + .pipe( + finalize(() => this.loaderService.hide()), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => + this.toastService.showSuccess('settings.accountSettings.connectedEmails.successMakePrimary') + ); + } + }, + }); } openConfirmDeleteEmail(email: AccountEmail) { @@ -114,15 +141,12 @@ export class ConnectedEmailsComponent { } deleteEmails(emailId: string) { - const currentDeletingIds = this.deletingEmailIds(); - currentDeletingIds.add(emailId); - this.deletingEmailIds.set(currentDeletingIds); + this.loaderService.show(); this.actions.deleteEmail(emailId).subscribe({ complete: () => { - const updatedDeletingIds = this.deletingEmailIds(); - updatedDeletingIds.delete(emailId); - this.deletingEmailIds.set(updatedDeletingIds); + this.loaderService.hide(); + this.toastService.showSuccess('settings.accountSettings.connectedEmails.successDelete'); }, }); } diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html index 732cb0488..90ffb5265 100644 --- a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.html @@ -1,25 +1,21 @@ - + diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss index 3f1e57889..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.scss @@ -1,5 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts index ebb7f98d1..1809e7ecf 100644 --- a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.ts @@ -2,22 +2,45 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Card } from 'primeng/card'; +import { finalize } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { ReadonlyInputComponent } from '@osf/shared/components'; +import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services'; + +import { ExternalIdentity } from '../../models'; import { AccountSettingsSelectors, DeleteExternalIdentity } from '../../store'; @Component({ selector: 'osf-connected-identities', - imports: [TranslatePipe], + imports: [Card, TranslatePipe, ReadonlyInputComponent], templateUrl: './connected-identities.component.html', styleUrl: './connected-identities.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConnectedIdentitiesComponent { + private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly loaderService = inject(LoaderService); + private readonly toastService = inject(ToastService); + readonly actions = createDispatchMap({ deleteExternalIdentity: DeleteExternalIdentity }); readonly externalIdentities = select(AccountSettingsSelectors.getExternalIdentities); - deleteExternalIdentity(id: string): void { - this.actions.deleteExternalIdentity(id); + deleteExternalIdentity(identity: ExternalIdentity): void { + this.customConfirmationService.confirmDelete({ + headerKey: 'settings.accountSettings.connectedIdentities.deleteDialog.header', + messageParams: { name: identity.id }, + messageKey: 'settings.accountSettings.connectedIdentities.deleteDialog.message', + onConfirm: () => { + this.loaderService.show(); + this.actions + .deleteExternalIdentity(identity.id) + .pipe(finalize(() => this.loaderService.hide())) + .subscribe(() => this.toastService.showSuccess('settings.accountSettings.connectedIdentities.successDelete')); + }, + }); } } diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html index 1e0f69d33..28d8815f8 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.html @@ -1,21 +1,21 @@ - + diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss index 3f1e57889..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.scss @@ -1,5 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts index 3c6050284..3fd742d32 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.ts @@ -1,20 +1,25 @@ -import { select } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; import { DialogService } from 'primeng/dynamicdialog'; import { Message } from 'primeng/message'; +import { filter } from 'rxjs'; + import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { AccountSettingsSelectors } from '../../store'; +import { LoaderService, ToastService } from '@osf/shared/services'; + +import { AccountSettingsSelectors, CancelDeactivationRequest, DeactivateAccount } from '../../store'; import { CancelDeactivationComponent } from '../cancel-deactivation/cancel-deactivation.component'; import { DeactivationWarningComponent } from '../deactivation-warning/deactivation-warning.component'; @Component({ selector: 'osf-deactivate-account', - imports: [Button, Message, TranslatePipe], + imports: [Button, Card, Message, TranslatePipe], templateUrl: './deactivate-account.component.html', styleUrl: './deactivate-account.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -22,28 +27,56 @@ import { DeactivationWarningComponent } from '../deactivation-warning/deactivati export class DeactivateAccountComponent { private readonly dialogService = inject(DialogService); private readonly translateService = inject(TranslateService); + private readonly toastService = inject(ToastService); + private readonly loaderService = inject(LoaderService); + + private readonly action = createDispatchMap({ + cancelDeactivationRequest: CancelDeactivationRequest, + deactivateAccount: DeactivateAccount, + }); protected accountSettings = select(AccountSettingsSelectors.getAccountSettings); deactivateAccount() { - this.dialogService.open(DeactivationWarningComponent, { - width: '552px', - focusOnShow: false, - header: this.translateService.instant('settings.accountSettings.deactivateAccount.dialog.deactivate.title'), - closeOnEscape: true, - modal: true, - closable: true, - }); + this.dialogService + .open(DeactivationWarningComponent, { + width: '552px', + focusOnShow: false, + header: this.translateService.instant('settings.accountSettings.deactivateAccount.dialog.deactivate.title'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe(filter((res: boolean) => res)) + .subscribe(() => { + this.loaderService.show(); + + // [NS] Hidden to avoid issues with development + // this.action.deactivateAccount().subscribe(() => { + this.toastService.showSuccess('settings.accountSettings.deactivateAccount.successDeactivation'); + this.loaderService.hide(); + // }); + }); } cancelDeactivation() { - this.dialogService.open(CancelDeactivationComponent, { - width: '552px', - focusOnShow: false, - header: this.translateService.instant('settings.accountSettings.deactivateAccount.dialog.undo.title'), - closeOnEscape: true, - modal: true, - closable: true, - }); + this.dialogService + .open(CancelDeactivationComponent, { + width: '552px', + focusOnShow: false, + header: this.translateService.instant('settings.accountSettings.deactivateAccount.dialog.undo.title'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe(filter((res: boolean) => res)) + .subscribe(() => { + this.loaderService.show(); + + this.action.cancelDeactivationRequest().subscribe(() => { + this.toastService.showSuccess('settings.accountSettings.deactivateAccount.successCancelDeactivation'); + this.loaderService.hide(); + }); + }); } } diff --git a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.html b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.html index 8680feb4e..979fc2d83 100644 --- a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.html +++ b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.html @@ -1,29 +1,27 @@ -
-
- +
+

+ {{ 'settings.accountSettings.deactivateAccount.warning.confirm' | translate }} +

- -
+

+ {{ 'settings.accountSettings.deactivateAccount.warning.description' | translate }} +

+
-
- +
+ - -
+
diff --git a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.scss b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.scss index 3f1e57889..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.scss +++ b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.scss @@ -1,5 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} diff --git a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.ts b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.ts index 245a86ff7..5d1cce682 100644 --- a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.ts +++ b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.ts @@ -16,6 +16,6 @@ export class DeactivationWarningComponent { dialogRef = inject(DynamicDialogRef); deactivateAccount(): void { - this.dialogRef.close(); + this.dialogRef.close(true); } } diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html index d734e802d..5b1cc1587 100644 --- a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.html @@ -1,23 +1,21 @@ - + diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss index 3f1e57889..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.scss @@ -1,5 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} 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 9777e5e15..82f6dff21 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 @@ -3,25 +3,31 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; import { Select } from 'primeng/select'; -import { ChangeDetectionStrategy, Component, effect, signal } from '@angular/core'; +import { finalize } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; +import { LoaderService, ToastService } from '@osf/shared/services'; import { Region } from '../../models'; import { AccountSettingsSelectors, UpdateRegion } from '../../store'; @Component({ selector: 'osf-default-storage-location', - imports: [Button, Select, FormsModule, TranslatePipe], + imports: [Button, Card, Select, FormsModule, TranslatePipe], templateUrl: './default-storage-location.component.html', styleUrl: './default-storage-location.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class DefaultStorageLocationComponent { - readonly actions = createDispatchMap({ updateRegion: UpdateRegion }); + private readonly actions = createDispatchMap({ updateRegion: UpdateRegion }); + private readonly loaderService = inject(LoaderService); + private readonly toastService = inject(ToastService); protected readonly currentUser = select(UserSelectors.getCurrentUser); protected readonly regions = select(AccountSettingsSelectors.getRegions); @@ -31,6 +37,7 @@ export class DefaultStorageLocationComponent { effect(() => { const user = this.currentUser(); const regions = this.regions(); + if (user && regions) { const defaultRegion = regions.find((region) => region.id === user.defaultRegionId); this.selectedRegion.set(defaultRegion); @@ -40,7 +47,14 @@ export class DefaultStorageLocationComponent { updateLocation(): void { if (this.selectedRegion()?.id) { - this.actions.updateRegion(this.selectedRegion()!.id); + this.loaderService.show(); + + this.actions + .updateRegion(this.selectedRegion()!.id) + .pipe(finalize(() => this.loaderService.hide())) + .subscribe(() => + this.toastService.showSuccess('settings.accountSettings.defaultStorageLocation.successUpdate') + ); } } } diff --git a/src/app/features/settings/account-settings/components/index.ts b/src/app/features/settings/account-settings/components/index.ts index 07fe11d78..4ba9fc73a 100644 --- a/src/app/features/settings/account-settings/components/index.ts +++ b/src/app/features/settings/account-settings/components/index.ts @@ -2,6 +2,7 @@ export { AddEmailComponent } from './add-email/add-email.component'; export { AffiliatedInstitutionsComponent } from './affiliated-institutions/affiliated-institutions.component'; export { CancelDeactivationComponent } from './cancel-deactivation/cancel-deactivation.component'; export { ChangePasswordComponent } from './change-password/change-password.component'; +export { ConfirmationSentDialogComponent } from './confirmation-sent-dialog/confirmation-sent-dialog.component'; export { ConnectedEmailsComponent } from './connected-emails/connected-emails.component'; export { ConnectedIdentitiesComponent } from './connected-identities/connected-identities.component'; export { DeactivateAccountComponent } from './deactivate-account/deactivate-account.component'; diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html index 07043777c..a97de4b6c 100644 --- a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.html @@ -1,27 +1,36 @@ - + diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss index 3f1e57889..e69de29bb 100644 --- a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.scss @@ -1,5 +0,0 @@ -@use "../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts index a8fa5f85b..4fe85b842 100644 --- a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.ts @@ -3,46 +3,51 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; import { RadioButton } from 'primeng/radiobutton'; -import { ChangeDetectionStrategy, Component, effect, signal } from '@angular/core'; +import { finalize } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; -import { ShareIndexingEnum } from '@osf/shared/enums'; +import { LoaderService, ToastService } from '@osf/shared/services'; import { UpdateIndexing } from '../../store'; @Component({ selector: 'osf-share-indexing', - imports: [Button, RadioButton, ReactiveFormsModule, FormsModule, TranslatePipe], + imports: [Button, Card, RadioButton, ReactiveFormsModule, FormsModule, TranslatePipe], templateUrl: './share-indexing.component.html', styleUrl: './share-indexing.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ShareIndexingComponent { - readonly actions = createDispatchMap({ updateIndexing: UpdateIndexing }); - protected indexing = signal(ShareIndexingEnum.None); + private readonly actions = createDispatchMap({ updateIndexing: UpdateIndexing }); + private readonly loaderService = inject(LoaderService); + private readonly toastService = inject(ToastService); + + private readonly indexing = select(UserSelectors.getShareIndexing); protected readonly currentUser = select(UserSelectors.getCurrentUser); - updateIndexing = () => { - if (this.currentUser()?.id) { - if (this.indexing() === ShareIndexingEnum.OptIn) { - this.actions.updateIndexing(true); - } else if (this.indexing() === ShareIndexingEnum.OutOf) { - this.actions.updateIndexing(false); - } + selectedOption = this.indexing(); + + get noChanges() { + return this.selectedOption === this.indexing(); + } + + updateIndexing() { + if (this.selectedOption === this.indexing()) { + return; + } + + if (this.currentUser()?.id && this.selectedOption !== undefined) { + this.loaderService.show(); + this.actions + .updateIndexing(this.selectedOption) + .pipe(finalize(() => this.loaderService.hide())) + .subscribe(() => this.toastService.showSuccess('settings.accountSettings.shareIndexing.successUpdate')); } - }; - - constructor() { - effect(() => { - const user = this.currentUser(); - if (user?.allowIndexing) { - this.indexing.set(ShareIndexingEnum.OptIn); - } else if (user?.allowIndexing === false) { - this.indexing.set(ShareIndexingEnum.OutOf); - } - }); } } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html deleted file mode 100644 index 63e4421ba..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.html +++ /dev/null @@ -1,28 +0,0 @@ -
-
- - -
- -
- - - -
-
diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss deleted file mode 100644 index b2606ff4d..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use "../../../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts deleted file mode 100644 index 1bd83ba06..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/configure-two-factor/configure-two-factor.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createDispatchMap } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; - -import { Button } from 'primeng/button'; -import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; - -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -import { AccountSettings } from '@osf/features/settings/account-settings/models'; -import { EnableTwoFactorAuth } from '@osf/features/settings/account-settings/store'; - -@Component({ - selector: 'osf-configure-two-factor', - imports: [Button, FormsModule, TranslatePipe], - templateUrl: './configure-two-factor.component.html', - styleUrl: './configure-two-factor.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ConfigureTwoFactorComponent { - private readonly actions = createDispatchMap({ enableTwoFactorAuth: EnableTwoFactorAuth }); - readonly config = inject(DynamicDialogConfig); - - dialogRef = inject(DynamicDialogRef); - - enableTwoFactor(): void { - const settings = this.config.data as AccountSettings; - settings.twoFactorEnabled = true; - this.actions.enableTwoFactorAuth(); - this.dialogRef.close(); - } -} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts deleted file mode 100644 index 7e5de57e0..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ConfigureTwoFactorComponent } from './configure-two-factor/configure-two-factor.component'; -export { VerifyTwoFactorComponent } from './verify-two-factor/verify-two-factor.component'; diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html deleted file mode 100644 index fd6fe754f..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.html +++ /dev/null @@ -1,24 +0,0 @@ -
- - -
- - - -
-
diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss deleted file mode 100644 index b2606ff4d..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use "../../../../account-settings.component.scss" as account-settings; - -:host { - @extend .account-setting; -} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts deleted file mode 100644 index 1b54ced15..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { provideStore } from '@ngxs/store'; - -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockPipe, MockProvider, MockProviders } from 'ng-mocks'; - -import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AccountSettingsState } from '@osf/features/settings/account-settings/store'; - -import { VerifyTwoFactorComponent } from './verify-two-factor.component'; - -describe('VerifyTwoFactorComponent', () => { - let component: VerifyTwoFactorComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [VerifyTwoFactorComponent, MockPipe(TranslatePipe)], - providers: [ - provideStore([AccountSettingsState]), - provideHttpClient(), - provideHttpClientTesting(), - MockProvider(TranslateService), - MockProviders(DynamicDialogRef, DynamicDialogConfig), - ], - }).compileComponents(); - - fixture = TestBed.createComponent(VerifyTwoFactorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts deleted file mode 100644 index 44bebe937..000000000 --- a/src/app/features/settings/account-settings/components/two-factor-auth/components/verify-two-factor/verify-two-factor.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createDispatchMap } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; - -import { Button } from 'primeng/button'; -import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; - -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; - -import { DisableTwoFactorAuth } from '@osf/features/settings/account-settings/store'; - -@Component({ - selector: 'osf-verify-two-factor', - imports: [Button, TranslatePipe], - templateUrl: './verify-two-factor.component.html', - styleUrl: './verify-two-factor.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class VerifyTwoFactorComponent { - private readonly actions = createDispatchMap({ disableTwoFactorAuth: DisableTwoFactorAuth }); - readonly config = inject(DynamicDialogConfig); - - dialogRef = inject(DynamicDialogRef); - - disableTwoFactor() { - this.actions.disableTwoFactorAuth(); - this.dialogRef.close(); - } -} diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html index 2064b82b3..09b68006a 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.html @@ -1,66 +1,63 @@ @if (accountSettings()) { - + } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss index c508e9f25..19c73f969 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.scss @@ -1,11 +1,3 @@ -@use "../../account-settings.component.scss" as account-settings; -@use "assets/styles/variables" as var; - -:host { - @extend .account-setting; - - ::ng-deep .token { - color: var.$red-1; - display: inline; - } +.token-container { + word-break: break-all; } diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts index 40405e9a4..3168d0961 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.ts @@ -1,28 +1,26 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; -import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { InputText } from 'primeng/inputtext'; +import { Card } from 'primeng/card'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; +import { InputMask } from 'primeng/inputmask'; -import { HttpErrorResponse } from '@angular/common/http'; -import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { UserSelectors } from '@osf/core/store/user'; +import { InputLimits } from '@osf/shared/constants'; +import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services'; -import { AccountSettings } from '../../models'; -import { AccountSettingsService } from '../../services'; -import { AccountSettingsSelectors, DisableTwoFactorAuth, SetAccountSettings } from '../../store'; - -import { ConfigureTwoFactorComponent, VerifyTwoFactorComponent } from './components'; +import { AccountSettingsSelectors, DisableTwoFactorAuth, EnableTwoFactorAuth, VerifyTwoFactorAuth } from '../../store'; import { QRCodeComponent } from 'angularx-qrcode'; @Component({ selector: 'osf-two-factor-auth', - imports: [Button, QRCodeComponent, ReactiveFormsModule, InputText, TranslatePipe], + imports: [Button, Card, QRCodeComponent, ReactiveFormsModule, InputMask, TranslatePipe], templateUrl: './two-factor-auth.component.html', styleUrl: './two-factor-auth.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -30,69 +28,70 @@ import { QRCodeComponent } from 'angularx-qrcode'; export class TwoFactorAuthComponent { private readonly actions = createDispatchMap({ disableTwoFactorAuth: DisableTwoFactorAuth, - setAccountSettings: SetAccountSettings, + enableTwoFactorAuth: EnableTwoFactorAuth, + verifyTwoFactorAuth: VerifyTwoFactorAuth, }); - private readonly dialogService = inject(DialogService); - private readonly accountSettingsService = inject(AccountSettingsService); - private readonly translateService = inject(TranslateService); + private readonly toastService = inject(ToastService); + private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly loaderService = inject(LoaderService); readonly accountSettings = select(AccountSettingsSelectors.getAccountSettings); readonly currentUser = select(UserSelectors.getCurrentUser); + + codeMaxLength = InputLimits.code.maxLength; + dialogRef: DynamicDialogRef | null = null; qrCodeLink = computed(() => { return `otpauth://totp/OSF:${this.currentUser()?.email}?secret=${this.accountSettings()?.secret}`; }); - verificationCode = new FormControl('', { - nonNullable: true, + verificationCode = new FormControl(null, { validators: [Validators.required], }); - errorMessage = signal(''); - configureTwoFactorAuth(): void { - this.dialogRef = this.dialogService.open(ConfigureTwoFactorComponent, { - width: '520px', - focusOnShow: false, - header: this.translateService.instant('settings.accountSettings.twoFactorAuth.dialog.configure.title'), - closeOnEscape: true, - modal: true, - closable: true, - data: this.accountSettings(), + this.customConfirmationService.confirmAccept({ + headerKey: 'settings.accountSettings.twoFactorAuth.configure.title', + messageKey: 'settings.accountSettings.twoFactorAuth.configure.description', + acceptLabelKey: 'settings.accountSettings.common.buttons.configure', + onConfirm: () => { + this.loaderService.show(); + this.actions.enableTwoFactorAuth().subscribe(() => this.loaderService.hide()); + }, }); } openDisableDialog() { - this.dialogRef = this.dialogService.open(VerifyTwoFactorComponent, { - width: '520px', - focusOnShow: false, - header: this.translateService.instant('settings.accountSettings.twoFactorAuth.dialog.disable.title'), - closeOnEscape: true, - modal: true, - closable: true, + this.customConfirmationService.confirmAccept({ + headerKey: 'settings.accountSettings.twoFactorAuth.disable.title', + messageKey: 'settings.accountSettings.twoFactorAuth.disable.message', + acceptLabelKey: 'settings.accountSettings.common.buttons.disable', + onConfirm: () => this.disableTwoFactor(), }); } enableTwoFactor(): void { - this.accountSettingsService.updateSettings({ two_factor_verification: this.verificationCode.value }).subscribe({ - next: (response: AccountSettings) => { - this.actions.setAccountSettings(response); - }, - error: (error: HttpErrorResponse) => { - if (error.error?.errors?.[0]?.detail) { - this.errorMessage.set(error.error.errors[0].detail); - } else { - this.errorMessage.set( - this.translateService.instant('settings.accountSettings.twoFactorAuth.verification.error') - ); - } + if (!this.verificationCode.value) { + return; + } + + this.loaderService.show(); + + this.actions.verifyTwoFactorAuth(this.verificationCode.value).subscribe({ + next: () => { + this.loaderService.hide(); + this.toastService.showSuccess('settings.accountSettings.twoFactorAuth.verification.success'); }, }); } disableTwoFactor(): void { - this.actions.disableTwoFactorAuth(); + this.loaderService.show(); + this.actions.disableTwoFactorAuth().subscribe(() => { + this.loaderService.hide(); + this.toastService.showSuccess('settings.accountSettings.twoFactorAuth.successDisable'); + }); } } 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 09dec97f0..993b133dc 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 @@ -80,9 +80,9 @@ export class AccountSettingsService { return this.jsonApiService .post< - ApiData + JsonApiResponse, null> >(`${environment.apiUrl}/users/${this.currentUser()?.id}/settings/emails/`, body) - .pipe(map((response) => MapEmail(response))); + .pipe(map((response) => MapEmail(response.data))); } deleteEmail(emailId: string): Observable { diff --git a/src/app/features/settings/account-settings/store/account-settings.actions.ts b/src/app/features/settings/account-settings/store/account-settings.actions.ts index 25d2c392c..3e38dae33 100644 --- a/src/app/features/settings/account-settings/store/account-settings.actions.ts +++ b/src/app/features/settings/account-settings/store/account-settings.actions.ts @@ -1,5 +1,3 @@ -import { AccountSettings } from '../models'; - export class GetEmails { static readonly type = '[AccountSettings] Get Emails'; } @@ -16,6 +14,15 @@ export class DeleteEmail { constructor(public email: string) {} } +export class ResendConfirmation { + static readonly type = '[AccountSettings] Resend Confirmation'; + + constructor( + public emailId: string, + public userId: string + ) {} +} + export class VerifyEmail { static readonly type = '[AccountSettings] Verify Email'; @@ -94,12 +101,6 @@ export class VerifyTwoFactorAuth { constructor(public code: string) {} } -export class SetAccountSettings { - static readonly type = '[AccountSettings] SetAccountSettings'; - - constructor(public accountSettings: AccountSettings) {} -} - export class DeactivateAccount { static readonly type = '[AccountSettings] Deactivate Account'; } 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 0c91d5e66..1e95cb543 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,12 +1,32 @@ -import { Institution } from '@shared/models'; +import { AsyncStateModel, Institution } from '@shared/models'; import { AccountEmail, AccountSettings, ExternalIdentity, Region } from '../models'; export interface AccountSettingsStateModel { - emails: AccountEmail[]; - emailsLoading: boolean; + emails: AsyncStateModel; regions: Region[]; externalIdentities: ExternalIdentity[]; accountSettings: AccountSettings; userInstitutions: Institution[]; } + +export const ACCOUNT_SETTINGS_STATE_DEFAULTS: AccountSettingsStateModel = { + emails: { + data: [], + isLoading: false, + error: null, + isSubmitting: false, + }, + regions: [], + externalIdentities: [], + accountSettings: { + twoFactorEnabled: false, + twoFactorConfirmed: false, + subscribeOsfGeneralEmail: false, + subscribeOsfHelpEmail: false, + deactivationRequested: false, + contactedDeactivation: false, + secret: '', + }, + userInstitutions: [], +}; 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 b109dcae5..3614770f5 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 @@ -10,12 +10,17 @@ import { AccountSettingsState } from './account-settings.state'; export class AccountSettingsSelectors { @Selector([AccountSettingsState]) static getEmails(state: AccountSettingsStateModel): AccountEmail[] { - return state.emails; + return state.emails.data; } @Selector([AccountSettingsState]) static isEmailsLoading(state: AccountSettingsStateModel): boolean { - return state.emailsLoading; + return state.emails.isLoading; + } + + @Selector([AccountSettingsState]) + static isEmailsSubmitting(state: AccountSettingsStateModel): boolean | undefined { + return state.emails.isSubmitting; } @Selector([AccountSettingsState]) diff --git a/src/app/features/settings/account-settings/store/account-settings.state.ts b/src/app/features/settings/account-settings/store/account-settings.state.ts index b6fe870fd..f5915fd57 100644 --- a/src/app/features/settings/account-settings/store/account-settings.state.ts +++ b/src/app/features/settings/account-settings/store/account-settings.state.ts @@ -1,10 +1,11 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { finalize, tap } from 'rxjs'; +import { catchError, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { SetCurrentUser } from '@core/store/user'; +import { handleSectionError } from '@osf/core/handlers'; import { InstitutionsService } from '@shared/services'; import { AccountSettingsService } from '../services'; @@ -24,33 +25,19 @@ import { GetRegions, GetUserInstitutions, MakePrimary, - SetAccountSettings, + ResendConfirmation, UpdateAccountSettings, UpdateIndexing, UpdateRegion, VerifyEmail, + VerifyTwoFactorAuth, } from './account-settings.actions'; -import { AccountSettingsStateModel } from './account-settings.model'; +import { ACCOUNT_SETTINGS_STATE_DEFAULTS, AccountSettingsStateModel } from './account-settings.model'; @Injectable() @State({ name: 'accountSettings', - defaults: { - emails: [], - emailsLoading: false, - regions: [], - externalIdentities: [], - accountSettings: { - twoFactorEnabled: false, - twoFactorConfirmed: false, - subscribeOsfGeneralEmail: false, - subscribeOsfHelpEmail: false, - deactivationRequested: false, - contactedDeactivation: false, - secret: '', - }, - userInstitutions: [], - }, + defaults: ACCOUNT_SETTINGS_STATE_DEFAULTS, }) export class AccountSettingsState { private readonly accountSettingsService = inject(AccountSettingsService); @@ -58,40 +45,77 @@ export class AccountSettingsState { @Action(GetEmails) getEmails(ctx: StateContext) { - ctx.patchState({ emailsLoading: true }); + const state = ctx.getState(); + + ctx.patchState({ emails: { ...state.emails, isLoading: true } }); return this.accountSettingsService.getEmails().pipe( - tap({ - next: (emails) => { - ctx.patchState({ - emails: emails, - }); - }, + tap((emails) => { + ctx.patchState({ + emails: { + data: emails, + isLoading: false, + error: null, + }, + }); }), - finalize(() => ctx.patchState({ emailsLoading: false })) + catchError((error) => handleSectionError(ctx, 'emails', error)) ); } @Action(AddEmail) addEmail(ctx: StateContext, action: AddEmail) { + const state = ctx.getState(); + ctx.patchState({ emails: { ...state.emails, isSubmitting: true } }); + return this.accountSettingsService.addEmail(action.email).pipe( tap((email) => { + ctx.patchState({ + emails: { + data: state.emails.data, + isSubmitting: false, + isLoading: false, + error: null, + }, + }); + if (email.emailAddress && !email.confirmed) { ctx.dispatch(GetEmails); } - }) + }), + catchError((error) => handleSectionError(ctx, 'emails', error)) ); } @Action(DeleteEmail) deleteEmail(ctx: StateContext, action: DeleteEmail) { + const state = ctx.getState(); + ctx.patchState({ emails: { ...state.emails, isLoading: true } }); + return this.accountSettingsService.deleteEmail(action.email).pipe( tap(() => { + ctx.patchState({ + emails: { + data: state.emails.data, + isSubmitting: false, + isLoading: false, + error: null, + }, + }); + ctx.dispatch(GetEmails); - }) + }), + catchError((error) => handleSectionError(ctx, 'emails', error)) ); } + @Action(ResendConfirmation) + resendConfirmation(ctx: StateContext, action: ResendConfirmation) { + return this.accountSettingsService + .resendConfirmation(action.emailId, action.userId) + .pipe(catchError((error) => throwError(() => error))); + } + @Action(VerifyEmail) verifyEmail(ctx: StateContext, action: VerifyEmail) { return this.accountSettingsService.verifyEmail(action.userId, action.emailId).pipe( @@ -99,7 +123,8 @@ export class AccountSettingsState { if (email.verified) { ctx.dispatch(GetEmails); } - }) + }), + catchError((error) => throwError(() => error)) ); } @@ -110,155 +135,120 @@ export class AccountSettingsState { if (email.verified) { ctx.dispatch(GetEmails); } - }) + }), + catchError((error) => throwError(() => error)) ); } @Action(GetRegions) getRegions(ctx: StateContext) { return this.accountSettingsService.getRegions().pipe( - tap({ - next: (regions) => ctx.patchState({ regions: regions }), - }) + tap((regions) => ctx.patchState({ regions: regions })), + catchError((error) => throwError(() => error)) ); } @Action(UpdateRegion) updateRegion(ctx: StateContext, action: UpdateRegion) { return this.accountSettingsService.updateLocation(action.regionId).pipe( - tap({ - next: (user) => { - ctx.dispatch(new SetCurrentUser(user)); - }, - }) + tap((user) => ctx.dispatch(new SetCurrentUser(user))), + catchError((error) => throwError(() => error)) ); } @Action(UpdateIndexing) updateIndexing(ctx: StateContext, action: UpdateIndexing) { return this.accountSettingsService.updateIndexing(action.allowIndexing).pipe( - tap({ - next: (user) => { - ctx.dispatch(new SetCurrentUser(user)); - }, - }) + tap((user) => ctx.dispatch(new SetCurrentUser(user))), + catchError((error) => throwError(() => error)) ); } @Action(GetExternalIdentities) getExternalIdentities(ctx: StateContext) { return this.accountSettingsService.getExternalIdentities().pipe( - tap({ - next: (identities) => ctx.patchState({ externalIdentities: identities }), - }) + tap((identities) => ctx.patchState({ externalIdentities: identities })), + catchError((error) => throwError(() => error)) ); } @Action(DeleteExternalIdentity) deleteExternalIdentity(ctx: StateContext, action: DeleteExternalIdentity) { return this.accountSettingsService.deleteExternalIdentity(action.externalId).pipe( - tap(() => { - ctx.dispatch(GetExternalIdentities); - }) + tap(() => ctx.dispatch(GetExternalIdentities)), + catchError((error) => throwError(() => error)) ); } @Action(GetUserInstitutions) getUserInstitutions(ctx: StateContext) { - return this.institutionsService - .getUserInstitutions() - .pipe(tap((userInstitutions) => ctx.patchState({ userInstitutions }))); + return this.institutionsService.getUserInstitutions().pipe( + tap((userInstitutions) => ctx.patchState({ userInstitutions })), + catchError((error) => throwError(() => error)) + ); } @Action(DeleteUserInstitution) deleteUserInstitution(ctx: StateContext, action: DeleteUserInstitution) { return this.institutionsService.deleteUserInstitution(action.id, action.userId).pipe( - tap(() => { - ctx.dispatch(GetUserInstitutions); - }) + tap(() => ctx.dispatch(GetUserInstitutions)), + catchError((error) => throwError(() => error)) ); } @Action(GetAccountSettings) getAccountSettings(ctx: StateContext) { return this.accountSettingsService.getSettings().pipe( - tap({ - next: (settings) => { - ctx.patchState({ - accountSettings: settings, - }); - }, - }) + tap((settings) => ctx.patchState({ accountSettings: settings })), + catchError((error) => throwError(() => error)) ); } @Action(UpdateAccountSettings) updateAccountSettings(ctx: StateContext, action: UpdateAccountSettings) { return this.accountSettingsService.updateSettings(action.accountSettings).pipe( - tap({ - next: (settings) => { - ctx.patchState({ - accountSettings: settings, - }); - }, - }) + tap((settings) => ctx.patchState({ accountSettings: settings })), + catchError((error) => throwError(() => error)) ); } @Action(DisableTwoFactorAuth) disableTwoFactorAuth(ctx: StateContext) { return this.accountSettingsService.updateSettings({ two_factor_enabled: 'false' }).pipe( - tap({ - next: (settings) => { - ctx.patchState({ - accountSettings: settings, - }); - }, - }) + tap((settings) => ctx.patchState({ accountSettings: settings })), + catchError((error) => throwError(() => error)) ); } @Action(EnableTwoFactorAuth) enableTwoFactorAuth(ctx: StateContext) { return this.accountSettingsService.updateSettings({ two_factor_enabled: 'true' }).pipe( - tap({ - next: (settings) => { - ctx.patchState({ - accountSettings: settings, - }); - }, - }) + tap((settings) => ctx.patchState({ accountSettings: settings })), + catchError((error) => throwError(() => error)) ); } - @Action(SetAccountSettings) - setAccountSettings(ctx: StateContext, action: SetAccountSettings) { - ctx.patchState({ accountSettings: action.accountSettings }); + @Action(VerifyTwoFactorAuth) + verifyTwoFactorAuth(ctx: StateContext, action: VerifyTwoFactorAuth) { + return this.accountSettingsService.updateSettings({ two_factor_verification: action.code }).pipe( + tap((settings) => ctx.patchState({ accountSettings: settings })), + catchError((error) => throwError(() => error)) + ); } @Action(DeactivateAccount) deactivateAccount(ctx: StateContext) { return this.accountSettingsService.updateSettings({ deactivation_requested: 'true' }).pipe( - tap({ - next: (settings) => { - ctx.patchState({ - accountSettings: settings, - }); - }, - }) + tap((settings) => ctx.patchState({ accountSettings: settings })), + catchError((error) => throwError(() => error)) ); } @Action(CancelDeactivationRequest) cancelDeactivationRequest(ctx: StateContext) { return this.accountSettingsService.updateSettings({ deactivation_requested: 'false' }).pipe( - tap({ - next: (settings) => { - ctx.patchState({ - accountSettings: settings, - }); - }, - }) + tap((settings) => ctx.patchState({ accountSettings: settings })), + catchError((error) => throwError(() => error)) ); } } diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 95f53a51b..77a2bcbc5 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -22,6 +22,7 @@ export { MarkdownComponent } from './markdown/markdown.component'; export { MyProjectsTableComponent } from './my-projects-table/my-projects-table.component'; export { PasswordInputHintComponent } from './password-input-hint/password-input-hint.component'; export { PieChartComponent } from './pie-chart/pie-chart.component'; +export { ReadonlyInputComponent } from './readonly-input/readonly-input.component'; export { ResourceCardComponent } from './resource-card/resource-card.component'; export { ResourceMetadataComponent } from './resource-metadata/resource-metadata.component'; export { ReusableFilterComponent } from './reusable-filter/reusable-filter.component'; diff --git a/src/app/shared/components/readonly-input/readonly-input.component.html b/src/app/shared/components/readonly-input/readonly-input.component.html new file mode 100644 index 000000000..1b506f279 --- /dev/null +++ b/src/app/shared/components/readonly-input/readonly-input.component.html @@ -0,0 +1,15 @@ + + + + diff --git a/src/app/shared/components/readonly-input/readonly-input.component.scss b/src/app/shared/components/readonly-input/readonly-input.component.scss new file mode 100644 index 000000000..bd4db068c --- /dev/null +++ b/src/app/shared/components/readonly-input/readonly-input.component.scss @@ -0,0 +1,7 @@ +.remove-icon { + color: var(--dark-blue-1); + + &:hover { + color: var(--dark-blue-2); + } +} diff --git a/src/app/shared/components/readonly-input/readonly-input.component.spec.ts b/src/app/shared/components/readonly-input/readonly-input.component.spec.ts new file mode 100644 index 000000000..e79b2d164 --- /dev/null +++ b/src/app/shared/components/readonly-input/readonly-input.component.spec.ts @@ -0,0 +1,113 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ReadonlyInputComponent } from './readonly-input.component'; + +describe('ReadonlyInputComponent', () => { + let component: ReadonlyInputComponent; + let fixture: ComponentFixture; + + const mockValue = 'test value'; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReadonlyInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ReadonlyInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display value when input is provided', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.detectChanges(); + + const inputElement = fixture.nativeElement.querySelector('input'); + expect(inputElement.value).toBe(mockValue); + }); + + it('should be readonly by default', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.detectChanges(); + + const inputElement = fixture.nativeElement.querySelector('input'); + expect(inputElement.readOnly).toBe(true); + }); + + it('should not be readonly when readonly input is false', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.componentRef.setInput('readonly', false); + fixture.detectChanges(); + + const inputElement = fixture.nativeElement.querySelector('input'); + expect(inputElement.readOnly).toBe(false); + }); + + it('should be disabled when disabled input is true', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.componentRef.setInput('disabled', true); + fixture.detectChanges(); + + const inputElement = fixture.nativeElement.querySelector('input'); + expect(inputElement.disabled).toBe(true); + }); + + it('should emit deleteItem when remove icon is clicked', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.detectChanges(); + + const deleteSpy = jest.spyOn(component.deleteItem, 'emit'); + const removeIcon = fixture.nativeElement.querySelector('.remove-icon'); + + removeIcon.click(); + + expect(deleteSpy).toHaveBeenCalled(); + }); + + it('should not emit deleteItem when disabled', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.componentRef.setInput('disabled', true); + fixture.detectChanges(); + + const deleteSpy = jest.spyOn(component.deleteItem, 'emit'); + const removeIcon = fixture.nativeElement.querySelector('.remove-icon'); + + removeIcon.click(); + + expect(deleteSpy).not.toHaveBeenCalled(); + }); + + it('should have remove icon with correct classes', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.detectChanges(); + + const removeIcon = fixture.nativeElement.querySelector('.remove-icon'); + expect(removeIcon).toBeTruthy(); + expect(removeIcon.classList.contains('fas')).toBe(true); + expect(removeIcon.classList.contains('fa-close')).toBe(true); + expect(removeIcon.classList.contains('cursor-pointer')).toBe(true); + }); + + it('should have disabled class on remove icon when disabled', () => { + fixture.componentRef.setInput('value', mockValue); + fixture.componentRef.setInput('disabled', true); + fixture.detectChanges(); + + const removeIcon = fixture.nativeElement.querySelector('.remove-icon'); + expect(removeIcon.classList.contains('disabled')).toBe(true); + }); + + it('should display placeholder when provided', () => { + const placeholder = 'Enter value'; + fixture.componentRef.setInput('value', mockValue); + fixture.componentRef.setInput('placeholder', placeholder); + fixture.detectChanges(); + + const inputElement = fixture.nativeElement.querySelector('input'); + expect(inputElement.placeholder).toBe(placeholder); + }); +}); diff --git a/src/app/shared/components/readonly-input/readonly-input.component.ts b/src/app/shared/components/readonly-input/readonly-input.component.ts new file mode 100644 index 000000000..1385ff827 --- /dev/null +++ b/src/app/shared/components/readonly-input/readonly-input.component.ts @@ -0,0 +1,21 @@ +import { IconField } from 'primeng/iconfield'; +import { InputIcon } from 'primeng/inputicon'; +import { InputText } from 'primeng/inputtext'; + +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; + +@Component({ + selector: 'osf-readonly-input', + imports: [IconField, InputIcon, InputText], + templateUrl: './readonly-input.component.html', + styleUrl: './readonly-input.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReadonlyInputComponent { + value = input.required(); + readonly = input(true); + disabled = input(false); + placeholder = input(''); + + deleteItem = output(); +} diff --git a/src/app/shared/constants/input-limits.const.ts b/src/app/shared/constants/input-limits.const.ts index 257b41555..5b512bdd3 100644 --- a/src/app/shared/constants/input-limits.const.ts +++ b/src/app/shared/constants/input-limits.const.ts @@ -28,4 +28,7 @@ export const InputLimits = { description: { maxLength: 250, }, + code: { + maxLength: 6, + }, }; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 029cfefcc..549e8d569 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1414,7 +1414,8 @@ "disable": "Disable", "enable": "Enable", "undo": "Undo", - "request": "Request" + "request": "Request", + "deactivate": "Deactivate" } }, "deactivateAccount": { @@ -1438,14 +1439,22 @@ "title": "Deactivate account" }, "undo": { - "title": "Undo deactivation request?" + "title": "Undo deactivation request?", + "message": "Are you sure you want to undo your account deactivation request? This will preserve your account status." } - } + }, + "successCancelDeactivation": "Deactivation request successfully undone.", + "successDeactivation": "An OSF administrator will contact you shortly to confirm your deactivation request." }, "affiliatedInstitutions": { "title": "Affiliated Institutions", "description": "Connect your account to institutions to access institutional features and services.", - "noInstitutions": "You have no affiliations." + "noInstitutions": "You have no affiliations.", + "deleteDialog": { + "header": "Delete institution", + "message": "Are you sure you want to delete {{name}} institution?" + }, + "successDelete": "Successfully deleted affiliated institution." }, "addEmail": { "title": "Add alternative email", @@ -1503,19 +1512,38 @@ "deleteDialog": { "header": "Delete email", "message": "Are you sure you want to delete {{name}} email?" - } + }, + "confirmationSentDialog": { + "header": "Confirmation email sent", + "message": "Your account now includes {{email}}. A confirmation email has been sent to that address. Click the link in the email to confirm this action." + }, + "resendDialog": { + "header": "Resend email confirmation", + "message": "Are you sure you want to resend email confirmation to {{email}}?" + }, + "makePrimaryDialog": { + "header": "Make email primary", + "message": "Are you sure you want to make {{email}} email primary?" + }, + "successResend": "Email successfully resent.", + "successMakePrimary": "Email successfully set as primary.", + "successDelete": "Email successfully deleted.", + "successAdd": "Email successfully added." }, "connectedIdentities": { "title": "Connected identities", "description": "Connected identities allow you to log in to the OSF via a third-party service. You can revoke these identifies.", - "noIdentities": "You have not authorized any external services to log in to the OSF." + "noIdentities": "You have not authorized any external services to log in to the OSF.", + "deleteDialog": { + "header": "Delete connected identity", + "message": "Are you sure you want to delete {{name}} identity?" + }, + "successDelete": "Successfully deleted connected identities." }, "defaultStorageLocation": { "title": "Default storage location", "description": "This location will be applied to new projects and components. It will not affect existing projects and components.", - "buttons": { - "update": "Update Location" - } + "successUpdate": "Successfully updated default storage location." }, "shareIndexing": { "title": "Opt out of SHARE indexing", @@ -1525,9 +1553,7 @@ "optOut": "Out of SHARE Indexing", "optIn": "Opt In To SHARE Indexing" }, - "buttons": { - "update": "Update" - } + "successUpdate": "Successfully updated SHARE indexing preference." }, "twoFactorAuth": { "title": "Two-factor authentication", @@ -1541,25 +1567,19 @@ }, "verification": { "label": "Enter your verification code:", - "error": "Verification code is invalid. Please try again." - }, - "dialog": { - "configure": { - "title": "Configure" - }, - "disable": { - "title": "Disable" - } + "error": "Verification code is invalid. Please try again.", + "success": "Two-factor authentication successfully set up." }, "configure": { - "description": { - "main": "Configuring two-factor authentication will not immediately activate this feature for your account.", - "steps": "You will need to follow the steps that appear below to complete the activation of two-factor authentication for your account." - } + "title": "Configure", + "description": "Configuring two-factor authentication will not immediately activate this feature for your account. You will need to follow the steps that appear below to complete the activation of two-factor authentication for your account." }, - "verify": { - "title": "Are you sure you want to disable two-factor authentication?" - } + "disable": { + "title": "Disable", + "message": "Are you sure you want to disable two-factor authentication?" + }, + "successDisable": "Two-factor authentication successfully disabled.", + "enterCode": "Enter 6-digit code" } } }, diff --git a/src/assets/styles/_common.scss b/src/assets/styles/_common.scss index 11a2f04e4..b370318a7 100644 --- a/src/assets/styles/_common.scss +++ b/src/assets/styles/_common.scss @@ -108,3 +108,8 @@ .fit-contain { object-fit: contain; } + +.token { + font-weight: 700; + color: var(--red-1); +} From e32f25628c009a6be30e9644c7a01411a316376f Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 16 Jul 2025 19:09:14 +0300 Subject: [PATCH 7/7] fix(my-projects): updated store inject --- src/app/features/my-projects/my-projects.component.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index db466a961..1bb9c9110 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -1,4 +1,4 @@ -import { createDispatchMap, select, Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; @@ -66,7 +66,6 @@ import { export class MyProjectsComponent implements OnInit { readonly destroyRef = inject(DestroyRef); readonly dialogService = inject(DialogService); - readonly store = inject(Store); readonly router = inject(Router); readonly route = inject(ActivatedRoute); readonly translateService = inject(TranslateService); @@ -118,7 +117,7 @@ export class MyProjectsComponent implements OnInit { } ngOnInit(): void { - this.store.dispatch(new GetBookmarksCollectionId()); + this.actions.getBookmarksCollectionId(); } setupCleanup(): void {