Basic frontend profile edit
This commit is contained in:
parent
47916c8b0b
commit
356394dcd8
@ -105,6 +105,7 @@ import { FileDropComponent } from './components/file-drop/file-drop.component'
|
|||||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||||
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
|
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||||
|
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
|
||||||
|
|
||||||
import localeAf from '@angular/common/locales/af'
|
import localeAf from '@angular/common/locales/af'
|
||||||
import localeAr from '@angular/common/locales/ar'
|
import localeAr from '@angular/common/locales/ar'
|
||||||
@ -256,6 +257,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
CustomFieldsComponent,
|
CustomFieldsComponent,
|
||||||
CustomFieldEditDialogComponent,
|
CustomFieldEditDialogComponent,
|
||||||
CustomFieldsDropdownComponent,
|
CustomFieldsDropdownComponent,
|
||||||
|
ProfileEditDialogComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -39,6 +39,11 @@
|
|||||||
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
|
||||||
|
<svg class="sidebaricon me-2" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#person"/>
|
||||||
|
</svg><ng-container i18n>My Profile</ng-container>
|
||||||
|
</button>
|
||||||
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
|
||||||
<svg class="sidebaricon me-2" fill="currentColor">
|
<svg class="sidebaricon me-2" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
fakeAsync,
|
fakeAsync,
|
||||||
tick,
|
tick,
|
||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { BrowserModule } from '@angular/platform-browser'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
@ -32,6 +32,7 @@ import { routes } from 'src/app/app-routing.module'
|
|||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||||
|
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||||
|
|
||||||
const saved_views = [
|
const saved_views = [
|
||||||
{
|
{
|
||||||
@ -86,6 +87,7 @@ describe('AppFrameComponent', () => {
|
|||||||
let documentListViewService: DocumentListViewService
|
let documentListViewService: DocumentListViewService
|
||||||
let router: Router
|
let router: Router
|
||||||
let savedViewSpy
|
let savedViewSpy
|
||||||
|
let modalService: NgbModal
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -98,6 +100,7 @@ describe('AppFrameComponent', () => {
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
|
NgbModalModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettingsService,
|
SettingsService,
|
||||||
@ -120,6 +123,7 @@ describe('AppFrameComponent', () => {
|
|||||||
ToastService,
|
ToastService,
|
||||||
OpenDocumentsService,
|
OpenDocumentsService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
NgbModal,
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
useValue: {
|
useValue: {
|
||||||
@ -148,6 +152,7 @@ describe('AppFrameComponent', () => {
|
|||||||
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
||||||
searchService = TestBed.inject(SearchService)
|
searchService = TestBed.inject(SearchService)
|
||||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||||
|
modalService = TestBed.inject(NgbModal)
|
||||||
router = TestBed.inject(Router)
|
router = TestBed.inject(Router)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
@ -363,4 +368,12 @@ describe('AppFrameComponent', () => {
|
|||||||
>)
|
>)
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support edit profile', () => {
|
||||||
|
const modalSpy = jest.spyOn(modalService, 'open')
|
||||||
|
component.editProfile()
|
||||||
|
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -39,6 +39,8 @@ import {
|
|||||||
CdkDragDrop,
|
CdkDragDrop,
|
||||||
moveItemInArray,
|
moveItemInArray,
|
||||||
} from '@angular/cdk/drag-drop'
|
} from '@angular/cdk/drag-drop'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-app-frame',
|
selector: 'pngx-app-frame',
|
||||||
@ -69,6 +71,7 @@ export class AppFrameComponent
|
|||||||
public settingsService: SettingsService,
|
public settingsService: SettingsService,
|
||||||
public tasksService: TasksService,
|
public tasksService: TasksService,
|
||||||
private readonly toastService: ToastService,
|
private readonly toastService: ToastService,
|
||||||
|
private modalService: NgbModal,
|
||||||
permissionsService: PermissionsService
|
permissionsService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@ -121,6 +124,13 @@ export class AppFrameComponent
|
|||||||
this.isMenuCollapsed = true
|
this.isMenuCollapsed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editProfile() {
|
||||||
|
this.modalService.open(ProfileEditDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
|
||||||
get openDocuments(): PaperlessDocument[] {
|
get openDocuments(): PaperlessDocument[] {
|
||||||
return this.openDocumentsService.getOpenDocuments()
|
return this.openDocumentsService.getOpenDocuments()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
<form [formGroup]="form" (ngSubmit)="save()" autocomplete="off">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pngx-input-text i18n-title title="Email" formControlName="email" [error]="error?.email"></pngx-input-text>
|
||||||
|
<pngx-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></pngx-input-password>
|
||||||
|
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
||||||
|
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,89 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
|
||||||
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import {
|
||||||
|
NgbActiveModal,
|
||||||
|
NgbModal,
|
||||||
|
NgbModalModule,
|
||||||
|
NgbModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { HttpClientModule } from '@angular/common/http'
|
||||||
|
import { TextComponent } from '../input/text/text.component'
|
||||||
|
import { PasswordComponent } from '../input/password/password.component'
|
||||||
|
import { of, throwError } from 'rxjs'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
|
||||||
|
const profile = {
|
||||||
|
email: 'foo@bar.com',
|
||||||
|
password: '*********',
|
||||||
|
first_name: 'foo',
|
||||||
|
last_name: 'bar',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ProfileEditDialogComponent', () => {
|
||||||
|
let component: ProfileEditDialogComponent
|
||||||
|
let fixture: ComponentFixture<ProfileEditDialogComponent>
|
||||||
|
let profileService: ProfileService
|
||||||
|
let toastService: ToastService
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
ProfileEditDialogComponent,
|
||||||
|
TextComponent,
|
||||||
|
PasswordComponent,
|
||||||
|
],
|
||||||
|
providers: [NgbActiveModal],
|
||||||
|
imports: [
|
||||||
|
HttpClientModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormsModule,
|
||||||
|
NgbModalModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
profileService = TestBed.inject(ProfileService)
|
||||||
|
toastService = TestBed.inject(ToastService)
|
||||||
|
fixture = TestBed.createComponent(ProfileEditDialogComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get profile on init, display in form', () => {
|
||||||
|
const getSpy = jest.spyOn(profileService, 'get')
|
||||||
|
getSpy.mockReturnValue(of(profile))
|
||||||
|
component.ngOnInit()
|
||||||
|
expect(getSpy).toHaveBeenCalled()
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.form.get('email').value).toEqual(profile.email)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update profile on save, display error if needed', () => {
|
||||||
|
const newProfile = {
|
||||||
|
email: 'foo@bar2.com',
|
||||||
|
password: profile.password,
|
||||||
|
first_name: 'foo2',
|
||||||
|
last_name: profile.last_name,
|
||||||
|
}
|
||||||
|
const updateSpy = jest.spyOn(profileService, 'update')
|
||||||
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save')))
|
||||||
|
component.save()
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
updateSpy.mockClear()
|
||||||
|
const infoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
|
component.form.patchValue(newProfile)
|
||||||
|
updateSpy.mockReturnValueOnce(of(newProfile))
|
||||||
|
component.save()
|
||||||
|
expect(updateSpy).toHaveBeenCalledWith(newProfile)
|
||||||
|
expect(infoSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should close on cancel', () => {
|
||||||
|
const closeSpy = jest.spyOn(component.activeModal, 'close')
|
||||||
|
component.cancel()
|
||||||
|
expect(closeSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,51 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-profile-edit-dialog',
|
||||||
|
templateUrl: './profile-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./profile-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class ProfileEditDialogComponent implements OnInit {
|
||||||
|
public networkActive: boolean = false
|
||||||
|
public error: any
|
||||||
|
|
||||||
|
public form = new FormGroup({
|
||||||
|
email: new FormControl(''),
|
||||||
|
password: new FormControl(null),
|
||||||
|
first_name: new FormControl(''),
|
||||||
|
last_name: new FormControl(''),
|
||||||
|
})
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private profileService: ProfileService,
|
||||||
|
public activeModal: NgbActiveModal,
|
||||||
|
private toastService: ToastService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.profileService.get().subscribe((profile) => {
|
||||||
|
this.form.patchValue(profile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const profile = Object.assign({}, this.form.value)
|
||||||
|
this.profileService.update(profile).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toastService.showInfo($localize`Profile updated successfully`)
|
||||||
|
this.activeModal.close()
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.showError($localize`Error saving profile`, error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.activeModal.close()
|
||||||
|
}
|
||||||
|
}
|
6
src-ui/src/app/data/user-profile.ts
Normal file
6
src-ui/src/app/data/user-profile.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface PaperlessUserProfile {
|
||||||
|
email?: string
|
||||||
|
password?: string
|
||||||
|
first_name?: string
|
||||||
|
last_name?: string
|
||||||
|
}
|
46
src-ui/src/app/services/profile.service.spec.ts
Normal file
46
src-ui/src/app/services/profile.service.spec.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { ProfileService } from './profile.service'
|
||||||
|
import {
|
||||||
|
HttpClientTestingModule,
|
||||||
|
HttpTestingController,
|
||||||
|
} from '@angular/common/http/testing'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
|
describe('ProfileService', () => {
|
||||||
|
let httpTestingController: HttpTestingController
|
||||||
|
let service: ProfileService
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [ProfileService],
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
})
|
||||||
|
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController)
|
||||||
|
service = TestBed.inject(ProfileService)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpTestingController.verify()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls get profile endpoint', () => {
|
||||||
|
service.get().subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}profile/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('GET')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls patch on update', () => {
|
||||||
|
service.update({ email: 'foo@bar.com' }).subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}profile/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('PATCH')
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
email: 'foo@bar.com',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
27
src-ui/src/app/services/profile.service.ts
Normal file
27
src-ui/src/app/services/profile.service.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { PaperlessUserProfile } from '../data/user-profile'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ProfileService {
|
||||||
|
private endpoint = 'profile'
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
get(): Observable<PaperlessUserProfile> {
|
||||||
|
return this.http.get<PaperlessUserProfile>(
|
||||||
|
`${environment.apiBaseUrl}${this.endpoint}/`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(profile: PaperlessUserProfile): Observable<PaperlessUserProfile> {
|
||||||
|
return this.http.patch<PaperlessUserProfile>(
|
||||||
|
`${environment.apiBaseUrl}${this.endpoint}/`,
|
||||||
|
profile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user