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 { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
||||||
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
||||||
import { ConfigComponent } from './components/admin/config/config.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 localeAf from '@angular/common/locales/af'
|
||||||
import localeAr from '@angular/common/locales/ar'
|
import localeAr from '@angular/common/locales/ar'
|
||||||
@ -267,6 +268,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
PreviewPopupComponent,
|
PreviewPopupComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
ConfigComponent,
|
ConfigComponent,
|
||||||
|
FileComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
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.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.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.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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,12 +15,15 @@ import { SwitchComponent } from '../../common/input/switch/switch.component'
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { SelectComponent } from '../../common/input/select/select.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', () => {
|
describe('ConfigComponent', () => {
|
||||||
let component: ConfigComponent
|
let component: ConfigComponent
|
||||||
let fixture: ComponentFixture<ConfigComponent>
|
let fixture: ComponentFixture<ConfigComponent>
|
||||||
let configService: ConfigService
|
let configService: ConfigService
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
|
let settingService: SettingsService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@ -30,6 +33,7 @@ describe('ConfigComponent', () => {
|
|||||||
SelectComponent,
|
SelectComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
|
FileComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
@ -44,6 +48,7 @@ describe('ConfigComponent', () => {
|
|||||||
|
|
||||||
configService = TestBed.inject(ConfigService)
|
configService = TestBed.inject(ConfigService)
|
||||||
toastService = TestBed.inject(ToastService)
|
toastService = TestBed.inject(ToastService)
|
||||||
|
settingService = TestBed.inject(SettingsService)
|
||||||
fixture = TestBed.createComponent(ConfigComponent)
|
fixture = TestBed.createComponent(ConfigComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@ -100,4 +105,39 @@ describe('ConfigComponent', () => {
|
|||||||
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
||||||
expect(component.errors).toEqual({ user_args: null })
|
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 { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-config',
|
selector: 'pngx-config',
|
||||||
@ -55,7 +56,8 @@ export class ConfigComponent
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private toastService: ToastService
|
private toastService: ToastService,
|
||||||
|
private settingsService: SettingsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.configForm.addControl('id', new FormControl())
|
this.configForm.addControl('id', new FormControl())
|
||||||
@ -145,6 +147,7 @@ export class ConfigComponent
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
this.initialize(config)
|
this.initialize(config)
|
||||||
this.store.next(config)
|
this.store.next(config)
|
||||||
|
this.settingsService.initializeSettings().subscribe()
|
||||||
this.toastService.showInfo($localize`Configuration updated`)
|
this.toastService.showInfo($localize`Configuration updated`)
|
||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
@ -160,4 +163,27 @@ export class ConfigComponent
|
|||||||
public discardChanges() {
|
public discardChanges() {
|
||||||
this.configForm.reset(this.initialConfig)
|
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 { LogoComponent } from './logo.component'
|
||||||
import { By } from '@angular/platform-browser'
|
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', () => {
|
describe('LogoComponent', () => {
|
||||||
let component: LogoComponent
|
let component: LogoComponent
|
||||||
let fixture: ComponentFixture<LogoComponent>
|
let fixture: ComponentFixture<LogoComponent>
|
||||||
|
let settingsService: SettingsService
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [LogoComponent],
|
declarations: [LogoComponent],
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
})
|
})
|
||||||
|
settingsService = TestBed.inject(SettingsService)
|
||||||
fixture = TestBed.createComponent(LogoComponent)
|
fixture = TestBed.createComponent(LogoComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@ -33,4 +39,9 @@ describe('LogoComponent', () => {
|
|||||||
'height:10em'
|
'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 { NotFoundComponent } from './not-found.component'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { LogoComponent } from '../common/logo/logo.component'
|
import { LogoComponent } from '../common/logo/logo.component'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
|
||||||
describe('NotFoundComponent', () => {
|
describe('NotFoundComponent', () => {
|
||||||
let component: NotFoundComponent
|
let component: NotFoundComponent
|
||||||
@ -10,6 +11,7 @@ describe('NotFoundComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [NotFoundComponent, LogoComponent],
|
declarations: [NotFoundComponent, LogoComponent],
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NotFoundComponent)
|
fixture = TestBed.createComponent(NotFoundComponent)
|
||||||
|
@ -39,4 +39,13 @@ describe('ConfigService', () => {
|
|||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('PATCH')
|
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)
|
.patch<PaperlessConfig>(`${this.baseUrl}${config.id}/`, config)
|
||||||
.pipe(first())
|
.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
|
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 "node_modules/bootstrap/scss/bootstrap";
|
||||||
@import "theme";
|
@import "theme";
|
||||||
@import "~@ng-select/ng-select/themes/default.theme.css";
|
@import "~@ng-select/ng-select/themes/default.theme.css";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user