Open Closed

Customizing PersonalSettings (Personal Info) page not working #1404


0
beuko created
  • ABP Framework version: v4.3.1
  • UI type: Angular
  • DB provider: EF Core
  • Tiered (MVC) or Identity Server Separated (Angular): no / no
  • Exception message and stack trace:
  • Steps to reproduce the issue:

Hello. I am trying to add boolean field to Personal Setting (Personal info) in Angular UI. Field already exists in AbpUsers table.

I am following this guide https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement.

First I installed module @abp/ng.account with yarn so I can get eAccountComponents.PersonalSettings for replacing. After I installed module I got bunch of errors in others components (only html classes)...

After that I created my custom component and added replaceble logic to AppComponent

import { Component } from '@angular/core';
import { CustomPersonalSettingsComponentComponent } from './custom-personal-settings-component/custom-personal-settings-component.component';
import { ReplaceableComponentsService } from '@abp/ng.core';
import { eAccountComponents } from '@abp/ng.account';

@Component({
  selector: 'app-root',
  template: `
    <abp-loader-bar></abp-loader-bar>
    <abp-dynamic-layout></abp-dynamic-layout>
  `,
})
export class AppComponent {

  constructor(
    private replaceableComponents: ReplaceableComponentsService, // injected the service
  ) {
    this.replaceableComponents.add({
      component: CustomPersonalSettingsComponentComponent,
      key: eAccountComponents.PersonalSettings,
    });
  }

}

After adding imports, I got error that abp-dynamics-layout is not a known element...

Even with all that errors, my project compiled successfully and I can use all my broken components (html). When I go to Personal info, it still show default component, not mine...

Anyone know why I got these errors in html after installing @abp/ng.account and why PersonalInfo replacing is not working? If I try and replace Manage profile component it works...


