#2 Amélioration générales des configurations

This commit is contained in:
FyloZ 2021-08-09 20:52:36 -04:00
parent 28cf2d04cd
commit 1a5c09cb41
Signed by: william
GPG Key ID: 835378AE9AF4AE97
26 changed files with 499 additions and 461 deletions

View File

@ -3,8 +3,7 @@ import {Routes, RouterModule} from '@angular/router'
import {CatalogComponent} from './pages/catalog/catalog.component' import {CatalogComponent} from './pages/catalog/catalog.component'
import {AdministrationComponent} from './pages/administration/administration.component' import {AdministrationComponent} from './pages/administration/administration.component'
import {MiscComponent} from './pages/others/misc.component' import {MiscComponent} from './pages/others/misc.component'
import {CreConfigEditor} from './modules/configuration/config' import {CreConfigEditor} from './modules/configuration/config-editor'
const routes: Routes = [{ const routes: Routes = [{
path: 'color', path: 'color',

View File

@ -1,4 +0,0 @@
<div *ngIf="configuration" [attr.title]="tooltip?.content" class="d-flex flex-row justify-content-between align-items-center">
<cre-checkbox-input [label]="label.content" [control]="config.control"></cre-checkbox-input>
<mat-hint>{{lastUpdated}}</mat-hint>
</div>

View File

@ -0,0 +1,6 @@
<cre-config-container [configuration]="config" [tooltip]="tooltip">
<div class="d-flex flex-row justify-content-between align-items-center">
<cre-checkbox-input [label]="label" [control]="control"></cre-checkbox-input>
<mat-hint>{{inputHint}}</mat-hint>
</div>
</cre-config-container>

View File

@ -0,0 +1,6 @@
<div
class="cre-config"
[class.cre-readonly-config]="readOnly"
[attr.title]="tooltip">
<ng-content></ng-content>
</div>

View File

@ -0,0 +1,12 @@
<cre-config-container [configuration]="config" [tooltip]="tooltip">
<cre-input
class="w-100"
type="text"
[label]="label"
[hint]="inputHint"
[control]="control"
[icon]="inputIcon"
[iconTitle]="inputIconTitle"
iconColor="warning">
</cre-input>
</cre-config-container>

View File

@ -0,0 +1,120 @@
<form *ngIf="form" [formGroup]="form">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-accent-button type="submit" (click)="onSubmit()">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<div class="d-flex flex-column" style="gap: 1.5rem">
<cre-config-section *ngIf="!emergencyMode" label="Apparence">
<cre-config-list>
<cre-image-config
label="Logo"
tooltip="Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
'Ctrl+F5')."
[configControl]="getConfigControl(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"
(invalidFormat)="invalidFormatConfirmBox.show()">
</cre-image-config>
<cre-image-config
label="Icône"
tooltip="Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
'Ctrl+F5')."
[configControl]="getConfigControl(keys.INSTANCE_ICON_PATH)" previewWidth="32px"
(invalidFormat)="invalidFormatConfirmBox.show()">
</cre-image-config>
</cre-config-list>
</cre-config-section>
<cre-config-section *ngIf="!emergencyMode" label="Données">
<cre-config-list class="pt-2">
<cre-period-config
label="Période d'expiration de l'approbation de l'échantillon des recettes"
[configControl]="getConfigControl(keys.RECIPE_APPROBATION_EXPIRATION)">
</cre-period-config>
<cre-period-config
label="Période d'expiration des kits de retouches complets"
tooltip="Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront
supprimés automatiquement."
[configControl]="getConfigControl(keys.TOUCH_UP_KIT_EXPIRATION)">
</cre-period-config>
<cre-bool-config
label="Activer le cache des PDFs générés"
tooltip="Cette option permet de stocker les PDFs générés sur le disque, ce qui permet d'accélérer
l'accès aux PDFs si la lecture des fichiers cachés sur le disque est plus rapide que la génération d'un
nouveau PDF."
[configControl]="getConfigControl(keys.TOUCH_UP_KIT_CACHE_PDF)">
</cre-bool-config>
</cre-config-list>
</cre-config-section>
<cre-config-section label="Système">
<cre-config-list>
<cre-text-config
*ngIf="!emergencyMode"
label="URL de l'instance"
tooltip="Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques."
[configControl]="getConfigControl(keys.INSTANCE_URL)">
</cre-text-config>
<cre-text-config
label="URL de la base de données"
[configControl]="getConfigControl(keys.DATABASE_URL)">
</cre-text-config>
<cre-text-config
label="Utilisateur de la base de données"
[configControl]="getConfigControl(keys.DATABASE_USER)">
</cre-text-config>
<cre-secure-config
label="Mot de passe de la base de données"
buttonLabel="Modifier le mot de passe de la base de données"
[configControl]="getConfigControl(keys.DATABASE_PASSWORD)">
</cre-secure-config>
<cre-text-config
label="Version de la base de données"
[configControl]="getConfigControl(keys.DATABASE_VERSION)">
</cre-text-config>
<cre-text-config
label="Version de Color Recipes Explorer"
[configControl]="getConfigControl(keys.BACKEND_BUILD_VERSION)">
</cre-text-config>
<cre-date-config
label="Date de compilation de Color Recipes Explorer"
[configControl]="getConfigControl(keys.BACKEND_BUILD_TIME)">
</cre-date-config>
<cre-text-config
label="Version de Java"
[configControl]="getConfigControl(keys.JAVA_VERSION)">
</cre-text-config>
<cre-text-config
label="Système d'exploitation"
[configControl]="getConfigControl(keys.OPERATING_SYSTEM)">
</cre-text-config>
</cre-config-list>
<cre-config-actions>
<cre-warn-button (click)="restartConfirmBox.show()">Redémarrer le serveur</cre-warn-button>
</cre-config-actions>
</cre-config-section>
</div>
</form>
<cre-confirm-box #invalidFormatConfirmBox message="Le format du fichier choisi n'est pas valide"></cre-confirm-box>
<cre-confirm-box #restartConfirmBox
message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
(confirm)="restart()"></cre-confirm-box>
<cre-confirm-box #restartingConfirmBox message="Le serveur est en cours de redémarrage" (cancel)="reload()"
(confirm)="reload()"></cre-confirm-box>

View File

@ -0,0 +1,94 @@
import {Component, ViewChild} from '@angular/core'
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
import {buildFormControl, Config, ConfigControl} from '../shared/model/config.model'
import {FormBuilder, FormControl, FormGroup} from '@angular/forms'
import {ConfigService} from '../shared/service/config.service'
import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
@Component({
selector: 'cre-config-editor',
templateUrl: 'config-editor.html'
})
export class CreConfigEditor extends ErrorHandlingComponent {
@ViewChild('restartingConfirmBox', {static: true}) restartConfirmBox: ConfirmBoxComponent
keys = {
INSTANCE_NAME: Config.INSTANCE_NAME,
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH,
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH,
INSTANCE_URL: Config.INSTANCE_URL,
DATABASE_URL: Config.DATABASE_URL,
DATABASE_USER: Config.DATABASE_USER,
DATABASE_PASSWORD: Config.DATABASE_PASSWORD,
DATABASE_VERSION: Config.DATABASE_VERSION,
RECIPE_APPROBATION_EXPIRATION: Config.RECIPE_APPROBATION_EXPIRATION,
TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF,
TOUCH_UP_KIT_EXPIRATION: Config.TOUCH_UP_KIT_EXPIRATION,
BACKEND_BUILD_VERSION: Config.BACKEND_BUILD_VERSION,
BACKEND_BUILD_TIME: Config.BACKEND_BUILD_TIME,
JAVA_VERSION: Config.JAVA_VERSION,
OPERATING_SYSTEM: Config.OPERATING_SYSTEM
}
configs = new Map<string, Config>()
form: FormGroup | null
constructor(
private configService: ConfigService,
formBuilder: FormBuilder,
errorService: ErrorService,
activatedRoute: ActivatedRoute,
router: Router
) {
super(errorService, activatedRoute, router)
this.fetchConfigurations(formBuilder)
}
getConfigControl(key: string): ConfigControl {
return {
config: this.configs.get(key),
control: this.form.controls[key] as FormControl
}
}
onSubmit() {
this.subscribe(
this.configService.setFromForm(this.form),
() => this.reload()
)
}
restart() {
this.subscribe(
this.configService.restart(),
() => this.restartConfirmBox.show()
)
}
reload() {
window.location.reload()
}
get emergencyMode(): boolean {
return this.configs.get(Config.EMERGENCY_MODE).content === 'true';
}
private fetchConfigurations(formBuilder: FormBuilder) {
this.subscribe(
this.configService.all,
configurations => this.buildForm(formBuilder, configurations)
)
}
private buildForm(formBuilder: FormBuilder, configurations: Config[]) {
const group = {}
configurations.forEach(config => {
group[config.key] = buildFormControl(config)
this.configs.set(config.key, config)
})
this.form = formBuilder.group(group)
}
}

View File

@ -0,0 +1,28 @@
<div class="cre-image-config-label">
<p>
{{label}}
</p>
</div>
<cre-config-container
[configuration]="config"
[tooltip]="tooltip">
<div class="d-flex flex-row justify-content-between align-items-center">
<cre-file-input
class="w-100"
accept="image/png,image/jpeg,image/x-icon,image/svg+xml"
[control]="control"
(selection)="updateImage($event)"
(invalidFormat)="invalidFormat.emit()">
</cre-file-input>
<div class="image-wrapper d-flex flex-column justify-content-end">
<div>
<img
[src]="updatedImage ? updatedImage : configuredImageUrl"
[attr.width]="previewWidth ? previewWidth : null"
class="mat-elevation-z3"/>
</div>
<mat-hint>{{lastUpdated}}</mat-hint>
</div>
</div>
</cre-config-container>

View File

@ -0,0 +1,7 @@
<cre-config-container [configuration]="config" [tooltip]="tooltip">
<cre-period-input
[control]="control"
[label]="label"
[hint]="inputHint">
</cre-period-input>
</cre-config-container>

View File

@ -1,8 +1,6 @@
<mat-card class="w-50 x-centered"> <mat-card class="w-50 x-centered">
<mat-card-header> <mat-card-header>
<mat-card-title> <mat-card-title>{{label}}</mat-card-title>
<ng-content select="cre-config-label"></ng-content>
</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content [class.no-action]="!hasActions"> <mat-card-content [class.no-action]="!hasActions">
<ng-content select="cre-config-list"></ng-content> <ng-content select="cre-config-list"></ng-content>

View File

@ -0,0 +1,24 @@
<cre-config-container [configuration]="config" [tooltip]="tooltip">
<cre-primary-button
class="w-100 mb-3"
(click)="onOpen()">
{{buttonLabel}}
</cre-primary-button>
<cre-prompt-dialog
[title]="label"
(cancel)="onCancel()">
<cre-dialog-body>
<cre-input
class="w-100"
type="password"
label="Nouvelle valeur"
[hint]="inputHint"
[control]="control"
[icon]="inputIcon"
[iconTitle]="inputIconTitle"
iconColor="warning">
</cre-input>
</cre-dialog-body>
</cre-prompt-dialog>
</cre-config-container>

View File

@ -0,0 +1,11 @@
<cre-config-container [tooltip]="tooltip">
<cre-input
class="w-100"
[control]="control"
[label]="label"
[hint]="inputHint"
[icon]="inputIcon"
[iconTitle]="inputIconTitle"
iconColor="warning">
</cre-input>
</cre-config-container>

View File

@ -1,12 +0,0 @@
<div *ngIf="configuration" [attr.title]="tooltip?.content">
<cre-input [class.has-hint]="configuration.editable"
class="w-100"
[type]="config.key === 'database.password' ? 'password' : 'text'"
[label]="label.content"
[hint]="configuration.editable ? lastUpdated : null"
[control]="config.control"
[icon]="configuration.requireRestart ? 'alert' : null"
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
iconColor="warning">
</cre-input>
</div>

View File

@ -1,33 +1,35 @@
import {NgModule} from '@angular/core' import {NgModule} from '@angular/core'
import { import {
CreConfig, CreBoolConfig,
CreConfigLabel,
CreConfigEditor,
CreConfigSection,
CreImageConfig,
CreConfigList,
CreConfigActions, CreConfigActions,
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig, CreSecureConfig CreConfigContainer,
CreConfigList,
CreConfigSection,
CreDateConfig,
CreImageConfig,
CrePeriodConfig,
CreSecureConfig,
CreTextConfig
} from './config' } from './config'
import {SharedModule} from '../shared/shared.module' import {SharedModule} from '../shared/shared.module'
import {CreInputsModule} from '../shared/components/inputs/inputs.module' import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module' import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module' import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {CreConfigEditor} from './config-editor'
@NgModule({ @NgModule({
declarations: [ declarations: [
CreConfigLabel,
CreConfigTooltip,
CreConfigEditor,
CreConfig,
CreImageConfig,
CreConfigSection,
CreConfigList, CreConfigList,
CreConfigActions, CreConfigActions,
CreConfigSection,
CreConfigContainer,
CreTextConfig,
CreImageConfig,
CreBoolConfig, CreBoolConfig,
CrePeriodConfig, CrePeriodConfig,
CreDateConfig, CreDateConfig,
CreSecureConfig CreSecureConfig,
CreConfigEditor
], ],
imports: [ imports: [
SharedModule, SharedModule,
@ -36,4 +38,5 @@ import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
CreButtonsModule CreButtonsModule
] ]
}) })
export class ConfigModule { } export class ConfigModule {
}

View File

@ -1,10 +1,10 @@
mat-hint mat-hint
font-size: .8em font-size: .8em
cre-config cre-config-container
display: block display: block
cre-input.has-hint .cre-config:not(.cre-editable-config)
margin-bottom: 1em margin-bottom: 1em
mat-hint mat-hint
@ -50,6 +50,3 @@ cre-image-config
mat-hint mat-hint
margin-top: .2em margin-top: .2em
//cre-secure-config button
//

View File

@ -1,58 +1,13 @@
import { import {AfterViewInit, Component, ContentChild, Directive, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core'
AfterViewInit,
Component,
ContentChild,
Directive,
ElementRef,
EventEmitter,
Input,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core'
import {ConfigService} from '../shared/service/config.service' import {ConfigService} from '../shared/service/config.service'
import {Config} from '../shared/model/config.model' import {Config, ConfigControl} from '../shared/model/config.model'
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component' import {SubscribingComponent} from '../shared/components/subscribing.component'
import {ErrorService} from '../shared/service/error.service' import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router' import {ActivatedRoute, Router} from '@angular/router'
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils' import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
import {FormControl, Validators} from '@angular/forms' import {AbstractControl} from '@angular/forms'
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
import {MatDialog} from '@angular/material/dialog'
import {CrePromptDialog} from '../shared/components/dialogs/dialogs' import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
@Directive({
selector: 'cre-config-label'
})
export class CreConfigLabel implements AfterViewInit {
content: string
constructor(
private element: ElementRef
) {
}
ngAfterViewInit(): void {
this.content = this.element.nativeElement.innerHTML
}
}
@Directive({
selector: 'cre-config-tooltip'
})
export class CreConfigTooltip implements AfterViewInit {
content: string
constructor(
private element: ElementRef
) {
}
ngAfterViewInit(): void {
this.content = this.element.nativeElement.innerHTML
}
}
@Directive({ @Directive({
selector: 'cre-config-list' selector: 'cre-config-list'
}) })
@ -71,6 +26,8 @@ export class CreConfigActions {
templateUrl: 'config-section.html' templateUrl: 'config-section.html'
}) })
export class CreConfigSection { export class CreConfigSection {
@Input() label: string
@ContentChild(CreConfigActions) actions: CreConfigActions @ContentChild(CreConfigActions) actions: CreConfigActions
get hasActions(): boolean { get hasActions(): boolean {
@ -79,17 +36,25 @@ export class CreConfigSection {
} }
@Component({ @Component({
selector: 'cre-config', selector: 'cre-config-container',
templateUrl: 'config.html', templateUrl: 'config-container.html',
styleUrls: ['config.sass'] styleUrls: ['config.sass'],
encapsulation: ViewEncapsulation.None
}) })
export class CreConfig extends SubscribingComponent { export class CreConfigContainer {
@Input() config: { key: string, control: FormControl } @Input() configuration?: Config
@Input() tooltip: string
@ContentChild(CreConfigLabel, {static: true}) label: CreConfigLabel get readOnly(): boolean {
@ContentChild(CreConfigTooltip, {static: true}) tooltip: CreConfigTooltip return !this.configuration?.editable ?? true
}
}
configuration: Config | null @Directive()
abstract class _CreConfigBase extends SubscribingComponent {
@Input() configControl: ConfigControl
@Input() label: string
@Input() tooltip?: string
constructor( constructor(
private configService: ConfigService, private configService: ConfigService,
@ -102,92 +67,99 @@ export class CreConfig extends SubscribingComponent {
ngOnInit() { ngOnInit() {
super.ngOnInit() super.ngOnInit()
this.subscribe(
this.configService.get(this.config.key),
config => this.setConfig(config)
)
} }
protected setConfig(config: Config) { get config(): Config {
this.configuration = config return this.configControl.config
this.config.control.setValue(config.content) }
if (!config.editable) {
this.config.control.disable() get control(): AbstractControl {
} return this.configControl.control
} }
get lastUpdated(): string { get lastUpdated(): string {
return 'Dernière mise à jour: ' + formatDateTime(this.configuration.lastUpdated) return 'Dernière mise à jour: ' + formatDateTime(this.config.lastUpdated)
}
get inputHint(): string {
return this.config?.editable ? this.lastUpdated : null
}
}
@Directive()
abstract class _CreTextConfigBase extends _CreConfigBase {
private static readonly REQUIRE_RESTART_ICON = 'alert'
private static readonly REQUIRE_RESTART_ICON_TITLE = 'Requiert un redémarrage'
get inputIcon(): string {
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON : null
}
get inputIconTitle(): string {
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON_TITLE : null
} }
} }
@Component({
selector: 'cre-text-config',
templateUrl: 'config-text.html',
styleUrls: ['config.sass']
})
export class CreTextConfig extends _CreTextConfigBase {
}
@Component({ @Component({
selector: 'cre-image-config', selector: 'cre-image-config',
templateUrl: 'image.html', templateUrl: 'config-image.html',
styleUrls: ['config.sass'], styleUrls: ['config.sass'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class CreImageConfig extends CreConfig { export class CreImageConfig extends _CreConfigBase {
@Input() previewWidth: string | null @Input() previewWidth: string | null
@Output() invalidFormat = new EventEmitter<void>() @Output() invalidFormat = new EventEmitter<void>()
updatedImage: any | null updatedImage: any | null
constructor(
configService: ConfigService,
errorService: ErrorService,
activatedRoute: ActivatedRoute,
router: Router
) {
super(configService, errorService, activatedRoute, router)
}
updateImage(file: File): any { updateImage(file: File): any {
readFile(file, (content) => this.updatedImage = content) readFile(file, (content) => this.updatedImage = content)
} }
get configuredImageUrl(): string { get configuredImageUrl(): string {
return getFileUrl(this.configuration.content) return getFileUrl(this.config.content)
} }
} }
@Component({ @Component({
selector: 'cre-bool-config', selector: 'cre-bool-config',
templateUrl: 'bool.html' templateUrl: 'config-bool.html'
}) })
export class CreBoolConfig extends CreConfig { export class CreBoolConfig extends _CreConfigBase {
protected setConfig(config: Config) {
super.setConfig(config)
this.config.control.setValue(config.content === 'true')
}
} }
@Component({ @Component({
selector: 'cre-period-config', selector: 'cre-period-config',
templateUrl: 'period.html' templateUrl: 'config-period.html'
}) })
export class CrePeriodConfig extends CreConfig { export class CrePeriodConfig extends _CreConfigBase {
} }
@Component({ @Component({
selector: 'cre-date-config', selector: 'cre-date-config',
templateUrl: 'date.html' templateUrl: 'config-date.html'
}) })
export class CreDateConfig extends CreConfig { export class CreDateConfig extends _CreTextConfigBase implements AfterViewInit {
protected setConfig(config: Config) { ngAfterViewInit(): void {
super.setConfig(config) this.control.setValue(formatDate(this.config.content))
this.config.control.setValue(formatDate(config.content))
} }
} }
@Component({ @Component({
selector: 'cre-secure-config', selector: 'cre-secure-config',
templateUrl: 'secure.html' templateUrl: 'config-secure.html'
}) })
export class CreSecureConfig extends CreConfig { export class CreSecureConfig extends _CreTextConfigBase {
@ViewChild(CrePromptDialog) dialog: CrePromptDialog @ViewChild(CrePromptDialog) dialog: CrePromptDialog
@Input() buttonLabel: string @Input() buttonLabel: string
@ -200,91 +172,15 @@ export class CreSecureConfig extends CreConfig {
activatedRoute: ActivatedRoute, activatedRoute: ActivatedRoute,
router: Router router: Router
) { ) {
super(configService, errorService, activatedRoute, router); super(configService, errorService, activatedRoute, router)
}
protected setConfig(config: Config) {
super.setConfig(config)
} }
onOpen() { onOpen() {
this.initialValue = this.config.control.value this.initialValue = this.control.value
this.dialog.show() this.dialog.show()
} }
onCancel() { onCancel() {
this.config.control.setValue(this.initialValue) this.control.setValue(this.initialValue)
}
}
@Component({
selector: 'cre-config-editor',
templateUrl: 'editor.html'
})
export class CreConfigEditor extends ErrorHandlingComponent {
@ViewChild('restartingConfirmBox', {static: true}) restartConfirmBox: ConfirmBoxComponent
keys = {
INSTANCE_NAME: Config.INSTANCE_NAME,
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH,
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH,
INSTANCE_URL: Config.INSTANCE_URL,
DATABASE_URL: Config.DATABASE_URL,
DATABASE_USER: Config.DATABASE_USER,
DATABASE_PASSWORD: Config.DATABASE_PASSWORD,
DATABASE_VERSION: Config.DATABASE_VERSION,
RECIPE_APPROBATION_EXPIRATION: Config.RECIPE_APPROBATION_EXPIRATION,
TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF,
TOUCH_UP_KIT_EXPIRATION: Config.TOUCH_UP_KIT_EXPIRATION,
BACKEND_BUILD_VERSION: Config.BACKEND_BUILD_VERSION,
BACKEND_BUILD_TIME: Config.BACKEND_BUILD_TIME,
JAVA_VERSION: Config.JAVA_VERSION,
OPERATING_SYSTEM: Config.OPERATING_SYSTEM
}
controls = new Map<string, FormControl>()
emergencyMode: string | null
constructor(
private configService: ConfigService,
errorService: ErrorService,
activatedRoute: ActivatedRoute,
router: Router
) {
super(errorService, activatedRoute, router)
for (let key in this.keys) {
this.controls.set(this.keys[key], new FormControl(null, Validators.required))
}
}
ngOnInit() {
this.subscribe(
this.configService.get(Config.EMERGENCY_MODE),
config => {
this.emergencyMode = config.content
}
)
}
getConfig(key: string) {
return {key, control: this.controls.get(key)}
}
save() {
this.subscribe(
this.configService.set(this.controls),
() => this.reload()
)
}
restart() {
this.subscribe(
this.configService.restart(),
() => this.restartConfirmBox.show()
)
}
reload() {
window.location.reload()
} }
} }

View File

@ -1,12 +0,0 @@
<div *ngIf="configuration" [attr.title]="tooltip?.content">
<cre-input [class.has-hint]="configuration.editable"
class="w-100"
type="text"
[label]="label.content"
[hint]="configuration.editable ? lastUpdated : null"
[control]="config.control"
[icon]="configuration.requireRestart ? 'alert' : null"
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
iconColor="warning">
</cre-input>
</div>

View File

@ -1,128 +0,0 @@
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color">Retour</cre-primary-button>
</cre-action-group>
<cre-action-group>
<cre-accent-button (click)="save()">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<div *ngIf="emergencyMode" class="d-flex flex-column" style="gap: 1.5rem">
<cre-config-section *ngIf="emergencyMode === 'false'">
<cre-config-label>Apparence</cre-config-label>
<cre-config-list>
<!-- <cre-config [config]="getConfig(keys.INSTANCE_NAME)">-->
<!-- <cre-config-label>Nom de l'instance</cre-config-label>-->
<!-- <cre-config-tooltip>-->
<!-- Affiché dans la barre de titre du navigateur ou en survolant l'onglet de la page dans le navigateur.-->
<!-- </cre-config-tooltip>-->
<!-- </cre-config>-->
<cre-image-config [config]="getConfig(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"
(invalidFormat)="invalidFormatConfirmBox.show()">
<cre-config-label>Logo</cre-config-label>
<cre-config-tooltip>
Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
'Ctrl+F5').
</cre-config-tooltip>
</cre-image-config>
<cre-image-config [config]="getConfig(keys.INSTANCE_ICON_PATH)" previewWidth="32px"
(invalidFormat)="invalidFormatConfirmBox.show()">
<cre-config-label>Icône</cre-config-label>
<cre-config-tooltip>
Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
'Ctrl+F5').
</cre-config-tooltip>
</cre-image-config>
</cre-config-list>
</cre-config-section>
<cre-config-section>
<cre-config-label>Données</cre-config-label>
<cre-config-list class="pt-2">
<cre-period-config [config]="getConfig(keys.RECIPE_APPROBATION_EXPIRATION)">
<cre-config-label>Période d'expiration de l'approbation de l'échantillon des recettes</cre-config-label>
</cre-period-config>
<cre-period-config [config]="getConfig(keys.TOUCH_UP_KIT_EXPIRATION)">
<cre-config-label>Période d'expiration des kits de retouches complets</cre-config-label>
<cre-config-tooltip>
Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront
supprimés automatiquement.
</cre-config-tooltip>
</cre-period-config>
<cre-bool-config [config]="getConfig(keys.TOUCH_UP_KIT_CACHE_PDF)">
<cre-config-label>Activer le cache des PDFs générés</cre-config-label>
<cre-config-tooltip>
Cette option permet de stocker les PDFs générés sur le disque, ce qui permet d'accélérer
l'accès aux PDFs si la lecture des fichiers cachés sur le disque est plus rapide que la génération d'un
nouveau PDF.
</cre-config-tooltip>
</cre-bool-config>
</cre-config-list>
</cre-config-section>
<cre-config-section>
<cre-config-label>Système</cre-config-label>
<cre-config-list>
<cre-config [config]="getConfig(keys.INSTANCE_URL)">
<cre-config-label>URL de l'instance</cre-config-label>
<cre-config-tooltip>
Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques.
</cre-config-tooltip>
</cre-config>
<cre-config [config]="getConfig(keys.DATABASE_URL)">
<cre-config-label>URL de la base de données</cre-config-label>
</cre-config>
<cre-config [config]="getConfig(keys.DATABASE_USER)">
<cre-config-label>Utilisateur de la base de données</cre-config-label>
</cre-config>
<!-- <cre-config [config]="getConfig(keys.DATABASE_PASSWORD)">-->
<!-- <cre-config-label>Mot de passe de la base de données</cre-config-label>-->
<!-- </cre-config>-->
<cre-secure-config
[config]="getConfig(keys.DATABASE_PASSWORD)"
buttonLabel="Modifier le mot de passe de la base de données">
<cre-config-label>Mot de passe de la base de données</cre-config-label>
</cre-secure-config>
<cre-config [config]="getConfig(keys.DATABASE_VERSION)">
<cre-config-label>Version de la base de données</cre-config-label>
</cre-config>
<cre-config [config]="getConfig(keys.BACKEND_BUILD_VERSION)">
<cre-config-label>Version de Color Recipes Explorer</cre-config-label>
</cre-config>
<cre-date-config [config]="getConfig(keys.BACKEND_BUILD_TIME)">
<cre-config-label>Date de compilation de Color Recipes Explorer</cre-config-label>
</cre-date-config>
<cre-config [config]="getConfig(keys.JAVA_VERSION)">
<cre-config-label>Version de Java</cre-config-label>
</cre-config>
<cre-config [config]="getConfig(keys.OPERATING_SYSTEM)">
<cre-config-label>Système d'exploitation</cre-config-label>
</cre-config>
</cre-config-list>
<cre-config-actions>
<cre-warn-button (click)="restartConfirmBox.show()">Redémarrer le serveur</cre-warn-button>
</cre-config-actions>
</cre-config-section>
</div>
<cre-confirm-box #invalidFormatConfirmBox message="Le format du fichier choisi n'est pas valide"></cre-confirm-box>
<cre-confirm-box #restartConfirmBox
message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
(confirm)="restart()"></cre-confirm-box>
<cre-confirm-box #restartingConfirmBox message="Le serveur est en cours de redémarrage" (cancel)="reload()"
(confirm)="reload()"></cre-confirm-box>

View File

@ -1,26 +0,0 @@
<div class="cre-image-config-label">
<p>
<ng-content select="cre-config-label"></ng-content>
</p>
</div>
<div *ngIf="configuration"
class="d-flex flex-row justify-content-between align-items-center"
[attr.title]="tooltip?.content">
<cre-file-input
class="w-100"
accept="image/png,image/jpeg,image/x-icon,image/svg+xml"
[control]="config.control"
(selection)="updateImage($event)"
(invalidFormat)="invalidFormat.emit()">
</cre-file-input>
<div class="image-wrapper d-flex flex-column justify-content-end">
<div>
<img
[src]="updatedImage ? updatedImage : configuredImageUrl"
[attr.width]="previewWidth ? previewWidth : null"
class="mat-elevation-z3"/>
</div>
<mat-hint>{{lastUpdated}}</mat-hint>
</div>
</div>

View File

@ -1,7 +0,0 @@
<div *ngIf="configuration" [attr.title]="tooltip?.content">
<cre-period-input
[control]="config.control"
[label]="label.content"
[hint]="lastUpdated">
</cre-period-input>
</div>

View File

@ -1,25 +0,0 @@
<div *ngIf="configuration" [attr.title]="tooltip?.content">
<cre-primary-button
class="w-100 mb-3"
(click)="onOpen()">
{{buttonLabel}}
</cre-primary-button>
<cre-prompt-dialog
[title]="label.content"
(cancel)="onCancel()">
<cre-dialog-body>
<cre-input
[class.has-hint]="configuration.editable"
class="w-100"
type="password"
label="Nouvelle valeur"
[hint]="configuration.editable ? lastUpdated : null"
[control]="config.control"
[icon]="configuration.requireRestart ? 'alert' : null"
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
iconColor="warning">
</cre-input>
</cre-dialog-body>
</cre-prompt-dialog>
</div>

View File

@ -4,7 +4,7 @@ import {ThemePalette} from '@angular/material/core'
@Component({ @Component({
selector: 'cre-button', selector: 'cre-button',
template: ` template: `
<button mat-raised-button [color]="color" [disabled]="disabled"> <button mat-raised-button [type]="type" [color]="color" [disabled]="disabled">
<ng-content></ng-content> <ng-content></ng-content>
</button> </button>
`, `,
@ -13,44 +13,48 @@ import {ThemePalette} from '@angular/material/core'
}) })
export class CreButtonComponent { export class CreButtonComponent {
@Input() color: ThemePalette @Input() color: ThemePalette
@Input() type = 'button'
@Input() disabled = false @Input() disabled = false
} }
@Component({ @Component({
selector: 'cre-primary-button', selector: 'cre-primary-button',
template: ` template: `
<cre-button color="primary" [disabled]="disabled"> <cre-button color="primary" [type]="type" [disabled]="disabled">
<ng-content></ng-content> <ng-content></ng-content>
</cre-button> </cre-button>
`, `,
styleUrls: ['buttons.sass'] styleUrls: ['buttons.sass']
}) })
export class CrePrimaryButtonComponent { export class CrePrimaryButtonComponent {
@Input() type = 'button'
@Input() disabled = false @Input() disabled = false
} }
@Component({ @Component({
selector: 'cre-accent-button', selector: 'cre-accent-button',
template: ` template: `
<cre-button color="accent" [disabled]="disabled"> <cre-button color="accent" [type]="type" [disabled]="disabled">
<ng-content></ng-content> <ng-content></ng-content>
</cre-button> </cre-button>
`, `,
styleUrls: ['buttons.sass'] styleUrls: ['buttons.sass']
}) })
export class CreAccentButtonComponent { export class CreAccentButtonComponent {
@Input() type = 'button'
@Input() disabled = false @Input() disabled = false
} }
@Component({ @Component({
selector: 'cre-warn-button', selector: 'cre-warn-button',
template: ` template: `
<cre-button color="warn" [disabled]="disabled"> <cre-button color="warn" [type]="type" [disabled]="disabled">
<ng-content></ng-content> <ng-content></ng-content>
</cre-button> </cre-button>
`, `,
styleUrls: ['buttons.sass'] styleUrls: ['buttons.sass']
}) })
export class CreWarnButtonComponent { export class CreWarnButtonComponent {
@Input() type = 'button'
@Input() disabled = false @Input() disabled = false
} }

View File

@ -1,25 +1,21 @@
<mat-form-field> <mat-form-field>
<mat-label>{{label}}</mat-label> <mat-label>{{label}}</mat-label>
<input
*ngIf="!control" <ng-container *ngIf="!control">
matInput <input
[type]="type" #input
[step]="step" matInput
[placeholder]="placeholder" [disabled]="disabled"
[(ngModel)]="value" [(ngModel)]="value"
[required]="required" (change)="valueChange.emit(value)"/>
[autocomplete]="autocomplete ? 'on' : 'off'" </ng-container>
[disabled]="disabled" <ng-container *ngIf="control">
(change)="valueChange.emit(value)"/> <input
<input #input
*ngIf="control" matInput
matInput [formControl]="control">
[type]="type" </ng-container>
[step]="step"
[placeholder]="placeholder"
[required]="required"
[formControl]="control"
[autocomplete]="autocomplete ? 'on' : 'off'"/>
<mat-icon <mat-icon
matSuffix matSuffix
[svgIcon]="icon" [svgIcon]="icon"

View File

@ -1,6 +1,8 @@
import { import {
AfterViewInit,
Component, Component,
ContentChild, ContentChild,
Directive,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, Input,
@ -17,7 +19,14 @@ import {Observable, Subject} from 'rxjs'
import {map, takeUntil} from 'rxjs/operators' import {map, takeUntil} from 'rxjs/operators'
import {MatChipInputEvent} from '@angular/material/chips' import {MatChipInputEvent} from '@angular/material/chips'
import {MatAutocomplete, MatAutocompleteSelectedEvent} from '@angular/material/autocomplete' import {MatAutocomplete, MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'
import {MatOptionSelectionChange} from '@angular/material/core'
@Directive()
abstract class _CreInputBase {
@Input() control: AbstractControl | null
@Input() label: string
@Input() value
@Input() disabled = false
}
@Component({ @Component({
selector: 'cre-input', selector: 'cre-input',
@ -25,24 +34,30 @@ import {MatOptionSelectionChange} from '@angular/material/core'
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
styleUrls: ['input.sass'] styleUrls: ['input.sass']
}) })
export class CreInputComponent { export class CreInputComponent extends _CreInputBase implements AfterViewInit {
@Input() control: FormControl | null
@Input() type = 'text' @Input() type = 'text'
@Input() label: string
@Input() icon: string @Input() icon: string
@Input() iconTitle: string | null @Input() iconTitle: string | null
@Input() required = true @Input() required = true
@Input() autocomplete = true @Input() autocomplete = true
@Input() placeholder: string | null @Input() placeholder: string | null
@Input() step = 1 @Input() step = 1
@Input() value
@Input() iconColor: string = 'black' @Input() iconColor: string = 'black'
@Input() disabled = false
@Input() hint: string | null @Input() hint: string | null
@Output() valueChange = new EventEmitter<any>() @Output() valueChange = new EventEmitter<any>()
@ViewChild('input') input: any
@ContentChild(TemplateRef) errors: TemplateRef<any> @ContentChild(TemplateRef) errors: TemplateRef<any>
ngAfterViewInit() {
const element = this.input.nativeElement
element.type = this.type
element.step = this.step.toString()
element.placeholder = this.placeholder
element.required = this.required
element.autocomplete = this.autocomplete ? 'on' : 'off'
}
} }
@Component({ @Component({
@ -51,7 +66,7 @@ export class CreInputComponent {
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class CreAutocompleteInputComponent { export class CreAutocompleteInputComponent {
@Input() control: FormControl | null @Input() control: AbstractControl | null
@Input() label: string @Input() label: string
@Input() icon: string @Input() icon: string
@Input() required = true @Input() required = true
@ -69,7 +84,7 @@ export class CreAutocompleteInputComponent {
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class CreChipInputComponent implements OnInit { export class CreChipInputComponent implements OnInit {
@Input() control: FormControl @Input() control: AbstractControl
@Input() label: string @Input() label: string
@Input() icon: string @Input() icon: string
@Input() required = true @Input() required = true
@ -118,7 +133,7 @@ export class CreChipInputComponent implements OnInit {
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class CreComboBoxComponent { export class CreComboBoxComponent {
@Input() control: FormControl @Input() control: AbstractControl
@Input() label: string @Input() label: string
@Input() icon: string @Input() icon: string
@Input() required = true @Input() required = true
@ -184,12 +199,16 @@ export class CreChipComboBoxComponent extends CreChipInputComponent implements O
selector: 'cre-checkbox-input', selector: 'cre-checkbox-input',
templateUrl: 'checkbox.html' templateUrl: 'checkbox.html'
}) })
export class CreCheckboxInputComponent { export class CreCheckboxInputComponent implements OnInit {
@Input() label: string @Input() label: string
@Input() control: FormControl @Input() control: AbstractControl
@Input() checked: boolean @Input() checked: boolean
@Output() checkedChange = new EventEmitter<boolean>() @Output() checkedChange = new EventEmitter<boolean>()
ngOnInit(): void {
this.control?.setValue(this.control.value === 'true')
}
} }
@Component({ @Component({
@ -200,7 +219,7 @@ export class CreFileInputComponent implements OnInit {
@Input() label: string @Input() label: string
@Input() icon: string @Input() icon: string
@Input() accept = '' @Input() accept = ''
@Input() control: FormControl | null @Input() control: AbstractControl | null
@Output() selection = new EventEmitter<File>() @Output() selection = new EventEmitter<File>()
@Output() invalidFormat = new EventEmitter<void>() @Output() invalidFormat = new EventEmitter<void>()
@ -234,7 +253,7 @@ export class CreFileInputComponent implements OnInit {
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class CrePeriodInputComponent implements OnInit { export class CrePeriodInputComponent implements OnInit {
@Input() control: FormControl @Input() control: AbstractControl
@Input() label: string @Input() label: string
@Input() hint: string | null @Input() hint: string | null
@ -264,9 +283,12 @@ export class CrePeriodInputComponent implements OnInit {
} }
private setValuesFromPeriod(period: string) { private setValuesFromPeriod(period: string) {
if (!period) {
return
}
const periodTypeChar = period.slice(-1) const periodTypeChar = period.slice(-1)
period = period.slice(1, -1) period = period.slice(1, -1)
this.selectControl.setValue(periodTypeChar) this.selectControl.setValue(periodTypeChar)
this.inputControl.setValue(period) this.inputControl.setValue(period)
} }

View File

@ -1,4 +1,4 @@
import {Form, FormControl} from '@angular/forms' import {AbstractControl, Form, FormControl, Validators} from '@angular/forms'
import {filterMap} from '../utils/map.utils' import {filterMap} from '../utils/map.utils'
export class Config { export class Config {
@ -24,6 +24,10 @@ export class Config {
Config.INSTANCE_ICON_PATH Config.INSTANCE_ICON_PATH
] ]
static readonly PASSWORD_CONFIG_KEYS = [
Config.DATABASE_PASSWORD
]
public key: string public key: string
public requireRestart: boolean public requireRestart: boolean
public editable: boolean public editable: boolean
@ -36,7 +40,20 @@ export class ConfigKeyContent {
public content: string public content: string
} }
export function filterConfigKeyControlMap(map: Map<string, FormControl>): Map<string, FormControl> { export class ConfigControl {
public config: Config
public control: AbstractControl
}
export function buildFormControl(config: Config): AbstractControl {
return new FormControl({value: config.content, disabled: !config.editable}, Validators.required)
}
export function configKeyIsPassword(key: string): boolean {
return Config.PASSWORD_CONFIG_KEYS.indexOf(key) >= 0
}
export function filterConfigKeyControlMap(map: Map<string, AbstractControl>): Map<string, AbstractControl> {
return filterMap(map, (key, control) => { return filterMap(map, (key, control) => {
return control.dirty && return control.dirty &&
Config.IMAGE_CONFIG_KEYS.indexOf(key) < 0 && // Filter image configs because they are sent to a different endpoint Config.IMAGE_CONFIG_KEYS.indexOf(key) < 0 && // Filter image configs because they are sent to a different endpoint
@ -45,17 +62,17 @@ export function filterConfigKeyControlMap(map: Map<string, FormControl>): Map<st
}) })
} }
export function filterImageConfigKeyControlMap(map: Map<string, FormControl>): Map<string, FormControl> { export function filterImageConfigKeyControlMap(map: Map<string, AbstractControl>): Map<string, AbstractControl> {
return filterMap(map, (key, control) => { return filterMap(map, (key, control) => {
return Config.IMAGE_CONFIG_KEYS.indexOf(key) >= 0 && control.dirty return Config.IMAGE_CONFIG_KEYS.indexOf(key) >= 0 && control.dirty
}) })
} }
export function mapToConfigKeyContent(key: string, control: FormControl): ConfigKeyContent { export function mapToConfigKeyContent(key: string, control: AbstractControl): ConfigKeyContent {
return {key, content: control.value} return {key, content: control.value}
} }
export function mapToConfigKeyContentArray(map: Map<string, FormControl>): ConfigKeyContent[] { export function mapToConfigKeyContentArray(map: Map<string, AbstractControl>): ConfigKeyContent[] {
const array: ConfigKeyContent[] = [] const array: ConfigKeyContent[] = []
map.forEach((control, key) => { map.forEach((control, key) => {
array.push(mapToConfigKeyContent(key, control)) array.push(mapToConfigKeyContent(key, control))

View File

@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'
import {Config, filterConfigKeyControlMap, filterImageConfigKeyControlMap, mapToConfigKeyContentArray} from '../model/config.model' import {Config, filterConfigKeyControlMap, filterImageConfigKeyControlMap, mapToConfigKeyContentArray} from '../model/config.model'
import {Observable} from 'rxjs' import {Observable} from 'rxjs'
import {ApiService} from './api.service' import {ApiService} from './api.service'
import {FormControl} from '@angular/forms' import {AbstractControl, FormGroup} from '@angular/forms'
import {transformMap} from '../utils/map.utils' import {transformMap} from '../utils/map.utils'
@Injectable({ @Injectable({
@ -14,11 +14,23 @@ export class ConfigService {
) { ) {
} }
get all(): Observable<Config[]> {
return this.api.get<Config[]>('/config')
}
get(key: string): Observable<Config> { get(key: string): Observable<Config> {
return this.api.get<Config>(`/config/${key}`) return this.api.get<Config>(`/config/${key}`)
} }
set(configs: Map<string, FormControl>): Observable<void> { setFromForm(form: FormGroup): Observable<void> {
const map = new Map<string, AbstractControl>()
for (let key in form.controls) {
map.set(key, form.controls[key])
}
return this.set(map);
}
set(configs: Map<string, AbstractControl>): Observable<void> {
const body = mapToConfigKeyContentArray(filterConfigKeyControlMap(configs)) const body = mapToConfigKeyContentArray(filterConfigKeyControlMap(configs))
const imageConfigs = filterImageConfigKeyControlMap(configs) const imageConfigs = filterImageConfigKeyControlMap(configs)
@ -38,7 +50,7 @@ export class ConfigService {
return this.api.post<void>('/config/restart') return this.api.post<void>('/config/restart')
} }
private setImages(configs: Map<string, FormControl>) { private setImages(configs: Map<string, AbstractControl>) {
const subscriptions = this.getImageConfigsSubscriptions(configs) const subscriptions = this.getImageConfigsSubscriptions(configs)
while (subscriptions.length > 0) { while (subscriptions.length > 0) {
const subscription = subscriptions.pop().subscribe({ const subscription = subscriptions.pop().subscribe({
@ -47,7 +59,7 @@ export class ConfigService {
} }
} }
private getImageConfigsSubscriptions(configs: Map<string, FormControl>): Observable<void>[] { private getImageConfigsSubscriptions(configs: Map<string, AbstractControl>): Observable<void>[] {
return transformMap(configs, (key, control) => { return transformMap(configs, (key, control) => {
return this.setImage(key, control.value) return this.setImage(key, control.value)
}) })