Frontend upload of app logo
This commit is contained in:
parent
60f37090d9
commit
3c8cde4e00
@ -110,6 +110,7 @@ import { DocumentLinkComponent } from './components/common/input/document-link/d
|
||||
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
||||
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
||||
import { ConfigComponent } from './components/admin/config/config.component'
|
||||
import { FileComponent } from './components/common/input/file/file.component'
|
||||
|
||||
import localeAf from '@angular/common/locales/af'
|
||||
import localeAr from '@angular/common/locales/ar'
|
||||
@ -267,6 +268,7 @@ function initializeApp(settings: SettingsService) {
|
||||
PreviewPopupComponent,
|
||||
SwitchComponent,
|
||||
ConfigComponent,
|
||||
FileComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -30,6 +30,7 @@
|
||||
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
|
||||
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,12 +15,15 @@ import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { SelectComponent } from '../../common/input/select/select.component'
|
||||
import { FileComponent } from '../../common/input/file/file.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
describe('ConfigComponent', () => {
|
||||
let component: ConfigComponent
|
||||
let fixture: ComponentFixture<ConfigComponent>
|
||||
let configService: ConfigService
|
||||
let toastService: ToastService
|
||||
let settingService: SettingsService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@ -30,6 +33,7 @@ describe('ConfigComponent', () => {
|
||||
SelectComponent,
|
||||
NumberComponent,
|
||||
SwitchComponent,
|
||||
FileComponent,
|
||||
PageHeaderComponent,
|
||||
],
|
||||
imports: [
|
||||
@ -44,6 +48,7 @@ describe('ConfigComponent', () => {
|
||||
|
||||
configService = TestBed.inject(ConfigService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
settingService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(ConfigComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
@ -100,4 +105,39 @@ describe('ConfigComponent', () => {
|
||||
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
||||
expect(component.errors).toEqual({ user_args: null })
|
||||
})
|
||||
|
||||
it('should upload file, show error if necessary', () => {
|
||||
const uploadSpy = jest.spyOn(configService, 'uploadFile')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
uploadSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('Error uploading file'))
|
||||
)
|
||||
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||
expect(uploadSpy).toHaveBeenCalled()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
uploadSpy.mockReturnValueOnce(
|
||||
of({ app_logo: 'https://example.com/logo/test.png' } as any)
|
||||
)
|
||||
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||
expect(component.initialConfig).toEqual({
|
||||
app_logo: 'https://example.com/logo/test.png',
|
||||
})
|
||||
})
|
||||
|
||||
it('should refresh ui settings after save or upload', () => {
|
||||
const saveSpy = jest.spyOn(configService, 'saveConfig')
|
||||
const initSpy = jest.spyOn(settingService, 'initializeSettings')
|
||||
saveSpy.mockReturnValueOnce(
|
||||
of({ output_type: OutputTypeConfig.PDF_A } as any)
|
||||
)
|
||||
component.saveConfig()
|
||||
expect(initSpy).toHaveBeenCalled()
|
||||
|
||||
const uploadSpy = jest.spyOn(configService, 'uploadFile')
|
||||
uploadSpy.mockReturnValueOnce(
|
||||
of({ app_logo: 'https://example.com/logo/test.png' } as any)
|
||||
)
|
||||
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||
expect(initSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
@ -19,6 +19,7 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-config',
|
||||
@ -55,7 +56,8 @@ export class ConfigComponent
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService
|
||||
private toastService: ToastService,
|
||||
private settingsService: SettingsService
|
||||
) {
|
||||
super()
|
||||
this.configForm.addControl('id', new FormControl())
|
||||
@ -145,6 +147,7 @@ export class ConfigComponent
|
||||
this.loading = false
|
||||
this.initialize(config)
|
||||
this.store.next(config)
|
||||
this.settingsService.initializeSettings().subscribe()
|
||||
this.toastService.showInfo($localize`Configuration updated`)
|
||||
},
|
||||
error: (e) => {
|
||||
@ -160,4 +163,27 @@ export class ConfigComponent
|
||||
public discardChanges() {
|
||||
this.configForm.reset(this.initialConfig)
|
||||
}
|
||||
|
||||
public uploadFile(file: File, key: string) {
|
||||
this.loading = true
|
||||
this.configService
|
||||
.uploadFile(file, this.configForm.value['id'], key)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier), first())
|
||||
.subscribe({
|
||||
next: (config) => {
|
||||
this.loading = false
|
||||
this.initialize(config)
|
||||
this.store.next(config)
|
||||
this.settingsService.initializeSettings().subscribe()
|
||||
this.toastService.showInfo($localize`File successfully updated`)
|
||||
},
|
||||
error: (e) => {
|
||||
this.loading = false
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred uploading file`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
@if (title) {
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
}
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group" [class.col-md-9]="horizontal" [class.is-invalid]="error">
|
||||
<input #fileInput type="file" class="form-control" [id]="inputId" (change)="onFile($event)" [disabled]="disabled">
|
||||
<button class="btn btn-outline-secondary py-0" type="button" (click)="onButton()" [disabled]="disabled || !file" i18n>Upload</button>
|
||||
</div>
|
||||
@if (filename) {
|
||||
<small class="form-text text-muted">{{filename}}</small>
|
||||
}
|
||||
<input #inputField type="hidden" class="form-control small" [(ngModel)]="value" [disabled]="true">
|
||||
@if (hint) {
|
||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
}
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,41 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { FileComponent } from './file.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
|
||||
describe('FileComponent', () => {
|
||||
let component: FileComponent
|
||||
let fixture: ComponentFixture<FileComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [FileComponent],
|
||||
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(FileComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should update file on change', () => {
|
||||
const event = { target: { files: [new File([], 'test.png')] } }
|
||||
component.onFile(event as any)
|
||||
expect(component.file.name).toEqual('test.png')
|
||||
})
|
||||
|
||||
it('should get filename', () => {
|
||||
component.value = 'https://example.com:8000/logo/filename.svg'
|
||||
expect(component.filename).toEqual('filename.svg')
|
||||
})
|
||||
|
||||
it('should fire upload event', () => {
|
||||
let firedFile
|
||||
component.file = new File([], 'test.png')
|
||||
component.upload.subscribe((file) => (firedFile = file))
|
||||
component.onButton()
|
||||
expect(firedFile.name).toEqual('test.png')
|
||||
expect(component.file).toBeUndefined()
|
||||
})
|
||||
})
|
@ -0,0 +1,47 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Output,
|
||||
ViewChild,
|
||||
forwardRef,
|
||||
} from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => FileComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'pngx-input-file',
|
||||
templateUrl: './file.component.html',
|
||||
styleUrl: './file.component.scss',
|
||||
})
|
||||
export class FileComponent extends AbstractInputComponent<string> {
|
||||
@Output()
|
||||
upload = new EventEmitter<File>()
|
||||
|
||||
public file: File
|
||||
|
||||
@ViewChild('fileInput') fileInput: ElementRef
|
||||
|
||||
get filename(): string {
|
||||
return this.value
|
||||
? this.value.substring(this.value.lastIndexOf('/') + 1)
|
||||
: null
|
||||
}
|
||||
|
||||
onFile(event: Event) {
|
||||
this.file = (event.target as HTMLInputElement).files[0]
|
||||
}
|
||||
|
||||
onButton() {
|
||||
this.upload.emit(this.file)
|
||||
this.file = undefined
|
||||
this.fileInput.nativeElement.value = null
|
||||
}
|
||||
}
|
@ -2,15 +2,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { LogoComponent } from './logo.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
|
||||
describe('LogoComponent', () => {
|
||||
let component: LogoComponent
|
||||
let fixture: ComponentFixture<LogoComponent>
|
||||
let settingsService: SettingsService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LogoComponent],
|
||||
imports: [HttpClientTestingModule],
|
||||
})
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(LogoComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
@ -33,4 +39,9 @@ describe('LogoComponent', () => {
|
||||
'height:10em'
|
||||
)
|
||||
})
|
||||
|
||||
it('should support getting custom logo', () => {
|
||||
settingsService.set(SETTINGS_KEYS.APP_LOGO, '/logo/test.png')
|
||||
expect(component.customLogo).toEqual('http://localhost:8000/logo/test.png')
|
||||
})
|
||||
})
|
||||
|
@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NotFoundComponent } from './not-found.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { LogoComponent } from '../common/logo/logo.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
|
||||
describe('NotFoundComponent', () => {
|
||||
let component: NotFoundComponent
|
||||
@ -10,6 +11,7 @@ describe('NotFoundComponent', () => {
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [NotFoundComponent, LogoComponent],
|
||||
imports: [HttpClientTestingModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(NotFoundComponent)
|
||||
|
@ -39,4 +39,13 @@ describe('ConfigService', () => {
|
||||
)
|
||||
expect(req.request.method).toEqual('PATCH')
|
||||
})
|
||||
|
||||
it('should support upload file with form data', () => {
|
||||
service.uploadFile(new File([], 'test.png'), 1, 'app_logo').subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}config/1/`
|
||||
)
|
||||
expect(req.request.method).toEqual('PATCH')
|
||||
expect(req.request.body).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
@ -24,4 +24,16 @@ export class ConfigService {
|
||||
.patch<PaperlessConfig>(`${this.baseUrl}${config.id}/`, config)
|
||||
.pipe(first())
|
||||
}
|
||||
|
||||
uploadFile(
|
||||
file: File,
|
||||
configID: number,
|
||||
configKey: string
|
||||
): Observable<PaperlessConfig> {
|
||||
let formData = new FormData()
|
||||
formData.append(configKey, file, file.name)
|
||||
return this.http
|
||||
.patch<PaperlessConfig>(`${this.baseUrl}${configID}/`, formData)
|
||||
.pipe(first())
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,9 @@ $grid-breakpoints: (
|
||||
xxxl: 2400px
|
||||
);
|
||||
|
||||
$form-file-button-bg: var(--bs-body-bg);
|
||||
$form-file-button-hover-bg: var(--pngx-bg-alt);
|
||||
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
@import "theme";
|
||||
@import "~@ng-select/ng-select/themes/default.theme.css";
|
||||
|
Loading…
x
Reference in New Issue
Block a user