13 Answer(s)
  • 0
    beuko created

    I fixed problems with html components.

    First I cleared cache, then deleted node_modules folder and reinstalled all modules with yarn install... Now I dont have any errors with html, but replacing PageSettings component still not working...

  • 0
    beuko created

    Any answer to this?

  • 0
    Mehmet created
    Support Team

    Hi,

    Please follow the steps below to replace personal settings component:

    • Run the following command to generate account module:
    yarn ng generate module account
    
    • Run command below to generate a component called CustomPersonalSettings:
    yarn ng generate component account/custom-personal-settings --skip-tests --inline-style
    
    • Replace the generated src/account/account.module.ts content with following:
    import { NgModule } from '@angular/core';
    import { AccountPublicModule } from '@volo/abp.ng.account/public';
    import { SharedModule } from '../shared/shared.module';
    import { CustomPersonalSettingsComponent } from './custom-personal-settings/custom-personal-settings.component';
    
    @NgModule({
      declarations: [CustomPersonalSettingsComponent],
      imports: [AccountPublicModule.forChild({}), SharedModule],
    })
    export class AccountModule {}
    
    • Replace the generated src/account/custom-personal-settings/custom-personal-settings.component.ts content with following:
    import { ConfigStateService, ProfileService, SubscriptionService } from '@abp/ng.core';
    import { Confirmation, ConfirmationService, ToasterService } from '@abp/ng.theme.shared';
    import { Component, EmbeddedViewRef, OnInit } from '@angular/core';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    import {
      AccountService,
      ManageProfileStateService,
      ProfileResponse,
    } from '@volo/abp.ng.account/public';
    import { Observable } from 'rxjs';
    import { filter, tap } from 'rxjs/operators';
    
    @Component({
      selector: 'app-custom-personal-settings',
      templateUrl: './custom-personal-settings.component.html',
      styles: [],
    })
    export class CustomPersonalSettingsComponent implements OnInit {
      storedProfile: ProfileResponse;
    
      profile$: Observable<ProfileResponse> = this.manageProfileState.getProfile$();
    
      modalVisible = false;
    
      modalBusy: boolean = false;
    
      modalRef: EmbeddedViewRef<any>;
    
      form: FormGroup;
    
      token: string = '';
    
      isEmailUpdateEnabled: boolean = true;
      isUserNameUpdateEnabled: boolean = true;
    
      showEmailVerificationBtn$ = this.manageProfileState.createStateStream(
        state => !state.hideEmailVerificationBtn
      );
    
      get userId(): string {
        return this.configState.getDeep('currentUser.id');
      }
    
      askForImmediatePhoneNumberVerification = () => {
        this.confirmationService
          .info('AbpAccount::DoYouWantToVerifyPhoneNumberMessage', 'AbpAccount::ConfirmYourPhoneNumber')
          .pipe(filter(status => status === Confirmation.Status.confirm))
          .subscribe(this.initPhoneNumberConfirmation);
      };
    
      checkPhoneNumberChanges = ({ phoneNumber }: ProfileResponse) => {
        if (phoneNumber && phoneNumber !== this.storedProfile?.phoneNumber) {
          this.askForImmediatePhoneNumberVerification();
        }
      };
    
      buildForm = (profile: ProfileResponse) => {
        this.form = this.fb.group({
          userName: [profile.userName, [Validators.required, Validators.maxLength(256)]],
          email: [profile.email, [Validators.required, Validators.email, Validators.maxLength(256)]],
          name: [profile.name || '', [Validators.maxLength(64)]],
          surname: [profile.surname || '', [Validators.maxLength(64)]],
          phoneNumber: [profile.phoneNumber || '', [Validators.maxLength(16)]],
        });
      };
    
      initPhoneNumberConfirmation = () => {
        this.accountService
          .sendPhoneNumberConfirmationToken({
            phoneNumber: this.form.value.phoneNumber,
            userId: this.userId,
          })
          .pipe(tap(() => (this.token = '')))
          .subscribe(this.openModal);
      };
    
      openModal = () => {
        this.modalVisible = true;
      };
    
      removeModal = () => {
        this.modalVisible = false;
      };
    
      setPhoneNumberAsConfirmed = () => {
        this.storedProfile.phoneNumberConfirmed = true;
      };
    
      constructor(
        private fb: FormBuilder,
        private accountService: AccountService,
        private toasterService: ToasterService,
        private confirmationService: ConfirmationService,
        private configState: ConfigStateService,
        private subscription: SubscriptionService,
        private manageProfileState: ManageProfileStateService,
        private profileService: ProfileService
      ) {}
    
      confirmPhoneNumber() {
        this.accountService
          .confirmPhoneNumber({ token: this.token, userId: this.userId })
          .pipe(tap(this.setPhoneNumberAsConfirmed), tap(this.removeModal))
          .subscribe(() => {
            this.toasterService.success('AbpAccount::Verified', '', { life: 5000 });
          });
      }
    
      sendEmailVerificationToken() {
        this.accountService
          .sendEmailConfirmationToken({
            appName: 'Angular',
            email: this.form.value.email,
            userId: this.userId,
          })
          .subscribe(() => {
            this.toasterService.success('AbpAccount::EmailConfirmationSentMessage', '', {
              messageLocalizationParams: [this.form.value.email],
            });
    
            this.manageProfileState.setHideEmailVerificationBtn(true);
          });
      }
    
      ngOnDestroy() {
        this.removeModal();
      }
    
      ngOnInit() {
        this.subscription.addOne(
          this.profile$.pipe(
            filter<ProfileResponse>(Boolean),
            tap(profile => (this.storedProfile = profile)),
            tap(this.checkPhoneNumberChanges)
          ),
          this.buildForm
        );
    
        const settings = this.configState.getSettings();
        this.isEmailUpdateEnabled =
          (settings['Abp.Identity.User.IsEmailUpdateEnabled'] || '').toLowerCase() !== 'false';
        this.isUserNameUpdateEnabled =
          (settings['Abp.Identity.User.IsUserNameUpdateEnabled'] || '').toLowerCase() !== 'false';
      }
    
      submit() {
        if (this.form.invalid) return;
    
        const { phoneNumberConfirmed, ...profile } = this.form.value;
    
        this.profileService.update(profile).subscribe(res => {
          this.manageProfileState.setProfile(res);
          this.toasterService.success('AbpAccount::PersonalSettingsSaved', '', { life: 5000 });
        });
      }
    }
    
    • Replace the generated src/account/custom-personal-settings/custom-personal-settings.component.html content with following:
    <form validateOnSubmit *ngIf="form" [formGroup]="form" (ngSubmit)="submit()">
      <div class="form-group">
        <label for="username">{{ 'AbpIdentity::DisplayName:UserName' | abpLocalization }}</label
        ><span> * </span
        ><input
          type="text"
          id="username"
          class="form-control"
          formControlName="userName"
          autofocus
          (keydown.space)="$event.preventDefault()"
          [readonly]="!isUserNameUpdateEnabled"
        />
      </div>
      <div class="row">
        <div class="col col-md-6">
          <div class="form-group">
            <label for="name">{{ 'AbpIdentity::DisplayName:Name' | abpLocalization }}</label
            ><input type="text" id="name" class="form-control" formControlName="name" />
          </div>
        </div>
        <div class="col col-md-6">
          <div class="form-group">
            <label for="surname">{{ 'AbpIdentity::DisplayName:Surname' | abpLocalization }}</label
            ><input type="text" id="surname" class="form-control" formControlName="surname" />
          </div>
        </div>
      </div>
      <div class="form-group">
        <label for="email-address">{{ 'AbpIdentity::DisplayName:Email' | abpLocalization }}</label
        ><span> * </span>
        <div class="input-group" validationTarget validationStyle>
          <input
            type="text"
            id="email-address"
            class="form-control"
            formControlName="email"
            [readonly]="!isEmailUpdateEnabled"
          />
          <div
            *ngIf="{
              edited: form.value.email !== storedProfile?.email,
              confirmed: storedProfile?.emailConfirmed
            } as data"
            class="input-group-append"
          >
            <abp-personal-settings-verify-button
              *ngIf="(showEmailVerificationBtn$ | async) || data.edited || data.confirmed"
              [verified]="data.confirmed"
              [edited]="data.edited"
              (btnClick)="sendEmailVerificationToken()"
            ></abp-personal-settings-verify-button>
          </div>
        </div>
      </div>
      <div class="form-group">
        <label for="phone-number">{{ 'AbpIdentity::DisplayName:PhoneNumber' | abpLocalization }}</label>
        <div class="input-group mb-3">
          <input type="text" id="phone-number" class="form-control" formControlName="phoneNumber" />
          <div class="input-group-append" *ngIf="storedProfile?.phoneNumber">
            <abp-personal-settings-verify-button
              [verified]="storedProfile?.phoneNumberConfirmed"
              [edited]="form.value.phoneNumber !== storedProfile?.phoneNumber"
              (btnClick)="initPhoneNumberConfirmation()"
            ></abp-personal-settings-verify-button>
          </div>
        </div>
      </div>
      <abp-button
        iconClass="fa fa-check"
        buttonClass="btn btn-primary color-white"
        buttonType="submit"
        [disabled]="form?.invalid"
      >
        {{ 'AbpIdentity::Save' | abpLocalization }}</abp-button
      >
    </form>
    
    <abp-modal [(visible)]="modalVisible" [busy]="modalBusy" size="sm">
      <ng-template #abpHeader>
        <h5>{{ 'AbpAccount::Verify' | abpLocalization }}</h5>
      </ng-template>
      <ng-template #abpBody>
        <form (ngSubmit)="confirmPhoneNumber()">
          <div class="mt-2">
            <p>
              {{ 'AbpAccount::ConfirmationTokenSentMessage' | abpLocalization: form.value.phoneNumber }}
            </p>
            <div class="form-group">
              <label for="confirm-phone-number">{{
                'AbpAccount::PhoneConfirmationToken' | abpLocalization
              }}</label>
              <input
                [(ngModel)]="token"
                id="confirm-phone-number"
                name="confirm-phone-number"
                class="form-control"
                type="text"
                autofocus
              />
            </div>
          </div>
        </form>
      </ng-template>
      <ng-template #abpFooter>
        <button abpClose type="button" class="btn btn-secondary">
          {{ 'AbpAccount::Cancel' | abpLocalization }}
        </button>
        <abp-button type="abp-button" iconClass="fa fa-check" (click)="confirmPhoneNumber()">
          {{ 'AbpAccount::Save' | abpLocalization }}
        </abp-button>
      </ng-template>
    </abp-modal>
    
    • Open src/app-routing.module.ts file modify account route like this:
      {
        path: 'account',
        loadChildren: () => import('./account/account.module').then(m => m.AccountModule),
      },
    
    • Open src/app.component.ts and replace the PersonalSettingsComponent as shown below:
    import { Component, OnInit } from '@angular/core';
    import {
      eAccountManageProfileTabNames,
      ManageProfileTabsService,
    } from '@volo/abp.ng.account/public/config';
    import { CustomPersonalSettingsComponent } from './account/custom-personal-settings/custom-personal-settings.component';
    
    @Component({
      selector: 'app-root',
      template: `
        &lt;abp-loader-bar&gt;&lt;/abp-loader-bar&gt;
        &lt;abp-dynamic-layout&gt;&lt;/abp-dynamic-layout&gt;
      `,
    })
    export class AppComponent implements OnInit {
      constructor(
        private manageProfileTabs: ManageProfileTabsService
      ) {}
    
      ngOnInit() {
        this.manageProfileTabs.patch(eAccountManageProfileTabNames.PersonalInfo, {
          component: CustomPersonalSettingsComponent,
        });
      }
    }
    

    You can replace a tab in the my profile page by using the ManageProfileTabsService.

    I hope this helps you.

  • 0
    beuko created

    Hello.

    Thanks, it works. Is there any guide on how to add aditional field to Profile.Response or how to extend Profile class and module to add new field so user can update it?

  • 0
    beuko created

    I am following this guide here: https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Overriding-Services#extending-data-transfer-objects but I don't have class YourProjectNameDtoExtensions in my solution.

    I created application with abp suite version 4.3.0.

  • 0
    maliming created
    Support Team

    hi

    You can create a MyProjectNameDtoExtensions file in your Application.Contracts layer.

    https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyProjectNameDtoExtensions.cs

    I will add this file to next version of app pro template.

  • 0
    beuko created

    Hi, thanks for answer.

    I created file but I can not extend ProfileDto and UpdateProfileDto. What am I missing? I have boolean field EmailNotifications, which I added to AppUser class and to MyProjectExtensionConfigurator. It shows on Administrations/Identity Managament/Users and also it works when I edit it there. Now I want to add this field to Personal info, so user can check/uncheck it himself. I followed your guide but can not add my custom property to ProfileDto. I found this line of code which populated ExtraProperties of ProfileDto but not also UpdateProfileDto. What am I doing wrong?

    Thanks for your answers!

  • 0
    maliming created
    Support Team

    hi beuko

    Can you share your full code about extending-data-transfer-objects?

  • 0
    beuko created

    AppUser.cs

    //added this line
    public bool EmailNotifications { get; protected set; }
    

    Application.Contracts/SKAPDtoExtensions.cs

    public static class SKAPDtoExtensions
        {
            private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
    
            public static void Configure()
            {
                OneTimeRunner.Run(() =>
                {
                    /* You can add extension properties to DTOs
                     * defined in the depended modules.
                     *
                     * Example:
                     *
                     * ObjectExtensionManager.Instance
                     *   .AddOrUpdateProperty<IdentityRoleDto, string>("Title");
                     *
                     * See the documentation for more:
                     * https://docs.abp.io/en/abp/latest/Object-Extensions
                     */
    
                    ObjectExtensionManager.Instance
                    .AddOrUpdateProperty<bool>(
                        new[]
                        {
                            typeof(ProfileDto),
                            typeof(UpdateProfileDto)
                        },
                        "EmailNotifications"
                    );
    
                });
            }
        }
    

    Domain.Shared/SKAPModuleExtensionConfigurator.cs

    public static class SKAPModuleExtensionConfigurator
        {
            private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
    
            public static void Configure()
            {
                OneTimeRunner.Run(() =>
                {
                    ConfigureExistingProperties();
                    ConfigureExtraProperties();
                });
            }
    
            private static void ConfigureExtraProperties()
            {
                /* You can configure extra properties for the
                 * entities defined in the modules used by your application.
                 *
                 * This class can be used to define these extra properties
                 * with a high level, easy to use API.
                 *
                 * Example: Add a new property to the user entity of the identity module
    
                   ObjectExtensionManager.Instance.Modules()
                      .ConfigureIdentity(identity =>
                      {
                          identity.ConfigureUser(user =>
                          {
                              user.AddOrUpdateProperty<string>( //property type: string
                                  "SocialSecurityNumber", //property name
                                  property =>
                                  {
                                      //validation rules
                                      property.Attributes.Add(new RequiredAttribute());
                                      property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4});
    
                                      //...other configurations for this property
                                  }
                              );
                          });
                      });
    
                 * See the documentation for more:
                 * https://docs.abp.io/en/abp/latest/Module-Entity-Extensions
                 */
    
    
                ObjectExtensionManager.Instance.Modules()
                .ConfigureIdentity(identity =>
                {
                    identity.ConfigureUser(user =>
                    {
                        user.AddOrUpdateProperty<bool>(
                            "EmailNotifications",
                            options =>
                            {
                                options.DefaultValue = false;
                            }
                        );
                    });
                });
    
            }
        }
    
  • 0
    liangshiwei created
    Support Team

    Hi,

    The profile UI does not support object extension, you can customize it via https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement

  • 0
    beuko created

    Hello liangshiwei,

    I already customized angular UI to show this field, now I need to get value from database (via DTO extensions) and also save it back to database.

  • 0
    liangshiwei created
    Support Team

    Hi,

    I think there is no problem, you can get value via API. and update profile.

    Don't forget call SKAPDtoExtensions.Configure() method in the ApplicationContractsModule

  • 0
    ServiceBot created
    Support Team

    This question has been automatically marked as stale because it has not had recent activity.