diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7057e34..24a2458 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,6 +3,7 @@ import {Routes, RouterModule} from '@angular/router' import {CatalogComponent} from './pages/catalog/catalog.component' import {AdministrationComponent} from './pages/administration/administration.component' import {MiscComponent} from './pages/others/misc.component' +import {CreConfigEditor} from './modules/configuration/config' const routes: Routes = [{ @@ -38,6 +39,10 @@ const routes: Routes = [{ }, { path: 'group', loadChildren: () => import('./modules/groups/group.module').then(m => m.GroupModule) + }, { + path: 'config', + loadChildren: () => import('./modules/configuration/config.module').then(m => m.ConfigModule), + component: CreConfigEditor }, { path: '', pathMatch: 'full', diff --git a/src/app/app.component.ts b/src/app/app.component.ts index df904ac..7f5758f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,6 +4,9 @@ import {AppState} from './modules/shared/app-state' import {SubscribingComponent} from './modules/shared/components/subscribing.component' import {ActivatedRoute, Router} from '@angular/router' import {ErrorService} from './modules/shared/service/error.service' +import {ConfigService} from './modules/shared/service/config.service' +import {Config} from './modules/shared/model/config.model' +import {environment} from '../environments/environment' @Component({ selector: 'cre-root', @@ -13,9 +16,11 @@ import {ErrorService} from './modules/shared/service/error.service' export class AppComponent extends SubscribingComponent { isOnline: boolean isServerOnline = true + favIcon: HTMLLinkElement = document.querySelector('#favicon') constructor( @Inject(PLATFORM_ID) private platformId: object, + private configService: ConfigService, private appState: AppState, errorService: ErrorService, router: Router, @@ -32,6 +37,8 @@ export class AppComponent extends SubscribingComponent { this.appState.serverOnline$, online => this.isServerOnline = online ) + + this.favIcon.href = environment.apiUrl + "/file?path=images%2Ficon" } reload() { diff --git a/src/app/modules/colors/components/step-list/step-list.component.html b/src/app/modules/colors/components/step-list/step-list.component.html index efb9c0c..c53850d 100644 --- a/src/app/modules/colors/components/step-list/step-list.component.html +++ b/src/app/modules/colors/components/step-list/step-list.component.html @@ -9,7 +9,7 @@ -

Aucun groupe n'est sélectionné

-

Il n'y a aucune étape définie pour ce groupe

+

Aucun groupe n'est sélectionné

+

Il n'y a aucune étape définie pour ce groupe

diff --git a/src/app/modules/colors/components/step-table/step-table.component.html b/src/app/modules/colors/components/step-table/step-table.component.html index 115f924..800b6a4 100644 --- a/src/app/modules/colors/components/step-table/step-table.component.html +++ b/src/app/modules/colors/components/step-table/step-table.component.html @@ -12,7 +12,7 @@ -

Aucun groupe n'est sélectionné

+

Aucun groupe n'est sélectionné

this.recipes = recipes + this.configService.get(Config.EMERGENCY_MODE), + config => { + if (config.content === "false") { + this.subscribe( + this.recipeService.allSortedByCompany, + recipes => this.recipes = recipes + ) + } else { + this.urlUtils.navigateTo("/admin/config") + } + } ) } diff --git a/src/app/modules/configuration/bool-config.html b/src/app/modules/configuration/bool-config.html new file mode 100644 index 0000000..53c694c --- /dev/null +++ b/src/app/modules/configuration/bool-config.html @@ -0,0 +1,4 @@ +
+ + Dernière mise à jour: {{lastUpdated}} +
diff --git a/src/app/modules/configuration/config-actions.html b/src/app/modules/configuration/config-actions.html new file mode 100644 index 0000000..d152dd7 --- /dev/null +++ b/src/app/modules/configuration/config-actions.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/modules/configuration/config-section.html b/src/app/modules/configuration/config-section.html new file mode 100644 index 0000000..08c2b94 --- /dev/null +++ b/src/app/modules/configuration/config-section.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/app/modules/configuration/config.html b/src/app/modules/configuration/config.html new file mode 100644 index 0000000..12f2ef0 --- /dev/null +++ b/src/app/modules/configuration/config.html @@ -0,0 +1,12 @@ +
+ + +
diff --git a/src/app/modules/configuration/config.module.ts b/src/app/modules/configuration/config.module.ts new file mode 100644 index 0000000..05dee32 --- /dev/null +++ b/src/app/modules/configuration/config.module.ts @@ -0,0 +1,35 @@ +import {NgModule} from '@angular/core' +import { + CreConfig, + CreConfigLabel, + CreConfigEditor, + CreConfigSection, + CreImageConfig, + CreConfigList, + CreConfigActions, + CreConfigTooltip +} from './config' +import {SharedModule} from '../shared/shared.module' +import {CreInputsModule} from '../shared/components/inputs/inputs.module' +import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module' +import {CreButtonsModule} from '../shared/components/buttons/buttons.module' + +@NgModule({ + declarations: [ + CreConfigLabel, + CreConfigTooltip, + CreConfigEditor, + CreConfig, + CreImageConfig, + CreConfigSection, + CreConfigList, + CreConfigActions + ], + imports: [ + SharedModule, + CreInputsModule, + CreActionBarModule, + CreButtonsModule + ] +}) +export class ConfigModule { } diff --git a/src/app/modules/configuration/config.sass b/src/app/modules/configuration/config.sass new file mode 100644 index 0000000..6812169 --- /dev/null +++ b/src/app/modules/configuration/config.sass @@ -0,0 +1,52 @@ +mat-hint + font-size: .8em + +cre-config + display: block + + cre-input.has-hint + margin-bottom: 1em + + mat-hint + font-size: 1em + +cre-image-config + display: block + border: 1px solid rgba(0, 0, 0, 0.42) + border-radius: 4px + padding: 8px + position: relative + margin-bottom: 1.34375em + transition: border-color 300ms + + &:hover + border: 2px solid black + padding: 7px + + .cre-image-config-label + top: -10px + left: 3px + + .cre-image-config-label + position: absolute + top: -9px + left: 4px + margin-bottom: 0 + background-color: white + padding: 0 5px + color: rgba(0, 0, 0, 0.52) + font-size: .8em + + .image-wrapper + width: 200px + text-align: right + + hr + margin: 0 0 .5em + background-color: rgba(0, 0, 0, 0.42) + + img + border: 2px solid black + + mat-hint + margin-top: .2em diff --git a/src/app/modules/configuration/config.ts b/src/app/modules/configuration/config.ts new file mode 100644 index 0000000..5332d13 --- /dev/null +++ b/src/app/modules/configuration/config.ts @@ -0,0 +1,228 @@ +import { + AfterViewInit, + Component, + ContentChild, + Directive, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core' +import {ConfigService} from '../shared/service/config.service' +import {Config} from '../shared/model/config.model' +import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component' +import {ErrorService} from '../shared/service/error.service' +import {ActivatedRoute, Router} from '@angular/router' +import {formatDateTime, readFile} from '../shared/utils/utils' +import {FormControl, Validators} from '@angular/forms' +import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component' + +@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({ + selector: 'cre-config-list' +}) +export class CreConfigList { +} + +@Component({ + selector: 'cre-config-actions', + templateUrl: 'config-actions.html' +}) +export class CreConfigActions { +} + +@Component({ + selector: 'cre-config-section', + templateUrl: 'config-section.html' +}) +export class CreConfigSection { + @ContentChild(CreConfigActions) actions: CreConfigActions + + get hasActions(): boolean { + return this.actions !== undefined + } +} + +@Component({ + selector: 'cre-config', + templateUrl: 'config.html', + styleUrls: ['config.sass'] +}) +export class CreConfig extends SubscribingComponent { + @Input() config: { key: string, control: FormControl } + + @ContentChild(CreConfigLabel, {static: true}) label: CreConfigLabel + @ContentChild(CreConfigTooltip, {static: true}) tooltip: CreConfigTooltip + + configuration: Config | null + + constructor( + private configService: ConfigService, + errorService: ErrorService, + activatedRoute: ActivatedRoute, + router: Router + ) { + super(errorService, activatedRoute, router) + } + + ngOnInit() { + super.ngOnInit() + + this.subscribe( + this.configService.get(this.config.key), + config => this.setConfig(config) + ) + } + + setConfig(config: Config) { + this.configuration = config + this.config.control.setValue(config.content) + if (!config.editable) { + this.config.control.disable() + } + } + + get lastUpdated(): string { + return formatDateTime(this.configuration.lastUpdated) + } +} + +@Component({ + selector: 'cre-image-config', + templateUrl: 'image-config.html', + styleUrls: ['config.sass'], + encapsulation: ViewEncapsulation.None +}) +export class CreImageConfig extends CreConfig { + @Input() previewWidth: string | null + + @Output() invalidFormat = new EventEmitter() + + updatedImage: any | null + + constructor( + configService: ConfigService, + errorService: ErrorService, + activatedRoute: ActivatedRoute, + router: Router + ) { + super(configService, errorService, activatedRoute, router) + } + + updateImage(file: File): any { + readFile(file, (content) => this.updatedImage = content) + } +} + +@Component({ + selector: 'cre-bool-config', + templateUrl: 'bool-config.html' +}) +export class CreBoolConfig extends CreConfig { + setConfig(config: Config) { + super.setConfig(config); + this.config.control.setValue(config.content === "true") + } +} + +@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, + TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF, + APP_VERSION: Config.APP_VERSION, + JAVA_VERSION: Config.JAVA_VERSION, + OPERATING_SYSTEM: Config.OPERATING_SYSTEM + } + controls = new Map() + 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[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[key]} + } + + save() { + this.subscribe( + this.configService.set(this.controls), + () => this.reload() + ) + } + + restart() { + this.subscribe( + this.configService.restart(), + () => this.restartConfirmBox.show() + ) + } + + reload() { + window.location.reload() + } +} diff --git a/src/app/modules/configuration/editor.html b/src/app/modules/configuration/editor.html new file mode 100644 index 0000000..0deb9df --- /dev/null +++ b/src/app/modules/configuration/editor.html @@ -0,0 +1,105 @@ + + + Retour + + + Enregistrer + + + +
+ + Apparence + + + + + + + + + + Logo + + 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'). + + + + + Icône + + 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'). + + + + + + + Kits de retouche + + + Activer le cache des PDFs générés + + 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. + + + + + + + Système + + + URL de l'instance + + Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques. + + + + + URL de la base de données + + + + Utilisateur de la base de données + + + + Mot de passe de la base de données + + + + Version de la base de données + + + + Version de Color Recipes Explorer + + + + Version de Java + + + + Système d'exploitation + + + + Redémarrer le serveur + + +
+ + + + diff --git a/src/app/modules/configuration/image-config.html b/src/app/modules/configuration/image-config.html new file mode 100644 index 0000000..6056487 --- /dev/null +++ b/src/app/modules/configuration/image-config.html @@ -0,0 +1,26 @@ +
+

+ +

+
+ +
+ + +
+
+ +
+ Dernière mise à jour:
{{lastUpdated}}
+
+
diff --git a/src/app/modules/shared/components/header/header.component.html b/src/app/modules/shared/components/header/header.component.html index 7a2bb30..a5382f8 100644 --- a/src/app/modules/shared/components/header/header.component.html +++ b/src/app/modules/shared/components/header/header.component.html @@ -19,7 +19,8 @@ Color Recipes Explorer + title="Color Recipes Explorer" + height="70px"/> diff --git a/src/app/modules/shared/components/header/header.component.ts b/src/app/modules/shared/components/header/header.component.ts index 470a396..882d013 100644 --- a/src/app/modules/shared/components/header/header.component.ts +++ b/src/app/modules/shared/components/header/header.component.ts @@ -5,6 +5,8 @@ import {Permission} from '../../model/user' import {AccountService} from '../../../accounts/services/account.service' import {SubscribingComponent} from '../subscribing.component' import {ErrorService} from '../../service/error.service' +import {ConfigService} from '../../service/config.service' +import {environment} from '../../../../../environments/environment' @Component({ selector: 'cre-header', @@ -22,6 +24,7 @@ export class HeaderComponent extends SubscribingComponent { constructor( private accountService: AccountService, + private configService: ConfigService, private appState: AppState, errorService: ErrorService, router: Router, @@ -62,6 +65,10 @@ export class HeaderComponent extends SubscribingComponent { super.ngOnDestroy() } + get logoUrl(): string { + return environment.apiUrl + "/file?path=images%2Flogo" + } + set activeLink(link: string) { this._activeLink = link this.router.navigate([link]) diff --git a/src/app/modules/shared/components/inputs/checkbox.html b/src/app/modules/shared/components/inputs/checkbox.html new file mode 100644 index 0000000..dbadabf --- /dev/null +++ b/src/app/modules/shared/components/inputs/checkbox.html @@ -0,0 +1,3 @@ + + {{label}} + diff --git a/src/app/modules/shared/components/inputs/file-input.html b/src/app/modules/shared/components/inputs/file-input.html new file mode 100644 index 0000000..5bf6c13 --- /dev/null +++ b/src/app/modules/shared/components/inputs/file-input.html @@ -0,0 +1,10 @@ +
+ Choisir un fichier + +
+

{{selectedFileName}}

+

Aucun fichier sélectionné

+
+
+ + diff --git a/src/app/modules/shared/components/inputs/input.html b/src/app/modules/shared/components/inputs/input.html index d7ef1cd..d123b75 100644 --- a/src/app/modules/shared/components/inputs/input.html +++ b/src/app/modules/shared/components/inputs/input.html @@ -9,6 +9,7 @@ [(ngModel)]="value" [required]="required" [autocomplete]="autocomplete ? 'on' : 'off'" + [disabled]="disabled" (change)="valueChange.emit(value)"/> - + + + {{hint}} Ce champ est requis () @@ -174,6 +180,50 @@ export class CreChipComboBoxComponent extends CreChipInputComponent implements O } } +@Component({ + selector: 'cre-checkbox-input', + templateUrl: 'checkbox.html' +}) +export class CreCheckboxInputComponent { + @Input() label: string + @Input() control: FormControl +} + +@Component({ + selector: 'cre-file-input', + templateUrl: 'file-input.html' +}) +export class CreFileInputComponent implements OnInit { + @Input() label: string + @Input() icon: string + @Input() accept = '' + @Input() control: FormControl | null + + @Output() selection = new EventEmitter() + @Output() invalidFormat = new EventEmitter() + + private acceptedMediaTypes: string[] + + ngOnInit(): void { + this.acceptedMediaTypes = this.accept.split(',') + } + + selectedFile: File | null + selectedFileName: string + + onFileSelected(event: any) { + this.selectedFile = event.target.files[0] + if (this.acceptedMediaTypes.indexOf(this.selectedFile.type) >= 0) { + this.selectedFileName = this.selectedFile.name + this.control?.setValue(this.selectedFile) + this.control?.markAsDirty() + this.selection.emit(this.selectedFile) + } else { + this.invalidFormat.emit() + } + } +} + export class ComboBoxEntry { constructor( public key: any, diff --git a/src/app/modules/shared/model/config.model.ts b/src/app/modules/shared/model/config.model.ts new file mode 100644 index 0000000..2d96d4d --- /dev/null +++ b/src/app/modules/shared/model/config.model.ts @@ -0,0 +1,24 @@ +export class Config { + static readonly INSTANCE_NAME = 'instance.name' + static readonly INSTANCE_LOGO_PATH = 'instance.logo.path' + static readonly INSTANCE_ICON_PATH = 'instance.icon.path' + static readonly INSTANCE_URL = 'instance.url' + static readonly DATABASE_URL = 'database.url' + static readonly DATABASE_USER = 'database.user' + static readonly DATABASE_PASSWORD = 'database.password' + static readonly DATABASE_VERSION = 'database.version.supported' + static readonly TOUCH_UP_KIT_CACHE_PDF = 'touchupkit.pdf.cache' + static readonly EMERGENCY_MODE = 'env.emergency' + static readonly APP_VERSION = 'env.version' + static readonly JAVA_VERSION = 'env.java.version' + static readonly OPERATING_SYSTEM = 'env.os' + + constructor( + public key: string, + public content: string, + public lastUpdated: string, + public requireRestart: boolean, + public editable: boolean + ) { + } +} diff --git a/src/app/modules/shared/service/config.service.ts b/src/app/modules/shared/service/config.service.ts new file mode 100644 index 0000000..880b697 --- /dev/null +++ b/src/app/modules/shared/service/config.service.ts @@ -0,0 +1,61 @@ +import {Injectable} from '@angular/core' +import {Config} from '../model/config.model' +import {Observable} from 'rxjs' +import {ApiService} from './api.service' +import {FormControl} from '@angular/forms' + +const imageConfigsKeys = [ + Config.INSTANCE_LOGO_PATH, + Config.INSTANCE_ICON_PATH +] + +@Injectable({ + providedIn: 'root' +}) +export class ConfigService { + constructor( + private api: ApiService + ) { + } + + get(key: string): Observable { + return this.api.get(`/config/${key}`) + } + + set(configs: Map): Observable { + const body = [] + for (let key in configs) { + const control = configs[key] + if (control.dirty && key.indexOf('path') < 0) { + body.push({key, content: control.value}) + } + } + + const subscriptions = [] + imageConfigsKeys.forEach(key => { + if (configs[key].dirty) { + subscriptions.push(this.setImage(key, configs[key].value)) + } + }) + + while (subscriptions.length > 0) { + const subscription = subscriptions.pop().subscribe({ + next: () => subscription.unsubscribe() + }) + } + + return this.api.put('/config', body) + } + + setImage(key: string, image: File): Observable { + const body = new FormData() + body.append('key', key) + body.append('image', image) + + return this.api.put('/config/image', body) + } + + restart(): Observable { + return this.api.post('/config/restart') + } +} diff --git a/src/app/modules/shared/utils/utils.ts b/src/app/modules/shared/utils/utils.ts index e9eaab1..4ff71e9 100644 --- a/src/app/modules/shared/utils/utils.ts +++ b/src/app/modules/shared/utils/utils.ts @@ -1,5 +1,5 @@ /** Returns [value] if it is not null or [or]. */ -import {DateTimeFormatter, LocalDate} from 'js-joda' +import {DateTimeFormatter, LocalDate, LocalDateTime} from 'js-joda' import {TouchUpKit} from '../model/touch-up-kit.model' import {environment} from '../../../../environments/environment' @@ -32,13 +32,27 @@ export function openRawUrl(url: string) { const dateFormatter = DateTimeFormatter .ofPattern('dd-MM-yyyy') +const dateTimeFormatter = DateTimeFormatter + .ofPattern('dd-MM-yyyy HH:mm:ss') export function formatDate(date: string): string { return LocalDate.parse(date).format(dateFormatter) } +export function formatDateTime(dateTime: string): string { + return LocalDateTime.parse(dateTime).format(dateTimeFormatter) +} + export function reduceDashes(arr: string[]): string { return arr.reduce((acc, cur) => { return `${acc} - ${cur}` }) } + +export function readFile(file: File, consumer: (any) => void) { + const reader = new FileReader() + reader.onload = (e: any) => { + consumer(e.target.result) + } + reader.readAsDataURL(file) +} diff --git a/src/app/pages/administration/administration.component.ts b/src/app/pages/administration/administration.component.ts index 3406db0..c67a13e 100644 --- a/src/app/pages/administration/administration.component.ts +++ b/src/app/pages/administration/administration.component.ts @@ -12,5 +12,6 @@ export class AdministrationComponent extends SubMenuComponent { links: NavLink[] = [ {route: '/admin/user', title: 'Utilisateurs', permission: Permission.VIEW_USERS}, {route: '/admin/group', title: 'Groupes', permission: Permission.VIEW_USERS}, + {route: '/admin/config', title: 'Configuration', permission: Permission.ADMIN} ] } diff --git a/src/assets/favicon.png b/src/assets/favicon.png index 22ef30a..1e877b7 100644 Binary files a/src/assets/favicon.png and b/src/assets/favicon.png differ diff --git a/src/index.html b/src/index.html index 852a73c..52117bc 100644 --- a/src/index.html +++ b/src/index.html @@ -5,7 +5,7 @@ Color Recipes Explorer - + diff --git a/src/styles.sass b/src/styles.sass index 5570317..218755c 100644 --- a/src/styles.sass +++ b/src/styles.sass @@ -170,7 +170,7 @@ div.empty .alert p margin-bottom: 0 -.empty-text +.light-text color: rgba(0, 0, 0, 0.54) .dark-background